This is an automated email from the ASF dual-hosted git repository. xiangfu0 pushed a commit to branch agent/experimental-fory-json-functions in repository https://gitbox.apache.org/repos/asf/pinot.git
commit 62c2db4b670153709edc03eb98bf70fcb53b5880 Author: Xiang Fu <[email protected]> AuthorDate: Wed Aug 12 12:50:01 2026 -0700 Benchmark Fory JSON function counterparts --- .../common/function/FastJsonPathExtractorTest.java | 1 + .../JsonExtractScalarTransformFunction.java | 6 +- .../core/data/function/JsonFunctionsTest.java | 9 + .../perf/BenchmarkJsonExtractScalarQuery.java | 198 +++++++++++++++++---- .../pinot/perf/BenchmarkJsonPathExtraction.java | 141 +++++++++++++-- 5 files changed, 302 insertions(+), 53 deletions(-) diff --git a/pinot-common/src/test/java/org/apache/pinot/common/function/FastJsonPathExtractorTest.java b/pinot-common/src/test/java/org/apache/pinot/common/function/FastJsonPathExtractorTest.java index 3212b705d1b..f4677acb672 100644 --- a/pinot-common/src/test/java/org/apache/pinot/common/function/FastJsonPathExtractorTest.java +++ b/pinot-common/src/test/java/org/apache/pinot/common/function/FastJsonPathExtractorTest.java @@ -397,6 +397,7 @@ public class FastJsonPathExtractorTest { assertEquals(invoke("jsonPathStringFast", json, "$.user.country", "DEFAULT"), "US"); assertEquals(invoke("jsonPathStringFirstMatch", json, "$.user.country", "DEFAULT"), "US"); assertEquals(invoke("jsonPathStringFast", json, "$.missing", "DEFAULT"), "DEFAULT"); + assertEquals(invoke("jsonPathLongFast", json, "$.user.age", -7L), 41L); assertEquals(invoke("jsonPathLongFirstMatch", json, "$.user.age", -7L), 41L); assertEquals(invoke("jsonPathDoubleFast", json, "$.user.score", -7.5d), 9.5d); /// A complex path must still resolve through the function by falling back to Jayway, i.e. produce exactly diff --git a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java index 43df67fa1f5..a16e73990d0 100644 --- a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java +++ b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java @@ -75,9 +75,9 @@ import org.roaringbitmap.RoaringBitmap; /// only for well-formed, duplicate-free JSON. `Fast` scans the full root value and retains Jayway's last-key-wins /// and malformed-document behavior; see [FastJsonPathExtractor] for one documented unaddressed-value edge case. /// `jsonExtractScalarFory` is experimental and must be selected explicitly. It accelerates simple paths over -/// `STRING` input for scalar result types other than `STRING` and `BIG_DECIMAL`. `BYTES` input, complex paths, -/// containers / array result types, precision-sensitive results, deeply nested documents, and Fory failures use -/// Jayway. Its name, supported envelope, and implementation can change while the integration is evaluated. +/// `STRING` input for scalar result types other than `STRING`, `JSON`, and `BIG_DECIMAL`. `BYTES` input, complex +/// paths, containers / array result types, precision-sensitive results, deeply nested documents, and Fory failures +/// use Jayway. Its name, supported envelope, and implementation can change while the integration is evaluated. /// /// **Arguments:** /// - `jsonField` — single-value `STRING` or `BYTES` column / transform expression containing JSON. diff --git a/pinot-core/src/test/java/org/apache/pinot/core/data/function/JsonFunctionsTest.java b/pinot-core/src/test/java/org/apache/pinot/core/data/function/JsonFunctionsTest.java index df97c4b2926..bbaed22658a 100644 --- a/pinot-core/src/test/java/org/apache/pinot/core/data/function/JsonFunctionsTest.java +++ b/pinot-core/src/test/java/org/apache/pinot/core/data/function/JsonFunctionsTest.java @@ -147,6 +147,15 @@ public class JsonFunctionsTest { inputs.add(new Object[]{ "json_path_double_fory(json, '$.ratio', -1.0)", Lists.newArrayList("json"), row12, 1.25 }); + inputs.add(new Object[]{ + "json_path_string_fast(json, '$.text', 'DEFAULT')", Lists.newArrayList("json"), row12, "value" + }); + inputs.add(new Object[]{ + "json_path_long_fast(json, '$.count', -1)", Lists.newArrayList("json"), row12, 10L + }); + inputs.add(new Object[]{ + "json_path_double_fast(json, '$.ratio', -1.0)", Lists.newArrayList("json"), row12, 1.25 + }); return inputs.toArray(new Object[0][]); } diff --git a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonExtractScalarQuery.java b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonExtractScalarQuery.java index 47fb9787b5f..a89f77aedca 100644 --- a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonExtractScalarQuery.java +++ b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonExtractScalarQuery.java @@ -21,7 +21,10 @@ package org.apache.pinot.perf; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; +import org.apache.pinot.common.function.ForyJsonPathExtractor; +import org.apache.pinot.common.function.SimpleJsonPath; import org.apache.pinot.common.request.context.ExpressionContext; import org.apache.pinot.common.request.context.LiteralContext; import org.apache.pinot.core.common.BlockValSet; @@ -53,6 +56,9 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; /// Measures the actual query-side `jsonExtractScalar*` ValueBlock loop. Each invocation processes a 128-row block; /// JMH normalizes throughput and allocation to one row via [OperationsPerInvocation]. Input projection is represented /// by a pre-materialized String array so the comparison isolates JSON extraction, type coercion, and result writing. +/// Result dispatch is selected once during setup, outside the measured per-row loop. The type-specific JSON literals +/// have equal encoded lengths and occupy the same early/late field locations so STRING, LONG, and DOUBLE comparisons +/// do not accidentally measure different document layouts. @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @Fork(1) @@ -63,8 +69,8 @@ public class BenchmarkJsonExtractScalarQuery { private static final int BLOCK_ROWS = 128; private static final TransformResultMetadata STRING_METADATA = new TransformResultMetadata(DataType.STRING, true, false); - private static final String BASE_JSON = "{" - + "\"earlyMetric\":17," + private static final String BASE_JSON_FORMAT = "{" + + "\"earlyValue\":%s," + "\"user\":{\"id\":\"u-19283\",\"country\":\"US\",\"tier\":\"gold\",\"age\":41}," + "\"event\":{\"name\":\"checkout\",\"cart\":[{\"sku\":\"A1\",\"qty\":2,\"price\":19.99}," + "{\"sku\":\"B7\",\"qty\":1,\"price\":129.0},{\"sku\":\"C3\",\"qty\":4,\"price\":3.5}]," @@ -75,7 +81,8 @@ public class BenchmarkJsonExtractScalarQuery { + "\"coordinates\":[-122.4194,37.7749]}," + "\"attributes\":{\"campaign\":\"summer-sale\",\"referrer\":\"search\",\"experiment\":\"checkout-v2\"}," + "\"flags\":[\"returning\",\"subscribed\",\"beta\"]," - + "\"lateMetric\":19} "; + + "\"lateValue\":%s} "; + private static final String LATE_FIELD_MARKER = "\"lateValue\":"; @Param({"early", "late"}) private String _fieldPosition; @@ -83,6 +90,15 @@ public class BenchmarkJsonExtractScalarQuery { @Param({"700", "8192", "65536"}) private int _documentBytes; + /// STRING is an intentional Jayway fallback for precision-safe coercion. Add `-p _resultType=STRING` explicitly to + /// characterize that public-function fallback; default trials cover only actual Fory streaming. + @Param({"LONG", "DOUBLE"}) + private DataType _resultType; + + /// Use `-p _pathResult=missing` to measure explicit defaults without multiplying the default suite. + @Param({"hit"}) + private String _pathResult; + private ValueBlock _valueBlock; private JsonExtractScalarTransformFunction _jayway; private JsonExtractScalarTransformFunction _fast; @@ -91,67 +107,181 @@ public class BenchmarkJsonExtractScalarQuery { @Setup public void setUp() { - String json = buildJson(_documentBytes); + if (_resultType != DataType.STRING && !ForyJsonPathExtractor.isAvailable()) { + throw new IllegalStateException("Fory JSON is unavailable; refusing to publish fallback results as Fory"); + } + String json = buildJson(_documentBytes, _resultType); String[] jsonRows = new String[BLOCK_ROWS]; Arrays.fill(jsonRows, json); TransformFunction input = new StringArrayTransformFunction(jsonRows); - String path = "early".equals(_fieldPosition) ? "$.earlyMetric" : "$.lateMetric"; - List<TransformFunction> arguments = List.of(input, literal(path), literal("LONG")); + boolean early = "early".equals(_fieldPosition); + boolean hit = "hit".equals(_pathResult); + String path = "$." + (early ? "early" : "late") + (hit ? "Value" : "Ghost"); + Object defaultValue = defaultValue(_resultType); + List<TransformFunction> arguments = List.of(input, literal(DataType.STRING, path), + literal(DataType.STRING, _resultType.name()), literal(_resultType, defaultValue)); _valueBlock = new FixedValueBlock(BLOCK_ROWS); - _jayway = new JsonExtractScalarTransformFunction(); - _fast = new JsonExtractScalarTransformFunction.Fast(); - _firstMatch = new JsonExtractScalarTransformFunction.FirstMatch(); - _fory = new JsonExtractScalarTransformFunction.Fory(); - for (JsonExtractScalarTransformFunction function : List.of(_jayway, _fast, _firstMatch, _fory)) { - function.init(arguments, Map.<String, ColumnContext>of(), false); - long expected = "early".equals(_fieldPosition) ? 17L : 19L; - long[] values = function.transformToLongValuesSV(_valueBlock); - if (values.length < BLOCK_ROWS || values[0] != expected || values[BLOCK_ROWS - 1] != expected) { - throw new IllegalStateException(function.getName() + " produced an unexpected query result"); + if (_resultType != DataType.STRING) { + Object directForyResult = ForyJsonPathExtractor.extract(json, SimpleJsonPath.compile(path)); + Object expectedForyResult = hit ? hitValue(_resultType, early) : null; + if (!Objects.equals(directForyResult, expectedForyResult)) { + throw new IllegalStateException("Direct Fory extraction produced an unexpected " + _resultType + " result"); } } + _jayway = initialize(new JsonExtractScalarTransformFunction(), arguments); + _fast = initialize(new JsonExtractScalarTransformFunction.Fast(), arguments); + _firstMatch = initialize(new JsonExtractScalarTransformFunction.FirstMatch(), arguments); + _fory = initialize(new JsonExtractScalarTransformFunction.Fory(), arguments); + + Object expectedRows = expectedRows(_resultType, hit ? hitValue(_resultType, early) : defaultValue); + Object jaywayRows = apply(_jayway); + assertResultsEqual("jsonExtractScalar", expectedRows, jaywayRows); + assertResultsEqual("jsonExtractScalarFast", jaywayRows, apply(_fast)); + assertResultsEqual("jsonExtractScalarFirstMatch", jaywayRows, apply(_firstMatch)); + assertResultsEqual("jsonExtractScalarFory", jaywayRows, apply(_fory)); } - private static LiteralTransformFunction literal(String value) { - return new LiteralTransformFunction(new LiteralContext(DataType.STRING, value)); + private JsonExtractScalarTransformFunction initialize(JsonExtractScalarTransformFunction function, + List<TransformFunction> arguments) { + function.init(arguments, Map.<String, ColumnContext>of(), false); + return function; } - private static String buildJson(int targetBytes) { - if (BASE_JSON.length() >= targetBytes) { - return BASE_JSON; + private Object apply(JsonExtractScalarTransformFunction function) { + switch (_resultType) { + case STRING: + return function.transformToStringValuesSV(_valueBlock); + case LONG: + return function.transformToLongValuesSV(_valueBlock); + case DOUBLE: + return function.transformToDoubleValuesSV(_valueBlock); + default: + throw new IllegalStateException("Unsupported benchmark result type: " + _resultType); } - String marker = "\"lateMetric\":"; - int markerOffset = BASE_JSON.indexOf(marker); + } + + private static LiteralTransformFunction literal(DataType dataType, Object value) { + return new LiteralTransformFunction(new LiteralContext(dataType, value)); + } + + private static String buildJson(int targetBytes, DataType resultType) { + String baseJson = String.format(BASE_JSON_FORMAT, jsonLiteral(resultType, true), jsonLiteral(resultType, false)); + if (baseJson.length() >= targetBytes) { + return baseJson; + } + int markerOffset = baseJson.indexOf(LATE_FIELD_MARKER); String paddingPrefix = "\"padding\":\""; String paddingSuffix = "\","; - int paddingLength = targetBytes - BASE_JSON.length() - paddingPrefix.length() - paddingSuffix.length(); - return BASE_JSON.substring(0, markerOffset) + paddingPrefix + "x".repeat(paddingLength) + paddingSuffix - + BASE_JSON.substring(markerOffset); + int paddingLength = targetBytes - baseJson.length() - paddingPrefix.length() - paddingSuffix.length(); + return baseJson.substring(0, markerOffset) + paddingPrefix + "x".repeat(paddingLength) + paddingSuffix + + baseJson.substring(markerOffset); + } + + private static String jsonLiteral(DataType resultType, boolean early) { + switch (resultType) { + case STRING: + return early ? "\"S\"" : "\"T\""; + case LONG: + return early ? "170" : "190"; + case DOUBLE: + return early ? "1.7" : "1.9"; + default: + throw new IllegalStateException("Unsupported benchmark result type: " + resultType); + } + } + + private static Object hitValue(DataType resultType, boolean early) { + switch (resultType) { + case STRING: + return early ? "S" : "T"; + case LONG: + return early ? 170L : 190L; + case DOUBLE: + return early ? 1.7d : 1.9d; + default: + throw new IllegalStateException("Unsupported benchmark result type: " + resultType); + } + } + + private static Object defaultValue(DataType resultType) { + switch (resultType) { + case STRING: + return "DEFAULT"; + case LONG: + return -1L; + case DOUBLE: + return -1d; + default: + throw new IllegalStateException("Unsupported benchmark result type: " + resultType); + } + } + + private static Object expectedRows(DataType resultType, Object expectedValue) { + switch (resultType) { + case STRING: + String[] stringValues = new String[BLOCK_ROWS]; + Arrays.fill(stringValues, (String) expectedValue); + return stringValues; + case LONG: + long[] longValues = new long[BLOCK_ROWS]; + Arrays.fill(longValues, (Long) expectedValue); + return longValues; + case DOUBLE: + double[] doubleValues = new double[BLOCK_ROWS]; + Arrays.fill(doubleValues, (Double) expectedValue); + return doubleValues; + default: + throw new IllegalStateException("Unsupported benchmark result type: " + resultType); + } + } + + private void assertResultsEqual(String functionName, Object expected, Object actual) { + boolean equal; + switch (_resultType) { + case STRING: + equal = Arrays.equals((String[]) expected, (String[]) actual); + break; + case LONG: + equal = Arrays.equals((long[]) expected, (long[]) actual); + break; + case DOUBLE: + equal = Arrays.equals((double[]) expected, (double[]) actual); + break; + default: + throw new IllegalStateException("Unsupported benchmark result type: " + _resultType); + } + if (!equal) { + throw new IllegalStateException(functionName + " produced an unexpected " + _resultType + " result for " + + _fieldPosition + '/' + _pathResult); + } } @Benchmark @OperationsPerInvocation(BLOCK_ROWS) - public long[] queryJayway() { - return _jayway.transformToLongValuesSV(_valueBlock); + public Object queryJayway() { + return apply(_jayway); } @Benchmark @OperationsPerInvocation(BLOCK_ROWS) - public long[] queryFast() { - return _fast.transformToLongValuesSV(_valueBlock); + public Object queryFast() { + return apply(_fast); } + /// Auxiliary comparator: unlike the primary parity-preserving variants, FirstMatch intentionally has weaker + /// duplicate-key and malformed-tail semantics. @Benchmark @OperationsPerInvocation(BLOCK_ROWS) - public long[] queryFirstMatch() { - return _firstMatch.transformToLongValuesSV(_valueBlock); + public Object queryFirstMatch() { + return apply(_firstMatch); } + /// For STRING, this measures the production Fory function's intentional Jayway fallback rather than Fory parsing. @Benchmark @OperationsPerInvocation(BLOCK_ROWS) - public long[] queryFory() { - return _fory.transformToLongValuesSV(_valueBlock); + public Object queryFory() { + return apply(_fory); } public static void main(String[] arguments) diff --git a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonPathExtraction.java b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonPathExtraction.java index 185e92635d1..2e1b8fa6d84 100644 --- a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonPathExtraction.java +++ b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonPathExtraction.java @@ -29,6 +29,7 @@ import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; import java.util.concurrent.TimeUnit; import org.apache.fory.json.ForyJson; import org.apache.pinot.common.function.FastJsonPathExtractor; +import org.apache.pinot.common.function.ForyJsonPathExtractor; import org.apache.pinot.common.function.SimpleJsonPath; import org.apache.pinot.common.function.scalar.JsonFunctions; import org.openjdk.jmh.annotations.Benchmark; @@ -55,9 +56,9 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; /// nested event payload, because that is what decides whether early exit can pay off. `documentBytes` pads the /// payload immediately before the late field so parser scaling is visible without changing the addressed values. /// -/// The single-column benchmarks are also the per-row cost of `jsonExtractScalar`: that transform function -/// does exactly `parseContext.parse(row).read(jsonPath)` per row, so measuring the extraction in isolation -/// measures it without the surrounding `ValueBlock` scaffolding. +/// The scalar-function methods measure the production `jsonPath*` ingestion wrappers. The lower-level single-column +/// methods isolate parser/traversal strategies; [BenchmarkJsonExtractScalarQuery] is the authoritative query-transform +/// measurement. @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @Fork(1) @@ -67,6 +68,9 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; public class BenchmarkJsonPathExtraction { private static final Predicate[] NO_PREDICATES = new Predicate[0]; private static final ForyJson FORY_JSON = ForyJson.builder().build(); + private static final String STRING_DEFAULT = "DEFAULT"; + private static final long LONG_DEFAULT = -1L; + private static final double DOUBLE_DEFAULT = -1.25d; /// Exactly the context `JsonExtractScalarTransformFunction` and `JsonFunctions` use. private static final ParseContext PARSE_CONTEXT = JsonPath.using( @@ -75,7 +79,7 @@ public class BenchmarkJsonPathExtraction { private static final String BASE_JSON = "{" + "\"ts\":1719878400123," - + "\"user\":{\"id\":\"u-19283\",\"country\":\"US\",\"tier\":\"gold\",\"age\":41}," + + "\"user\":{\"id\":\"u-19283\",\"country\":\"US\",\"score\":17.25,\"age\":41}," + "\"event\":{\"name\":\"checkout\",\"cart\":[{\"sku\":\"A1\",\"qty\":2,\"price\":19.99}," + "{\"sku\":\"B7\",\"qty\":1,\"price\":149.5},{\"sku\":\"C3\",\"qty\":5,\"price\":3.25}]," + "\"total\":352.73,\"currency\":\"USD\"}," @@ -85,7 +89,7 @@ public class BenchmarkJsonPathExtraction { + "\"tags\":[\"mobile\",\"ios\",\"returning\",\"promo-eligible\",\"newsletter\"]," + "\"session\":{\"id\":\"s-aaaabbbbccccdddd\",\"start\":1719878300000,\"pages\":14,\"referrer\":" + "\"https://example.com/landing?utm_source=x&utm_medium=y\"}," - + "\"trailer\":{\"country\":\"DE\",\"note\":\"last field in the document\"}" + + "\"trailer\":{\"country\":\"DE\",\"double\":19.25,\"long\":19}" + "}"; /// Four derived columns, spread through the document, as an ingestion `transformConfigs` would pull. @@ -98,9 +102,17 @@ public class BenchmarkJsonPathExtraction { @Param({"700", "8192", "65536"}) private int _documentBytes; + /// Use `-p _valueCase=missing` to measure the explicit-default path without doubling the default benchmark suite. + @Param({"hit"}) + private String _valueCase; + private String _json; private String _path; + private String _longPath; + private String _doublePath; private SimpleJsonPath _simplePath; + private SimpleJsonPath _simpleLongPath; + private SimpleJsonPath _simpleDoublePath; private SimpleJsonPath[] _simpleFourPaths; private Object[] _fourResults; @@ -113,19 +125,44 @@ public class BenchmarkJsonPathExtraction { @Setup(Level.Trial) public void setUp() { + if (!ForyJsonPathExtractor.isAvailable()) { + throw new IllegalStateException("Fory JSON is unavailable; refusing to publish fallback results as Fory"); + } _json = buildJson(_documentBytes); - _path = "early".equals(_fieldPosition) ? "$.user.country" : "$.trailer.country"; + boolean missing; + if ("hit".equals(_valueCase)) { + missing = false; + } else if ("missing".equals(_valueCase)) { + missing = true; + } else { + throw new IllegalArgumentException("Unsupported value case: " + _valueCase); + } + if (missing) { + _path = "$.missing"; + _longPath = _path; + _doublePath = _path; + } else if ("early".equals(_fieldPosition)) { + _path = "$.user.country"; + _longPath = "$.user.age"; + _doublePath = "$.user.score"; + } else { + _path = "$.trailer.country"; + _longPath = "$.trailer.long"; + _doublePath = "$.trailer.double"; + } _simplePath = SimpleJsonPath.compile(_path); + _simpleLongPath = SimpleJsonPath.compile(_longPath); + _simpleDoublePath = SimpleJsonPath.compile(_doublePath); _simpleFourPaths = new SimpleJsonPath[FOUR_PATHS.length]; for (int i = 0; i < FOUR_PATHS.length; i++) { _simpleFourPaths[i] = SimpleJsonPath.compile(FOUR_PATHS[i]); } _fourResults = new Object[FOUR_PATHS.length]; - String expected = "early".equals(_fieldPosition) ? "US" : "DE"; - if (!expected.equals(JsonFunctions.jsonPathStringFory(_json, _path, ""))) { - throw new IllegalStateException("Fory result does not match the expected value for " + _path); - } + String expectedString = missing ? STRING_DEFAULT : "early".equals(_fieldPosition) ? "US" : "DE"; + long expectedLong = missing ? LONG_DEFAULT : "early".equals(_fieldPosition) ? 41L : 19L; + double expectedDouble = missing ? DOUBLE_DEFAULT : "early".equals(_fieldPosition) ? 17.25d : 19.25d; + verifyScalarFunctions(expectedString, expectedLong, expectedDouble); for (String path : FOUR_PATHS) { String jayway = JsonFunctions.jsonPathString(_json, path, ""); String fory = JsonFunctions.jsonPathStringFory(_json, path, ""); @@ -139,6 +176,42 @@ public class BenchmarkJsonPathExtraction { } } + private void verifyScalarFunctions(String expectedString, long expectedLong, double expectedDouble) { + Object extractedString = ForyJsonPathExtractor.extract(_json, _simplePath); + Object extractedLong = ForyJsonPathExtractor.extract(_json, _simpleLongPath); + Object extractedDouble = ForyJsonPathExtractor.extract(_json, _simpleDoublePath); + if ("missing".equals(_valueCase)) { + if (extractedString != null || extractedLong != null || extractedDouble != null) { + throw new IllegalStateException("Fory returned a value for a missing benchmark path"); + } + } else if (!expectedString.equals(extractedString) || ((Number) extractedLong).longValue() != expectedLong + || Double.compare(((Number) extractedDouble).doubleValue(), expectedDouble) != 0) { + throw new IllegalStateException("Direct Fory extraction produced an unexpected benchmark result"); + } + + String jaywayString = JsonFunctions.jsonPathString(_json, _path, STRING_DEFAULT); + String fastString = JsonFunctions.jsonPathStringFast(_json, _path, STRING_DEFAULT); + String foryString = JsonFunctions.jsonPathStringFory(_json, _path, STRING_DEFAULT); + if (!expectedString.equals(jaywayString) || !jaywayString.equals(fastString) || !jaywayString.equals(foryString)) { + throw new IllegalStateException("JSON string extractors disagree for " + _path); + } + + long jaywayLong = JsonFunctions.jsonPathLong(_json, _longPath, LONG_DEFAULT); + long fastLong = JsonFunctions.jsonPathLongFast(_json, _longPath, LONG_DEFAULT); + long foryLong = JsonFunctions.jsonPathLongFory(_json, _longPath, LONG_DEFAULT); + if (jaywayLong != expectedLong || fastLong != jaywayLong || foryLong != jaywayLong) { + throw new IllegalStateException("JSON long extractors disagree for " + _longPath); + } + + double jaywayDouble = JsonFunctions.jsonPathDouble(_json, _doublePath, DOUBLE_DEFAULT); + double fastDouble = JsonFunctions.jsonPathDoubleFast(_json, _doublePath, DOUBLE_DEFAULT); + double foryDouble = JsonFunctions.jsonPathDoubleFory(_json, _doublePath, DOUBLE_DEFAULT); + if (Double.compare(jaywayDouble, expectedDouble) != 0 || Double.compare(fastDouble, jaywayDouble) != 0 + || Double.compare(foryDouble, jaywayDouble) != 0) { + throw new IllegalStateException("JSON double extractors disagree for " + _doublePath); + } + } + private static String buildJson(int targetBytes) { if (BASE_JSON.length() >= targetBytes) { return BASE_JSON; @@ -160,7 +233,7 @@ public class BenchmarkJsonPathExtraction { return PARSE_CONTEXT.parse(_json).read(_path, NO_PREDICATES); } - /// Fory parse plus the same Jayway tree traversal used by the production wrapper. + /// Fory dynamic-tree parsing plus Jayway traversal, retained as a non-production comparison baseline. @Benchmark public Object foryOneColumn() { Object root = FORY_JSON.fromJson(_json, Object.class); @@ -180,25 +253,61 @@ public class BenchmarkJsonPathExtraction { /// The existing Jayway scalar function (applicability check + String coercion). @Benchmark public String jsonPathStringJayway() { - return JsonFunctions.jsonPathString(_json, _path, ""); + return JsonFunctions.jsonPathString(_json, _path, STRING_DEFAULT); } /// The production Fory scalar function (applicability check + String coercion). @Benchmark public String jsonPathStringFory() { - return JsonFunctions.jsonPathStringFory(_json, _path, ""); + return JsonFunctions.jsonPathStringFory(_json, _path, STRING_DEFAULT); } - /// The opt-in fast scalar function, full scan (exact parity). + /// The opt-in fast scalar function, full scan with fallback for unsupported or failed extraction. @Benchmark public String jsonPathStringFast() { - return JsonFunctions.jsonPathStringFast(_json, _path, ""); + return JsonFunctions.jsonPathStringFast(_json, _path, STRING_DEFAULT); } /// The opt-in fast scalar function, early exit / first match. @Benchmark public String jsonPathStringFirstMatch() { - return JsonFunctions.jsonPathStringFirstMatch(_json, _path, ""); + return JsonFunctions.jsonPathStringFirstMatch(_json, _path, STRING_DEFAULT); + } + + /// The existing Jayway scalar function (applicability check + long coercion). + @Benchmark + public long jsonPathLongJayway() { + return JsonFunctions.jsonPathLong(_json, _longPath, LONG_DEFAULT); + } + + /// The opt-in fast scalar function, full scan with fallback for unsupported or failed extraction. + @Benchmark + public long jsonPathLongFast() { + return JsonFunctions.jsonPathLongFast(_json, _longPath, LONG_DEFAULT); + } + + /// The production Fory scalar function (applicability check + long coercion). + @Benchmark + public long jsonPathLongFory() { + return JsonFunctions.jsonPathLongFory(_json, _longPath, LONG_DEFAULT); + } + + /// The existing Jayway scalar function (applicability check + double coercion). + @Benchmark + public double jsonPathDoubleJayway() { + return JsonFunctions.jsonPathDouble(_json, _doublePath, DOUBLE_DEFAULT); + } + + /// The opt-in fast scalar function, full scan with fallback for unsupported or failed extraction. + @Benchmark + public double jsonPathDoubleFast() { + return JsonFunctions.jsonPathDoubleFast(_json, _doublePath, DOUBLE_DEFAULT); + } + + /// The production Fory scalar function (applicability check + double coercion). + @Benchmark + public double jsonPathDoubleFory() { + return JsonFunctions.jsonPathDoubleFory(_json, _doublePath, DOUBLE_DEFAULT); } @Benchmark --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
