Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Comment on lines +120 to +124
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious—when you run this, what sort of results to you get?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It runs two APIs in sequence, then gives two stacked statistical summaries at the end, one for each API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I should have been more clear. Approximately how much of a performance difference are you getting between the two APIs when you run this command?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approximately a 30-ish percentage improvement in primary_score. While this command is a rough comparison between two APIs, the result aligns well with our more extensive experiments run in SampleTime mode across three ion datasets, where we saw a ~37.09% improvement in primary_score.



## Tips

As the JMH output warns: "Do not assume the numbers tell you what you want them to tell." Benchmarking
Expand Down
22 changes: 22 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@
<artifactId>ion-java-path-extraction</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>com.amazon.ion</groupId>
<artifactId>ion-element</artifactId>
<version>[1.3.0, )</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>1.6.20</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-collections-immutable-jvm</artifactId>
<version>0.3.4</version>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>junit</groupId>
Expand All @@ -117,6 +132,13 @@
<configuration>
<source>8</source>
<target>8</target>
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
Expand Down
7 changes: 6 additions & 1 deletion src/com/amazon/ion/benchmark/API.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions src/com/amazon/ion/benchmark/CborJacksonMeasurableReadTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,14 @@ void fullyReadDomFromFile(SideEffectConsumer consumer) throws IOException {
consumer.consume(iterator.next());
}
}

@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.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,9 @@ CBORGenerator newWriter(OutputStream outputStream) throws IOException {
void closeWriter(CBORGenerator generator) throws IOException {
generator.close();
}

@Override
void generateWriteInstructionsElement(Consumer<WriteInstruction<CBORGenerator>> instructionsSink) throws IOException {
throw new UnsupportedOperationException("IonElement API is not supported for CBOR format. Use ion_binary or ion_text format instead.");
}
}
39 changes: 37 additions & 2 deletions src/com/amazon/ion/benchmark/IonMeasurableReadTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.ElementLoader;

import java.io.IOException;
import java.nio.file.Path;
Expand All @@ -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.
Expand Down Expand Up @@ -67,6 +71,11 @@ private int pathExtractorCallback(IonReader reader) {
} else {
reusableLobBuffer = null;
}

// Initialize IonElement loader for ION_ELEMENT_DOM API
if (options.api == API.ION_ELEMENT_DOM) {
elementLoader = ElementLoader.createIonElementLoader();
}
}

@Override
Expand Down Expand Up @@ -240,15 +249,41 @@ public void traverseFromFile(List<String> 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();
}

@Override
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();
}

@Override
public void fullyReadElementFromBuffer(SideEffectConsumer consumer) throws IOException {
sideEffectConsumer = consumer;
IonReader reader = readerBuilder.build(buffer);
Iterable<AnyElement> 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<AnyElement> elements = elementLoader.loadAllElements(reader);
for (AnyElement element : elements) {
consumer.consume(element);
}
reader.close();
}
}
40 changes: 40 additions & 0 deletions src/com/amazon/ion/benchmark/IonMeasurableWriteTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.ElementLoader;

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;
Expand All @@ -24,6 +29,7 @@
class IonMeasurableWriteTask extends MeasurableWriteTask<IonWriter> {

private final IonUtilities.IonWriterSupplier writerBuilder;
private IonElementLoader elementLoader;

/**
* @param inputPath path to the data to re-write.
Expand All @@ -39,6 +45,11 @@ class IonMeasurableWriteTask extends MeasurableWriteTask<IonWriter> {
} 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 = ElementLoader.createIonElementLoader();
}
}

/**
Expand Down Expand Up @@ -195,4 +206,33 @@ public void closeWriter(IonWriter writer) throws IOException {
writer.close();
}

@Override
void generateWriteInstructionsElement(Consumer<WriteInstruction<IonWriter>> instructionsSink) throws IOException {
Iterable<AnyElement> elements;
if (options.limit == Integer.MAX_VALUE) {
try (IonReader reader = IonUtilities.newReaderBuilderForInput(options).build(options.newInputStream(inputFile))) {
elements = elementLoader.loadAllElements(reader);
}
} else {
List<AnyElement> limitedElements = new ArrayList<>();
try (IonReader reader = IonUtilities.newReaderBuilderForInput(options).build(options.newInputStream(inputFile))) {
int count = 0;
while (count++ < options.limit && reader.next() != null) {
limitedElements.add(elementLoader.loadCurrentElement(reader));
}
}
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);
}
}
10 changes: 10 additions & 0 deletions src/com/amazon/ion/benchmark/JsonJacksonMeasurableReadTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,14 @@ void fullyReadDomFromFile(SideEffectConsumer consumer) throws IOException {
consumer.consume(iterator.next());
}
}

@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.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,9 @@ JsonGenerator newWriter(OutputStream outputStream) throws IOException {
void closeWriter(JsonGenerator generator) throws IOException {
generator.close();
}

@Override
void generateWriteInstructionsElement(Consumer<WriteInstruction<JsonGenerator>> instructionsSink) throws IOException {
throw new UnsupportedOperationException("IonElement API is not supported for JSON format. Use ion_binary or ion_text format instead.");
}
}
6 changes: 3 additions & 3 deletions src/com/amazon/ion/benchmark/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,10 @@ public class Main {
+ " -f --format <type> 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 <api> The API to exercise (dom or streaming). For the ion-binary or "
+ " -a --api <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-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"

+ " -I --ion-imports-for-input <file> A file containing a sequence of Ion symbol tables, or the string "
Expand Down
20 changes: 20 additions & 0 deletions src/com/amazon/ion/benchmark/MeasurableReadTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker—it would be nice if we could add new APIs to the benchmark CLI without having to add more methods for each one. (Especially since they are unlikely to be applicable for all data formats.) Have you thought about any ways this could be factored to make it more easily extensible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, the current approach is a straightforward one but doesn't scale well. A capability-based system where each format declares which APIs it supports might be better. I'd be happy to refactor this in a follow-up if it's worth doing.


/**
* 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();
Expand Down Expand Up @@ -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.");
}
Expand Down
11 changes: 11 additions & 0 deletions src/com/amazon/ion/benchmark/MeasurableWriteTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ interface WriteInstruction<T> {
*/
abstract void generateWriteInstructionsStreaming(Consumer<WriteInstruction<T>> 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<WriteInstruction<T>> instructionsSink) throws IOException;

/**
* @return a new writer context instance.
* @param outputStream the OutputStream to which the new writer will write.
Expand All @@ -96,6 +104,9 @@ public void setUpTrial() throws IOException {
case DOM:
generateWriteInstructionsDom(writeInstructions::add);
break;
case ION_ELEMENT_DOM:
generateWriteInstructionsElement(writeInstructions::add);
break;
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/com/amazon/ion/benchmark/OptionsCombinationBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ static <T> T getOrDefault(IonStruct options, String fieldName, Function<IonValue
limit = getOrDefault(optionsCombinationStruct, LIMIT_NAME, val -> ((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());
}
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/com/amazon/ion/benchmark/OptionsMatrixBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ abstract class OptionsMatrixBase {
static final Predicate<IonStruct> 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<IonStruct> 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<IonStruct> OPTION_ONLY_APPLIES_TO_JSON = s -> {
return Format.JSON.name().equals(getStringValue(s, FORMAT_NAME));
};
Expand Down
Loading