From 99345bb661a671f4236ddee986699a665cdb5179 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 17 Jul 2026 12:36:05 +0800 Subject: [PATCH 1/9] perf(java): accelerate short JSON field names --- .../fory/json/reader/Latin1JsonReader.java | 23 +++++++++++++++++++ .../fory/json/reader/Utf8JsonReader.java | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) 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..773922f68a 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 @@ -2522,6 +2522,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++] != '"') { From a18f6bd31866b0a32dd7c0982fdbd4ffc4edcf65 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 17 Jul 2026 13:12:22 +0800 Subject: [PATCH 2/9] perf(java): accelerate long JSON strings --- .../fory/json/writer/StringJsonWriter.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) 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); From 5e9643907c6f8111c3aa805e8870292487295828 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 17 Jul 2026 13:35:54 +0800 Subject: [PATCH 3/9] perf(java): pack dynamic JSON field prefixes --- .../fory/json/codegen/Utf8WriterCodegen.java | 49 ++++++------------ .../fory/json/writer/Utf8JsonWriter.java | 51 ++++++++++++++++--- 2 files changed, 59 insertions(+), 41 deletions(-) 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..1fa0ca63a7 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 @@ -101,10 +101,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 +116,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 +209,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 +237,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 +262,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 +364,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 +380,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/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index ae6cfc11fe..f430f62fb3 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); From 0a534d1f1cf331b444ec0fcea25d14eb83e91429 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 17 Jul 2026 17:39:25 +0800 Subject: [PATCH 4/9] perf(java): isolate generated JSON write groups Route complete codec and generated member-group calls through shared polymorphic trampoline sites so C2 cannot recursively absorb the full writer graph while group-local scalar calls still inline.\n\nOpenJDK 25 JsonSerializationSuite.foryToJsonBytes: 10.317M ops/s, 504.000 B/op. Full fory-json suite: 742 tests passed. --- .agents/languages/java.md | 6 + .../fory-performance-optimization/SKILL.md | 4 + .../java/org/apache/fory/json/ForyJson.java | 16 +- .../apache/fory/json/codec/ArrayCodec.java | 8 +- .../fory/json/codec/ClosedSubtypeCodec.java | 8 +- .../fory/json/codec/CollectionCodec.java | 12 +- .../fory/json/codec/JsonTrampolineInvoke.java | 77 ++++++ .../org/apache/fory/json/codec/MapCodec.java | 4 +- .../apache/fory/json/codec/ObjectCodec.java | 8 +- .../apache/fory/json/codec/ScalarCodecs.java | 16 +- .../fory/json/codegen/JsonWriterCodegen.java | 242 ++++++++++++++++-- .../json/codegen/StringWriterCodegen.java | 21 ++ .../fory/json/codegen/Utf8WriterCodegen.java | 21 ++ .../apache/fory/json/meta/JsonFieldInfo.java | 21 +- .../resolver/InheritedJsonValueCodec.java | 5 +- 15 files changed, 402 insertions(+), 67 deletions(-) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java diff --git a/.agents/languages/java.md b/.agents/languages/java.md index 856db7dc00..8a4f58698b 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -92,6 +92,12 @@ 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, route multiple hot receiver implementations through one + shared ordinary-class trampoline `invokeinterface` bytecode. The polymorphic call site prevents C2 + from recursively inlining the whole child or member subtree into its parent, while each receiver + remains an independent compilation unit whose internal scalar operations can still inline. Do not + replace that boundary with per-generated-class interface calls or a receiver-specific trampoline + 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..2c152c6a92 100644 --- a/.agents/skills/fory-performance-optimization/SKILL.md +++ b/.agents/skills/fory-performance-optimization/SKILL.md @@ -20,6 +20,10 @@ 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, route multiple hot receiver implementations through one + shared trampoline `invokeinterface` bytecode. 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/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index d9ed4f2c6c..fcdeb09a70 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -28,6 +28,7 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; +import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf16JsonReader; import org.apache.fory.json.reader.Utf8JsonReader; @@ -120,7 +121,7 @@ public String toJson(Object value) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); - typeInfo.stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(typeInfo.stringWriter(), writer, value); } } finally { state.typeResolver.unlockJIT(); @@ -174,7 +175,7 @@ public byte[] toJsonBytes(Object value) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); - typeInfo.utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(typeInfo.utf8Writer(), writer, value); } } finally { state.typeResolver.unlockJIT(); @@ -223,7 +224,7 @@ public void writeJsonTo(Object value, OutputStream output) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); - typeInfo.utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(typeInfo.utf8Writer(), writer, value); } } finally { state.typeResolver.unlockJIT(); @@ -267,7 +268,8 @@ private String toJsonDeclared(Object value, Type type, Class fallback) { try { state.typeResolver.lockJIT(); try { - state.rootTypeInfo(type, fallback).stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue( + state.rootTypeInfo(type, fallback).stringWriter(), writer, value); } finally { state.typeResolver.unlockJIT(); } @@ -288,7 +290,8 @@ private byte[] toJsonBytesDeclared(Object value, Type type, Class fallback) { try { state.typeResolver.lockJIT(); try { - state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value( + state.rootTypeInfo(type, fallback).utf8Writer(), writer, value); } finally { state.typeResolver.unlockJIT(); } @@ -310,7 +313,8 @@ private void writeJsonDeclared(Object value, Type type, Class fallback, Outpu try { state.typeResolver.lockJIT(); try { - state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value( + state.rootTypeInfo(type, fallback).utf8Writer(), writer, value); } finally { state.typeResolver.unlockJIT(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index c3b514e911..654ad42ba9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -1585,7 +1585,7 @@ public void writeString(StringJsonWriter writer, T value) { writer.writeArrayStart(); for (int i = 0; i < array.length; i++) { writer.writeComma(i); - codec.writeString(writer, array[i]); + JsonTrampolineInvoke.writeStringValue(codec, writer, array[i]); } writer.writeArrayEnd(); } @@ -1601,7 +1601,7 @@ public void writeUtf8(Utf8JsonWriter writer, T value) { writer.writeArrayStart(); for (int i = 0; i < array.length; i++) { writer.writeComma(i); - codec.writeUtf8(writer, array[i]); + JsonTrampolineInvoke.writeUtf8Value(codec, writer, array[i]); } writer.writeArrayEnd(); } @@ -1770,7 +1770,7 @@ public void writeString(StringJsonWriter writer, T value) { int length = Array.getLength(value); for (int i = 0; i < length; i++) { writer.writeComma(i); - codec.writeString(writer, Array.get(value, i)); + JsonTrampolineInvoke.writeStringValue(codec, writer, Array.get(value, i)); } writer.writeArrayEnd(); } @@ -1786,7 +1786,7 @@ public void writeUtf8(Utf8JsonWriter writer, T value) { int length = Array.getLength(value); for (int i = 0; i < length; i++) { writer.writeComma(i); - codec.writeUtf8(writer, Array.get(value, i)); + JsonTrampolineInvoke.writeUtf8Value(codec, writer, Array.get(value, i)); } writer.writeArrayEnd(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java index c118415083..28262624bd 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java @@ -131,14 +131,14 @@ public void writeString(StringJsonWriter writer, Object value) { writer.writeObjectStart(); writer.writeRawValue( definition.stringSubtypePrefixes[index], definition.stringUtf16SubtypePrefixes[index]); - children[index].stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(children[index].stringWriter(), writer, value); writer.writeObjectEnd(); return; } writer.writeArrayStart(); writer.writeRawValue( definition.stringSubtypePrefixes[index], definition.stringUtf16SubtypePrefixes[index]); - children[index].stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(children[index].stringWriter(), writer, value); writer.writeArrayEnd(); } @@ -159,13 +159,13 @@ public void writeUtf8(Utf8JsonWriter writer, Object value) { if (definition.inclusion == Inclusion.WRAPPER_OBJECT) { writer.writeObjectStart(); writer.writeRawValue(definition.utf8SubtypePrefixes[index]); - children[index].utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(children[index].utf8Writer(), writer, value); writer.writeObjectEnd(); return; } writer.writeArrayStart(); writer.writeRawValue(definition.utf8SubtypePrefixes[index]); - children[index].utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(children[index].utf8Writer(), writer, value); writer.writeArrayEnd(); } 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..a4c3f4b249 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 @@ -749,7 +749,7 @@ public void writeString(StringJsonWriter writer, Collection value) { int index = 0; for (Object element : value) { writer.writeComma(index++); - codec.writeString(writer, element); + JsonTrampolineInvoke.writeStringValue(codec, writer, element); } writer.writeArrayEnd(); } @@ -765,7 +765,7 @@ public void writeUtf8(Utf8JsonWriter writer, Collection value) { int index = 0; for (Object element : value) { writer.writeComma(index++); - codec.writeUtf8(writer, element); + JsonTrampolineInvoke.writeUtf8Value(codec, writer, element); } writer.writeArrayEnd(); } @@ -846,13 +846,13 @@ public void writeString(StringJsonWriter writer, Collection value) { for (int index = 0, size = list.size(); index < size; index++) { Object element = list.get(index); writer.writeComma(index); - codec.writeString(writer, element); + JsonTrampolineInvoke.writeStringValue(codec, writer, element); } } else { int index = 0; for (Object element : value) { writer.writeComma(index++); - codec.writeString(writer, element); + JsonTrampolineInvoke.writeStringValue(codec, writer, element); } } writer.writeArrayEnd(); @@ -871,13 +871,13 @@ public void writeUtf8(Utf8JsonWriter writer, Collection value) { for (int index = 0, size = list.size(); index < size; index++) { Object element = list.get(index); writer.writeComma(index); - codec.writeUtf8(writer, element); + JsonTrampolineInvoke.writeUtf8Value(codec, writer, element); } } else { int index = 0; for (Object element : value) { writer.writeComma(index++); - codec.writeUtf8(writer, element); + JsonTrampolineInvoke.writeUtf8Value(codec, writer, element); } } writer.writeArrayEnd(); 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..fa2107a399 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java @@ -0,0 +1,77 @@ +/* + * 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.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; + +/** + * Shared interface invocation sites used by JSON codec dispatch. + * + *

JSON codec paths call these ordinary-class methods instead of emitting an interface-call + * bytecode at each caller. Multiple hot receiver implementations therefore share one call-site + * profile. On HotSpot, that polymorphic profile keeps C2 from recursively inlining a complete + * child-codec or member-group graph into its parent. Each receiver is still compiled as an + * independent unit, where its scalar writer calls remain eligible for inlining. + * + *

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 group of UTF-8 member writes bound to that codec instance. */ + @Internal + public interface Utf8WriteGroup { + int write(Utf8JsonWriter writer, Object value, int written); + } + + /** A generated-codec-owned group of String member writes bound to that codec instance. */ + @Internal + public interface StringWriteGroup { + int write(StringJsonWriter writer, Object value, int written); + } + + private JsonTrampolineInvoke() {} + + /** Invokes a UTF-8 child codec through the shared complete-value call site. */ + public static void writeUtf8Value(Utf8WriterCodec codec, Utf8JsonWriter writer, T value) { + codec.writeUtf8(writer, value); + } + + /** Invokes a String child codec through the shared complete-value call site. */ + public static void writeStringValue( + StringWriterCodec codec, StringJsonWriter writer, T value) { + codec.writeString(writer, value); + } + + /** Invokes a UTF-8 member group through the shared group call site. */ + public static int writeUtf8Group( + Utf8WriteGroup group, Utf8JsonWriter writer, Object value, int written) { + return group.write(writer, value, written); + } + + /** Invokes a String member group through the shared group call site. */ + public static int writeStringGroup( + StringWriteGroup group, StringJsonWriter writer, Object value, int written) { + return group.write(writer, value, written); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java index 305df441bf..c6b19254dc 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java @@ -379,7 +379,7 @@ public void writeString(StringJsonWriter writer, Map value) { for (Map.Entry entry : value.entrySet()) { writer.writeComma(index++); writeKey(writer, entry.getKey(), keyCodec); - codec.writeString(writer, entry.getValue()); + JsonTrampolineInvoke.writeStringValue(codec, writer, entry.getValue()); } writer.writeObjectEnd(); } @@ -396,7 +396,7 @@ public void writeUtf8(Utf8JsonWriter writer, Map value) { for (Map.Entry entry : value.entrySet()) { writer.writeComma(index++); writeKey(writer, entry.getKey(), keyCodec); - codec.writeUtf8(writer, entry.getValue()); + JsonTrampolineInvoke.writeUtf8Value(codec, writer, entry.getValue()); } writer.writeObjectEnd(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java index ace30ddc9c..be534a6c7e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java @@ -294,7 +294,7 @@ public final int writeStringAny( } writer.writeComma(written); writer.writeFieldName(name); - codec.writeString(writer, entry.getValue()); + JsonTrampolineInvoke.writeStringValue(codec, writer, entry.getValue()); written = 1; } return written; @@ -318,7 +318,7 @@ public final int writeUtf8Any( } writer.writeComma(written); writer.writeFieldName(name); - codec.writeUtf8(writer, entry.getValue()); + JsonTrampolineInvoke.writeUtf8Value(codec, writer, entry.getValue()); written = 1; } return written; @@ -351,7 +351,7 @@ private ForyJsonException reservedAnyName(String name) { public void writeString(StringJsonWriter writer, T value) { StringWriterCodec codec = writer.typeResolver().stringWriter(this); if (codec != this) { - codec.writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(codec, writer, value); } else if (value == null) { writer.writeNull(); } else { @@ -363,7 +363,7 @@ public void writeString(StringJsonWriter writer, T value) { public void writeUtf8(Utf8JsonWriter writer, T value) { Utf8WriterCodec codec = writer.typeResolver().utf8Writer(this); if (codec != this) { - codec.writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(codec, writer, value); } else if (value == null) { writer.writeNull(); } else { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index dd1ad5e80e..77a4c95af5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -127,7 +127,7 @@ public void writeString(StringJsonWriter writer, Object value) { return; } JsonTypeInfo typeInfo = writer.typeResolver().getRuntimeTypeInfo(value.getClass()); - typeInfo.stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(typeInfo.stringWriter(), writer, value); } @Override @@ -137,7 +137,7 @@ public void writeUtf8(Utf8JsonWriter writer, Object value) { return; } JsonTypeInfo typeInfo = writer.typeResolver().getRuntimeTypeInfo(value.getClass()); - typeInfo.utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(typeInfo.utf8Writer(), writer, value); } @Override @@ -2653,7 +2653,7 @@ public void writeString(StringJsonWriter writer, AtomicReference value) { if (value == null) { writer.writeNull(); } else { - valueTypeInfo.stringWriter().writeString(writer, value.get()); + JsonTrampolineInvoke.writeStringValue(valueTypeInfo.stringWriter(), writer, value.get()); } } @@ -2662,7 +2662,7 @@ public void writeUtf8(Utf8JsonWriter writer, AtomicReference value) { if (value == null) { writer.writeNull(); } else { - valueTypeInfo.utf8Writer().writeUtf8(writer, value.get()); + JsonTrampolineInvoke.writeUtf8Value(valueTypeInfo.utf8Writer(), writer, value.get()); } } @@ -2952,7 +2952,7 @@ public void writeString(StringJsonWriter writer, AtomicReferenceArray value) writer.writeArrayStart(); for (int i = 0, length = array.length(); i < length; i++) { writer.writeComma(i); - codec.writeString(writer, array.get(i)); + JsonTrampolineInvoke.writeStringValue(codec, writer, array.get(i)); } writer.writeArrayEnd(); } @@ -2968,7 +2968,7 @@ public void writeUtf8(Utf8JsonWriter writer, AtomicReferenceArray value) { writer.writeArrayStart(); for (int i = 0, length = array.length(); i < length; i++) { writer.writeComma(i); - codec.writeUtf8(writer, array.get(i)); + JsonTrampolineInvoke.writeUtf8Value(codec, writer, array.get(i)); } writer.writeArrayEnd(); } @@ -3070,7 +3070,7 @@ public void writeString(StringJsonWriter writer, Optional value) { } Optional optional = value; if (optional.isPresent()) { - valueTypeInfo.stringWriter().writeString(writer, optional.get()); + JsonTrampolineInvoke.writeStringValue(valueTypeInfo.stringWriter(), writer, optional.get()); } else { writer.writeNull(); } @@ -3084,7 +3084,7 @@ public void writeUtf8(Utf8JsonWriter writer, Optional value) { } Optional optional = value; if (optional.isPresent()) { - valueTypeInfo.utf8Writer().writeUtf8(writer, optional.get()); + JsonTrampolineInvoke.writeUtf8Value(valueTypeInfo.utf8Writer(), writer, optional.get()); } else { writer.writeNull(); } 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..a3e77a2637 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,7 @@ 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.JsonTrampolineInvoke; import org.apache.fory.json.codec.JsonUnwrappedInfo; import org.apache.fory.json.codec.JsonUnwrappedInfo.Group; import org.apache.fory.json.codec.JsonUnwrappedInfo.WriteEntry; @@ -51,14 +52,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; @@ -77,14 +80,22 @@ abstract class JsonWriterCodegen { 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 writeValueMethod(); + abstract String writeAnyMethod(); abstract int splitMemberThreshold(); + abstract int memberPrefixCost(JsonFieldInfo property); + abstract PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused); abstract void addPrefixFields( @@ -197,13 +208,6 @@ 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. @@ -234,6 +238,13 @@ 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(), @@ -841,7 +852,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 +863,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 +875,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 +1047,195 @@ 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); + // Keep all generated callers on the shared interface-call BCI. Its multiple hot group + // receivers stop C2 from absorbing the complete member graph into this writer, while each + // receiver can compile independently and inline the scalar writes inside its own group. + Expression call = + new Expression.StaticInvoke( + JsonTrampolineInvoke.class, + writeMemberGroupMethod(), + TypeRef.of(int.class), + fieldRef(fieldName, memberGroupType()), + writer, + object, + written) + .inline(); + expressions.add(new Expression.Assign(written, call)); + 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); + } + Reference written = new Reference("written", TypeRef.of(int.class)); + body.add(new Expression.Return(written)); + addGeneratedMethod( + builder.context(), + "final", + methodName, + body, + int.class, + writerType(), + "writer", + ownerType, + "object", + int.class, + "written"); + } + + 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 int write(" + + writerType + + " writer, Object value, int written) {\n" + + " return " + + builder.codecClassName(ownerType) + + ".this." + + methodName + + "(writer, (" + + valueType + + ") value, written);\n" + + " }\n" + + "};"); + } + + 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; @@ -1256,7 +1453,10 @@ private Expression writeCodec( object && property.writeRawType() == ownerType ? new Reference("this", TypeRef.of(completeWriterType())) : fieldRef("w" + id, codecFieldType(property)); - return new Expression.Invoke(codec, writeMethod(), writer, value); + // A shared interface-call BCI prevents a monomorphic generated call site from recursively + // pulling the complete child-codec graph into its parent. + return new Expression.StaticInvoke( + JsonTrampolineInvoke.class, writeValueMethod(), codec, writer, value); } private boolean storesWriteCodec(JsonFieldInfo property) { 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..6b5cde31d1 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; @@ -65,6 +66,21 @@ String memberGroupMethod() { return "writeStringMembers"; } + @Override + Class memberGroupType() { + return JsonTrampolineInvoke.StringWriteGroup.class; + } + + @Override + String writeMemberGroupMethod() { + return "writeStringGroup"; + } + + @Override + String writeValueMethod() { + return "writeStringValue"; + } + @Override String writeAnyMethod() { return "writeStringAny"; @@ -75,6 +91,11 @@ int splitMemberThreshold() { return MIN_SPLIT_MEMBERS; } + @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/Utf8WriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java index 1fa0ca63a7..923cd6f578 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; @@ -74,6 +75,21 @@ String memberGroupMethod() { return "writeUtf8Members"; } + @Override + Class memberGroupType() { + return JsonTrampolineInvoke.Utf8WriteGroup.class; + } + + @Override + String writeMemberGroupMethod() { + return "writeUtf8Group"; + } + + @Override + String writeValueMethod() { + return "writeUtf8Value"; + } + @Override String writeAnyMethod() { return "writeUtf8Any"; @@ -84,6 +100,11 @@ int splitMemberThreshold() { return MIN_SPLIT_MEMBERS; } + @Override + int memberPrefixCost(JsonFieldInfo property) { + return usesPrefix(property) ? 1 : 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/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index 00b2c73903..88f1111e3e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -28,6 +28,7 @@ import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.annotation.JsonCodec; import org.apache.fory.json.codec.CodecUtils; +import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.reader.JsonReader; import org.apache.fory.json.reader.Latin1JsonReader; @@ -832,7 +833,7 @@ private boolean writeStringObject(StringJsonWriter writer, Object object, int in return false; } writer.writeFieldName(this, index); - writeTypeInfo.stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); return true; } @@ -962,7 +963,7 @@ private boolean writeStringArray(StringJsonWriter writer, Object object, int ind } // Field metadata owns omission only. Once present, the registered codec owns null semantics. writer.writeFieldName(this, index); - writeTypeInfo.stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); return true; } @@ -972,7 +973,7 @@ private boolean writeStringCollection(StringJsonWriter writer, Object object, in return false; } writer.writeFieldName(this, index); - writeTypeInfo.stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); return true; } @@ -982,7 +983,7 @@ private boolean writeStringMap(StringJsonWriter writer, Object object, int index return false; } writer.writeFieldName(this, index); - writeTypeInfo.stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); return true; } @@ -992,7 +993,7 @@ private boolean writeStringPojo(StringJsonWriter writer, Object object, int inde return false; } writer.writeFieldName(this, index); - writeTypeInfo.stringWriter().writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); return true; } @@ -1072,7 +1073,7 @@ private boolean writeUtf8Object(Utf8JsonWriter writer, Object object, int index) return false; } writer.writeFieldName(this, index); - writeTypeInfo.utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); return true; } @@ -1138,7 +1139,7 @@ private boolean writeUtf8Array(Utf8JsonWriter writer, Object object, int index) } // Field metadata owns omission only. Once present, the registered codec owns null semantics. writer.writeFieldName(this, index); - writeTypeInfo.utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); return true; } @@ -1148,7 +1149,7 @@ private boolean writeUtf8Collection(Utf8JsonWriter writer, Object object, int in return false; } writer.writeFieldName(this, index); - writeTypeInfo.utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); return true; } @@ -1158,7 +1159,7 @@ private boolean writeUtf8Map(Utf8JsonWriter writer, Object object, int index) { return false; } writer.writeFieldName(this, index); - writeTypeInfo.utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); return true; } @@ -1168,7 +1169,7 @@ private boolean writeUtf8Pojo(Utf8JsonWriter writer, Object object, int index) { return false; } writer.writeFieldName(this, index); - writeTypeInfo.utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); return true; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/InheritedJsonValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/InheritedJsonValueCodec.java index f1d487a531..d7453f38f6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/InheritedJsonValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/InheritedJsonValueCodec.java @@ -21,6 +21,7 @@ import java.lang.reflect.Type; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf16JsonReader; @@ -53,12 +54,12 @@ final class InheritedJsonValueCodec implements JsonValueCodec { @Override public void writeString(StringJsonWriter writer, Object value) { - delegate.writeString(writer, value); + JsonTrampolineInvoke.writeStringValue(delegate, writer, value); } @Override public void writeUtf8(Utf8JsonWriter writer, Object value) { - delegate.writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8Value(delegate, writer, value); } @Override From 452100f5d2bc6c9a651b1210ecb0c6d5b3cf19f5 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 17 Jul 2026 20:05:53 +0800 Subject: [PATCH 5/9] perf(java): reduce generated JSON read scans --- .../apache/fory/json/codegen/JsonReaderCodegen.java | 12 +++++++----- .../org/apache/fory/json/reader/Utf8JsonReader.java | 5 ++--- 2 files changed, 9 insertions(+), 8 deletions(-) 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..b91caefaf7 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 @@ -160,10 +160,12 @@ 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"); @@ -245,7 +247,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 +473,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 +577,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 +650,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);", 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 773922f68a..bcce0db057 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(); } From 323bf3e240eb1ec93ccb8d31748ddb43659dae8a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 17 Jul 2026 20:34:24 +0800 Subject: [PATCH 6/9] perf(java): bound UTF-8 string scanning --- .../fory/json/reader/Utf8JsonReader.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) 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 bcce0db057..826df88088 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 @@ -1899,6 +1899,10 @@ private String readStringToken() { } int start = position; int offset = start; + // Keep the complete bounded scanner as one independently compiled scalar operation. If C2 + // copies it into every generated string-field case, the enclosing object reader becomes a + // large and unstable compilation unit. These are real word probes for short and medium + // strings; the unbounded loop and end-of-input tail remain in their separate methods. if (offset + Long.BYTES <= inputLength) { long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); if (stopMask != 0) { @@ -1917,6 +1921,41 @@ private String readStringToken() { return readStringWordStop(start, offset, stopMask); } offset += Long.BYTES; + if (offset + Long.BYTES <= inputLength) { + stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + return readStringWordStop(start, offset, stopMask); + } + offset += Long.BYTES; + if (offset + Long.BYTES <= inputLength) { + stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + return readStringWordStop(start, offset, stopMask); + } + offset += Long.BYTES; + if (offset + Long.BYTES <= inputLength) { + stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + return readStringWordStop(start, offset, stopMask); + } + offset += Long.BYTES; + if (offset + Long.BYTES <= inputLength) { + stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + return readStringWordStop(start, offset, stopMask); + } + offset += Long.BYTES; + if (offset + Long.BYTES <= inputLength) { + stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + return readStringWordStop(start, offset, stopMask); + } + offset += Long.BYTES; + } + } + } + } + } } } } From 1cbc5789f8de5e6b89727482481c7098a008913e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 18 Jul 2026 03:52:23 +0800 Subject: [PATCH 7/9] perf(json): select generated writer boundaries Keep handwritten and root codec dispatch direct, and let generated object writers choose shared C2 boundaries from representation-aware resolved subtree cost. JDK 25 MediaContent: UTF-8 10.440M -> 10.502M ops/s at 504 B/op; String 6.916M -> 6.982M ops/s at 528 B/op. The Users/Clients serialization screen had no regression over 1%. All 743 fory-json tests, Spotless, and Checkstyle pass. --- .agents/languages/java.md | 11 +- .../fory-performance-optimization/SKILL.md | 9 +- .../java/org/apache/fory/json/ForyJson.java | 16 +- .../apache/fory/json/codec/ArrayCodec.java | 8 +- .../fory/json/codec/ClosedSubtypeCodec.java | 8 +- .../fory/json/codec/CollectionCodec.java | 12 +- .../fory/json/codec/JsonTrampolineInvoke.java | 49 ++--- .../org/apache/fory/json/codec/MapCodec.java | 4 +- .../apache/fory/json/codec/ObjectCodec.java | 8 +- .../apache/fory/json/codec/ScalarCodecs.java | 16 +- .../apache/fory/json/codegen/JsonCodegen.java | 4 +- .../fory/json/codegen/JsonWriterCodegen.java | 172 ++++++++++++++---- .../json/codegen/StringWriterCodegen.java | 11 +- .../fory/json/codegen/Utf8WriterCodegen.java | 11 +- .../apache/fory/json/meta/JsonFieldInfo.java | 21 +-- .../resolver/InheritedJsonValueCodec.java | 5 +- .../fory/json/resolver/JsonTypeInfo.java | 8 + .../fory/json/JsonGeneratedCodecTest.java | 50 +++++ 18 files changed, 285 insertions(+), 138 deletions(-) diff --git a/.agents/languages/java.md b/.agents/languages/java.md index 8a4f58698b..3c1c35d639 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -92,12 +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, route multiple hot receiver implementations through one +- 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 the whole child or member subtree into its parent, while each receiver - remains an independent compilation unit whose internal scalar operations can still inline. Do not - replace that boundary with per-generated-class interface calls or a receiver-specific trampoline - without `PrintInlining` proof. + 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 2c152c6a92..1228a48716 100644 --- a/.agents/skills/fory-performance-optimization/SKILL.md +++ b/.agents/skills/fory-performance-optimization/SKILL.md @@ -20,10 +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, route multiple hot receiver implementations through one - shared trampoline `invokeinterface` bytecode. Verify with `PrintInlining` that C2 retains this - outer polymorphic boundary while each independently compiled receiver still inlines its internal - scalar subtree. +- 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/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index fcdeb09a70..d9ed4f2c6c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -28,7 +28,6 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; -import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf16JsonReader; import org.apache.fory.json.reader.Utf8JsonReader; @@ -121,7 +120,7 @@ public String toJson(Object value) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); - JsonTrampolineInvoke.writeStringValue(typeInfo.stringWriter(), writer, value); + typeInfo.stringWriter().writeString(writer, value); } } finally { state.typeResolver.unlockJIT(); @@ -175,7 +174,7 @@ public byte[] toJsonBytes(Object value) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); - JsonTrampolineInvoke.writeUtf8Value(typeInfo.utf8Writer(), writer, value); + typeInfo.utf8Writer().writeUtf8(writer, value); } } finally { state.typeResolver.unlockJIT(); @@ -224,7 +223,7 @@ public void writeJsonTo(Object value, OutputStream output) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); - JsonTrampolineInvoke.writeUtf8Value(typeInfo.utf8Writer(), writer, value); + typeInfo.utf8Writer().writeUtf8(writer, value); } } finally { state.typeResolver.unlockJIT(); @@ -268,8 +267,7 @@ private String toJsonDeclared(Object value, Type type, Class fallback) { try { state.typeResolver.lockJIT(); try { - JsonTrampolineInvoke.writeStringValue( - state.rootTypeInfo(type, fallback).stringWriter(), writer, value); + state.rootTypeInfo(type, fallback).stringWriter().writeString(writer, value); } finally { state.typeResolver.unlockJIT(); } @@ -290,8 +288,7 @@ private byte[] toJsonBytesDeclared(Object value, Type type, Class fallback) { try { state.typeResolver.lockJIT(); try { - JsonTrampolineInvoke.writeUtf8Value( - state.rootTypeInfo(type, fallback).utf8Writer(), writer, value); + state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value); } finally { state.typeResolver.unlockJIT(); } @@ -313,8 +310,7 @@ private void writeJsonDeclared(Object value, Type type, Class fallback, Outpu try { state.typeResolver.lockJIT(); try { - JsonTrampolineInvoke.writeUtf8Value( - state.rootTypeInfo(type, fallback).utf8Writer(), writer, value); + state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value); } finally { state.typeResolver.unlockJIT(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 654ad42ba9..c3b514e911 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -1585,7 +1585,7 @@ public void writeString(StringJsonWriter writer, T value) { writer.writeArrayStart(); for (int i = 0; i < array.length; i++) { writer.writeComma(i); - JsonTrampolineInvoke.writeStringValue(codec, writer, array[i]); + codec.writeString(writer, array[i]); } writer.writeArrayEnd(); } @@ -1601,7 +1601,7 @@ public void writeUtf8(Utf8JsonWriter writer, T value) { writer.writeArrayStart(); for (int i = 0; i < array.length; i++) { writer.writeComma(i); - JsonTrampolineInvoke.writeUtf8Value(codec, writer, array[i]); + codec.writeUtf8(writer, array[i]); } writer.writeArrayEnd(); } @@ -1770,7 +1770,7 @@ public void writeString(StringJsonWriter writer, T value) { int length = Array.getLength(value); for (int i = 0; i < length; i++) { writer.writeComma(i); - JsonTrampolineInvoke.writeStringValue(codec, writer, Array.get(value, i)); + codec.writeString(writer, Array.get(value, i)); } writer.writeArrayEnd(); } @@ -1786,7 +1786,7 @@ public void writeUtf8(Utf8JsonWriter writer, T value) { int length = Array.getLength(value); for (int i = 0; i < length; i++) { writer.writeComma(i); - JsonTrampolineInvoke.writeUtf8Value(codec, writer, Array.get(value, i)); + codec.writeUtf8(writer, Array.get(value, i)); } writer.writeArrayEnd(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java index 28262624bd..c118415083 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java @@ -131,14 +131,14 @@ public void writeString(StringJsonWriter writer, Object value) { writer.writeObjectStart(); writer.writeRawValue( definition.stringSubtypePrefixes[index], definition.stringUtf16SubtypePrefixes[index]); - JsonTrampolineInvoke.writeStringValue(children[index].stringWriter(), writer, value); + children[index].stringWriter().writeString(writer, value); writer.writeObjectEnd(); return; } writer.writeArrayStart(); writer.writeRawValue( definition.stringSubtypePrefixes[index], definition.stringUtf16SubtypePrefixes[index]); - JsonTrampolineInvoke.writeStringValue(children[index].stringWriter(), writer, value); + children[index].stringWriter().writeString(writer, value); writer.writeArrayEnd(); } @@ -159,13 +159,13 @@ public void writeUtf8(Utf8JsonWriter writer, Object value) { if (definition.inclusion == Inclusion.WRAPPER_OBJECT) { writer.writeObjectStart(); writer.writeRawValue(definition.utf8SubtypePrefixes[index]); - JsonTrampolineInvoke.writeUtf8Value(children[index].utf8Writer(), writer, value); + children[index].utf8Writer().writeUtf8(writer, value); writer.writeObjectEnd(); return; } writer.writeArrayStart(); writer.writeRawValue(definition.utf8SubtypePrefixes[index]); - JsonTrampolineInvoke.writeUtf8Value(children[index].utf8Writer(), writer, value); + children[index].utf8Writer().writeUtf8(writer, value); writer.writeArrayEnd(); } 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 a4c3f4b249..4f527df8eb 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 @@ -749,7 +749,7 @@ public void writeString(StringJsonWriter writer, Collection value) { int index = 0; for (Object element : value) { writer.writeComma(index++); - JsonTrampolineInvoke.writeStringValue(codec, writer, element); + codec.writeString(writer, element); } writer.writeArrayEnd(); } @@ -765,7 +765,7 @@ public void writeUtf8(Utf8JsonWriter writer, Collection value) { int index = 0; for (Object element : value) { writer.writeComma(index++); - JsonTrampolineInvoke.writeUtf8Value(codec, writer, element); + codec.writeUtf8(writer, element); } writer.writeArrayEnd(); } @@ -846,13 +846,13 @@ public void writeString(StringJsonWriter writer, Collection value) { for (int index = 0, size = list.size(); index < size; index++) { Object element = list.get(index); writer.writeComma(index); - JsonTrampolineInvoke.writeStringValue(codec, writer, element); + codec.writeString(writer, element); } } else { int index = 0; for (Object element : value) { writer.writeComma(index++); - JsonTrampolineInvoke.writeStringValue(codec, writer, element); + codec.writeString(writer, element); } } writer.writeArrayEnd(); @@ -871,13 +871,13 @@ public void writeUtf8(Utf8JsonWriter writer, Collection value) { for (int index = 0, size = list.size(); index < size; index++) { Object element = list.get(index); writer.writeComma(index); - JsonTrampolineInvoke.writeUtf8Value(codec, writer, element); + codec.writeUtf8(writer, element); } } else { int index = 0; for (Object element : value) { writer.writeComma(index++); - JsonTrampolineInvoke.writeUtf8Value(codec, writer, element); + codec.writeUtf8(writer, element); } } writer.writeArrayEnd(); 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 index fa2107a399..be5518dd5e 100644 --- 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 @@ -24,13 +24,16 @@ import org.apache.fory.json.writer.Utf8JsonWriter; /** - * Shared interface invocation sites used by JSON codec dispatch. + * Shared interface invocation sites emitted by generated JSON codecs. * - *

JSON codec paths call these ordinary-class methods instead of emitting an interface-call - * bytecode at each caller. Multiple hot receiver implementations therefore share one call-site - * profile. On HotSpot, that polymorphic profile keeps C2 from recursively inlining a complete - * child-codec or member-group graph into its parent. Each receiver is still compiled as an - * independent unit, where its scalar writer calls remain eligible for inlining. + *

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. Handwritten + * codecs and generated scalar or small-subtree calls 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 @@ -38,40 +41,28 @@ */ @Internal public final class JsonTrampolineInvoke { - /** A generated-codec-owned group of UTF-8 member writes bound to that codec instance. */ + /** A generated-codec-owned UTF-8 write group bound to that codec instance. */ @Internal public interface Utf8WriteGroup { - int write(Utf8JsonWriter writer, Object value, int written); + void write(Utf8JsonWriter writer, Object value); } - /** A generated-codec-owned group of String member writes bound to that codec instance. */ + /** A generated-codec-owned String write group bound to that codec instance. */ @Internal public interface StringWriteGroup { - int write(StringJsonWriter writer, Object value, int written); + void write(StringJsonWriter writer, Object value); } private JsonTrampolineInvoke() {} - /** Invokes a UTF-8 child codec through the shared complete-value call site. */ - public static void writeUtf8Value(Utf8WriterCodec codec, Utf8JsonWriter writer, T value) { - codec.writeUtf8(writer, value); - } - - /** Invokes a String child codec through the shared complete-value call site. */ - public static void writeStringValue( - StringWriterCodec codec, StringJsonWriter writer, T value) { - codec.writeString(writer, value); - } - - /** Invokes a UTF-8 member group through the shared group call site. */ - public static int writeUtf8Group( - Utf8WriteGroup group, Utf8JsonWriter writer, Object value, int written) { - return group.write(writer, value, written); + /** 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 member group through the shared group call site. */ - public static int writeStringGroup( - StringWriteGroup group, StringJsonWriter writer, Object value, int written) { - return group.write(writer, value, written); + /** 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); } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java index c6b19254dc..305df441bf 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java @@ -379,7 +379,7 @@ public void writeString(StringJsonWriter writer, Map value) { for (Map.Entry entry : value.entrySet()) { writer.writeComma(index++); writeKey(writer, entry.getKey(), keyCodec); - JsonTrampolineInvoke.writeStringValue(codec, writer, entry.getValue()); + codec.writeString(writer, entry.getValue()); } writer.writeObjectEnd(); } @@ -396,7 +396,7 @@ public void writeUtf8(Utf8JsonWriter writer, Map value) { for (Map.Entry entry : value.entrySet()) { writer.writeComma(index++); writeKey(writer, entry.getKey(), keyCodec); - JsonTrampolineInvoke.writeUtf8Value(codec, writer, entry.getValue()); + codec.writeUtf8(writer, entry.getValue()); } writer.writeObjectEnd(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java index be534a6c7e..ace30ddc9c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java @@ -294,7 +294,7 @@ public final int writeStringAny( } writer.writeComma(written); writer.writeFieldName(name); - JsonTrampolineInvoke.writeStringValue(codec, writer, entry.getValue()); + codec.writeString(writer, entry.getValue()); written = 1; } return written; @@ -318,7 +318,7 @@ public final int writeUtf8Any( } writer.writeComma(written); writer.writeFieldName(name); - JsonTrampolineInvoke.writeUtf8Value(codec, writer, entry.getValue()); + codec.writeUtf8(writer, entry.getValue()); written = 1; } return written; @@ -351,7 +351,7 @@ private ForyJsonException reservedAnyName(String name) { public void writeString(StringJsonWriter writer, T value) { StringWriterCodec codec = writer.typeResolver().stringWriter(this); if (codec != this) { - JsonTrampolineInvoke.writeStringValue(codec, writer, value); + codec.writeString(writer, value); } else if (value == null) { writer.writeNull(); } else { @@ -363,7 +363,7 @@ public void writeString(StringJsonWriter writer, T value) { public void writeUtf8(Utf8JsonWriter writer, T value) { Utf8WriterCodec codec = writer.typeResolver().utf8Writer(this); if (codec != this) { - JsonTrampolineInvoke.writeUtf8Value(codec, writer, value); + codec.writeUtf8(writer, value); } else if (value == null) { writer.writeNull(); } else { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 77a4c95af5..dd1ad5e80e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -127,7 +127,7 @@ public void writeString(StringJsonWriter writer, Object value) { return; } JsonTypeInfo typeInfo = writer.typeResolver().getRuntimeTypeInfo(value.getClass()); - JsonTrampolineInvoke.writeStringValue(typeInfo.stringWriter(), writer, value); + typeInfo.stringWriter().writeString(writer, value); } @Override @@ -137,7 +137,7 @@ public void writeUtf8(Utf8JsonWriter writer, Object value) { return; } JsonTypeInfo typeInfo = writer.typeResolver().getRuntimeTypeInfo(value.getClass()); - JsonTrampolineInvoke.writeUtf8Value(typeInfo.utf8Writer(), writer, value); + typeInfo.utf8Writer().writeUtf8(writer, value); } @Override @@ -2653,7 +2653,7 @@ public void writeString(StringJsonWriter writer, AtomicReference value) { if (value == null) { writer.writeNull(); } else { - JsonTrampolineInvoke.writeStringValue(valueTypeInfo.stringWriter(), writer, value.get()); + valueTypeInfo.stringWriter().writeString(writer, value.get()); } } @@ -2662,7 +2662,7 @@ public void writeUtf8(Utf8JsonWriter writer, AtomicReference value) { if (value == null) { writer.writeNull(); } else { - JsonTrampolineInvoke.writeUtf8Value(valueTypeInfo.utf8Writer(), writer, value.get()); + valueTypeInfo.utf8Writer().writeUtf8(writer, value.get()); } } @@ -2952,7 +2952,7 @@ public void writeString(StringJsonWriter writer, AtomicReferenceArray value) writer.writeArrayStart(); for (int i = 0, length = array.length(); i < length; i++) { writer.writeComma(i); - JsonTrampolineInvoke.writeStringValue(codec, writer, array.get(i)); + codec.writeString(writer, array.get(i)); } writer.writeArrayEnd(); } @@ -2968,7 +2968,7 @@ public void writeUtf8(Utf8JsonWriter writer, AtomicReferenceArray value) { writer.writeArrayStart(); for (int i = 0, length = array.length(); i < length; i++) { writer.writeComma(i); - JsonTrampolineInvoke.writeUtf8Value(codec, writer, array.get(i)); + codec.writeUtf8(writer, array.get(i)); } writer.writeArrayEnd(); } @@ -3070,7 +3070,7 @@ public void writeString(StringJsonWriter writer, Optional value) { } Optional optional = value; if (optional.isPresent()) { - JsonTrampolineInvoke.writeStringValue(valueTypeInfo.stringWriter(), writer, optional.get()); + valueTypeInfo.stringWriter().writeString(writer, optional.get()); } else { writer.writeNull(); } @@ -3084,7 +3084,7 @@ public void writeUtf8(Utf8JsonWriter writer, Optional value) { } Optional optional = value; if (optional.isPresent()) { - JsonTrampolineInvoke.writeUtf8Value(valueTypeInfo.utf8Writer(), writer, optional.get()); + valueTypeInfo.utf8Writer().writeUtf8(writer, optional.get()); } else { writer.writeNull(); } 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..7dbe7d89cd 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); } 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 a3e77a2637..32be0318df 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 @@ -88,12 +88,12 @@ abstract class JsonWriterCodegen { abstract String writeMemberGroupMethod(); - abstract String writeValueMethod(); - abstract String writeAnyMethod(); abstract int splitMemberThreshold(); + abstract int minimumObjectWriteCost(); + abstract int memberPrefixCost(JsonFieldInfo property); abstract PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused); @@ -189,11 +189,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++) { @@ -209,9 +213,17 @@ String genWriterCode( } } 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)); + if (objectWriteBoundary) { + ctx.implementsInterfaces(completeWriterType, ctx.type(memberGroupType())); + } + if (objectWriteBoundary || properties.length >= splitMemberThreshold()) { String objectMethod = writeMethod() + "Object"; addGeneratedMethod( ctx, @@ -224,7 +236,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 = @@ -248,7 +272,13 @@ builder, properties, objectStartFused, new Reference("object", TypeRef.of(type)) 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", @@ -1081,20 +1111,17 @@ private void addMemberGroups( String methodName = memberGroupMethod() + groupIndex; addMemberGroupMethod(builder, members, start, end, methodName); addMemberGroupField(builder, fieldName, methodName); - // Keep all generated callers on the shared interface-call BCI. Its multiple hot group - // receivers stop C2 from absorbing the complete member graph into this writer, while each - // receiver can compile independently and inline the scalar writes inside its own group. - Expression call = + // 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(), - TypeRef.of(int.class), - fieldRef(fieldName, memberGroupType()), - writer, - object, - written) - .inline(); - expressions.add(new Expression.Assign(written, call)); + JsonTrampolineInvoke.class, + writeMemberGroupMethod(), + fieldRef(fieldName, memberGroupType()), + writer, + object)); remainingCost -= rangeCost(members, start, end); start = end; } @@ -1111,20 +1138,16 @@ private void addMemberGroupMethod( for (int i = start; i < end; i++) { body.add(members.get(i).expression); } - Reference written = new Reference("written", TypeRef.of(int.class)); - body.add(new Expression.Return(written)); addGeneratedMethod( builder.context(), "final", methodName, body, - int.class, + void.class, writerType(), "writer", ownerType, - "object", - int.class, - "written"); + "object"); } private void addMemberGroupField( @@ -1141,20 +1164,98 @@ private void addMemberGroupField( + " = new " + groupType + "() {\n" - + " @Override public int write(" + + " @Override public void write(" + writerType - + " writer, Object value, int written) {\n" - + " return " + + " writer, Object value) {\n" + + " " + builder.codecClassName(ownerType) + ".this." + methodName + "(writer, (" + valueType - + ") value, written);\n" + + ") 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) { @@ -1453,10 +1554,9 @@ private Expression writeCodec( object && property.writeRawType() == ownerType ? new Reference("this", TypeRef.of(completeWriterType())) : fieldRef("w" + id, codecFieldType(property)); - // A shared interface-call BCI prevents a monomorphic generated call site from recursively - // pulling the complete child-codec graph into its parent. - return new Expression.StaticInvoke( - JsonTrampolineInvoke.class, writeValueMethod(), codec, writer, value); + // 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 boolean storesWriteCodec(JsonFieldInfo property) { 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 6b5cde31d1..5ffd845038 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 @@ -31,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); @@ -76,11 +77,6 @@ String writeMemberGroupMethod() { return "writeStringGroup"; } - @Override - String writeValueMethod() { - return "writeStringValue"; - } - @Override String writeAnyMethod() { return "writeStringAny"; @@ -91,6 +87,11 @@ int splitMemberThreshold() { return MIN_SPLIT_MEMBERS; } + @Override + int minimumObjectWriteCost() { + return MIN_OBJECT_WRITE_COST; + } + @Override int memberPrefixCost(JsonFieldInfo property) { return usesPrefix(property) ? 2 : 0; 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 923cd6f578..321aaf896e 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 @@ -40,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); @@ -85,11 +86,6 @@ String writeMemberGroupMethod() { return "writeUtf8Group"; } - @Override - String writeValueMethod() { - return "writeUtf8Value"; - } - @Override String writeAnyMethod() { return "writeUtf8Any"; @@ -100,6 +96,11 @@ int splitMemberThreshold() { return MIN_SPLIT_MEMBERS; } + @Override + int minimumObjectWriteCost() { + return MIN_OBJECT_WRITE_COST; + } + @Override int memberPrefixCost(JsonFieldInfo property) { return usesPrefix(property) ? 1 : 0; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index 88f1111e3e..00b2c73903 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -28,7 +28,6 @@ import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.annotation.JsonCodec; import org.apache.fory.json.codec.CodecUtils; -import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.reader.JsonReader; import org.apache.fory.json.reader.Latin1JsonReader; @@ -833,7 +832,7 @@ private boolean writeStringObject(StringJsonWriter writer, Object object, int in return false; } writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); + writeTypeInfo.stringWriter().writeString(writer, value); return true; } @@ -963,7 +962,7 @@ private boolean writeStringArray(StringJsonWriter writer, Object object, int ind } // Field metadata owns omission only. Once present, the registered codec owns null semantics. writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); + writeTypeInfo.stringWriter().writeString(writer, value); return true; } @@ -973,7 +972,7 @@ private boolean writeStringCollection(StringJsonWriter writer, Object object, in return false; } writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); + writeTypeInfo.stringWriter().writeString(writer, value); return true; } @@ -983,7 +982,7 @@ private boolean writeStringMap(StringJsonWriter writer, Object object, int index return false; } writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); + writeTypeInfo.stringWriter().writeString(writer, value); return true; } @@ -993,7 +992,7 @@ private boolean writeStringPojo(StringJsonWriter writer, Object object, int inde return false; } writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeStringValue(writeTypeInfo.stringWriter(), writer, value); + writeTypeInfo.stringWriter().writeString(writer, value); return true; } @@ -1073,7 +1072,7 @@ private boolean writeUtf8Object(Utf8JsonWriter writer, Object object, int index) return false; } writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); + writeTypeInfo.utf8Writer().writeUtf8(writer, value); return true; } @@ -1139,7 +1138,7 @@ private boolean writeUtf8Array(Utf8JsonWriter writer, Object object, int index) } // Field metadata owns omission only. Once present, the registered codec owns null semantics. writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); + writeTypeInfo.utf8Writer().writeUtf8(writer, value); return true; } @@ -1149,7 +1148,7 @@ private boolean writeUtf8Collection(Utf8JsonWriter writer, Object object, int in return false; } writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); + writeTypeInfo.utf8Writer().writeUtf8(writer, value); return true; } @@ -1159,7 +1158,7 @@ private boolean writeUtf8Map(Utf8JsonWriter writer, Object object, int index) { return false; } writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); + writeTypeInfo.utf8Writer().writeUtf8(writer, value); return true; } @@ -1169,7 +1168,7 @@ private boolean writeUtf8Pojo(Utf8JsonWriter writer, Object object, int index) { return false; } writer.writeFieldName(this, index); - JsonTrampolineInvoke.writeUtf8Value(writeTypeInfo.utf8Writer(), writer, value); + writeTypeInfo.utf8Writer().writeUtf8(writer, value); return true; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/InheritedJsonValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/InheritedJsonValueCodec.java index d7453f38f6..f1d487a531 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/InheritedJsonValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/InheritedJsonValueCodec.java @@ -21,7 +21,6 @@ import java.lang.reflect.Type; import org.apache.fory.json.ForyJsonException; -import org.apache.fory.json.codec.JsonTrampolineInvoke; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf16JsonReader; @@ -54,12 +53,12 @@ final class InheritedJsonValueCodec implements JsonValueCodec { @Override public void writeString(StringJsonWriter writer, Object value) { - JsonTrampolineInvoke.writeStringValue(delegate, writer, value); + delegate.writeString(writer, value); } @Override public void writeUtf8(Utf8JsonWriter writer, Object value) { - JsonTrampolineInvoke.writeUtf8Value(delegate, writer, value); + delegate.writeUtf8(writer, value); } @Override 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..209047a227 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 writer subtrees, if available. */ + @Internal + public ObjectCodec objectCodec() { + return objectCodec; + } + @Internal public boolean usesAnnotationCodec() { return annotationCodec; 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; From 5141a8d0a86c125f89816474134acb07803fe5c4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 18 Jul 2026 18:36:32 +0800 Subject: [PATCH 8/9] perf(json): stabilize generated codec hot paths Keep large generated read/write subtrees behind codegen-owned call boundaries, let generated roots own eligible object collection loops, and preserve compact Latin1 string leaves behind a real medium writer boundary. Handwritten codecs, readers, writers, and roots remain direct. JDK 25 Users 1000KB serialization: Fory 11,887.815 ops/s vs Wast 10,714.921 ops/s (+10.95%), with effectively zero allocation. Full Java reactor and fory-json tests pass; Spotless and Checkstyle pass. --- .../fory/json/codec/CollectionCodec.java | 12 + .../fory/json/codec/JsonTrampolineInvoke.java | 30 +- .../apache/fory/json/codegen/JsonCodegen.java | 6 +- .../fory/json/codegen/JsonReaderCodegen.java | 338 +++++++++++++++++- .../fory/json/codegen/JsonWriterCodegen.java | 95 ++++- .../json/codegen/Latin1ReaderCodegen.java | 5 + .../json/codegen/StringWriterCodegen.java | 10 + .../fory/json/codegen/Utf16ReaderCodegen.java | 5 + .../fory/json/codegen/Utf8ReaderCodegen.java | 26 ++ .../fory/json/codegen/Utf8WriterCodegen.java | 10 + .../fory/json/reader/Utf8JsonReader.java | 42 +-- .../fory/json/resolver/JsonTypeInfo.java | 2 +- .../fory/json/writer/Utf8JsonWriter.java | 107 ++++-- .../org/apache/fory/json/JsonStringTest.java | 12 +- 14 files changed, 616 insertions(+), 84 deletions(-) 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 index be5518dd5e..64e3e05bbe 100644 --- 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 @@ -20,6 +20,7 @@ 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; @@ -32,8 +33,10 @@ * 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. Handwritten - * codecs and generated scalar or small-subtree calls remain direct. + *

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 @@ -53,6 +56,18 @@ 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. */ @@ -65,4 +80,15 @@ 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 7dbe7d89cd..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 @@ -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 b91caefaf7..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), @@ -171,6 +206,7 @@ String genReaderCode( "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(); } @@ -183,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); @@ -1119,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, @@ -2173,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(); @@ -2196,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)), @@ -2221,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; @@ -2459,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; } @@ -2468,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 @@ -2477,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(); @@ -2905,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 32be0318df..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,7 @@ 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; @@ -44,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; /** @@ -78,6 +80,10 @@ abstract class JsonWriterCodegen { abstract Class completeWriterType(); + abstract Object resolvedWriter(JsonFieldInfo property); + + abstract String writerCapabilityMethod(); + abstract String writeMethod(); // This names split methods in complete writers and direct helpers in Any writers. It is not a @@ -219,7 +225,10 @@ String genWriterCode( // 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)); + boolean objectWriteBoundary = + ownsObjectWriteBoundary(owner, writeSubtreeCost(owner)) + || properties.length < splitMemberThreshold() + && ownsGeneratedObjectCollectionLoop(owner); if (objectWriteBoundary) { ctx.implementsInterfaces(completeWriterType, ctx.type(memberGroupType())); } @@ -1383,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, @@ -1507,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); @@ -1559,6 +1572,84 @@ private Expression writeCodec( 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 5ffd845038..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 @@ -57,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"; 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 321aaf896e..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 @@ -66,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"; 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 826df88088..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 @@ -1899,10 +1899,9 @@ private String readStringToken() { } int start = position; int offset = start; - // Keep the complete bounded scanner as one independently compiled scalar operation. If C2 - // copies it into every generated string-field case, the enclosing object reader becomes a - // large and unstable compilation unit. These are real word probes for short and medium - // strings; the unbounded loop and end-of-input tail remain in their separate methods. + // 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) { @@ -1921,41 +1920,6 @@ private String readStringToken() { return readStringWordStop(start, offset, stopMask); } offset += Long.BYTES; - if (offset + Long.BYTES <= inputLength) { - stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); - if (stopMask != 0) { - return readStringWordStop(start, offset, stopMask); - } - offset += Long.BYTES; - if (offset + Long.BYTES <= inputLength) { - stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); - if (stopMask != 0) { - return readStringWordStop(start, offset, stopMask); - } - offset += Long.BYTES; - if (offset + Long.BYTES <= inputLength) { - stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); - if (stopMask != 0) { - return readStringWordStop(start, offset, stopMask); - } - offset += Long.BYTES; - if (offset + Long.BYTES <= inputLength) { - stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); - if (stopMask != 0) { - return readStringWordStop(start, offset, stopMask); - } - offset += Long.BYTES; - if (offset + Long.BYTES <= inputLength) { - stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); - if (stopMask != 0) { - return readStringWordStop(start, offset, stopMask); - } - offset += Long.BYTES; - } - } - } - } - } } } } 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 209047a227..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 @@ -138,7 +138,7 @@ public boolean usesDefaultObjectCodec() { return defaultObjectCodec; } - /** Returns the raw object codec used to evaluate generated writer subtrees, if available. */ + /** Returns the raw object codec used to evaluate generated codec subtrees, if available. */ @Internal public ObjectCodec objectCodec() { return objectCodec; 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 f430f62fb3..33dd6a8e16 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 @@ -1026,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); } @@ -1088,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) { @@ -1145,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; @@ -1201,21 +1185,80 @@ 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); + int tailOffset = length - Long.BYTES; + long tail = LittleEndian.getInt64(value, tailOffset); + if (!isJsonAsciiWords(word0, word1, 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) '"'; @@ -1223,19 +1266,21 @@ private boolean writeLatin1String25To31(byte[] value, int length) { 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/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 From 1bf21ad0292db870540a348546a6da769badb5fd Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 18 Jul 2026 19:21:52 +0800 Subject: [PATCH 9/9] perf(json): avoid repeated medium string writes --- .../fory/json/writer/Utf8JsonWriter.java | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) 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 33dd6a8e16..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 @@ -1252,14 +1252,46 @@ private boolean writeLatin1String17To24(byte[] value, int length) { bytes[pos++] = (byte) '"'; long word0 = LittleEndian.getInt64(value, 0); long word1 = LittleEndian.getInt64(value, 8); - int tailOffset = length - Long.BYTES; - long tail = LittleEndian.getInt64(value, tailOffset); - if (!isJsonAsciiWords(word0, word1, tail)) { + if (!isJsonAsciiWords(word0, word1)) { return false; } LittleEndian.putInt64(bytes, pos, word0); LittleEndian.putInt64(bytes, pos + 8, word1); - 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;