From 85bdf53ad0104dbe3ee98da8014a617d4db22a0e Mon Sep 17 00:00:00 2001 From: zihaowang678 Date: Mon, 28 Jul 2025 11:15:21 -0700 Subject: [PATCH 01/12] add IonElement support --- pom.xml | 15 +++++++ src/com/amazon/ion/benchmark/API.java | 7 ++- .../CborJacksonMeasurableReadTask.java | 19 +++++--- .../CborJacksonMeasurableWriteTask.java | 5 +++ .../ion/benchmark/IonMeasurableReadTask.java | 33 ++++++++++++++ .../ion/benchmark/IonMeasurableWriteTask.java | 44 +++++++++++++++++++ .../JsonJacksonMeasurableReadTask.java | 19 +++++--- .../JsonJacksonMeasurableWriteTask.java | 5 +++ src/com/amazon/ion/benchmark/Main.java | 6 +-- .../ion/benchmark/MeasurableReadTask.java | 20 +++++++++ .../ion/benchmark/MeasurableWriteTask.java | 11 +++++ .../ion/benchmark/OptionsCombinationBase.java | 5 +++ .../ion/benchmark/OptionsMatrixBase.java | 3 ++ 13 files changed, 178 insertions(+), 14 deletions(-) diff --git a/pom.xml b/pom.xml index 567b428..d095648 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,21 @@ ion-java-path-extraction 1.2.0 + + com.amazon.ion + ion-element + 1.3.0 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.6.20 + + + org.jetbrains.kotlinx + kotlinx-collections-immutable-jvm + 0.3.4 + junit diff --git a/src/com/amazon/ion/benchmark/API.java b/src/com/amazon/ion/benchmark/API.java index 45d8d1f..d312fb2 100644 --- a/src/com/amazon/ion/benchmark/API.java +++ b/src/com/amazon/ion/benchmark/API.java @@ -13,5 +13,10 @@ enum API { /** * For Ion: the DOM APIs (IonLoader, IonValue, etc.). For JSON (Jackson): JsonNode via ObjectMapper. */ - DOM + DOM, + + /** + * For Ion: the IonElement APIs from ion-element-kotlin. For JSON (Jackson): Not supported. + */ + ION_ELEMENT_DOM } diff --git a/src/com/amazon/ion/benchmark/CborJacksonMeasurableReadTask.java b/src/com/amazon/ion/benchmark/CborJacksonMeasurableReadTask.java index 5f1d718..b19a47a 100644 --- a/src/com/amazon/ion/benchmark/CborJacksonMeasurableReadTask.java +++ b/src/com/amazon/ion/benchmark/CborJacksonMeasurableReadTask.java @@ -150,10 +150,19 @@ void fullyReadDomFromBuffer(SideEffectConsumer consumer) throws IOException { @Override void fullyReadDomFromFile(SideEffectConsumer consumer) throws IOException { - CBORMapper mapper = JacksonUtilities.newCborObjectMapper(cborFactory, options); - Iterator iterator = mapper.reader().createParser(options.newInputStream(inputFile)).readValuesAs(JsonNode.class); - while (iterator.hasNext()) { - consumer.consume(iterator.next()); - } + sideEffectConsumer = consumer; + CBORMapper objectMapper = new CBORMapper(cborFactory); + JsonNode tree = objectMapper.readTree(options.newInputStream(inputFile)); + consumer.consume(tree); + } + + @Override + public void fullyReadElementFromBuffer(SideEffectConsumer consumer) throws IOException { + throw new UnsupportedOperationException("IonElement API is not supported for CBOR format. Use ion_binary or ion_text format instead."); + } + + @Override + public void fullyReadElementFromFile(SideEffectConsumer consumer) throws IOException { + throw new UnsupportedOperationException("IonElement API is not supported for CBOR format. Use ion_binary or ion_text format instead."); } } diff --git a/src/com/amazon/ion/benchmark/CborJacksonMeasurableWriteTask.java b/src/com/amazon/ion/benchmark/CborJacksonMeasurableWriteTask.java index bddd5b5..c942145 100644 --- a/src/com/amazon/ion/benchmark/CborJacksonMeasurableWriteTask.java +++ b/src/com/amazon/ion/benchmark/CborJacksonMeasurableWriteTask.java @@ -150,4 +150,9 @@ CBORGenerator newWriter(OutputStream outputStream) throws IOException { void closeWriter(CBORGenerator generator) throws IOException { generator.close(); } + + @Override + void generateWriteInstructionsElement(Consumer> instructionsSink) throws IOException { + throw new UnsupportedOperationException("IonElement API is not supported for CBOR format. Use ion_binary or ion_text format instead."); + } } diff --git a/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java b/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java index b954246..bd70137 100644 --- a/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java +++ b/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java @@ -7,6 +7,9 @@ import com.amazon.ion.system.IonReaderBuilder; import com.amazon.ionpathextraction.PathExtractor; import com.amazon.ionpathextraction.PathExtractorBuilder; +import com.amazon.ionelement.api.AnyElement; +import com.amazon.ionelement.api.IonElementLoader; +import com.amazon.ionelement.api.IonElementLoaderOptions; import java.io.IOException; import java.nio.file.Path; @@ -25,6 +28,7 @@ class IonMeasurableReadTask extends MeasurableReadTask { private final byte[] reusableLobBuffer; private IonReaderBuilder readerBuilder; private SideEffectConsumer sideEffectConsumer = null; + private IonElementLoader elementLoader; /** * Returns the next power of two greater than or equal to the given value. @@ -67,6 +71,13 @@ private int pathExtractorCallback(IonReader reader) { } else { reusableLobBuffer = null; } + + // Initialize IonElement loader for ION_ELEMENT_DOM API + if (options.api == API.ION_ELEMENT_DOM) { + elementLoader = new com.amazon.ionelement.impl.IonElementLoaderImpl( + new IonElementLoaderOptions(false) + ); + } } @Override @@ -251,4 +262,26 @@ public void fullyReadDomFromFile(SideEffectConsumer consumer) throws IOException ionSystem.newLoader().load(reader); reader.close(); } + + @Override + public void fullyReadElementFromBuffer(SideEffectConsumer consumer) throws IOException { + sideEffectConsumer = consumer; + IonReader reader = readerBuilder.build(buffer); + Iterable elements = elementLoader.loadAllElements(reader); + for (AnyElement element : elements) { + consumer.consume(element); + } + reader.close(); + } + + @Override + public void fullyReadElementFromFile(SideEffectConsumer consumer) throws IOException { + sideEffectConsumer = consumer; + IonReader reader = readerBuilder.build(options.newInputStream(inputFile)); + Iterable elements = elementLoader.loadAllElements(reader); + for (AnyElement element : elements) { + consumer.consume(element); + } + reader.close(); + } } diff --git a/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java b/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java index d439ef7..b14985e 100644 --- a/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java +++ b/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java @@ -7,13 +7,18 @@ import com.amazon.ion.IonWriter; import com.amazon.ion.SymbolToken; import com.amazon.ion.Timestamp; +import com.amazon.ionelement.api.AnyElement; +import com.amazon.ionelement.api.IonElementLoader; +import com.amazon.ionelement.api.IonElementLoaderOptions; import java.io.IOException; import java.io.OutputStream; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.function.Consumer; import static com.amazon.ion.benchmark.Constants.ION_SYSTEM; @@ -24,6 +29,7 @@ class IonMeasurableWriteTask extends MeasurableWriteTask { private final IonUtilities.IonWriterSupplier writerBuilder; + private IonElementLoader elementLoader; /** * @param inputPath path to the data to re-write. @@ -39,6 +45,13 @@ class IonMeasurableWriteTask extends MeasurableWriteTask { } else { throw new IllegalStateException("IonFormatWriter is compatible only with ION_TEXT and ION_BINARY"); } + + // Initialize IonElement loader for ION_ELEMENT_DOM API + if (options.api == API.ION_ELEMENT_DOM) { + elementLoader = new com.amazon.ionelement.impl.IonElementLoaderImpl( + new IonElementLoaderOptions(false) + ); + } } /** @@ -195,4 +208,35 @@ public void closeWriter(IonWriter writer) throws IOException { writer.close(); } + @Override + void generateWriteInstructionsElement(Consumer> instructionsSink) throws IOException { + Iterable elements; + if (options.limit == Integer.MAX_VALUE) { + try (IonReader reader = IonUtilities.newReaderBuilderForInput(options).build(options.newInputStream(inputFile))) { + elements = elementLoader.loadAllElements(reader); + } + } else { + List limitedElements = new ArrayList<>(); + try (IonReader reader = IonUtilities.newReaderBuilderForInput(options).build(options.newInputStream(inputFile))) { + Iterable allElements = elementLoader.loadAllElements(reader); + int count = 0; + for (AnyElement element : allElements) { + limitedElements.add(element); + if (++count >= options.limit) break; + } + } + elements = limitedElements; + } + + // Convert IonElements to write instructions + int elementCount = 0; + for (AnyElement element : elements) { + instructionsSink.accept(element::writeTo); + elementCount++; + if (options.flushPeriod != null && elementCount % options.flushPeriod == 0) { + instructionsSink.accept(IonWriter::flush); + } + } + instructionsSink.accept(IonWriter::finish); + } } diff --git a/src/com/amazon/ion/benchmark/JsonJacksonMeasurableReadTask.java b/src/com/amazon/ion/benchmark/JsonJacksonMeasurableReadTask.java index 2cc066c..f61bf26 100644 --- a/src/com/amazon/ion/benchmark/JsonJacksonMeasurableReadTask.java +++ b/src/com/amazon/ion/benchmark/JsonJacksonMeasurableReadTask.java @@ -139,10 +139,19 @@ void fullyReadDomFromBuffer(SideEffectConsumer consumer) throws IOException { @Override void fullyReadDomFromFile(SideEffectConsumer consumer) throws IOException { - ObjectMapper mapper = JacksonUtilities.newJsonObjectMapper(jsonFactory, options); - Iterator iterator = mapper.reader().createParser(options.newInputStream(inputFile)).readValuesAs(JsonNode.class); - while (iterator.hasNext()) { - consumer.consume(iterator.next()); - } + sideEffectConsumer = consumer; + ObjectMapper objectMapper = JacksonUtilities.newJsonObjectMapper(jsonFactory, options); + JsonNode tree = objectMapper.readTree(options.newInputStream(inputFile)); + consumer.consume(tree); + } + + @Override + public void fullyReadElementFromBuffer(SideEffectConsumer consumer) throws IOException { + throw new UnsupportedOperationException("IonElement API is not supported for JSON format. Use ion_binary or ion_text format instead."); + } + + @Override + public void fullyReadElementFromFile(SideEffectConsumer consumer) throws IOException { + throw new UnsupportedOperationException("IonElement API is not supported for JSON format. Use ion_binary or ion_text format instead."); } } diff --git a/src/com/amazon/ion/benchmark/JsonJacksonMeasurableWriteTask.java b/src/com/amazon/ion/benchmark/JsonJacksonMeasurableWriteTask.java index 0cd5c9a..47ba546 100644 --- a/src/com/amazon/ion/benchmark/JsonJacksonMeasurableWriteTask.java +++ b/src/com/amazon/ion/benchmark/JsonJacksonMeasurableWriteTask.java @@ -142,4 +142,9 @@ JsonGenerator newWriter(OutputStream outputStream) throws IOException { void closeWriter(JsonGenerator generator) throws IOException { generator.close(); } + + @Override + void generateWriteInstructionsElement(Consumer> instructionsSink) throws IOException { + throw new UnsupportedOperationException("IonElement API is not supported for JSON format. Use ion_binary or ion_text format instead."); + } } diff --git a/src/com/amazon/ion/benchmark/Main.java b/src/com/amazon/ion/benchmark/Main.java index f4fbf7b..bc4821f 100644 --- a/src/com/amazon/ion/benchmark/Main.java +++ b/src/com/amazon/ion/benchmark/Main.java @@ -133,10 +133,10 @@ public class Main { + " -f --format Format to benchmark, from the set (ion_binary | ion_text | json | " + "cbor). May be specified multiple times to compare different formats. [default: ion_binary]\n" - + " -a --api The API to exercise (dom or streaming). For the ion-binary or " + + " -a --api The API to exercise (dom, streaming, or ion_element_dom). For the ion-binary or " + "ion-text formats, 'streaming' causes IonReader/IonWriter to be used while 'dom' causes IonLoader to be " - + "used. For Jackson JSON, 'streaming' causes JsonParser/JsonGenerator to be used while 'dom' causes " - + "ObjectMapper to materialize JsonNode instances. May be specified multiple times to compare both " + + "used, and 'ion_element_dom' causes IonElement from ion-element to be used with dedicated code paths. For Jackson JSON, 'streaming' causes JsonParser/JsonGenerator to be used while 'dom' causes " + + "ObjectMapper to materialize JsonNode instances. 'ion_element_dom' is not supported for JSON or CBOR formats. May be specified multiple times to compare " + "APIs. [default: streaming]\n" + " -I --ion-imports-for-input A file containing a sequence of Ion symbol tables, or the string " diff --git a/src/com/amazon/ion/benchmark/MeasurableReadTask.java b/src/com/amazon/ion/benchmark/MeasurableReadTask.java index d77bf75..59e5003 100644 --- a/src/com/amazon/ion/benchmark/MeasurableReadTask.java +++ b/src/com/amazon/ion/benchmark/MeasurableReadTask.java @@ -84,6 +84,20 @@ abstract class MeasurableReadTask implements MeasurableTask { */ abstract void fullyReadDomFromFile(SideEffectConsumer consumer) throws IOException; + /** + * Initialize the element loader and perform a fully-materialized deep read of the data from a buffer using + * IonElement API. The "loader" is defined as any context that is tied to a single stream. + * @throws IOException if thrown during reading. + */ + abstract void fullyReadElementFromBuffer(SideEffectConsumer consumer) throws IOException; + + /** + * Initialize the element loader and perform a fully-materialized deep read of the data from a file using + * IonElement API. The "loader" is defined as any context that is tied to a single stream. + * @throws IOException if thrown during reading. + */ + abstract void fullyReadElementFromFile(SideEffectConsumer consumer) throws IOException; + @Override public void setUpTrial() throws IOException { inputFile = options.convertFileIfNecessary(originalFile).toFile(); @@ -124,6 +138,12 @@ public final Task getTask() { } else { return this::fullyReadDomFromFile; } + } else if (options.api == API.ION_ELEMENT_DOM) { + if (buffer != null) { + return this::fullyReadElementFromBuffer; + } else { + return this::fullyReadElementFromFile; + } } else { throw new IllegalStateException("Illegal combination of options."); } diff --git a/src/com/amazon/ion/benchmark/MeasurableWriteTask.java b/src/com/amazon/ion/benchmark/MeasurableWriteTask.java index 496c9fa..51f316b 100644 --- a/src/com/amazon/ion/benchmark/MeasurableWriteTask.java +++ b/src/com/amazon/ion/benchmark/MeasurableWriteTask.java @@ -72,6 +72,14 @@ interface WriteInstruction { */ abstract void generateWriteInstructionsStreaming(Consumer> instructionsSink) throws IOException; + /** + * Generate a sequence of WriteInstructions that re-write the input file with the configured options using the + * IonElement API. + * @param instructionsSink the sink for the sequence of generated WriteInstructions. + * @throws IOException if thrown when generating WriteInstructions. + */ + abstract void generateWriteInstructionsElement(Consumer> instructionsSink) throws IOException; + /** * @return a new writer context instance. * @param outputStream the OutputStream to which the new writer will write. @@ -96,6 +104,9 @@ public void setUpTrial() throws IOException { case DOM: generateWriteInstructionsDom(writeInstructions::add); break; + case ION_ELEMENT_DOM: + generateWriteInstructionsElement(writeInstructions::add); + break; } } diff --git a/src/com/amazon/ion/benchmark/OptionsCombinationBase.java b/src/com/amazon/ion/benchmark/OptionsCombinationBase.java index 32be898..6c94d39 100644 --- a/src/com/amazon/ion/benchmark/OptionsCombinationBase.java +++ b/src/com/amazon/ion/benchmark/OptionsCombinationBase.java @@ -92,6 +92,11 @@ static T getOrDefault(IonStruct options, String fieldName, Function ((IonInt) val).intValue(), Integer.MAX_VALUE); jsonUseBigDecimals = getOrDefault(optionsCombinationStruct, JSON_USE_BIG_DECIMALS_NAME, val -> ((IonBool) val).booleanValue(), true); autoFlush = getOrDefault(optionsCombinationStruct, AUTO_FLUSH_ENABLED, val -> ((IonBool) val).booleanValue(), false); + + // Validate that ION_ELEMENT_DOM is only used with Ion formats + if (api == API.ION_ELEMENT_DOM && !format.isIon()) { + throw new IllegalArgumentException("ION_ELEMENT_DOM API can only be used with Ion formats (ion_binary or ion_text), not with " + format.name().toLowerCase()); + } } /** diff --git a/src/com/amazon/ion/benchmark/OptionsMatrixBase.java b/src/com/amazon/ion/benchmark/OptionsMatrixBase.java index b971002..f1e062c 100644 --- a/src/com/amazon/ion/benchmark/OptionsMatrixBase.java +++ b/src/com/amazon/ion/benchmark/OptionsMatrixBase.java @@ -54,6 +54,9 @@ abstract class OptionsMatrixBase { static final Predicate OPTION_ONLY_APPLIES_TO_ION_STREAMING = s -> { return OPTION_ONLY_APPLIES_TO_ION.test(s) && API.STREAMING.name().equals(getStringValue(s, API_NAME)); }; + static final Predicate OPTION_ONLY_APPLIES_TO_ION_ELEMENT_DOM = s -> { + return OPTION_ONLY_APPLIES_TO_ION.test(s) && API.ION_ELEMENT_DOM.name().equals(getStringValue(s, API_NAME)); + }; static final Predicate OPTION_ONLY_APPLIES_TO_JSON = s -> { return Format.JSON.name().equals(getStringValue(s, FORMAT_NAME)); }; From 36354796c4416f2f7f405648decff19a3b2d6389 Mon Sep 17 00:00:00 2001 From: zihaowang678 Date: Mon, 28 Jul 2025 11:32:04 -0700 Subject: [PATCH 02/12] add tests for IonElement --- tst/com/amazon/ion/benchmark/OptionsTest.java | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/tst/com/amazon/ion/benchmark/OptionsTest.java b/tst/com/amazon/ion/benchmark/OptionsTest.java index 2500951..c2e128b 100644 --- a/tst/com/amazon/ion/benchmark/OptionsTest.java +++ b/tst/com/amazon/ion/benchmark/OptionsTest.java @@ -660,6 +660,46 @@ public void writeTextUsingDom() throws Exception { assertTrue(expectedCombinations.isEmpty()); } + @Test + public void writeTextUsingIonElementDom() throws Exception { + List optionsCombinations = parseOptionsCombinations( + "write", + "--format", + "ion_text", + "--api", + "ion_element_dom", + "--io-type", + "buffer", + "--io-type", + "file", + "textStructs.ion" + ); + assertEquals(2, optionsCombinations.size()); + List expectedCombinations = new ArrayList<>(2); + expectedCombinations.add(ExpectedWriteOptionsCombination.defaultOptions() + .api(API.ION_ELEMENT_DOM) + .format(Format.ION_TEXT) + .ioType(IoType.BUFFER) + ); + expectedCombinations.add(ExpectedWriteOptionsCombination.defaultOptions() + .api(API.ION_ELEMENT_DOM) + .format(Format.ION_TEXT) + .ioType(IoType.FILE) + ); + + for (WriteOptionsCombination optionsCombination : optionsCombinations) { + expectedCombinations.removeIf(candidate -> { + return candidate.api == API.ION_ELEMENT_DOM + && candidate.format == Format.ION_TEXT + && candidate.ioType == optionsCombination.ioType; + }); + + assertWriteTaskExecutesCorrectly("binaryStructs.10n", optionsCombination, Format.ION_TEXT, optionsCombination.ioType); + assertWriteTaskExecutesCorrectly("textStructs.ion", optionsCombination, Format.ION_TEXT, optionsCombination.ioType); + } + assertTrue(expectedCombinations.isEmpty()); + } + @Test public void readBothTextAndIonUsingBothDomAndReader() throws Exception { List optionsCombinations = parseOptionsCombinations( @@ -718,6 +758,51 @@ public void readBothTextAndIonUsingBothDomAndReader() throws Exception { assertTrue(expectedCombinations.isEmpty()); } + @Test + public void readBothTextAndIonUsingIonElementDom() throws Exception { + List optionsCombinations = parseOptionsCombinations( + "read", + "--format", + "ion_text", + "--format", + "ion_binary", + "--io-type", + "buffer", + "--api", + "ion_element_dom", + "binaryStructs.10n" + ); + assertEquals(2, optionsCombinations.size()); + List expectedCombinations = new ArrayList<>(2); + expectedCombinations.add(ExpectedReadOptionsCombination.defaultOptions() + .ioType(IoType.BUFFER) + .format(Format.ION_TEXT) + .api(API.ION_ELEMENT_DOM) + ); + expectedCombinations.add(ExpectedReadOptionsCombination.defaultOptions() + .ioType(IoType.BUFFER) + .format(Format.ION_BINARY) + .api(API.ION_ELEMENT_DOM) + ); + for (ReadOptionsCombination optionsCombination : optionsCombinations) { + expectedCombinations.removeIf(candidate -> candidate.format == optionsCombination.format + && candidate.api == optionsCombination.api); + assertReadTaskExecutesCorrectly( + "binaryStructs.10n", + optionsCombination, + optionsCombination.format, + optionsCombination.format != Format.ION_BINARY + ); + assertReadTaskExecutesCorrectly( + "textStructs.ion", + optionsCombination, + optionsCombination.format, + optionsCombination.format != Format.ION_TEXT + ); + } + assertTrue(expectedCombinations.isEmpty()); + } + @Test public void readBinaryWithLimit() throws Exception { List optionsCombinations = parseOptionsCombinations( @@ -782,6 +867,37 @@ public void readBinaryWithLimitFromFileUsingDom() throws Exception { assertTrue(expectedCombinations.isEmpty()); } + @Test + public void readBinaryWithLimitFromFileUsingIonElementDom() throws Exception { + List optionsCombinations = parseOptionsCombinations( + "read", + "--limit", + "1", + "--io-type", + "file", + "--format", + "ion_text", + "--format", + "ion_binary", + "--api", + "ion_element_dom", + "binaryStructs.10n" + ); + assertEquals(2, optionsCombinations.size()); + List expectedCombinations = new ArrayList<>(2); + expectedCombinations.add(ExpectedReadOptionsCombination.defaultOptions().limit(1).api(API.ION_ELEMENT_DOM).format(Format.ION_BINARY)); + expectedCombinations.add(ExpectedReadOptionsCombination.defaultOptions().limit(1).api(API.ION_ELEMENT_DOM).format(Format.ION_TEXT)); + + for (ReadOptionsCombination optionsCombination : optionsCombinations) { + expectedCombinations.removeIf(candidate -> candidate.format == optionsCombination.format); + assertEquals(1, optionsCombination.limit); + + assertReadTaskExecutesCorrectly("binaryStructs.10n", optionsCombination, optionsCombination.format, true); + assertReadTaskExecutesCorrectly("textStructs.ion", optionsCombination, optionsCombination.format, true); + } + assertTrue(expectedCombinations.isEmpty()); + } + @Test public void writeBinaryWithLimitUsingWriterAndDOM() throws Exception { List optionsCombinations = parseOptionsCombinations( @@ -809,6 +925,30 @@ public void writeBinaryWithLimitUsingWriterAndDOM() throws Exception { assertTrue(expectedCombinations.isEmpty()); } + @Test + public void writeBinaryWithLimitUsingIonElementDOM() throws Exception { + List optionsCombinations = parseOptionsCombinations( + "write", + "--limit", + "1", + "--api", + "ion_element_dom", + "binaryStructs.10n" + ); + assertEquals(1, optionsCombinations.size()); + List expectedCombinations = new ArrayList<>(1); + expectedCombinations.add(ExpectedWriteOptionsCombination.defaultOptions().limit(1).api(API.ION_ELEMENT_DOM)); + + for (WriteOptionsCombination optionsCombination : optionsCombinations) { + expectedCombinations.removeIf(candidate -> candidate.api == optionsCombination.api); + assertEquals(1, optionsCombination.limit); + + assertWriteTaskExecutesCorrectly("binaryStructs.10n", optionsCombination, Format.ION_BINARY, IoType.FILE); + assertWriteTaskExecutesCorrectly("textStructs.ion", optionsCombination, Format.ION_BINARY, IoType.FILE); + } + assertTrue(expectedCombinations.isEmpty()); + } + @Test public void profileWithMultipleCombinationsRaisesError() { assertThrows( From 6830675699d0a32e8aa6005fb687d03a60e9121d Mon Sep 17 00:00:00 2001 From: zihaowang678 Date: Mon, 28 Jul 2025 16:19:15 -0700 Subject: [PATCH 03/12] update README.md --- README.md | 17 ++++++++++++++--- pom.xml | 7 +++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1e5614a..77bf2eb 100644 --- a/README.md +++ b/README.md @@ -101,18 +101,29 @@ ion-java-benchmark write --ion-imports-for-benchmark tables.ion \ Benchmark a full-traversal read of data equivalent to exampleWithImports.10n, which declares the shared symbol table imports provided by inputTables.ion, re-encoded (if necessary) using the shared symbol tables provided by benchmarkTables.ion, inputTables.ion, and no shared symbol tables. Produce -results from using both the DOM and IonReader APIs. +results from using the DOM, IonReader and IonElement APIs. ``` ion-java-benchmark read --ion-imports-for-input inputTables.ion \ --ion-imports-for-benchmark benchmarkTables.ion \ --ion-imports-for-benchmark auto \ --ion-imports-for-benchmark none \ - --ion-api dom \ - --ion-api streaming \ + --api dom \ + --api streaming \ + --api ion_element_dom \ exampleWithImports.10n ``` +Benchmark a full-traversal read of `example.10n` using the IonElement API from ion-element-kotlin, +comparing performance against the traditional DOM API. + +``` +ion-java-benchmark read --api dom \ + --api ion_element_dom \ + example.10n +``` + + ## Tips As the JMH output warns: "Do not assume the numbers tell you what you want them to tell." Benchmarking diff --git a/pom.xml b/pom.xml index d095648..5a462c4 100644 --- a/pom.xml +++ b/pom.xml @@ -132,6 +132,13 @@ 8 8 + + + org.openjdk.jmh + jmh-generator-annprocess + 1.23 + + From 4fa0573359aee27141179f5cea0a34e4cfcb6431 Mon Sep 17 00:00:00 2001 From: "Laurie Z. Wang" <96194843+zihaowang678@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:29:13 -0700 Subject: [PATCH 04/12] Update src/com/amazon/ion/benchmark/IonMeasurableReadTask.java Co-authored-by: Matthew Pope <81593196+popematt@users.noreply.github.com> --- src/com/amazon/ion/benchmark/IonMeasurableReadTask.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java b/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java index bd70137..6b84677 100644 --- a/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java +++ b/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java @@ -74,9 +74,7 @@ private int pathExtractorCallback(IonReader reader) { // Initialize IonElement loader for ION_ELEMENT_DOM API if (options.api == API.ION_ELEMENT_DOM) { - elementLoader = new com.amazon.ionelement.impl.IonElementLoaderImpl( - new IonElementLoaderOptions(false) - ); + elementLoader = ElementLoader.createIonElementLoader(); } } From 4ea012d95d2f9c2adb04cb812eea67bf7760ce73 Mon Sep 17 00:00:00 2001 From: "Laurie Z. Wang" <96194843+zihaowang678@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:29:29 -0700 Subject: [PATCH 05/12] Update src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java Co-authored-by: Matthew Pope <81593196+popematt@users.noreply.github.com> --- src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java b/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java index b14985e..d896ca4 100644 --- a/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java +++ b/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java @@ -48,9 +48,7 @@ class IonMeasurableWriteTask extends MeasurableWriteTask { // Initialize IonElement loader for ION_ELEMENT_DOM API if (options.api == API.ION_ELEMENT_DOM) { - elementLoader = new com.amazon.ionelement.impl.IonElementLoaderImpl( - new IonElementLoaderOptions(false) - ); + elementLoader = ElementLoader.createIonElementLoader(); } } From 53b4038c6e93655999e6651a4daeaf122faa1a35 Mon Sep 17 00:00:00 2001 From: "Laurie Z. Wang" <96194843+zihaowang678@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:30:47 -0700 Subject: [PATCH 06/12] Update src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java Co-authored-by: Matthew Pope <81593196+popematt@users.noreply.github.com> --- src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java b/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java index d896ca4..b9eeeac 100644 --- a/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java +++ b/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java @@ -216,11 +216,9 @@ void generateWriteInstructionsElement(Consumer> inst } else { List limitedElements = new ArrayList<>(); try (IonReader reader = IonUtilities.newReaderBuilderForInput(options).build(options.newInputStream(inputFile))) { - Iterable allElements = elementLoader.loadAllElements(reader); int count = 0; - for (AnyElement element : allElements) { - limitedElements.add(element); - if (++count >= options.limit) break; + while (count++ < options.limit && reader.next() != null) { + limitedElements.add(elementLoader.loadCurrentElement(reader)); } } elements = limitedElements; From 51df1e1c5490f4f4aeffc75b404dafc45f86a726 Mon Sep 17 00:00:00 2001 From: "Laurie Z. Wang" <96194843+zihaowang678@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:31:00 -0700 Subject: [PATCH 07/12] Update pom.xml Co-authored-by: Matthew Pope <81593196+popematt@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5a462c4..19747bf 100644 --- a/pom.xml +++ b/pom.xml @@ -100,7 +100,7 @@ com.amazon.ion ion-element - 1.3.0 + [1.3.0, ) org.jetbrains.kotlin From d97e8111ae94440f1505fb87b7d4f38e18663212 Mon Sep 17 00:00:00 2001 From: zihaowang678 Date: Thu, 31 Jul 2025 11:33:24 -0700 Subject: [PATCH 08/12] revert unrelated changes; import ElementLoader --- .../ion/benchmark/CborJacksonMeasurableReadTask.java | 9 +++++---- src/com/amazon/ion/benchmark/IonMeasurableReadTask.java | 2 +- src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java | 2 +- .../ion/benchmark/JsonJacksonMeasurableReadTask.java | 9 +++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/com/amazon/ion/benchmark/CborJacksonMeasurableReadTask.java b/src/com/amazon/ion/benchmark/CborJacksonMeasurableReadTask.java index b19a47a..a391420 100644 --- a/src/com/amazon/ion/benchmark/CborJacksonMeasurableReadTask.java +++ b/src/com/amazon/ion/benchmark/CborJacksonMeasurableReadTask.java @@ -150,10 +150,11 @@ void fullyReadDomFromBuffer(SideEffectConsumer consumer) throws IOException { @Override void fullyReadDomFromFile(SideEffectConsumer consumer) throws IOException { - sideEffectConsumer = consumer; - CBORMapper objectMapper = new CBORMapper(cborFactory); - JsonNode tree = objectMapper.readTree(options.newInputStream(inputFile)); - consumer.consume(tree); + CBORMapper mapper = JacksonUtilities.newCborObjectMapper(cborFactory, options); + Iterator iterator = mapper.reader().createParser(options.newInputStream(inputFile)).readValuesAs(JsonNode.class); + while (iterator.hasNext()) { + consumer.consume(iterator.next()); + } } @Override diff --git a/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java b/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java index 6b84677..e1ec6eb 100644 --- a/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java +++ b/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java @@ -9,7 +9,7 @@ import com.amazon.ionpathextraction.PathExtractorBuilder; import com.amazon.ionelement.api.AnyElement; import com.amazon.ionelement.api.IonElementLoader; -import com.amazon.ionelement.api.IonElementLoaderOptions; +import com.amazon.ionelement.api.ElementLoader; import java.io.IOException; import java.nio.file.Path; diff --git a/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java b/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java index b9eeeac..3a2faf6 100644 --- a/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java +++ b/src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java @@ -9,7 +9,7 @@ import com.amazon.ion.Timestamp; import com.amazon.ionelement.api.AnyElement; import com.amazon.ionelement.api.IonElementLoader; -import com.amazon.ionelement.api.IonElementLoaderOptions; +import com.amazon.ionelement.api.ElementLoader; import java.io.IOException; import java.io.OutputStream; diff --git a/src/com/amazon/ion/benchmark/JsonJacksonMeasurableReadTask.java b/src/com/amazon/ion/benchmark/JsonJacksonMeasurableReadTask.java index f61bf26..cf201f2 100644 --- a/src/com/amazon/ion/benchmark/JsonJacksonMeasurableReadTask.java +++ b/src/com/amazon/ion/benchmark/JsonJacksonMeasurableReadTask.java @@ -139,10 +139,11 @@ void fullyReadDomFromBuffer(SideEffectConsumer consumer) throws IOException { @Override void fullyReadDomFromFile(SideEffectConsumer consumer) throws IOException { - sideEffectConsumer = consumer; - ObjectMapper objectMapper = JacksonUtilities.newJsonObjectMapper(jsonFactory, options); - JsonNode tree = objectMapper.readTree(options.newInputStream(inputFile)); - consumer.consume(tree); + ObjectMapper mapper = JacksonUtilities.newJsonObjectMapper(jsonFactory, options); + Iterator iterator = mapper.reader().createParser(options.newInputStream(inputFile)).readValuesAs(JsonNode.class); + while (iterator.hasNext()) { + consumer.consume(iterator.next()); + } } @Override From 4f501a61d4e2da3877caac5995fcbd15f2fc1a32 Mon Sep 17 00:00:00 2001 From: "Laurie Z. Wang" <96194843+zihaowang678@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:18:49 -0700 Subject: [PATCH 09/12] Update src/com/amazon/ion/benchmark/Main.java Co-authored-by: Tyler Gregg --- src/com/amazon/ion/benchmark/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/amazon/ion/benchmark/Main.java b/src/com/amazon/ion/benchmark/Main.java index bc4821f..781f4f3 100644 --- a/src/com/amazon/ion/benchmark/Main.java +++ b/src/com/amazon/ion/benchmark/Main.java @@ -135,7 +135,7 @@ public class Main { + " -a --api The API to exercise (dom, streaming, or ion_element_dom). For the ion-binary or " + "ion-text formats, 'streaming' causes IonReader/IonWriter to be used while 'dom' causes IonLoader to be " - + "used, and 'ion_element_dom' causes IonElement from ion-element to be used with dedicated code paths. For Jackson JSON, 'streaming' causes JsonParser/JsonGenerator to be used while 'dom' causes " + + "used, and 'ion_element_dom' causes IonElement from ion-element-kotlin to be used with dedicated code paths. For Jackson JSON, 'streaming' causes JsonParser/JsonGenerator to be used while 'dom' causes " + "ObjectMapper to materialize JsonNode instances. 'ion_element_dom' is not supported for JSON or CBOR formats. May be specified multiple times to compare " + "APIs. [default: streaming]\n" From 35cc9f5717f8bdcf2b30fa9ad31c21b399190cab Mon Sep 17 00:00:00 2001 From: "Laurie Z. Wang" <96194843+zihaowang678@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:19:14 -0700 Subject: [PATCH 10/12] Update tst/com/amazon/ion/benchmark/OptionsTest.java Co-authored-by: Tyler Gregg --- tst/com/amazon/ion/benchmark/OptionsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tst/com/amazon/ion/benchmark/OptionsTest.java b/tst/com/amazon/ion/benchmark/OptionsTest.java index c2e128b..0c0c607 100644 --- a/tst/com/amazon/ion/benchmark/OptionsTest.java +++ b/tst/com/amazon/ion/benchmark/OptionsTest.java @@ -868,7 +868,7 @@ public void readBinaryWithLimitFromFileUsingDom() throws Exception { } @Test - public void readBinaryWithLimitFromFileUsingIonElementDom() throws Exception { + public void readBinaryAndTextWithLimitFromFileUsingIonElementDom() throws Exception { List optionsCombinations = parseOptionsCombinations( "read", "--limit", From 0746e71d67f7c9a327177a79bfada3f5414a5110 Mon Sep 17 00:00:00 2001 From: "Laurie Z. Wang" <96194843+zihaowang678@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:19:37 -0700 Subject: [PATCH 11/12] Update tst/com/amazon/ion/benchmark/OptionsTest.java Co-authored-by: Tyler Gregg --- tst/com/amazon/ion/benchmark/OptionsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tst/com/amazon/ion/benchmark/OptionsTest.java b/tst/com/amazon/ion/benchmark/OptionsTest.java index 0c0c607..30df465 100644 --- a/tst/com/amazon/ion/benchmark/OptionsTest.java +++ b/tst/com/amazon/ion/benchmark/OptionsTest.java @@ -759,7 +759,7 @@ public void readBothTextAndIonUsingBothDomAndReader() throws Exception { } @Test - public void readBothTextAndIonUsingIonElementDom() throws Exception { + public void readBothTextAndBinaryUsingIonElementDom() throws Exception { List optionsCombinations = parseOptionsCombinations( "read", "--format", From e40263aa4008af78a0277c50ecda74a807fa2c2b Mon Sep 17 00:00:00 2001 From: zihaowang678 Date: Thu, 31 Jul 2025 12:42:21 -0700 Subject: [PATCH 12/12] Add consumption loops to DOM read methods --- src/com/amazon/ion/benchmark/IonMeasurableReadTask.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java b/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java index e1ec6eb..69ce341 100644 --- a/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java +++ b/src/com/amazon/ion/benchmark/IonMeasurableReadTask.java @@ -249,7 +249,9 @@ public void traverseFromFile(List paths, SideEffectConsumer consumer) th public void fullyReadDomFromBuffer(SideEffectConsumer consumer) throws IOException { sideEffectConsumer = consumer; IonReader reader = readerBuilder.build(buffer); - ionSystem.newLoader().load(reader); + for (com.amazon.ion.IonValue value : ionSystem.newLoader().load(reader)) { + consumer.consume(value); + } reader.close(); } @@ -257,7 +259,9 @@ public void fullyReadDomFromBuffer(SideEffectConsumer consumer) throws IOExcepti public void fullyReadDomFromFile(SideEffectConsumer consumer) throws IOException { sideEffectConsumer = consumer; IonReader reader = readerBuilder.build(options.newInputStream(inputFile)); - ionSystem.newLoader().load(reader); + for (com.amazon.ion.IonValue value : ionSystem.newLoader().load(reader)) { + consumer.consume(value); + } reader.close(); }