diff --git a/.agents/languages/java.md b/.agents/languages/java.md index 856db7dc00..3c1c35d639 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -92,6 +92,13 @@ Load this file when changing anything under `java/` or when Java drives a cross- by generated serializers, do extra self-review: inspect the generated output impact, preserve unsafe/codegen optimizations unless intentionally changing them, and run validation appropriate to the regression risk. +- For HotSpot generated-code boundaries, use cold type-subtree cost to select only generated child + or member graphs that need isolation, then route their hot receiver implementations through one + shared ordinary-class trampoline `invokeinterface` bytecode. The polymorphic call site prevents C2 + from recursively inlining several large field graphs into one parent, while each receiver remains + an independent compilation unit whose internal scalar operations can still inline. Do not apply + the trampoline blanketly to handwritten codecs, root facades, or small generated subtrees, and do + not replace it with per-generated-class call sites without `PrintInlining` proof. - Android and JVM serializers must use a unified wire protocol: each side must be able to deserialize data written by the other side. If implementation paths diverge, the writer must emit enough metadata for either reader to identify and parse that path correctly; add both diff --git a/.agents/skills/fory-performance-optimization/SKILL.md b/.agents/skills/fory-performance-optimization/SKILL.md index a66955da8f..1228a48716 100644 --- a/.agents/skills/fory-performance-optimization/SKILL.md +++ b/.agents/skills/fory-performance-optimization/SKILL.md @@ -20,6 +20,11 @@ Deliver measurable performance improvements in Apache Fory without protocol drif - Keep only measured wins or explicitly requested architecture cleanups. - Revert speculative changes that do not pay off. - Align with reference runtimes (usually C++ first, then Rust/Java) when behavior and ownership models differ. +- When containing HotSpot-generated code, select boundaries from cold type-subtree cost and route + only the large generated receiver implementations through one shared trampoline `invokeinterface` + bytecode. Do not blanket handwritten codecs, root facades, or small generated subtrees. Verify + with `PrintInlining` that C2 retains this outer polymorphic boundary while each independently + compiled receiver still inlines its internal scalar subtree. ## Enforce Hard Constraints diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index 4f527df8eb..56e96a4e23 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -833,6 +833,18 @@ private ObjectCollectionCodec(CollectionFactory factory, JsonTypeInfo elementTyp this.elementTypeInfo = elementTypeInfo; } + /** + * Returns the resolved element binding for generated parent codecs that own a schema-local + * collection loop. + * + *

The binding remains live because generated element capabilities may replace the initial + * object codec after this collection codec is constructed. + */ + @Internal + public JsonTypeInfo elementTypeInfo() { + return elementTypeInfo; + } + @Override public void writeString(StringJsonWriter writer, Collection value) { if (value == null) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java new file mode 100644 index 0000000000..64e3e05bbe --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java @@ -0,0 +1,94 @@ +/* + * 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. + */ + +package org.apache.fory.json.codec; + +import org.apache.fory.annotation.Internal; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; + +/** + * Shared interface invocation sites emitted by generated JSON codecs. + * + *

Generated codecs call these ordinary-class methods instead of emitting an interface-call + * bytecode at each generated caller. Multiple hot generated receiver implementations therefore + * share one call-site profile. On HotSpot, that polymorphic profile keeps C2 from recursively + * inlining several large generated field graphs into one parent compilation. Each receiver is still + * compiled as an independent unit, where its scalar writer calls remain eligible for inlining. + * + *

The code generator selects these boundaries from resolved type-subtree cost. These interfaces + * are a codegen implementation detail: handwritten codecs, containers, readers, and writers must + * keep their owned calls direct so unrelated workloads do not share this type profile. Generated + * scalar or small-subtree calls also remain direct. + * + *

Keep each method as a direct interface invocation. Specializing a method for one generated + * codec or moving the invocation into generated source makes its call site monomorphic and can + * restore the recursive inlining this boundary is intended to contain. + */ +@Internal +public final class JsonTrampolineInvoke { + /** A generated-codec-owned UTF-8 write group bound to that codec instance. */ + @Internal + public interface Utf8WriteGroup { + void write(Utf8JsonWriter writer, Object value); + } + + /** A generated-codec-owned String write group bound to that codec instance. */ + @Internal + public interface StringWriteGroup { + void write(StringJsonWriter writer, Object value); + } + + /** A generated-codec-owned UTF-8 read group bound to that codec instance. */ + @Internal + public interface Utf8ReadGroup { + boolean read(Utf8JsonReader reader, Object value, long[] fieldHashes); + } + + /** A generated-codec-owned complex UTF-8 field tree bound to that codec instance. */ + @Internal + public interface Utf8ReadField { + void read(Utf8JsonReader reader, Object value); + } + + private JsonTrampolineInvoke() {} + + /** Invokes a UTF-8 group through the shared generated-code call site. */ + public static void writeUtf8Group(Utf8WriteGroup group, Utf8JsonWriter writer, Object value) { + group.write(writer, value); + } + + /** Invokes a String group through the shared generated-code call site. */ + public static void writeStringGroup( + StringWriteGroup group, StringJsonWriter writer, Object value) { + group.write(writer, value); + } + + /** Invokes a UTF-8 read group through the shared generated-code call site. */ + public static boolean readUtf8Group( + Utf8ReadGroup group, Utf8JsonReader reader, Object value, long[] fieldHashes) { + return group.read(reader, value, fieldHashes); + } + + /** Invokes a complex UTF-8 field tree through the shared generated-code call site. */ + public static void readUtf8Field(Utf8ReadField field, Utf8JsonReader reader, Object value) { + field.read(reader, value); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index 135006e139..4498b9a01c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -185,7 +185,7 @@ private Class buildStringWriter(ObjectCodec codec) { AnyInfo any = codec.anyInfo(); String code = any == null || any.writeField() == null && any.writeGetter() == null - ? new StringWriterCodegen(this).genWriterCode(builder, type, codec.writeFields()) + ? new StringWriterCodegen(this).genWriterCode(builder, type, codec, codec.writeFields()) : new StringWriterCodegen(this) .genAnyWriterCode(builder, type, codec.writeFields(), any); return compileCodecClass(generatedPackage, className, code); @@ -206,7 +206,7 @@ private Class buildUtf8Writer(ObjectCodec codec) { AnyInfo any = codec.anyInfo(); String code = any == null || any.writeField() == null && any.writeGetter() == null - ? new Utf8WriterCodegen(this).genWriterCode(builder, type, codec.writeFields()) + ? new Utf8WriterCodegen(this).genWriterCode(builder, type, codec, codec.writeFields()) : new Utf8WriterCodegen(this).genAnyWriterCode(builder, type, codec.writeFields(), any); return compileCodecClass(generatedPackage, className, code); } @@ -227,7 +227,7 @@ private Class buildLatin1Reader(ObjectCodec codec) { String code = any == null || any.readField() == null && any.readSetter() == null ? new Latin1ReaderCodegen(this) - .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) + .genReaderCode(builder, type, codec, codec.readFields(), codec.creatorInfo()) : new Latin1ReaderCodegen(this) .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); return compileCodecClass(generatedPackage, className, code); @@ -249,7 +249,7 @@ private Class buildUtf16Reader(ObjectCodec codec) { String code = any == null || any.readField() == null && any.readSetter() == null ? new Utf16ReaderCodegen(this) - .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) + .genReaderCode(builder, type, codec, codec.readFields(), codec.creatorInfo()) : new Utf16ReaderCodegen(this) .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); return compileCodecClass(generatedPackage, className, code); @@ -271,7 +271,7 @@ private Class buildUtf8Reader(ObjectCodec codec) { String code = any == null || any.readField() == null && any.readSetter() == null ? new Utf8ReaderCodegen(this) - .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) + .genReaderCode(builder, type, codec, codec.readFields(), codec.creatorInfo()) : new Utf8ReaderCodegen(this) .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); return compileCodecClass(generatedPackage, className, code); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 8a3766f89f..8582d47793 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -26,12 +26,14 @@ import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.IdentityHashMap; import java.util.Map; import org.apache.fory.codegen.Code; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.Expression; import org.apache.fory.codegen.Expression.Reference; import org.apache.fory.json.codec.CollectionCodec; +import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.codec.JsonUnwrappedInfo; import org.apache.fory.json.codec.JsonUnwrappedInfo.Group; import org.apache.fory.json.codec.JsonUnwrappedInfo.ReadRoute; @@ -60,6 +62,11 @@ */ abstract class JsonReaderCodegen { private static final int MIN_SPLIT_READ_FIELDS = 8; + private static final int MIN_BOUND_READ_TREE_COST = 8; + private static final int MIN_BOUND_READ_COST = 24; + private static final int TARGET_BOUND_READ_COST = 18; + private static final int MAX_BOUND_READ_FIELDS = 8; + private static final int MIN_BOUND_READ_GROUPS = 3; private static final int READ_FIELD_GROUP_SIZE = 2; private static final int READ_FIELD_SWITCH_SIZE = 8; private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; @@ -70,6 +77,8 @@ abstract class JsonReaderCodegen { private AnyInfo any; private Class ownerType; private boolean storesSelfReader; + private boolean boundReadGroups; + private boolean[] boundReadFields; JsonReaderCodegen(JsonCodegen codegen) { this.codegen = codegen; @@ -83,6 +92,24 @@ abstract class JsonReaderCodegen { abstract Class readerArrayType(); + Class readGroupType() { + throw new UnsupportedOperationException(); + } + + String readGroupInvokeMethod() { + throw new UnsupportedOperationException(); + } + + Class readFieldType() { + throw new UnsupportedOperationException(); + } + + String readFieldInvokeMethod() { + throw new UnsupportedOperationException(); + } + + abstract boolean usesBoundReadGroups(); + abstract String readMethod(); abstract String readEnumMethod(boolean tokenValueRead, boolean hashFallback); @@ -119,12 +146,18 @@ final Class readNestedType(JsonFieldInfo property) { String genReaderCode( JsonGeneratedCodecBuilder builder, Class type, + ObjectCodec owner, JsonFieldInfo[] properties, JsonCreatorInfo creatorInfo) { ownerType = type; + boundReadGroups = false; + boundReadFields = null; if (creatorInfo != null) { return genCreatorReaderCode(builder, type, creatorInfo); } + int subtreeCost = readSubtreeCost(owner, new IdentityHashMap<>()); + boundReadGroups = shouldBoundReadGroups(owner, subtreeCost); + boundReadFields = boundReadFields(owner); Class readerType = readerType(); String readMethod = readMethod(); String slowMethod = readMethod + "Slow"; @@ -146,6 +179,8 @@ String genReaderCode( ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "o" + i); } } + addReadGroupFields(builder, readMethod, type, properties); + addBoundReadFieldFields(builder, readMethod, type, properties); addGeneratedConstructor( ctx, readerConstructorExpression(type, properties), @@ -160,15 +195,18 @@ String genReaderCode( fastReadExpression(builder, readMethod, slowMethod, type, properties).genCode(ctx); String bodyCode = body.code(); bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode); + // Generated entries normally start at the current value token. The next-token check preserves + // the whitespace-aware fallback for roots while avoiding empty scans for nested objects. ctx.addMethod( "@Override public final", readMethod, - "if (reader.tryReadNullToken()) {\n" + " return null;\n" + "}\n" + bodyCode, + "if (reader.tryReadNextNullToken()) {\n" + " return null;\n" + "}\n" + bodyCode, Object.class, readerType, "reader"); addFastReadGroupMethods(ctx, builder, readMethod, slowMethod, readerType, type, properties); addReadFieldMethods(ctx, builder, readMethod, readerType, type, properties); + addBoundReadFieldMethods(ctx, builder, readMethod, readerType, type, properties); addSlowReadMethods(ctx, builder, slowMethod, readerType, type, properties); return ctx.genCode(); } @@ -181,6 +219,8 @@ String genAnyReaderCode( AnyInfo any) { this.any = any; ownerType = type; + boundReadGroups = false; + boundReadFields = null; storesSelfReader = JsonCodegen.storesSelfReader(type, properties, creatorInfo != null, any); if (creatorInfo != null) { return genAnyCreatorReaderCode(builder, type, creatorInfo); @@ -245,7 +285,7 @@ String genAnyReaderCode( ctx.addMethod( "@Override public final", readMethod, - "if (reader.tryReadNullToken()) {\n return null;\n}\n" + "if (reader.tryReadNextNullToken()) {\n return null;\n}\n" + "return this." + anyReadMethod + "(reader);", @@ -471,7 +511,7 @@ String genUnwrappedReaderCode( ctx.addMethod( "@Override public final", readMethod(), - "if (reader.tryReadNullToken()) {\n return null;\n}\n" + code, + "if (reader.tryReadNextNullToken()) {\n return null;\n}\n" + code, Object.class, readerType(), "reader"); @@ -575,7 +615,7 @@ private String genCreatorReaderCode( ctx.addMethod( "@Override public final", readMethod(), - "if (reader.tryReadNullToken()) {\n return null;\n}\n" + bodyCode, + "if (reader.tryReadNextNullToken()) {\n return null;\n}\n" + bodyCode, Object.class, concreteReaderType, "reader"); @@ -648,7 +688,7 @@ private String genAnyCreatorReaderCode( ctx.addMethod( "@Override public final", readMethod, - "if (reader.tryReadNullToken()) {\n return null;\n}\n" + "if (reader.tryReadNextNullToken()) {\n return null;\n}\n" + "return this." + anyReadMethod + "(reader);", @@ -1117,6 +1157,119 @@ private void addFastReadGroupMethods( } } + private void addReadGroupFields( + JsonGeneratedCodecBuilder builder, + String readMethod, + Class type, + JsonFieldInfo[] properties) { + if (!boundReadGroups) { + return; + } + for (int start = 0; start < properties.length; ) { + addReadGroupField(builder, readMethod, type, start); + start = readGroupEnd(properties, start); + } + } + + private void addReadGroupField( + JsonGeneratedCodecBuilder builder, String readMethod, Class type, int start) { + CodegenContext ctx = builder.context(); + ctx.addImports(JsonTrampolineInvoke.class, readGroupType()); + String groupType = ctx.type(readGroupType()); + String concreteReaderType = ctx.type(readerType()); + String valueType = ctx.type(type); + String fieldName = readGroupField(start); + ctx.addField(true, groupType, fieldName, null); + ctx.addInitCode( + "this." + + fieldName + + " = new " + + groupType + + "() {\n" + + " @Override public boolean read(" + + concreteReaderType + + " reader, Object value, long[] fieldHashes) {\n" + + " return " + + builder.codecClassName(type) + + ".this." + + readGroupMethod(readMethod, start) + + "(reader, (" + + valueType + + ") value, fieldHashes);\n" + + " }\n" + + "};"); + } + + private void addBoundReadFieldFields( + JsonGeneratedCodecBuilder builder, + String readMethod, + Class type, + JsonFieldInfo[] properties) { + if (boundReadFields == null) { + return; + } + for (int index = 0; index < properties.length; index++) { + if (boundReadFields[index]) { + addBoundReadFieldField(builder, readMethod, type, index); + } + } + } + + private void addBoundReadFieldField( + JsonGeneratedCodecBuilder builder, String readMethod, Class type, int index) { + CodegenContext ctx = builder.context(); + ctx.addImports(JsonTrampolineInvoke.class, readFieldType()); + String fieldType = ctx.type(readFieldType()); + String concreteReaderType = ctx.type(readerType()); + String valueType = ctx.type(type); + String fieldName = boundReadField(index); + ctx.addField(true, fieldType, fieldName, null); + ctx.addInitCode( + "this." + + fieldName + + " = new " + + fieldType + + "() {\n" + + " @Override public void read(" + + concreteReaderType + + " reader, Object value) {\n" + + " " + + builder.codecClassName(type) + + ".this." + + boundReadFieldMethod(readMethod, index) + + "(reader, (" + + valueType + + ") value);\n" + + " }\n" + + "};"); + } + + private void addBoundReadFieldMethods( + CodegenContext ctx, + JsonGeneratedCodecBuilder builder, + String readMethod, + Class readerType, + Class type, + JsonFieldInfo[] properties) { + if (boundReadFields == null) { + return; + } + for (int index = 0; index < properties.length; index++) { + if (boundReadFields[index]) { + addGeneratedMethod( + ctx, + "final", + boundReadFieldMethod(readMethod, index), + readField(builder, type, properties[index], index, objectParam(type), false), + void.class, + readerType, + "reader", + type, + "object"); + } + } + } + private void addReadFieldMethods( CodegenContext ctx, JsonGeneratedCodecBuilder builder, @@ -2171,7 +2324,18 @@ private Expression fastReadGroupExpression( JsonFieldInfo[] properties, int start, int end) { - Expression object = objectParam(type); + return fastReadGroupExpression( + builder, slowMethod, type, properties, objectParam(type), start, end); + } + + private Expression fastReadGroupExpression( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + Expression object, + int start, + int end) { Expression hashes = new Reference("fieldHashes", TypeRef.of(long[].class)); Expression[] skips = new Expression[properties.length]; Expression.ListExpression expressions = new Expression.ListExpression(); @@ -2194,6 +2358,16 @@ private Expression fastReadGroupExpression( private Expression readGroupCall( String readMethod, int start, Expression object, Expression hashes) { + if (boundReadGroups) { + return new Expression.StaticInvoke( + JsonTrampolineInvoke.class, + readGroupInvokeMethod(), + TypeRef.of(boolean.class), + fieldRef(readGroupField(start), readGroupType()), + readerRef(), + object, + hashes); + } if (any == null) { return new Expression.Invoke( new Reference("this", TypeRef.of(Object.class)), @@ -2219,6 +2393,18 @@ private Expression readGroupCall( anyMapRef()); } + private static String readGroupField(int start) { + return "readGroup" + start; + } + + private static String boundReadFieldMethod(String readMethod, int index) { + return readMethod + "Tree" + index; + } + + private static String boundReadField(int index) { + return "readTree" + index; + } + private Expression anyMap(JsonGeneratedCodecBuilder builder, Expression object) { if (any == null || any.readField() == null) { return null; @@ -2457,6 +2643,128 @@ final long packedNameMask(int length) { return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L; } + private boolean shouldBoundReadGroups(ObjectCodec owner, int subtreeCost) { + return usesBoundReadGroups() + && usesOrdinaryReader(owner) + && shouldSplitFastRead(owner.readFields()) + && subtreeCost >= MIN_BOUND_READ_COST; + } + + private boolean[] boundReadFields(ObjectCodec owner) { + if (!usesBoundReadGroups() || !usesOrdinaryReader(owner)) { + return null; + } + JsonFieldInfo[] properties = owner.readFields(); + boolean[] fields = new boolean[properties.length]; + boolean anyBound = false; + for (int i = 0; i < properties.length; i++) { + JsonFieldKind kind = properties[i].readKind(); + if ((kind == JsonFieldKind.OBJECT + || kind == JsonFieldKind.ARRAY + || kind == JsonFieldKind.COLLECTION + || kind == JsonFieldKind.MAP) + && readFieldTreeCost(properties[i], new IdentityHashMap<>()) + >= MIN_BOUND_READ_TREE_COST) { + fields[i] = true; + anyBound = true; + } + } + return anyBound ? fields : null; + } + + private int readFieldTreeCost( + JsonFieldInfo property, IdentityHashMap, Boolean> active) { + int cost = readFieldCost(property); + JsonFieldKind kind = property.readKind(); + if (kind == JsonFieldKind.ARRAY + || kind == JsonFieldKind.COLLECTION + || kind == JsonFieldKind.MAP) { + // A container owns a loop plus child dispatch even when its resolved child is not a POJO. + return cost + 4; + } + ObjectCodec child = property.readTypeInfo().objectCodec(); + if (child == null || active.put(child, Boolean.TRUE) != null) { + return cost; + } + for (JsonFieldInfo childProperty : child.readFields()) { + cost += readFieldTreeCost(childProperty, active); + if (cost >= MIN_BOUND_READ_TREE_COST) { + break; + } + } + active.remove(child); + return cost; + } + + private int readSubtreeCost( + ObjectCodec owner, IdentityHashMap, Boolean> active) { + if (active.put(owner, Boolean.TRUE) != null) { + return 0; + } + int cost = 0; + for (JsonFieldInfo property : owner.readFields()) { + cost += readFieldCost(property); + ObjectCodec child = property.readTypeInfo().objectCodec(); + if (child != null && !hasLocalReadBoundary(child)) { + cost += readSubtreeCost(child, active); + } + if (cost >= MIN_BOUND_READ_COST) { + break; + } + } + active.remove(owner); + return cost; + } + + private boolean hasLocalReadBoundary(ObjectCodec owner) { + if (!usesOrdinaryReader(owner) || !shouldSplitFastRead(owner.readFields())) { + return false; + } + int cost = 0; + for (JsonFieldInfo property : owner.readFields()) { + cost += readFieldCost(property); + if (cost >= MIN_BOUND_READ_COST) { + return true; + } + } + return false; + } + + private static boolean usesOrdinaryReader(ObjectCodec owner) { + AnyInfo any = owner.anyInfo(); + return owner.creatorInfo() == null + && owner.unwrappedInfo() == null + && (any == null || any.readField() == null && any.readSetter() == null); + } + + private static int readFieldCost(JsonFieldInfo property) { + JsonFieldKind kind = property.readKind(); + if (kind == null) { + return 0; + } + switch (kind) { + case BOOLEAN: + case BYTE: + case SHORT: + case INT: + case LONG: + case FLOAT: + case DOUBLE: + case CHAR: + return 2; + case STRING: + return 3; + case ENUM: + case ARRAY: + case COLLECTION: + case MAP: + case OBJECT: + return 4; + default: + throw new IllegalStateException("Unsupported read kind " + kind); + } + } + final boolean shouldSplitFastRead(JsonFieldInfo[] properties) { return properties.length >= MIN_SPLIT_READ_FIELDS; } @@ -2466,6 +2774,17 @@ final String readGroupMethod(String readMethod, int start) { } final int readGroupEnd(JsonFieldInfo[] properties, int start) { + if (boundReadGroups) { + int maxFields = + Math.min(MAX_BOUND_READ_FIELDS, divideRoundUp(properties.length, MIN_BOUND_READ_GROUPS)); + int end = start; + int cost = 0; + while (end < properties.length && end - start < maxFields && cost < TARGET_BOUND_READ_COST) { + cost += readFieldCost(properties[end]); + end++; + } + return end; + } int end = start + 1; while (end < properties.length && end - start < READ_FIELD_GROUP_SIZE @@ -2475,6 +2794,10 @@ && canPairReadFields(properties[end - 1], properties[end])) { return end; } + private static int divideRoundUp(int value, int divisor) { + return (value + divisor - 1) / divisor; + } + final boolean canPairReadFields(JsonFieldInfo left, JsonFieldInfo right) { JsonFieldKind leftKind = left.readKind(); JsonFieldKind rightKind = right.readKind(); @@ -2903,12 +3226,19 @@ private Expression fieldSwitchRange( Expression unknown) { Expression.Switch.Case[] cases = new Expression.Switch.Case[end - start]; for (int i = start; i < end; i++) { + Expression read = + boundReadFields != null && boundReadFields[i] + ? new Expression.StaticInvoke( + JsonTrampolineInvoke.class, + readFieldInvokeMethod(), + TypeRef.of(void.class), + fieldRef(boundReadField(i), readFieldType()), + readerRef(), + object) + : readField(builder, type, properties[i], i, object, false); cases[i - start] = new Expression.Switch.Case( - i, - new Expression.ListExpression( - readField(builder, type, properties[i], i, object, false), - new Expression.Break())); + i, new Expression.ListExpression(read, new Expression.Break())); } return new Expression.Switch(fieldIndex, cases, unknown); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index d892bb3909..c354175ff9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -36,6 +36,8 @@ import org.apache.fory.codegen.Expression.Reference; import org.apache.fory.codegen.ExpressionOptimizer; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.CollectionCodec.ObjectCollectionCodec; +import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.codec.JsonUnwrappedInfo; import org.apache.fory.json.codec.JsonUnwrappedInfo.Group; import org.apache.fory.json.codec.JsonUnwrappedInfo.WriteEntry; @@ -43,6 +45,7 @@ import org.apache.fory.json.codec.ObjectCodec.AnyInfo; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.json.resolver.JsonTypeInfo; import org.apache.fory.reflect.TypeRef; /** @@ -51,14 +54,16 @@ *

The concrete generators own representation-specific field prefixes, scalar stores, and child * capability types. This base shares source-construction algorithms only after the concrete writer * is selected; it is not a runtime output mode. Generated writers retain precomputed field tokens - * and concrete child capabilities, fuse safe object prefixes, and split wide objects into bounded - * methods to protect compiler and inlining budgets without adding per-field dispatch. + * and concrete child capabilities, fuse safe object prefixes, and split wide objects into bound + * write groups that contain recursive JIT expansion without adding per-field dispatch. */ abstract class JsonWriterCodegen { - // Bound field logic in independently compiled generated methods. The entry method keeps a small - // tail inline below; wide objects use fewer bounded calls without adding runtime dispatch. + // Bound field logic in independently compiled generated methods. Complete writers use real bound + // write groups, while Any and unwrapped writers retain direct generated-helper calls. private static final int MAX_MEMBERS_PER_METHOD = 16; private static final int INLINE_TAIL_MEMBERS = 4; + private static final int MIN_TRAMPOLINE_GROUPS = 3; + private static final int MIN_MEMBERS_PER_GROUP = 3; final JsonCodegen codegen; private Class ownerType; @@ -75,16 +80,28 @@ abstract class JsonWriterCodegen { abstract Class completeWriterType(); + abstract Object resolvedWriter(JsonFieldInfo property); + + abstract String writerCapabilityMethod(); + abstract String writeMethod(); - // This names private split methods in ordinary complete writers. It is not a partial-object - // capability; keep the generated literal stable so unaffected writer source remains identical. + // This names split methods in complete writers and direct helpers in Any writers. It is not a + // partial-object capability. abstract String memberGroupMethod(); + abstract Class memberGroupType(); + + abstract String writeMemberGroupMethod(); + abstract String writeAnyMethod(); abstract int splitMemberThreshold(); + abstract int minimumObjectWriteCost(); + + abstract int memberPrefixCost(JsonFieldInfo property); + abstract PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused); abstract void addPrefixFields( @@ -178,11 +195,15 @@ private static void addGeneratedConstructor( } String genWriterCode( - JsonGeneratedCodecBuilder builder, Class type, JsonFieldInfo[] properties) { + JsonGeneratedCodecBuilder builder, + Class type, + ObjectCodec owner, + JsonFieldInfo[] properties) { ownerType = type; CodegenContext ctx = builder.context(); ctx.addImports(writerType()); - ctx.implementsInterfaces(JsonCodegen.generatedCodecType(ctx, completeWriterType())); + String completeWriterType = JsonCodegen.generatedCodecType(ctx, completeWriterType()); + ctx.implementsInterfaces(completeWriterType); boolean objectStartFused = canFuseObjectStart(properties); PrefixFields prefixFields = prefixFields(properties, objectStartFused); for (int i = 0; i < properties.length; i++) { @@ -197,17 +218,21 @@ String genWriterCode( addPrefixFields(ctx, property, i, prefixFields); } } - addGeneratedConstructor( - ctx, - writerConstructorExpression(properties, prefixFields), - JsonFieldInfo[].class, - "properties", - JsonCodegen.generatedCodecArrayType(ctx, codecArrayType()), - "codecs"); String bodyCode; - // Keep the sole nullable capability entry small for wide POJOs. Callers can inline its null - // ownership without absorbing the independently compiled field graph into a container loop. - if (properties.length >= splitMemberThreshold()) { + // The generated type owns this decision: only a sufficiently large resolved object subtree + // gets a shared boundary. Handwritten containers stay direct, while scalar calls inside the + // independently compiled generated body remain eligible for normal inlining. + // Wide objects already contain their field graph behind generated member-group boundaries. + // Adding another boundary at the complete object entry only adds dispatch; reserve this entry + // boundary for medium objects whose unsplit body can still absorb a large child subtree. + boolean objectWriteBoundary = + ownsObjectWriteBoundary(owner, writeSubtreeCost(owner)) + || properties.length < splitMemberThreshold() + && ownsGeneratedObjectCollectionLoop(owner); + if (objectWriteBoundary) { + ctx.implementsInterfaces(completeWriterType, ctx.type(memberGroupType())); + } + if (objectWriteBoundary || properties.length >= splitMemberThreshold()) { String objectMethod = writeMethod() + "Object"; addGeneratedMethod( ctx, @@ -220,7 +245,19 @@ builder, properties, objectStartFused, new Reference("object", TypeRef.of(type)) "writer", type, "object"); - bodyCode = "this." + objectMethod + "(writer, (" + ctx.type(type) + ") value);\n"; + if (objectWriteBoundary) { + String fieldName = "objectWrite"; + addObjectWriteField(builder, fieldName, objectMethod); + bodyCode = + ctx.type(JsonTrampolineInvoke.class) + + "." + + writeMemberGroupMethod() + + "(this." + + fieldName + + ", writer, value);\n"; + } else { + bodyCode = "this." + objectMethod + "(writer, (" + ctx.type(type) + ") value);\n"; + } } else { ctx.clearExprState(); Expression castObject = @@ -234,10 +271,23 @@ builder, properties, objectStartFused, new Reference("object", TypeRef.of(type)) bodyCode = body.code(); bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode); } + addGeneratedConstructor( + ctx, + writerConstructorExpression(properties, prefixFields), + JsonFieldInfo[].class, + "properties", + JsonCodegen.generatedCodecArrayType(ctx, codecArrayType()), + "codecs"); ctx.addMethod( "@Override public final", writeMethod(), - "if (value == null) {\n" + " writer.writeNull();\n" + " return;\n" + "}\n" + bodyCode, + objectWriteBoundary + ? bodyCode + : "if (value == null) {\n" + + " writer.writeNull();\n" + + " return;\n" + + "}\n" + + bodyCode, void.class, writerType(), "writer", @@ -841,7 +891,7 @@ private Expression writeExpression( } boolean commaKnown = objectStartFused; boolean splitMembers = properties.length >= splitMemberThreshold(); - List memberGroup = splitMembers ? new ArrayList<>(MAX_MEMBERS_PER_METHOD) : null; + List memberGroup = splitMembers ? new ArrayList<>() : null; for (int i = firstProperty; i < properties.length; i++) { Expression member; if (objectStartFused && i == 0) { @@ -852,13 +902,10 @@ private Expression writeExpression( member = writeProp(builder, properties[i], i, commaKnown, index, object, writer); } if (splitMembers && commaKnown) { - memberGroup.add(member); - if (memberGroup.size() == MAX_MEMBERS_PER_METHOD) { - addMemberGroup(builder, expressions, memberGroup, object, writer); - } + memberGroup.add(new MemberExpression(member, memberCost(properties[i]))); } else { if (splitMembers) { - addMemberGroup(builder, expressions, memberGroup, object, writer); + addDirectMembers(expressions, memberGroup); } expressions.add(member); } @@ -867,7 +914,7 @@ private Expression writeExpression( } } if (splitMembers) { - addMemberGroup(builder, expressions, memberGroup, object, writer, true); + addMemberGroups(builder, expressions, memberGroup, object, writer, index); } expressions.add(new Expression.Invoke(writer, "writeObjectEnd")); return expressions; @@ -1039,6 +1086,266 @@ private void addMemberGroup( memberGroup.clear(); } + private static void addDirectMembers( + Expression.ListExpression expressions, List members) { + for (MemberExpression member : members) { + expressions.add(member.expression); + } + members.clear(); + } + + private void addMemberGroups( + JsonGeneratedCodecBuilder builder, + Expression.ListExpression expressions, + List members, + Expression object, + Reference writer, + Expression written) { + if (members.size() < MIN_TRAMPOLINE_GROUPS * MIN_MEMBERS_PER_GROUP) { + addDirectMembers(expressions, members); + return; + } + if (written == null) { + written = new Expression.Variable("written", Expression.Literal.ofInt(1)); + expressions.add(written); + } + int groupCount = + Math.max(MIN_TRAMPOLINE_GROUPS, divideRoundUp(members.size(), MAX_MEMBERS_PER_METHOD)); + int start = 0; + int remainingCost = totalMemberCost(members); + for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) { + int remainingGroups = groupCount - groupIndex; + int end = memberGroupEnd(members, start, remainingCost, remainingGroups); + String fieldName = "group" + groupIndex; + String methodName = memberGroupMethod() + groupIndex; + addMemberGroupMethod(builder, members, start, end, methodName); + addMemberGroupField(builder, fieldName, methodName); + // Complete medium objects and real member groups share this BCI. Their combined receiver + // population keeps the generated boundary polymorphic without synthetic receiver types. + // Keep this void invocation as a statement: marking it inline drops the call from generated + // source because an inline void invocation has no value for the expression builder to emit. + expressions.add( + new Expression.StaticInvoke( + JsonTrampolineInvoke.class, + writeMemberGroupMethod(), + fieldRef(fieldName, memberGroupType()), + writer, + object)); + remainingCost -= rangeCost(members, start, end); + start = end; + } + members.clear(); + } + + private void addMemberGroupMethod( + JsonGeneratedCodecBuilder builder, + List members, + int start, + int end, + String methodName) { + Expression.ListExpression body = new Expression.ListExpression(); + for (int i = start; i < end; i++) { + body.add(members.get(i).expression); + } + addGeneratedMethod( + builder.context(), + "final", + methodName, + body, + void.class, + writerType(), + "writer", + ownerType, + "object"); + } + + private void addMemberGroupField( + JsonGeneratedCodecBuilder builder, String fieldName, String methodName) { + CodegenContext ctx = builder.context(); + ctx.addImports(JsonTrampolineInvoke.class, memberGroupType()); + String groupType = ctx.type(memberGroupType()); + String writerType = ctx.type(writerType()); + String valueType = ctx.type(ownerType); + ctx.addField(true, groupType, fieldName, null); + ctx.addInitCode( + "this." + + fieldName + + " = new " + + groupType + + "() {\n" + + " @Override public void write(" + + writerType + + " writer, Object value) {\n" + + " " + + builder.codecClassName(ownerType) + + ".this." + + methodName + + "(writer, (" + + valueType + + ") value);\n" + + " }\n" + + "};"); + } + + private void addObjectWriteField( + JsonGeneratedCodecBuilder builder, String fieldName, String methodName) { + CodegenContext ctx = builder.context(); + ctx.addImports(JsonTrampolineInvoke.class, memberGroupType()); + String objectWriteType = ctx.type(memberGroupType()); + String valueType = ctx.type(ownerType); + ctx.addField(true, objectWriteType, fieldName, null); + // Retain the receiver behind its interface-typed field. Passing an exact `this` through the + // static helper lets C2 recover the generated class and erase the shared polymorphic boundary. + ctx.addInitCode("this." + fieldName + " = this;"); + ctx.addMethod( + "public final", + "write", + "if (value == null) {\n" + + " writer.writeNull();\n" + + " return;\n" + + "}\n" + + "this." + + methodName + + "(writer, (" + + valueType + + ") value);", + void.class, + writerType(), + "writer", + Object.class, + "value"); + } + + private int writeSubtreeCost(ObjectCodec owner) { + return writeSubtreeCost(owner, new IdentityHashMap<>()); + } + + // Stop once the representation-specific boundary threshold is proven. The identity stack makes + // self-recursive schemas finite without discounting the same child used by distinct fields. + private int writeSubtreeCost( + ObjectCodec owner, IdentityHashMap, Boolean> active) { + // A wide child already exposes only its small coordinator to its parent; its real field bodies + // are behind generated group calls and must not be charged again to the parent's inline graph. + if (ownsMemberGroupBoundaries(owner)) { + return 0; + } + if (active.put(owner, Boolean.TRUE) != null) { + return 0; + } + int cost = 0; + for (JsonFieldInfo property : owner.writeFields()) { + cost += memberCost(property); + ObjectCodec child = property.writeTypeInfo().objectCodec(); + if (child != null) { + int childCost = writeSubtreeCost(child, active); + if (!ownsObjectWriteBoundary(child, childCost)) { + cost += childCost; + } + } + if (cost >= minimumObjectWriteCost()) { + break; + } + } + active.remove(owner); + return cost; + } + + private boolean ownsMemberGroupBoundaries(ObjectCodec owner) { + return usesOrdinaryWriter(owner) && owner.writeFields().length >= splitMemberThreshold(); + } + + private boolean ownsObjectWriteBoundary(ObjectCodec owner, int subtreeCost) { + return usesOrdinaryWriter(owner) + && owner.writeFields().length < splitMemberThreshold() + && subtreeCost >= minimumObjectWriteCost(); + } + + private static boolean usesOrdinaryWriter(ObjectCodec owner) { + AnyInfo any = owner.anyInfo(); + return any == null || any.writeField() == null && any.writeGetter() == null; + } + + private static int memberGroupEnd( + List members, int start, int remainingCost, int remainingGroups) { + if (remainingGroups == 1) { + return members.size(); + } + int targetCost = divideRoundUp(remainingCost, remainingGroups); + int minimumEnd = + Math.max(start + 1, members.size() - (remainingGroups - 1) * MAX_MEMBERS_PER_METHOD); + int maximumEnd = Math.min(members.size() - remainingGroups + 1, start + MAX_MEMBERS_PER_METHOD); + int cost = 0; + int end = start; + while (end < maximumEnd && (end < minimumEnd || cost < targetCost)) { + cost += members.get(end).cost; + end++; + } + return end; + } + + private static int totalMemberCost(List members) { + return rangeCost(members, 0, members.size()); + } + + private static int rangeCost(List members, int start, int end) { + int cost = 0; + for (int i = start; i < end; i++) { + cost += members.get(i).cost; + } + return cost; + } + + private static int divideRoundUp(int value, int divisor) { + return (value + divisor - 1) / divisor; + } + + private int memberCost(JsonFieldInfo property) { + // These relative weights balance contiguous generated groups. The independent member-count + // bound above remains the hard compiler-size guard; actual bytecode size is verified from the + // generated validation matrix rather than inferred from this estimate. + int cost; + switch (property.writeKind()) { + case BYTE: + case SHORT: + case INT: + case LONG: + case FLOAT: + case DOUBLE: + case CHAR: + case BOOLEAN: + cost = 2; + break; + case STRING: + case ENUM: + cost = 3; + break; + case ARRAY: + case COLLECTION: + case MAP: + case OBJECT: + default: + cost = 4; + break; + } + if (!property.writeRawType().isPrimitive()) { + cost++; + } + if (property.writeNull()) { + cost++; + } + return cost + memberPrefixCost(property); + } + + private static final class MemberExpression { + private final Expression expression; + private final int cost; + + private MemberExpression(Expression expression, int cost) { + this.expression = expression; + this.cost = cost; + } + } + private static boolean canFuseObjectStart(JsonFieldInfo[] properties) { if (properties.length == 0 || !properties[0].writeRawType().isPrimitive()) { return false; @@ -1085,7 +1392,8 @@ private Expression writeProp( || kind == JsonFieldKind.ARRAY && writeExactArray(property, value, writer) == null || kind == JsonFieldKind.OBJECT && writeExactScalar(property, value, writer) == null || kind == JsonFieldKind.COLLECTION - && !JsonCodegen.writesStringCollectionDirectly(property); + && !JsonCodegen.writesStringCollectionDirectly(property) + && !writesGeneratedObjectCollectionDirectly(property); if (onlyCodec) { return new Expression.ListExpression( value, @@ -1209,6 +1517,9 @@ private Expression writeValue( if (JsonCodegen.writesStringCollectionDirectly(property)) { return writeStringCollection(value, writer); } + if (writesGeneratedObjectCollectionDirectly(property)) { + return writeObjectCollection(property, id, value, writer); + } return writeCodec(property, id, value, writer); case OBJECT: Expression scalar = writeExactScalar(property, value, writer); @@ -1256,9 +1567,89 @@ private Expression writeCodec( object && property.writeRawType() == ownerType ? new Reference("this", TypeRef.of(completeWriterType())) : fieldRef("w" + id, codecFieldType(property)); + // A generated child whose resolved field graph is large owns its boundary at its own entry. + // Small children stay direct and can inline; the parent does not apply a second blanket edge. return new Expression.Invoke(codec, writeMethod(), writer, value); } + private Expression writeObjectCollection( + JsonFieldInfo property, int id, Expression value, Expression writer) { + Reference collectionCodec = fieldRef("w" + id, ObjectCollectionCodec.class); + Expression elementTypeInfo = + new Expression.Invoke(collectionCodec, "elementTypeInfo", TypeRef.of(JsonTypeInfo.class)) + .inline(); + Expression elementWriter = + new Expression.Variable( + "elementWriter" + id, + new Expression.Invoke( + elementTypeInfo, writerCapabilityMethod(), TypeRef.of(completeWriterType())) + .inline()); + Expression list = + new Expression.Variable( + "list" + id, new Expression.Cast(value, TypeRef.of(ArrayList.class))); + Expression size = + new Expression.Variable( + "size" + id, new Expression.Invoke(list, "size", TypeRef.of(int.class)).inline()); + Expression loop = + new Expression.ForLoop( + Expression.Literal.ofInt(0), + size, + Expression.Literal.ofInt(1), + elementIndex -> { + Expression element = + new Expression.Variable( + "element" + id, + new Expression.Invoke( + list, "get", "", TypeRef.of(Object.class), false, false, elementIndex) + .inline()); + return new Expression.ListExpression( + element, + new Expression.Invoke(writer, "writeComma", elementIndex), + new Expression.Invoke(elementWriter, writeMethod(), writer, element)); + }); + Expression exactArrayList = + new Expression.ListExpression( + list, + elementWriter, + size, + new Expression.Invoke(writer, "writeArrayStart"), + loop, + new Expression.Invoke(writer, "writeArrayEnd")); + Expression exactClass = + eq( + new Expression.Invoke(value, "getClass", TypeRef.of(Class.class)).inline(), + Expression.Literal.ofClass(ArrayList.class)); + return new Expression.If(exactClass, exactArrayList, writeCodec(property, id, value, writer)); + } + + private boolean ownsGeneratedObjectCollectionLoop(ObjectCodec owner) { + if (!usesOrdinaryWriter(owner)) { + return false; + } + for (JsonFieldInfo property : owner.writeFields()) { + if (writesGeneratedObjectCollectionDirectly(property)) { + return true; + } + } + return false; + } + + private boolean writesGeneratedObjectCollectionDirectly(JsonFieldInfo property) { + if (property.writeKind() != JsonFieldKind.COLLECTION) { + return false; + } + Object writer = resolvedWriter(property); + if (writer.getClass() != ObjectCollectionCodec.class) { + return false; + } + ObjectCodec elementOwner = ((ObjectCollectionCodec) writer).elementTypeInfo().objectCodec(); + if (elementOwner == null || !codegen.canCompileWriter(elementOwner)) { + return false; + } + return ownsMemberGroupBoundaries(elementOwner) + || ownsObjectWriteBoundary(elementOwner, writeSubtreeCost(elementOwner)); + } + private boolean storesWriteCodec(JsonFieldInfo property) { return usesWriteCodec(property) && (!property.writeTypeInfo().usesDefaultObjectCodec() diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java index 10d0346e6b..8ae5bbe343 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java @@ -52,6 +52,11 @@ Class readerArrayType() { return Latin1ReaderCodec[].class; } + @Override + boolean usesBoundReadGroups() { + return false; + } + @Override String readMethod() { return "readLatin1"; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java index 732c8d9433..b938acb163 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java @@ -23,6 +23,7 @@ import org.apache.fory.codegen.Expression; import org.apache.fory.codegen.Expression.Reference; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.codec.StringWriterCodec; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.writer.StringJsonWriter; @@ -30,6 +31,7 @@ final class StringWriterCodegen extends JsonWriterCodegen { private static final int MIN_SPLIT_MEMBERS = 10; + private static final int MIN_OBJECT_WRITE_COST = 30; StringWriterCodegen(JsonCodegen codegen) { super(codegen); @@ -55,6 +57,16 @@ Class completeWriterType() { return StringWriterCodec.class; } + @Override + Object resolvedWriter(JsonFieldInfo property) { + return property.writeTypeInfo().stringWriter(); + } + + @Override + String writerCapabilityMethod() { + return "stringWriter"; + } + @Override String writeMethod() { return "writeString"; @@ -65,6 +77,16 @@ String memberGroupMethod() { return "writeStringMembers"; } + @Override + Class memberGroupType() { + return JsonTrampolineInvoke.StringWriteGroup.class; + } + + @Override + String writeMemberGroupMethod() { + return "writeStringGroup"; + } + @Override String writeAnyMethod() { return "writeStringAny"; @@ -75,6 +97,16 @@ int splitMemberThreshold() { return MIN_SPLIT_MEMBERS; } + @Override + int minimumObjectWriteCost() { + return MIN_OBJECT_WRITE_COST; + } + + @Override + int memberPrefixCost(JsonFieldInfo property) { + return usesPrefix(property) ? 2 : 0; + } + @Override PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused) { PrefixFields fields = new PrefixFields(properties.length); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java index b0588bf4a5..bf261564bd 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java @@ -51,6 +51,11 @@ Class readerArrayType() { return Utf16ReaderCodec[].class; } + @Override + boolean usesBoundReadGroups() { + return false; + } + @Override String readMethod() { return "readUtf16"; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java index ce01fa5810..cc2e45daaf 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java @@ -21,6 +21,7 @@ import org.apache.fory.codegen.Expression; import org.apache.fory.codegen.Expression.Reference; +import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.codec.Utf8ReaderCodec; import org.apache.fory.json.meta.JsonAsciiToken; import org.apache.fory.json.meta.JsonFieldInfo; @@ -52,6 +53,31 @@ Class readerArrayType() { return Utf8ReaderCodec[].class; } + @Override + Class readGroupType() { + return JsonTrampolineInvoke.Utf8ReadGroup.class; + } + + @Override + String readGroupInvokeMethod() { + return "readUtf8Group"; + } + + @Override + Class readFieldType() { + return JsonTrampolineInvoke.Utf8ReadField.class; + } + + @Override + String readFieldInvokeMethod() { + return "readUtf8Field"; + } + + @Override + boolean usesBoundReadGroups() { + return true; + } + @Override String readMethod() { return "readUtf8"; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java index ab0ccb84ff..42e3d55c0e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java @@ -30,6 +30,7 @@ import org.apache.fory.codegen.Expression.Reference; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.ArrayCodec; +import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.Utf8WriterCodec; import org.apache.fory.json.meta.JsonFieldInfo; @@ -39,6 +40,7 @@ final class Utf8WriterCodegen extends JsonWriterCodegen { private static final int MIN_SPLIT_MEMBERS = 12; + private static final int MIN_OBJECT_WRITE_COST = 24; Utf8WriterCodegen(JsonCodegen codegen) { super(codegen); @@ -64,6 +66,16 @@ Class completeWriterType() { return Utf8WriterCodec.class; } + @Override + Object resolvedWriter(JsonFieldInfo property) { + return property.writeTypeInfo().utf8Writer(); + } + + @Override + String writerCapabilityMethod() { + return "utf8Writer"; + } + @Override String writeMethod() { return "writeUtf8"; @@ -74,6 +86,16 @@ String memberGroupMethod() { return "writeUtf8Members"; } + @Override + Class memberGroupType() { + return JsonTrampolineInvoke.Utf8WriteGroup.class; + } + + @Override + String writeMemberGroupMethod() { + return "writeUtf8Group"; + } + @Override String writeAnyMethod() { return "writeUtf8Any"; @@ -84,6 +106,16 @@ int splitMemberThreshold() { return MIN_SPLIT_MEMBERS; } + @Override + int minimumObjectWriteCost() { + return MIN_OBJECT_WRITE_COST; + } + + @Override + int memberPrefixCost(JsonFieldInfo property) { + return usesPrefix(property) ? 1 : 0; + } + @Override PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused) { PrefixFields fields = new PrefixFields(properties.length); @@ -101,10 +133,7 @@ && canPackObjectStartString(property)) { fields.name[i] = true; } } else if (!commaKnown) { - if (property.writesRawString() - || !canUsePackedDynamicPrefix(property) - || !canPackSinglePrefix(property, false) - || !canPackSinglePrefix(property, true)) { + if (!canPackPrefix(property, false) || !canPackPrefix(property, true)) { fields.name[i] = true; fields.comma[i] = true; } @@ -119,22 +148,6 @@ && canPackObjectStartString(property)) { return fields; } - private boolean canUsePackedDynamicPrefix(JsonFieldInfo property) { - if (property.writeNull() && !property.writeRawType().isPrimitive()) { - return false; - } - switch (property.writeKind()) { - case BYTE: - case SHORT: - case INT: - case LONG: - case STRING: - return true; - default: - return false; - } - } - @Override void addPrefixFields(CodegenContext ctx, JsonFieldInfo property, int id, PrefixFields fields) { if (fields.name[id]) { @@ -228,7 +241,7 @@ Expression writeNumberField( } return new Expression.Invoke(writer, method, utf8PrefixRef(true, id), value); } - if (canPackSinglePrefix(property, false) && canPackSinglePrefix(property, true)) { + if (canPackPrefix(property, false) && canPackPrefix(property, true)) { return new Expression.ListExpression( new Expression.Invoke(writer, method, packedDynamicPrefixArgs(property, index, value)), increment(index)); @@ -256,7 +269,7 @@ Expression writeStringField( } return new Expression.Invoke(writer, "writeStringField", utf8PrefixRef(true, id), value); } - if (canPackSinglePrefix(property, false) && canPackSinglePrefix(property, true)) { + if (canPackPrefix(property, false) && canPackPrefix(property, true)) { return new Expression.ListExpression( new Expression.Invoke( writer, "writeStringField", packedDynamicPrefixArgs(property, index, value)), @@ -281,6 +294,11 @@ Expression writeFieldName( if (commaKnown && canPackPrefix(property, true)) { return new Expression.Invoke(writer, "writeRawValue", packedPrefixArgs(property, true)); } + if (!commaKnown && canPackPrefix(property, false) && canPackPrefix(property, true)) { + return new Expression.ListExpression( + new Expression.Invoke(writer, "writeRawValue", packedDynamicPrefixArgs(property, index)), + increment(index)); + } Expression prefix = commaKnown ? utf8PrefixRef(true, id) @@ -378,13 +396,15 @@ private static Expression[] packedDynamicPrefixArgs( JsonFieldInfo property, Expression index, Expression... extraArgs) { byte[] namePrefix = property.utf8NamePrefix(); byte[] commaPrefix = property.utf8CommaNamePrefix(); - Expression[] args = new Expression[5 + extraArgs.length]; + Expression[] args = new Expression[7 + extraArgs.length]; args[0] = Expression.Literal.ofLong(packedPrefixWord(namePrefix, 0)); - args[1] = Expression.Literal.ofLong(packedPrefixWord(commaPrefix, 0)); - args[2] = Expression.Literal.ofInt(namePrefix.length); - args[3] = Expression.Literal.ofInt(commaPrefix.length); - args[4] = index; - System.arraycopy(extraArgs, 0, args, 5, extraArgs.length); + args[1] = Expression.Literal.ofLong(packedPrefixWord(namePrefix, Long.BYTES)); + args[2] = Expression.Literal.ofLong(packedPrefixWord(commaPrefix, 0)); + args[3] = Expression.Literal.ofLong(packedPrefixWord(commaPrefix, Long.BYTES)); + args[4] = Expression.Literal.ofInt(namePrefix.length); + args[5] = Expression.Literal.ofInt(commaPrefix.length); + args[6] = index; + System.arraycopy(extraArgs, 0, args, 7, extraArgs.length); return args; } @@ -392,9 +412,4 @@ private static boolean canPackPrefix(JsonFieldInfo property, boolean comma) { int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length; return length <= Long.BYTES * 2; } - - private static boolean canPackSinglePrefix(JsonFieldInfo property, boolean comma) { - int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length; - return length <= Long.BYTES; - } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index cbe2a16ec0..cf0e25c674 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -2416,6 +2416,29 @@ private long readQuotedStringHash() { } private long readQuotedStringHashToken() { + byte[] bytes = input; + int mark = position; + int nameOffset = mark + 1; + if (nameOffset + Long.BYTES < bytes.length && bytes[mark] == '"') { + long word = LittleEndian.getInt64(bytes, nameOffset); + long stopMask = asciiStringStopMask(word); + if (stopMask == 0) { + if (bytes[nameOffset + Long.BYTES] == '"') { + position = nameOffset + Long.BYTES + 1; + return word; + } + } else { + int nameLength = Long.numberOfTrailingZeros(stopMask) >>> 3; + if (nameLength > 0 && ((word >>> (nameLength << 3)) & 0xFF) == '"') { + position = nameOffset + nameLength + 1; + return word & ((1L << (nameLength << 3)) - 1); + } + } + } + return readQuotedStringHashSlow(); + } + + private long readQuotedStringHashSlow() { byte[] bytes = input; int length = bytes.length; if (position >= length || bytes[position++] != '"') { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index b6b9492cdf..3d69badcc3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -1839,12 +1839,11 @@ public String readNullableString() { public String readNextNullableString() { if (position < input.length) { int ch = input[position]; - if (ch == '"') { - return readStringToken(); - } if (ch == 'n' && tryReadNullLiteral()) { return null; } + // Quotes use this shared non-whitespace branch. A separate quote return duplicates the + // string scanner when this method is inlined into generated readers. if (ch > ' ' || !isWhitespace(ch)) { return readStringToken(); } @@ -1900,6 +1899,9 @@ private String readStringToken() { } int start = position; int offset = start; + // Three direct probes keep short JSON strings in one compact scalar path. Generated codecs + // isolate large field subtrees instead of changing this general reader method's size to steer + // C2; longer strings continue in the shared unbounded tail. if (offset + Long.BYTES <= inputLength) { long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); if (stopMask != 0) { @@ -2522,6 +2524,29 @@ private long readQuotedStringHash() { } private long readQuotedStringHashToken() { + byte[] bytes = input; + int mark = position; + int nameOffset = mark + 1; + if (nameOffset + Long.BYTES < bytes.length && bytes[mark] == '"') { + long word = LittleEndian.getInt64(bytes, nameOffset); + long stopMask = stringStopMask(word); + if (stopMask == 0) { + if (bytes[nameOffset + Long.BYTES] == '"') { + position = nameOffset + Long.BYTES + 1; + return word; + } + } else { + int nameLength = Long.numberOfTrailingZeros(stopMask) >>> 3; + if (nameLength > 0 && ((word >>> (nameLength << 3)) & 0xFF) == '"') { + position = nameOffset + nameLength + 1; + return word & ((1L << (nameLength << 3)) - 1); + } + } + } + return readQuotedStringHashSlow(); + } + + private long readQuotedStringHashSlow() { byte[] bytes = input; int length = bytes.length; if (position >= length || bytes[position++] != '"') { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java index 95024dfcd9..762abe2b82 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java @@ -53,6 +53,7 @@ public final class JsonTypeInfo { private Latin1ReaderCodec latin1Reader; private Utf16ReaderCodec utf16Reader; private Utf8ReaderCodec utf8Reader; + private final ObjectCodec objectCodec; private final boolean defaultObjectCodec; private final boolean annotationCodec; @@ -78,6 +79,7 @@ public final class JsonTypeInfo { // Only the raw-class ObjectCodec can be replaced by raw-class generated capabilities. // ParameterizedObjectCodec owns binding-specific field types and must remain the slot owner. defaultObjectCodec = codec.getClass() == ObjectCodec.class; + objectCodec = defaultObjectCodec ? (ObjectCodec) codec : null; } public Type type() { @@ -136,6 +138,12 @@ public boolean usesDefaultObjectCodec() { return defaultObjectCodec; } + /** Returns the raw object codec used to evaluate generated codec subtrees, if available. */ + @Internal + public ObjectCodec objectCodec() { + return objectCodec; + } + @Internal public boolean usesAnnotationCodec() { return annotationCodec; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 3a6a619575..56843447cf 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -1437,6 +1437,9 @@ private void writeStringCharsNoEnsure(CharSequence value) { private void writeLatin1StringNoEnsure(byte[] value) { int length = value.length; + if (length >= 32 && writeLongAsciiStringNoEnsure(value, length)) { + return; + } byte[] bytes = buffer; int pos = position; bytes[pos++] = (byte) '"'; @@ -1484,6 +1487,50 @@ private void writeLatin1StringNoEnsure(byte[] value) { position = pos; } + private boolean writeLongAsciiStringNoEnsure(byte[] value, int length) { + if (!isJsonAsciiBytes(value, length)) { + return false; + } + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + System.arraycopy(value, 0, bytes, pos, length); + pos += length; + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + + private static boolean isJsonAsciiBytes(byte[] value, int length) { + int i = 0; + int upperBound = length & ~15; + for (; i < upperBound; i += 16) { + long word0 = LittleEndian.getInt64(value, i); + long word1 = LittleEndian.getInt64(value, i + 8); + if (!isJsonAsciiWords(word0, word1)) { + return false; + } + } + upperBound = length & ~7; + for (; i < upperBound; i += 8) { + if (!isJsonAsciiWord(LittleEndian.getInt64(value, i))) { + return false; + } + } + if (i + 4 <= length) { + if (!isJsonAsciiInt(LittleEndian.getInt32(value, i))) { + return false; + } + i += 4; + } + for (; i < length; i++) { + if (!isJsonLatin1Byte(value[i]) || value[i] < 0) { + return false; + } + } + return true; + } + private void writeStringSlow(String value, int index, int length) { for (int i = index; i < length; i++) { char ch = value.charAt(i); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index ae6cfc11fe..afd0ab37e4 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -540,16 +540,18 @@ public void writeIntField(byte[] namePrefix, byte[] commaNamePrefix, int index, } public void writeIntField( - long namePrefix, - long commaPrefix, + long namePrefix0, + long namePrefix1, + long commaPrefix0, + long commaPrefix1, int namePrefixLength, int commaPrefixLength, int index, int value) { if (index == 0) { - writeIntField(namePrefix, 0L, namePrefixLength, value); + writeIntField(namePrefix0, namePrefix1, namePrefixLength, value); } else { - writeIntField(commaPrefix, 0L, commaPrefixLength, value); + writeIntField(commaPrefix0, commaPrefix1, commaPrefixLength, value); } } @@ -587,6 +589,22 @@ public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, writeLongField(prefix, value); } + public void writeLongField( + long namePrefix0, + long namePrefix1, + long commaPrefix0, + long commaPrefix1, + int namePrefixLength, + int commaPrefixLength, + int index, + long value) { + if (index == 0) { + writeLongField(namePrefix0, namePrefix1, namePrefixLength, value); + } else { + writeLongField(commaPrefix0, commaPrefix1, commaPrefixLength, value); + } + } + public void writeLongField(byte[] prefix, long value) { ensure(prefix.length + 20); writeRawNoEnsure(prefix); @@ -628,16 +646,18 @@ public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int inde } public void writeStringField( - long namePrefix, - long commaPrefix, + long namePrefix0, + long namePrefix1, + long commaPrefix0, + long commaPrefix1, int namePrefixLength, int commaPrefixLength, int index, String value) { if (index == 0) { - writeStringField(namePrefix, 0L, namePrefixLength, value); + writeStringField(namePrefix0, namePrefix1, namePrefixLength, value); } else { - writeStringField(commaPrefix, 0L, commaPrefixLength, value); + writeStringField(commaPrefix0, commaPrefix1, commaPrefixLength, value); } } @@ -860,6 +880,21 @@ public void writeRawValue(long prefix0, long prefix1, int prefixLength) { writePackedRawNoEnsure(prefix0, prefix1, prefixLength); } + public void writeRawValue( + long namePrefix0, + long namePrefix1, + long commaPrefix0, + long commaPrefix1, + int namePrefixLength, + int commaPrefixLength, + int index) { + if (index == 0) { + writeRawValue(namePrefix0, namePrefix1, namePrefixLength); + } else { + writeRawValue(commaPrefix0, commaPrefix1, commaPrefixLength); + } + } + /** Writes a byte array as a quoted Base64 JSON string without an intermediate String. */ public void writeBase64(byte[] value) { int encodedLength = base64Length(value.length); @@ -991,8 +1026,11 @@ private boolean writeLatin1StringNoEnsure(byte[] value) { } private boolean writeLatin1StringNoEnsure(byte[] value, int length) { - if (length < 32) { - return writeShortLatin1StringNoEnsure(value, length); + if (length <= 16) { + return writeLatin1String0To16(value, length); + } + if (length <= 56) { + return writeMediumLatin1StringNoEnsure(value, length); } return writeLongLatin1StringNoEnsure(value, length); } @@ -1053,18 +1091,13 @@ private static boolean isJsonAsciiBytes(byte[] value, int length) { return true; } - private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { - if (length > 24) { - return writeLatin1String25To31(value, length); - } - if (length > 16) { - return writeLatin1String17To24(value, length); - } + private boolean writeLatin1String0To16(byte[] value, int length) { byte[] bytes = buffer; int pos = position; bytes[pos++] = (byte) '"'; - // Short compact strings dominate generated JSON writers. Keep the 8-16 byte path exact here; - // longer short-string bands stay in helpers so this common path remains small. + // Keep the complete one/two-machine-word path in one real owner. Splitting the sub-8 body lets + // C2 absorb different short-string graphs into generated field groups depending on compilation + // order, while merging longer bands makes this common compilation unit unnecessarily large. // Position is published only after complete validation, so every false return leaves the // writer cursor unchanged even though scratch bytes may already have been overwritten. if (length >= 8) { @@ -1110,21 +1143,7 @@ private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { } bytes[pos++] = tail; } - } else { - // Keep the sub-8 tail outside this method so the common word-sized short-string path stays - // small enough to inline into generated object writers. - return writeLatin1String0To7(value, length); - } - bytes[pos++] = (byte) '"'; - position = pos; - return true; - } - - private boolean writeLatin1String0To7(byte[] value, int length) { - byte[] bytes = buffer; - int pos = position; - bytes[pos++] = (byte) '"'; - if (length >= 4) { + } else if (length >= 4) { int word = LittleEndian.getInt32(value, 0); if (!isJsonAsciiInt(word)) { return false; @@ -1166,41 +1185,134 @@ private boolean writeLatin1String0To7(byte[] value, int length) { return true; } - private boolean writeLatin1String25To31(byte[] value, int length) { + private boolean writeMediumLatin1StringNoEnsure(byte[] value, int length) { + // Keep medium validation behind one real C2 boundary while leaving the common bands compact. + // The 33-56 branches contain actual fixed-word work; the larger scanner remains a separate + // long-string owner so it cannot enter generated field groups through this method. + if (length <= 24) { + return writeLatin1String17To24(value, length); + } + if (length <= 32) { + return writeLatin1String25To32(value, length); + } byte[] bytes = buffer; int pos = position; bytes[pos++] = (byte) '"'; long word0 = LittleEndian.getInt64(value, 0); long word1 = LittleEndian.getInt64(value, 8); long word2 = LittleEndian.getInt64(value, 16); + long word3 = LittleEndian.getInt64(value, 24); int tailOffset = length - Long.BYTES; long tail = LittleEndian.getInt64(value, tailOffset); - if (!isJsonAsciiWords(word0, word1, word2, tail)) { + if (length <= 40) { + if (!isJsonAsciiWords(word0, word1, word2, word3) || !isJsonAsciiWord(tail)) { + return false; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + LittleEndian.putInt64(bytes, pos + 16, word2); + LittleEndian.putInt64(bytes, pos + 24, word3); + LittleEndian.putInt64(bytes, pos + tailOffset, tail); + } else { + long word4 = LittleEndian.getInt64(value, 32); + if (length <= 48) { + if (!isJsonAsciiWords(word0, word1, word2, word3) || !isJsonAsciiWords(word4, tail)) { + return false; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + LittleEndian.putInt64(bytes, pos + 16, word2); + LittleEndian.putInt64(bytes, pos + 24, word3); + LittleEndian.putInt64(bytes, pos + 32, word4); + LittleEndian.putInt64(bytes, pos + tailOffset, tail); + } else { + long word5 = LittleEndian.getInt64(value, 40); + if (!isJsonAsciiWords(word0, word1, word2, word3) + || !isJsonAsciiWords(word4, word5, tail)) { + return false; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + LittleEndian.putInt64(bytes, pos + 16, word2); + LittleEndian.putInt64(bytes, pos + 24, word3); + LittleEndian.putInt64(bytes, pos + 32, word4); + LittleEndian.putInt64(bytes, pos + 40, word5); + LittleEndian.putInt64(bytes, pos + tailOffset, tail); + } + } + pos += length; + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + + private boolean writeLatin1String17To24(byte[] value, int length) { + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + long word0 = LittleEndian.getInt64(value, 0); + long word1 = LittleEndian.getInt64(value, 8); + if (!isJsonAsciiWords(word0, word1)) { return false; } LittleEndian.putInt64(bytes, pos, word0); LittleEndian.putInt64(bytes, pos + 8, word1); - LittleEndian.putInt64(bytes, pos + 16, word2); - LittleEndian.putInt64(bytes, pos + tailOffset, tail); + // The first two words already cover 16 bytes. Consume only the real remainder so + // non-word-aligned values do not repeat validation and destination stores. + int index = 16; + if (index + Long.BYTES <= length) { + long tail = LittleEndian.getInt64(value, index); + if (!isJsonAsciiWord(tail)) { + return false; + } + LittleEndian.putInt64(bytes, pos + index, tail); + } else { + if (index + Integer.BYTES <= length) { + int tail = LittleEndian.getInt32(value, index); + if (!isJsonAsciiInt(tail)) { + return false; + } + LittleEndian.putInt32(bytes, pos + index, tail); + index += Integer.BYTES; + } + if (index + Short.BYTES <= length) { + int tail = (value[index] & 0xFF) | ((value[index + 1] & 0xFF) << 8); + if (!isJsonAsciiShort(tail)) { + return false; + } + bytes[pos + index] = (byte) tail; + bytes[pos + index + 1] = (byte) (tail >>> 8); + index += Short.BYTES; + } + if (index < length) { + byte tail = value[index]; + if (!isJsonAsciiByte(tail)) { + return false; + } + bytes[pos + index] = tail; + } + } pos += length; bytes[pos++] = (byte) '"'; position = pos; return true; } - private boolean writeLatin1String17To24(byte[] value, int length) { + private boolean writeLatin1String25To32(byte[] value, int length) { byte[] bytes = buffer; int pos = position; bytes[pos++] = (byte) '"'; long word0 = LittleEndian.getInt64(value, 0); long word1 = LittleEndian.getInt64(value, 8); + long word2 = LittleEndian.getInt64(value, 16); int tailOffset = length - Long.BYTES; long tail = LittleEndian.getInt64(value, tailOffset); - if (!isJsonAsciiWords(word0, word1, tail)) { + if (!isJsonAsciiWords(word0, word1, word2, tail)) { return false; } LittleEndian.putInt64(bytes, pos, word0); LittleEndian.putInt64(bytes, pos + 8, word1); + LittleEndian.putInt64(bytes, pos + 16, word2); LittleEndian.putInt64(bytes, pos + tailOffset, tail); pos += length; bytes[pos++] = (byte) '"'; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index 19e47be754..c1e3cf05c3 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -33,6 +33,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.data.GeneratedCollectionFields; import org.apache.fory.json.data.PublicFields; import org.apache.fory.json.data.RecursiveChild; @@ -43,6 +44,7 @@ import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; import org.apache.fory.json.resolver.JsonTypeResolver; import org.testng.annotations.Test; @@ -112,6 +114,41 @@ public void writeGeneratedTokenLanes(boolean codegen) { assertGeneratedWhenSupported(json, TokenValues.class, codegen); } + @Test + public void generatedWriteBoundaries() { + ForyJson json = newJson(true); + GroupedFields fields = new GroupedFields(); + fields.f0 = "zero"; + fields.f1 = "one"; + fields.f2 = "two"; + fields.f3 = "three"; + fields.f4 = "four"; + fields.f5 = "five"; + GroupedContainer container = new GroupedContainer(); + container.values = Arrays.asList(fields); + String expected = + "{\"values\":[{\"f0\":\"zero\",\"f1\":\"one\",\"f2\":\"two\"," + + "\"f3\":\"three\",\"f4\":\"four\",\"f5\":\"five\"}]}"; + assertEquals(json.toJson(container), expected); + assertEquals(new String(json.toJsonBytes(container), StandardCharsets.UTF_8), expected); + assertEquals(json.toJson(null, GroupedFields.class), "null"); + assertEquals( + new String(json.toJsonBytes(null, GroupedFields.class), StandardCharsets.UTF_8), "null"); + + JsonTypeResolver resolver = primaryTypeResolver(json); + JsonTypeInfo grouped = resolver.getTypeInfo(GroupedFields.class, GroupedFields.class); + assertTrue(grouped.stringWriter() instanceof JsonTrampolineInvoke.StringWriteGroup); + assertTrue(grouped.utf8Writer() instanceof JsonTrampolineInvoke.Utf8WriteGroup); + + JsonTypeInfo small = resolver.getTypeInfo(PublicFields.class, PublicFields.class); + assertFalse(small.stringWriter() instanceof JsonTrampolineInvoke.StringWriteGroup); + assertFalse(small.utf8Writer() instanceof JsonTrampolineInvoke.Utf8WriteGroup); + + JsonTypeInfo wide = resolver.getTypeInfo(WideFields.class, WideFields.class); + assertFalse(wide.stringWriter() instanceof JsonTrampolineInvoke.StringWriteGroup); + assertFalse(wide.utf8Writer() instanceof JsonTrampolineInvoke.Utf8WriteGroup); + } + @Test(dataProvider = "enableCodegen") public void readGeneratedObjectCollection(boolean codegen) { ForyJson json = newJson(codegen); @@ -336,6 +373,19 @@ public static class WideFields { public String f13; } + public static final class GroupedContainer { + public List values; + } + + public static final class GroupedFields { + public String f0; + public String f1; + public String f2; + public String f3; + public String f4; + public String f5; + } + public static final class ObjectCollections { public List values; public Set set; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index 4e9b246472..366d94a58a 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -429,7 +429,8 @@ public void writeStringScanBoundaries() { ForyJson json = newJson(); for (int length : new int[] { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 20, 23, 24, 25, 30, 31, 32, 33, 63, 64, 65 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 20, 23, 24, 25, 30, 31, 32, 33, 39, 40, 41, + 47, 48, 49, 55, 56, 57, 63, 64, 65 }) { String value = repeat('a', length); String expected = "\"" + value + "\""; @@ -468,6 +469,15 @@ public void writeStringScanBoundaries() { + "\\n\""; assertEquals(json.toJson(escaped30), expected30); assertEquals(new String(json.toJsonBytes(escaped30), StandardCharsets.UTF_8), expected30); + + for (int length : new int[] {40, 48, 56, 64}) { + String prefix = repeat('e', length - 1); + String escapedBoundary = prefix + "\n"; + String expectedBoundary = "\"" + prefix + "\\n\""; + assertEquals(json.toJson(escapedBoundary), expectedBoundary); + assertEquals( + new String(json.toJsonBytes(escapedBoundary), StandardCharsets.UTF_8), expectedBoundary); + } } @Test