This is an automated email from the ASF dual-hosted git repository. imaxon pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/asterixdb.git
commit 000223e821b0b334312c103e37233ca400c2ee95 Author: Ian Maxon <[email protected]> AuthorDate: Thu Feb 22 01:20:33 2024 -0800 [ASTERIXDB-3356][RT] Add min/max cardinality to plan - user model changes: no - storage format changes: no - interface changes: no Details: - Add min and max cardinality from profile to plan - Add test to check format of plan with profile annotations Change-Id: Ibdacf4e6b156a3b6ef15b4420f4102c122f8af1d Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/18174 Integration-Tests: Jenkins <[email protected]> Contrib: Ian Maxon <[email protected]> Reviewed-by: Ian Maxon <[email protected]> Reviewed-by: Vijay Sarathy <[email protected]> Tested-by: Jenkins <[email protected]> --- .../apache/asterix/test/common/TestExecutor.java | 7 +- .../src/test/resources/runtimets/profiled.xml | 8 + .../profile/plansleep/plansleep.1.ddl.sqlpp | 62 ++++ .../profile/plansleep/plansleep.2.update.sqlpp | 32 ++ .../profile/plansleep/plansleep.3.plans.sqlpp | 27 ++ .../profile/plansleep/plansleep.90.ddl.sqlpp | 20 ++ .../results/profile/plansleep/sleep.3.regexjson | 390 +++++++++++++++++++++ .../LogicalOperatorPrettyPrintVisitorJson.java | 54 ++- 8 files changed, 584 insertions(+), 16 deletions(-) diff --git a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java index 9d62c5216e..e6b46623f5 100644 --- a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java +++ b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java @@ -831,9 +831,10 @@ public class TestExecutor { List<Parameter> newParams = setFormatInAccept(fmt) ? params : upsertParam(params, QueryServiceRequestParameters.Parameter.FORMAT.str(), ParameterTypeEnum.STRING, fmt.extension()); - - newParams = upsertParam(newParams, QueryServiceRequestParameters.Parameter.PLAN_FORMAT.str(), - ParameterTypeEnum.STRING, DEFAULT_PLAN_FORMAT); + String planFormatKey = QueryServiceRequestParameters.Parameter.PLAN_FORMAT.str(); + if (!newParams.stream().anyMatch(p -> p.getName().equals(planFormatKey))) { + newParams = upsertParam(newParams, planFormatKey, ParameterTypeEnum.STRING, DEFAULT_PLAN_FORMAT); + } final Optional<String> maxReadsOptional = extractMaxResultReads(str); if (maxReadsOptional.isPresent()) { newParams = upsertParam(newParams, QueryServiceRequestParameters.Parameter.MAX_RESULT_READS.str(), diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/profiled.xml b/asterixdb/asterix-app/src/test/resources/runtimets/profiled.xml index d3664d4cc1..b3f222a557 100644 --- a/asterixdb/asterix-app/src/test/resources/runtimets/profiled.xml +++ b/asterixdb/asterix-app/src/test/resources/runtimets/profiled.xml @@ -44,6 +44,14 @@ <output-dir compare="Text">sleep</output-dir> </compilation-unit> </test-case> + <test-case FilePath="profile"> + <compilation-unit name="plansleep"> + <parameter name="profile" value="timings" type="string"/> + <parameter name="optimized-logical-plan" value="true" type="string"/> + <parameter name="plan-format" value="json" type="string"/> + <output-dir compare="Clean-JSON">plansleep</output-dir> + </compilation-unit> + </test-case> <test-case FilePath="profile"> <compilation-unit name="non-unary-subplan"> <parameter name="profile" value="timings" type="string"/> diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.1.ddl.sqlpp new file mode 100644 index 0000000000..aedce7b327 --- /dev/null +++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.1.ddl.sqlpp @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +/* + * Description : Testing that "off" is supplied in request parameter "profile". + * Expected Res : Success with expected result not having "profile" field. + */ + +drop dataverse test if exists; +create dataverse test; + +use test; + +create type test.AddressType as +{ + number : bigint, + street : string, + city : string +}; + +create type test.CustomerType as + closed { + cid : bigint, + name : string, + age : bigint?, + address : AddressType?, + lastorder : { + oid : bigint, + total : float + } +}; + +create type test.OrderType as +{ + oid : bigint, + cid : bigint, + orderstatus : string, + orderpriority : string, + clerk : string, + total : float +}; + +create dataset Customers(CustomerType) primary key cid; + +create dataset Orders(OrderType) primary key oid; + +create dataset Customers2(CustomerType) primary key cid; diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.2.update.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.2.update.sqlpp new file mode 100644 index 0000000000..07af22ec33 --- /dev/null +++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.2.update.sqlpp @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use test; + +load dataset Customers using localfs + ((`path`=`asterix_nc1://data/custord-tiny/customer-tiny-neg.adm`), + (`format`=`adm`)); + +load dataset Orders using localfs + ((`path`=`asterix_nc1://data/custord-tiny/order-tiny.adm`), + (`format`=`adm`)); + +load dataset Customers2 using localfs + ((`path`=`asterix_nc1://data/custord-tiny/customer-tiny.adm`), + (`format`=`adm`)); diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.3.plans.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.3.plans.sqlpp new file mode 100644 index 0000000000..6c35376332 --- /dev/null +++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.3.plans.sqlpp @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +-- compareunorderedarray=true +USE test; + +SELECT count(*) AS customers, city +FROM Customers c +WHERE c.age <65 +GROUP BY c.address.city +ORDER BY sleep(city,1700); \ No newline at end of file diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.90.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.90.ddl.sqlpp new file mode 100644 index 0000000000..f12a2b7f77 --- /dev/null +++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/profile/plansleep/plansleep.90.ddl.sqlpp @@ -0,0 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +drop dataverse test; \ No newline at end of file diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/profile/plansleep/sleep.3.regexjson b/asterixdb/asterix-app/src/test/resources/runtimets/results/profile/plansleep/sleep.3.regexjson new file mode 100644 index 0000000000..111ded8c9d --- /dev/null +++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/profile/plansleep/sleep.3.regexjson @@ -0,0 +1,390 @@ +{ + "optimizedLogicalPlan": { + "operator": "distribute-result", + "expressions": [ + "$$48" + ], + "operatorId": "1.1", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "physical-operator": "DISTRIBUTE_RESULT", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "exchange", + "operatorId": "1.2", + "runtime-id": "R{.+}", + "physical-operator": "ONE_TO_ONE_EXCHANGE", + "execution-mode": "PARTITIONED", + "optimizer-estimates":"R{.+}", + "inputs": [ + { + "operator": "project", + "variables": [ + "$$48" + ], + "operatorId": "1.3", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 3, + "max-cardinality": 3, + "physical-operator": "STREAM_PROJECT", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "exchange", + "operatorId": "1.4", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "physical-operator": "SORT_MERGE_EXCHANGE [$$49(ASC) ]", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "order", + "order-by-list": [ + { + "order": "ASC", + "expression": "$$49" + } + ], + "operatorId": "1.5", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "physical-operator": "STABLE_SORT [$$49(ASC)]", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "exchange", + "operatorId": "1.6", + "runtime-id": "R{.+}", + "physical-operator": "ONE_TO_ONE_EXCHANGE", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "project", + "variables": [ + "$$48", + "$$49" + ], + "operatorId": "1.7", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 3, + "max-cardinality": 3, + "physical-operator": "STREAM_PROJECT", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "assign", + "variables": [ + "$$49" + ], + "expressions": [ + "sleep($$city, 1700)" + ], + "operatorId": "1.8", + "runtime-id": "R{.+}", + "min-time": "R{5.+}", + "max-time": "R{5.+}", + "min-cardinality": 3, + "max-cardinality": 3, + "physical-operator": "ASSIGN", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "project", + "variables": [ + "$$city", + "$$48" + ], + "operatorId": "1.9", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 3, + "max-cardinality": 3, + "physical-operator": "STREAM_PROJECT", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "assign", + "variables": [ + "$$48" + ], + "expressions": [ + "{\"customers\": $$52, \"city\": $$city}" + ], + "operatorId": "1.10", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 3, + "max-cardinality": 3, + "physical-operator": "ASSIGN", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "exchange", + "operatorId": "1.11", + "runtime-id": "R{.+}", + "physical-operator": "ONE_TO_ONE_EXCHANGE", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "group-by", + "group-by-list": [ + { + "variable": "$$city", + "expression": "$$56" + } + ], + "subplan": [ + { + "operator": "aggregate", + "variables": [ + "$$52" + ], + "expressions": [ + "agg-sql-sum($$55)" + ], + "operatorId": "1.11.1", + "physical-operator": "AGGREGATE", + "execution-mode": "LOCAL", + "inputs": [ + { + "operator": "nested-tuple-source", + "operatorId": "1.11.2", + "physical-operator": "NESTED_TUPLE_SOURCE", + "execution-mode": "LOCAL" + } + ] + } + ], + "operatorId": "1.12", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 3, + "max-cardinality": 3, + "physical-operator": "SORT_GROUP_BY[$$56]", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "exchange", + "operatorId": "1.13", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "physical-operator": "HASH_PARTITION_EXCHANGE [$$56]", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "group-by", + "group-by-list": [ + { + "variable": "$$56", + "expression": "$$50" + } + ], + "subplan": [ + { + "operator": "aggregate", + "variables": [ + "$$55" + ], + "expressions": [ + "agg-sql-count(1)" + ], + "operatorId": "1.13.1", + "physical-operator": "AGGREGATE", + "execution-mode": "LOCAL", + "inputs": [ + { + "operator": "nested-tuple-source", + "operatorId": "1.13.2", + "physical-operator": "NESTED_TUPLE_SOURCE", + "execution-mode": "LOCAL" + } + ] + } + ], + "operatorId": "1.14", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "physical-operator": "SORT_GROUP_BY[$$50]", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "exchange", + "operatorId": "1.15", + "runtime-id": "R{.+}", + "physical-operator": "ONE_TO_ONE_EXCHANGE", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "project", + "variables": [ + "$$50" + ], + "operatorId": "1.16", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 5, + "max-cardinality": 5, + "physical-operator": "STREAM_PROJECT", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "assign", + "variables": [ + "$$50" + ], + "expressions": [ + "$$c.getField(3).getField(2)" + ], + "operatorId": "1.17", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 5, + "max-cardinality": 5, + "physical-operator": "ASSIGN", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "select", + "condition": "lt($$c.getField(2), 65)", + "operatorId": "1.18", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 5, + "max-cardinality": 5, + "physical-operator": "STREAM_SELECT", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "project", + "variables": [ + "$$c" + ], + "operatorId": "1.19", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 10, + "max-cardinality": 10, + "physical-operator": "STREAM_PROJECT", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "exchange", + "operatorId": "1.20", + "runtime-id": "R{.+}", + "physical-operator": "ONE_TO_ONE_EXCHANGE", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "data-scan", + "variables": [ + "$$51", + "$$c" + ], + "data-source": "test.Customers", + "operatorId": "1.21", + "runtime-id": "R{.+}", + "min-time": "R{[0-9.]+}", + "max-time": "R{[0-9.]+}", + "min-cardinality": 10, + "max-cardinality": 10, + "physical-operator": "DATASOURCE_SCAN", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "exchange", + "operatorId": "1.22", + "runtime-id": "R{.+}", + "physical-operator": "ONE_TO_ONE_EXCHANGE", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}", + "inputs": [ + { + "operator": "empty-tuple-source", + "operatorId": "1.23", + "runtime-id": "R{.+}", + "physical-operator": "EMPTY_TUPLE_SOURCE", + "execution-mode": "PARTITIONED", + "optimizer-estimates": "R{.+}" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } +} diff --git a/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/prettyprint/LogicalOperatorPrettyPrintVisitorJson.java b/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/prettyprint/LogicalOperatorPrettyPrintVisitorJson.java index d1a356cb45..36c33e611f 100644 --- a/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/prettyprint/LogicalOperatorPrettyPrintVisitorJson.java +++ b/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/prettyprint/LogicalOperatorPrettyPrintVisitorJson.java @@ -228,19 +228,32 @@ public class LogicalOperatorPrettyPrintVisitorJson extends AbstractLogicalOperat } private class OperatorProfile { - Map<String, Pair<Double, Double>> activities; + Map<String, Pair<Double, Double>> activityTimes; + Map<String, Pair<Long, Long>> activityCards; OperatorProfile() { - activities = new HashMap<>(); + activityTimes = new HashMap<>(); + activityCards = new HashMap<>(); + } + + void updateOperator(String extendedOpId, double time, long cardinality) { + updateMinMax(time, extendedOpId, activityTimes); + if (cardinality > 0) { + updateMinMax(cardinality, extendedOpId, activityCards); + } } void updateOperator(String extendedOpId, double time) { - Pair<Double, Double> times = activities.computeIfAbsent(extendedOpId, i -> new Pair(time, time)); - if (times.getFirst() > time) { - times.setFirst(time); + updateMinMax(time, extendedOpId, activityTimes); + } + + private <T extends Comparable<T>> void updateMinMax(T comp, String id, Map<String, Pair<T, T>> opMap) { + Pair<T, T> times = opMap.computeIfAbsent(id, i -> new Pair(comp, comp)); + if (times.getFirst().compareTo(comp) > 0) { + times.setFirst(comp); } - if (times.getSecond() < time) { - times.setSecond(time); + if (times.getSecond().compareTo(comp) < 0) { + times.setSecond(comp); } } } @@ -257,6 +270,11 @@ public class LogicalOperatorPrettyPrintVisitorJson extends AbstractLogicalOperat for (JsonNode counters : task.get("counters")) { OperatorProfile info = profiledOps.computeIfAbsent(counters.get("runtime-id").asText(), i -> new OperatorProfile()); + JsonNode card = counters.get("cardinality-out"); + if (card != null) { + info.updateOperator(acIdFromName(counters.get("name").asText()).getLocalId(), + counters.get("run-time").asDouble(), counters.get("cardinality-out").asLong(-1)); + } info.updateOperator(acIdFromName(counters.get("name").asText()).getLocalId(), counters.get("run-time").asDouble()); } @@ -343,16 +361,26 @@ public class LogicalOperatorPrettyPrintVisitorJson extends AbstractLogicalOperat jsonGenerator.writeStringField("runtime-id", od); OperatorProfile info = profile.get(od); if (info != null) { - if (info.activities.size() == 1) { - Pair<Double, Double> minMax = info.activities.values().iterator().next(); + if (info.activityTimes.size() == 1) { + Pair<Double, Double> minMax = info.activityTimes.values().iterator().next(); jsonGenerator.writeNumberField("min-time", minMax.first); jsonGenerator.writeNumberField("max-time", minMax.second); + if (info.activityCards.size() > 0) { + Pair<Long, Long> minMaxCard = info.activityCards.values().iterator().next(); + jsonGenerator.writeNumberField("min-cardinality", minMaxCard.first); + jsonGenerator.writeNumberField("max-cardinality", minMaxCard.second); + } } else { jsonGenerator.writeObjectFieldStart("times"); - for (Map.Entry<String, Pair<Double, Double>> ac : info.activities.entrySet()) { - jsonGenerator.writeObjectFieldStart(ac.getKey()); - jsonGenerator.writeNumberField("min-time", ac.getValue().first); - jsonGenerator.writeNumberField("max-time", ac.getValue().second); + for (String acId : info.activityTimes.keySet()) { + jsonGenerator.writeObjectFieldStart(acId); + jsonGenerator.writeNumberField("min-time", info.activityTimes.get(acId).first); + jsonGenerator.writeNumberField("max-time", info.activityTimes.get(acId).second); + Pair<Long, Long> cards = info.activityCards.get(acId); + if (cards != null) { + jsonGenerator.writeNumberField("min-cardinality", info.activityCards.get(acId).first); + jsonGenerator.writeNumberField("max-cardinality", info.activityCards.get(acId).second); + } jsonGenerator.writeEndObject(); } jsonGenerator.writeEndObject();
