From 63563cd7b75983494b59a4eddca27df401b6aa2a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 02:28:08 +0800 Subject: [PATCH 01/20] refactor(json): centralize canonical codec ownership Remove duplicate canonical ObjectCodec state from JsonTypeInfo. Make JsonTypeResolver own canonical codec identity for source generation and capability construction so generated parent shape does not depend on child publication order. --- .../fory/json/codec/ClosedSubtypeCodec.java | 2 +- .../fory/json/codec/CollectionCodec.java | 16 +- .../apache/fory/json/codegen/JsonCodegen.java | 153 +++++++++------- .../fory/json/codegen/JsonReaderCodegen.java | 29 +-- .../fory/json/codegen/JsonWriterCodegen.java | 12 +- .../json/codegen/Latin1ReaderCodegen.java | 7 +- .../json/codegen/StringWriterCodegen.java | 7 +- .../fory/json/codegen/Utf16ReaderCodegen.java | 7 +- .../fory/json/codegen/Utf8ReaderCodegen.java | 7 +- .../fory/json/codegen/Utf8WriterCodegen.java | 7 +- .../fory/json/resolver/JsonTypeInfo.java | 17 +- .../fory/json/resolver/JsonTypeResolver.java | 169 ++++++++++-------- .../fory/json/JsonAsyncCompilationTest.java | 44 ++++- 13 files changed, 289 insertions(+), 188 deletions(-) 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..8efdf25770 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 @@ -85,7 +85,7 @@ public void resolve(JsonTypeResolver resolver) { Class subtype = definition.classes[i]; JsonTypeInfo child = resolver.getTypeInfo(subtype, subtype); if (definition.inclusion == Inclusion.PROPERTY) { - if (!child.usesDefaultObjectCodec()) { + if (resolver.canonicalObjectCodec(child) == null) { throw new ForyJsonException( "Inline JSON subtype requires the default object representation: " + subtype); } 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..777a31f6b0 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 @@ -89,17 +89,23 @@ public static CollectionCodec create( Class elementRawType = CodecUtils.rawType(elementType, Object.class); CollectionFactory factory = collectionFactory(rawType, elementRawType); JsonTypeInfo elementTypeInfo = resolver.getTypeInfo(elementType, elementRawType); - return create(factory, elementTypeInfo); + return create(factory, elementTypeInfo, resolver.canonicalObjectCodec(elementTypeInfo) != null); } @Internal public static CollectionCodec create( - Class rawType, Class elementRawType, JsonTypeInfo elementTypeInfo) { - return create(collectionFactory(rawType, elementRawType), elementTypeInfo); + Class rawType, + Class elementRawType, + JsonTypeInfo elementTypeInfo, + JsonTypeResolver resolver) { + return create( + collectionFactory(rawType, elementRawType), + elementTypeInfo, + resolver.canonicalObjectCodec(elementTypeInfo) != null); } private static CollectionCodec create( - CollectionFactory factory, JsonTypeInfo elementTypeInfo) { + CollectionFactory factory, JsonTypeInfo elementTypeInfo, boolean objectElement) { Object elementCodec = elementTypeInfo.stringWriter(); if (elementCodec == ScalarCodecs.StringCodec.INSTANCE) { return new StringCollectionCodec(factory); @@ -131,7 +137,7 @@ private static CollectionCodec create( if (elementCodec == ScalarCodecs.BigDecimalCodec.INSTANCE) { return new BigDecimalCollectionCodec(factory); } - if (elementTypeInfo.usesDefaultObjectCodec()) { + if (objectElement) { return new ObjectCollectionCodec(factory, elementTypeInfo); } return new GenericCollectionCodec(factory, elementTypeInfo); 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..4fc2df460d 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 @@ -43,14 +43,16 @@ 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.json.resolver.JsonTypeResolver; /** * Shared generated-class owner for the five concrete object-codec capabilities. * *

One instance belongs to one {@link org.apache.fory.json.resolver.JsonSharedRegistry}. Separate * concurrent caches single-flight one generated class per Java type and capability, so using only - * one input or output representation never generates the other paths. Expression construction, - * source generation, and Janino compilation happen without a resolver-local JIT lock. + * one input or output representation never generates the other paths. A resolver is passed only to + * the active source-generation call for short canonical metadata lookups; this shared owner and its + * class caches never retain it. * *

This class owns classes only. Resolver-local generated instances, child capability capture, * {@link JsonTypeInfo} slot installation, and generated parent-field updates belong to {@link @@ -89,11 +91,10 @@ public JsonCodegen(int codegenHash, ClassLoader jsonLoader) { /** * Compiles one concrete capability from fully resolved object metadata. * - *

These compile methods run without a resolver-local JIT lock. Source-generation decisions may - * inspect active codec classes only for non-default bindings, whose capability fields are never - * replaced by generated raw-object codecs. Mutable default-object child capabilities are read - * only by {@link org.apache.fory.json.resolver.JsonTypeResolver} while it constructs a - * resolver-local instance under its JIT lock. + *

Source generation and Janino compilation are not enclosed by a resolver-local JIT lock. + * Canonical child metadata is read through short resolver-owned lookups; source shape never + * depends on mutable capability slots. Active codec classes are inspected only for non-canonical + * bindings, whose capability fields are never replaced by generated raw-object codecs. * *

Generated classes are cached here because this object is shared by every pooled resolver of * one Fory JSON instance. Concurrent map computation provides generated-class single-flight; @@ -102,43 +103,48 @@ public JsonCodegen(int codegenHash, ClassLoader jsonLoader) { * JsonJITContext} callbacks. */ @Internal - public Class compileStringWriter(ObjectCodec codec) { + public Class compileStringWriter(ObjectCodec codec, JsonTypeResolver resolver) { if (!canCompileWriter(codec)) { return null; } - return stringWriterClasses.computeIfAbsent(codec.type(), ignored -> buildStringWriter(codec)); + return stringWriterClasses.computeIfAbsent( + codec.type(), ignored -> buildStringWriter(codec, resolver)); } @Internal - public Class compileUtf8Writer(ObjectCodec codec) { + public Class compileUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver) { if (!canCompileWriter(codec)) { return null; } - return utf8WriterClasses.computeIfAbsent(codec.type(), ignored -> buildUtf8Writer(codec)); + return utf8WriterClasses.computeIfAbsent( + codec.type(), ignored -> buildUtf8Writer(codec, resolver)); } @Internal - public Class compileLatin1Reader(ObjectCodec codec) { + public Class compileLatin1Reader(ObjectCodec codec, JsonTypeResolver resolver) { if (!canCompileReader(codec)) { return null; } - return latin1ReaderClasses.computeIfAbsent(codec.type(), ignored -> buildLatin1Reader(codec)); + return latin1ReaderClasses.computeIfAbsent( + codec.type(), ignored -> buildLatin1Reader(codec, resolver)); } @Internal - public Class compileUtf16Reader(ObjectCodec codec) { + public Class compileUtf16Reader(ObjectCodec codec, JsonTypeResolver resolver) { if (!canCompileReader(codec)) { return null; } - return utf16ReaderClasses.computeIfAbsent(codec.type(), ignored -> buildUtf16Reader(codec)); + return utf16ReaderClasses.computeIfAbsent( + codec.type(), ignored -> buildUtf16Reader(codec, resolver)); } @Internal - public Class compileUtf8Reader(ObjectCodec codec) { + public Class compileUtf8Reader(ObjectCodec codec, JsonTypeResolver resolver) { if (!canCompileReader(codec)) { return null; } - return utf8ReaderClasses.computeIfAbsent(codec.type(), ignored -> buildUtf8Reader(codec)); + return utf8ReaderClasses.computeIfAbsent( + codec.type(), ignored -> buildUtf8Reader(codec, resolver)); } @Internal @@ -170,7 +176,7 @@ private String jitId(Class type, String role) { return qualifiedClassName(CodeGenerator.getPackage(type), className(type, role)); } - private Class buildStringWriter(ObjectCodec codec) { + private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "StringWriter"); @@ -179,19 +185,21 @@ private Class buildStringWriter(ObjectCodec codec) { JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { String code = - new StringWriterCodegen(this).genUnwrappedWriterCode(builder, type, codec, unwrapped); + new StringWriterCodegen(this, resolver) + .genUnwrappedWriterCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } 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) + ? new StringWriterCodegen(this, resolver) + .genWriterCode(builder, type, codec.writeFields()) + : new StringWriterCodegen(this, resolver) .genAnyWriterCode(builder, type, codec.writeFields(), any); return compileCodecClass(generatedPackage, className, code); } - private Class buildUtf8Writer(ObjectCodec codec) { + private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "Utf8Writer"); @@ -200,18 +208,21 @@ private Class buildUtf8Writer(ObjectCodec codec) { JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { String code = - new Utf8WriterCodegen(this).genUnwrappedWriterCode(builder, type, codec, unwrapped); + new Utf8WriterCodegen(this, resolver) + .genUnwrappedWriterCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } 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).genAnyWriterCode(builder, type, codec.writeFields(), any); + ? new Utf8WriterCodegen(this, resolver) + .genWriterCode(builder, type, codec.writeFields()) + : new Utf8WriterCodegen(this, resolver) + .genAnyWriterCode(builder, type, codec.writeFields(), any); return compileCodecClass(generatedPackage, className, code); } - private Class buildLatin1Reader(ObjectCodec codec) { + private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "Latin1Reader"); @@ -220,20 +231,21 @@ private Class buildLatin1Reader(ObjectCodec codec) { JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { String code = - new Latin1ReaderCodegen(this).genUnwrappedReaderCode(builder, type, codec, unwrapped); + new Latin1ReaderCodegen(this, resolver) + .genUnwrappedReaderCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); String code = any == null || any.readField() == null && any.readSetter() == null - ? new Latin1ReaderCodegen(this) + ? new Latin1ReaderCodegen(this, resolver) .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) - : new Latin1ReaderCodegen(this) + : new Latin1ReaderCodegen(this, resolver) .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); return compileCodecClass(generatedPackage, className, code); } - private Class buildUtf16Reader(ObjectCodec codec) { + private Class buildUtf16Reader(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "Utf16Reader"); @@ -242,20 +254,21 @@ private Class buildUtf16Reader(ObjectCodec codec) { JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { String code = - new Utf16ReaderCodegen(this).genUnwrappedReaderCode(builder, type, codec, unwrapped); + new Utf16ReaderCodegen(this, resolver) + .genUnwrappedReaderCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); String code = any == null || any.readField() == null && any.readSetter() == null - ? new Utf16ReaderCodegen(this) + ? new Utf16ReaderCodegen(this, resolver) .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) - : new Utf16ReaderCodegen(this) + : new Utf16ReaderCodegen(this, resolver) .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); return compileCodecClass(generatedPackage, className, code); } - private Class buildUtf8Reader(ObjectCodec codec) { + private Class buildUtf8Reader(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "Utf8Reader"); @@ -264,15 +277,16 @@ private Class buildUtf8Reader(ObjectCodec codec) { JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { String code = - new Utf8ReaderCodegen(this).genUnwrappedReaderCode(builder, type, codec, unwrapped); + new Utf8ReaderCodegen(this, resolver) + .genUnwrappedReaderCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); String code = any == null || any.readField() == null && any.readSetter() == null - ? new Utf8ReaderCodegen(this) + ? new Utf8ReaderCodegen(this, resolver) .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) - : new Utf8ReaderCodegen(this) + : new Utf8ReaderCodegen(this, resolver) .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); return compileCodecClass(generatedPackage, className, code); } @@ -434,11 +448,11 @@ private boolean canCompileAnyRead(AnyInfo any, boolean creator) { return isVisible(any.valueRawType()); } - Class stringWriterFieldType(JsonTypeInfo typeInfo) { + Class stringWriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { if (typeInfo.usesAnnotationCodec()) { return StringWriterCodec.class; } - if (typeInfo.usesDefaultObjectCodec()) { + if (resolver.canonicalObjectCodec(typeInfo) != null) { return StringWriterCodec.class; } Object codec = typeInfo.stringWriter(); @@ -449,11 +463,11 @@ Class stringWriterFieldType(JsonTypeInfo typeInfo) { return StringWriterCodec.class; } - Class utf8WriterFieldType(JsonTypeInfo typeInfo) { + Class utf8WriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { if (typeInfo.usesAnnotationCodec()) { return Utf8WriterCodec.class; } - if (typeInfo.usesDefaultObjectCodec()) { + if (resolver.canonicalObjectCodec(typeInfo) != null) { return Utf8WriterCodec.class; } Object codec = typeInfo.utf8Writer(); @@ -464,11 +478,11 @@ Class utf8WriterFieldType(JsonTypeInfo typeInfo) { return Utf8WriterCodec.class; } - Class latin1ReaderFieldType(JsonTypeInfo typeInfo) { + Class latin1ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { if (typeInfo.usesAnnotationCodec()) { return Latin1ReaderCodec.class; } - if (typeInfo.usesDefaultObjectCodec()) { + if (resolver.canonicalObjectCodec(typeInfo) != null) { return Latin1ReaderCodec.class; } Class type = typeInfo.latin1Reader().getClass(); @@ -478,11 +492,11 @@ Class latin1ReaderFieldType(JsonTypeInfo typeInfo) { return Latin1ReaderCodec.class; } - Class utf16ReaderFieldType(JsonTypeInfo typeInfo) { + Class utf16ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { if (typeInfo.usesAnnotationCodec()) { return Utf16ReaderCodec.class; } - if (typeInfo.usesDefaultObjectCodec()) { + if (resolver.canonicalObjectCodec(typeInfo) != null) { return Utf16ReaderCodec.class; } Class type = typeInfo.utf16Reader().getClass(); @@ -492,11 +506,11 @@ Class utf16ReaderFieldType(JsonTypeInfo typeInfo) { return Utf16ReaderCodec.class; } - Class utf8ReaderFieldType(JsonTypeInfo typeInfo) { + Class utf8ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { if (typeInfo.usesAnnotationCodec()) { return Utf8ReaderCodec.class; } - if (typeInfo.usesDefaultObjectCodec()) { + if (resolver.canonicalObjectCodec(typeInfo) != null) { return Utf8ReaderCodec.class; } Class type = typeInfo.utf8Reader().getClass(); @@ -507,10 +521,10 @@ Class utf8ReaderFieldType(JsonTypeInfo typeInfo) { } @Internal - public static Class readNestedType(JsonFieldInfo property) { + public static Class readNestedType(JsonFieldInfo property, JsonTypeResolver resolver) { if (property.readKind() == JsonFieldKind.OBJECT && property.readRawType() != Object.class - && property.readTypeInfo().usesDefaultObjectCodec()) { + && resolver.canonicalObjectCodec(property.readTypeInfo()) != null) { return property.readRawType(); } return null; @@ -536,19 +550,22 @@ static boolean writesStringCollectionDirectly(JsonFieldInfo property) { == CollectionCodec.StringCollectionCodec.class; } - private static JsonTypeInfo writeObjectTypeInfo(JsonFieldInfo property) { + private static JsonTypeInfo writeObjectTypeInfo( + JsonFieldInfo property, JsonTypeResolver resolver) { JsonTypeInfo typeInfo = property.writeTypeInfo(); - return usesWriteCodec(property) && typeInfo.usesDefaultObjectCodec() ? typeInfo : null; + return usesWriteCodec(property) && resolver.canonicalObjectCodec(typeInfo) != null + ? typeInfo + : null; } @Internal - public static Class writeNestedType(JsonFieldInfo property) { - JsonTypeInfo typeInfo = writeObjectTypeInfo(property); + public static Class writeNestedType(JsonFieldInfo property, JsonTypeResolver resolver) { + JsonTypeInfo typeInfo = writeObjectTypeInfo(property, resolver); return typeInfo == null ? null : typeInfo.rawType(); } @Internal - public static boolean usesReadCodec(JsonFieldInfo property) { + public static boolean usesReadCodec(JsonFieldInfo property, JsonTypeResolver resolver) { switch (property.readKind()) { case ENUM: case ARRAY: @@ -556,36 +573,38 @@ public static boolean usesReadCodec(JsonFieldInfo property) { case MAP: return true; case OBJECT: - return !usesReadObjectCodec(property); + return !usesReadObjectCodec(property, resolver); default: return false; } } - static boolean usesReadObjectCodec(JsonFieldInfo property) { + static boolean usesReadObjectCodec(JsonFieldInfo property, JsonTypeResolver resolver) { return property.readKind() == JsonFieldKind.OBJECT && property.readRawType() != Object.class - && property.readTypeInfo().usesDefaultObjectCodec(); + && resolver.canonicalObjectCodec(property.readTypeInfo()) != null; } - static boolean storesReadObjectCodec(Class type, JsonFieldInfo property) { - Class nestedType = readNestedType(property); + static boolean storesReadObjectCodec( + Class type, JsonFieldInfo property, JsonTypeResolver resolver) { + Class nestedType = readNestedType(property, resolver); return nestedType != null && nestedType != type; } @Internal - public static boolean storesSelfReader(ObjectCodec owner) { + public static boolean storesSelfReader(ObjectCodec owner, JsonTypeResolver resolver) { AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { return false; } - if (storesSelfReader(owner.type(), owner.readFields(), owner.creatorInfo() != null, any)) { + if (storesSelfReader( + owner.type(), owner.readFields(), owner.creatorInfo() != null, any, resolver)) { return true; } JsonUnwrappedInfo unwrapped = owner.unwrappedInfo(); if (unwrapped != null) { for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { - if (route.field() != null && readNestedType(route.field()) == owner.type()) { + if (route.field() != null && readNestedType(route.field(), resolver) == owner.type()) { return true; } } @@ -594,15 +613,19 @@ public static boolean storesSelfReader(ObjectCodec owner) { } static boolean storesSelfReader( - Class type, JsonFieldInfo[] properties, boolean creator, AnyInfo any) { - if (any.valueRawType() == type && any.valueTypeInfo().usesDefaultObjectCodec()) { + Class type, + JsonFieldInfo[] properties, + boolean creator, + AnyInfo any, + JsonTypeResolver resolver) { + if (any.valueRawType() == type && resolver.canonicalObjectCodec(any.valueTypeInfo()) != null) { return true; } if (creator) { return false; } for (JsonFieldInfo property : properties) { - if (readNestedType(property) == type) { + if (readNestedType(property, resolver) == type) { return true; } } 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..2e4ff5d4ba 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 @@ -44,6 +44,7 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; import org.apache.fory.json.meta.JsonFieldTable; +import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.reflect.TypeRef; import org.apache.fory.type.TypeUtils; @@ -67,12 +68,14 @@ abstract class JsonReaderCodegen { private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL; final JsonCodegen codegen; + final JsonTypeResolver resolver; private AnyInfo any; private Class ownerType; private boolean storesSelfReader; - JsonReaderCodegen(JsonCodegen codegen) { + JsonReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { this.codegen = codegen; + this.resolver = resolver; } abstract Class codecFieldType(JsonFieldInfo property); @@ -113,7 +116,7 @@ abstract Expression readEnumField( abstract Reference readerRef(); final Class readNestedType(JsonFieldInfo property) { - return JsonCodegen.readNestedType(property); + return JsonCodegen.readNestedType(property, resolver); } String genReaderCode( @@ -139,7 +142,7 @@ String genReaderCode( if (usesReadInfo(properties[i])) { ctx.addField(JsonFieldInfo.class, "rp" + i); } - if (JsonCodegen.usesReadCodec(properties[i])) { + if (JsonCodegen.usesReadCodec(properties[i], resolver)) { ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(properties[i])), "r" + i); } if (storesReadObjectCodec(type, properties[i])) { @@ -181,7 +184,8 @@ String genAnyReaderCode( AnyInfo any) { this.any = any; ownerType = type; - storesSelfReader = JsonCodegen.storesSelfReader(type, properties, creatorInfo != null, any); + storesSelfReader = + JsonCodegen.storesSelfReader(type, properties, creatorInfo != null, any, resolver); if (creatorInfo != null) { return genAnyCreatorReaderCode(builder, type, creatorInfo); } @@ -292,7 +296,7 @@ String genUnwrappedReaderCode( if (rootCreator != null) { ctx.addField(JsonCreatorInfo.class, "creator"); } - storesSelfReader = JsonCodegen.storesSelfReader(owner); + storesSelfReader = JsonCodegen.storesSelfReader(owner, resolver); addSelfReaderField(ctx); boolean storesAnyReader = any != null && storesAnyReader(type); if (storesAnyReader) { @@ -322,7 +326,7 @@ String genUnwrappedReaderCode( if (usesReadInfo(field)) { ctx.addField(JsonFieldInfo.class, "rp" + id); } - if (JsonCodegen.usesReadCodec(field)) { + if (JsonCodegen.usesReadCodec(field, resolver)) { ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(field)), "r" + id); } if (storesReadObjectCodec(type, field)) { @@ -483,7 +487,7 @@ private void addReaderFields(CodegenContext ctx, Class type, JsonFieldInfo[] if (usesReadInfo(properties[i])) { ctx.addField(JsonFieldInfo.class, "rp" + i); } - if (JsonCodegen.usesReadCodec(properties[i])) { + if (JsonCodegen.usesReadCodec(properties[i], resolver)) { ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(properties[i])), "r" + i); } if (storesReadObjectCodec(type, properties[i])) { @@ -1295,7 +1299,7 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr hashes, new Expression.Invoke(property, "nameHash", TypeRef.of(long.class)).inline(), id)); - if (JsonCodegen.usesReadCodec(properties[i])) { + if (JsonCodegen.usesReadCodec(properties[i], resolver)) { Class codecType = codecFieldType(properties[i]); expressions.add( new Expression.Assign( @@ -1414,7 +1418,7 @@ private void addUnwrappedReaderAssignment( new Expression.Assign( new Reference("this.rp" + id, TypeRef.of(JsonFieldInfo.class)), property)); } - if (JsonCodegen.usesReadCodec(field)) { + if (JsonCodegen.usesReadCodec(field, resolver)) { Class codecType = codecFieldType(field); expressions.add( new Expression.Assign( @@ -3048,7 +3052,8 @@ private Expression nestedSelfReaderRef() { } private boolean storesAnyReader(Class type) { - return !any.valueTypeInfo().usesDefaultObjectCodec() || any.valueTypeInfo().rawType() != type; + return resolver.canonicalObjectCodec(any.valueTypeInfo()) == null + || any.valueTypeInfo().rawType() != type; } private Expression anyReaderRef() { @@ -3462,7 +3467,7 @@ final boolean usesReadInfo(JsonFieldInfo property) { final boolean usesReadObjectCodec(JsonFieldInfo property) { return property.readKind() == JsonFieldKind.OBJECT && property.readRawType() != Object.class - && property.readTypeInfo().usesDefaultObjectCodec(); + && resolver.canonicalObjectCodec(property.readTypeInfo()) != null; } final boolean storesReadObjectCodec(Class type, JsonFieldInfo property) { @@ -3644,7 +3649,7 @@ final Expression readObject( int id, Expression object) { if (property.readRawType() == Object.class - || !property.readTypeInfo().usesDefaultObjectCodec()) { + || resolver.canonicalObjectCodec(property.readTypeInfo()) == null) { return readResolvedField(builder, property, id, object); } return builder.setField(property, object, readObjectValue(type, property, id)); 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..a47f8e5987 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 @@ -43,6 +43,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.JsonTypeResolver; import org.apache.fory.reflect.TypeRef; /** @@ -61,10 +62,12 @@ abstract class JsonWriterCodegen { private static final int INLINE_TAIL_MEMBERS = 4; final JsonCodegen codegen; + final JsonTypeResolver resolver; private Class ownerType; - JsonWriterCodegen(JsonCodegen codegen) { + JsonWriterCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { this.codegen = codegen; + this.resolver = resolver; } abstract Class codecFieldType(JsonFieldInfo property); @@ -744,7 +747,8 @@ private Expression anyWriterConstructorExpression( } private boolean storesAnyWriter(AnyInfo any) { - return !any.valueTypeInfo().usesDefaultObjectCodec() || any.valueRawType() != ownerType; + return resolver.canonicalObjectCodec(any.valueTypeInfo()) == null + || any.valueRawType() != ownerType; } static final class PrefixFields { @@ -1251,7 +1255,7 @@ static long packedPrefixWord(byte[] prefix, int offset) { private Expression writeCodec( JsonFieldInfo property, int id, Expression value, Expression writer) { - boolean object = property.writeTypeInfo().usesDefaultObjectCodec(); + boolean object = resolver.canonicalObjectCodec(property.writeTypeInfo()) != null; Expression codec = object && property.writeRawType() == ownerType ? new Reference("this", TypeRef.of(completeWriterType())) @@ -1261,7 +1265,7 @@ private Expression writeCodec( private boolean storesWriteCodec(JsonFieldInfo property) { return usesWriteCodec(property) - && (!property.writeTypeInfo().usesDefaultObjectCodec() + && (resolver.canonicalObjectCodec(property.writeTypeInfo()) == null || property.writeRawType() != ownerType); } 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..8f6f403dd9 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 @@ -25,16 +25,17 @@ import org.apache.fory.json.meta.JsonAsciiToken; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.reflect.TypeRef; final class Latin1ReaderCodegen extends JsonReaderCodegen { - Latin1ReaderCodegen(JsonCodegen codegen) { - super(codegen); + Latin1ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { + super(codegen, resolver); } @Override Class codecFieldType(JsonFieldInfo property) { - return codegen.latin1ReaderFieldType(property.readTypeInfo()); + return codegen.latin1ReaderFieldType(property.readTypeInfo(), resolver); } @Override 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..6d489cc34d 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 @@ -25,19 +25,20 @@ import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.StringWriterCodec; import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.reflect.TypeRef; final class StringWriterCodegen extends JsonWriterCodegen { private static final int MIN_SPLIT_MEMBERS = 10; - StringWriterCodegen(JsonCodegen codegen) { - super(codegen); + StringWriterCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { + super(codegen, resolver); } @Override Class codecFieldType(JsonFieldInfo property) { - return codegen.stringWriterFieldType(property.writeTypeInfo()); + return codegen.stringWriterFieldType(property.writeTypeInfo(), resolver); } @Override 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..10e5427aae 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 @@ -24,16 +24,17 @@ import org.apache.fory.json.codec.Utf16ReaderCodec; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.reflect.TypeRef; final class Utf16ReaderCodegen extends JsonReaderCodegen { - Utf16ReaderCodegen(JsonCodegen codegen) { - super(codegen); + Utf16ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { + super(codegen, resolver); } @Override Class codecFieldType(JsonFieldInfo property) { - return codegen.utf16ReaderFieldType(property.readTypeInfo()); + return codegen.utf16ReaderFieldType(property.readTypeInfo(), resolver); } @Override 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..b2842044fd 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 @@ -25,16 +25,17 @@ import org.apache.fory.json.meta.JsonAsciiToken; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.reflect.TypeRef; final class Utf8ReaderCodegen extends JsonReaderCodegen { - Utf8ReaderCodegen(JsonCodegen codegen) { - super(codegen); + Utf8ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { + super(codegen, resolver); } @Override Class codecFieldType(JsonFieldInfo property) { - return codegen.utf8ReaderFieldType(property.readTypeInfo()); + return codegen.utf8ReaderFieldType(property.readTypeInfo(), resolver); } @Override 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..e599dae870 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 @@ -34,19 +34,20 @@ import org.apache.fory.json.codec.Utf8WriterCodec; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.reflect.TypeRef; final class Utf8WriterCodegen extends JsonWriterCodegen { private static final int MIN_SPLIT_MEMBERS = 12; - Utf8WriterCodegen(JsonCodegen codegen) { - super(codegen); + Utf8WriterCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { + super(codegen, resolver); } @Override Class codecFieldType(JsonFieldInfo property) { - return codegen.utf8WriterFieldType(property.writeTypeInfo()); + return codegen.utf8WriterFieldType(property.writeTypeInfo(), resolver); } @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..bdd3804e5a 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 @@ -39,10 +39,11 @@ * dispatch. These five fields are the sole installed capability state; there are no parallel * per-path maps to reconcile on the hot path. * - *

Only the canonical exact raw-class {@link ObjectCodec} binding may receive generated - * replacements. Custom codecs, parameterized object bindings, containers, scalars, and dynamic - * {@code Object} bindings retain their original semantic owner. Each complete capability is - * installed in its own independently lazy slot. + *

{@link JsonTypeResolver} owns canonical exact raw-class {@link ObjectCodec} identity and the + * corresponding stable metadata owner. This binding stores only installed capabilities; custom + * codecs, parameterized object bindings, containers, scalars, and dynamic {@code Object} bindings + * retain their original semantic owner. Each complete capability is installed in its own + * independently lazy slot. */ public final class JsonTypeInfo { private final Type type; @@ -53,7 +54,6 @@ public final class JsonTypeInfo { private Latin1ReaderCodec latin1Reader; private Utf16ReaderCodec utf16Reader; private Utf8ReaderCodec utf8Reader; - private final boolean defaultObjectCodec; private final boolean annotationCodec; JsonTypeInfo(Type type, Class rawType, JsonFieldKind kind, JsonValueCodec codec) { @@ -75,9 +75,6 @@ public final class JsonTypeInfo { latin1Reader = codec; utf16Reader = codec; utf8Reader = codec; - // 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; } public Type type() { @@ -132,10 +129,6 @@ void setUtf8Reader(Utf8ReaderCodec utf8Reader) { this.utf8Reader = utf8Reader; } - public boolean usesDefaultObjectCodec() { - return defaultObjectCodec; - } - @Internal public boolean usesAnnotationCodec() { return annotationCodec; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index d054f6561c..74855f3e80 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -162,7 +162,7 @@ public ObjectCodec getUnwrappedObjectCodec(Class rawType) { if (typeInfo != null) { // Generated capabilities replace type-info slots; objectCodecs retains the stable metadata // owner used to build an unwrapped parent. - return typeInfo.usesDefaultObjectCodec() ? objectCodecs.get(key) : null; + return canonicalObjectCodec(typeInfo); } if (customTypeInfo(rawType, rawType) != null || sharedRegistry.subTypesInfo(rawType) != null) { return null; @@ -178,10 +178,34 @@ public ObjectCodec getUnwrappedObjectCodec(Class rawType) { } typeInfo = newTypeInfo(rawType, rawType, codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo); + registerObjectTypeInfo(typeInfo, codec); return codec; } + /** + * Returns the stable metadata owner for a canonical raw-class object binding, or {@code null}. + * + *

This lookup never constructs metadata or requests compilation. Source generation may call it + * outside a root operation; the short resolver-local lock protects the ordinary owner maps + * without coupling one generated class compilation to another. + */ + @Internal + public ObjectCodec canonicalObjectCodec(JsonTypeInfo typeInfo) { + jitContext.lock(); + try { + return canonicalObjectOwner(typeInfo); + } finally { + jitContext.unlock(); + } + } + + private ObjectCodec canonicalObjectOwner(JsonTypeInfo typeInfo) { + if (rawObjectTypeInfos.get(typeInfo.rawType()) != typeInfo) { + return null; + } + return objectCodecs.get(typeInfo.rawType()); + } + public JsonTypeInfo getTypeInfo(Type declaredType, Class fallback) { Class rawType = CodecUtils.rawType(declaredType, fallback); Object key = typeInfoKey(declaredType, rawType); @@ -301,7 +325,7 @@ private JsonTypeInfo resolveTypeInfo(Type declaredType, Class rawType, JsonCo return newTypeInfo( declaredType, rawType, - CollectionCodec.create(rawType, elementType.getRawType(), elementInfo)); + CollectionCodec.create(rawType, elementType.getRawType(), elementInfo, this)); } if (Map.class.isAssignableFrom(rawType)) { if (hasElement || hasContent || !hasKey && !hasMapValue) { @@ -486,14 +510,28 @@ public JsonTypeInfo getRuntimeTypeInfo(Class runtimeType) { } private JsonTypeInfo resolveRuntimeTypeInfo(Class runtimeType, Object key) { - JsonTypeInfo typeInfo; - typeInfo = buildRuntimeTypeInfo(runtimeType); + JsonTypeInfo typeInfo = customTypeInfo(runtimeType, runtimeType); + JsonValueCodec codec = null; + if (typeInfo == null) { + sharedRegistry.checkSecure(runtimeType); + TypeRef typeRef = TypeRef.of(runtimeType); + codec = + runtimeType == Object.class + ? getObjectCodec(typeRef) + : sharedRegistry.createCodec(runtimeType, typeRef, this); + if (codec == null) { + codec = getObjectCodec(typeRef); + } + typeInfo = newTypeInfo(runtimeType, runtimeType, codec); + } JsonTypeInfo recursiveTypeInfo = typeInfos.get(key); if (recursiveTypeInfo != null) { return recursiveTypeInfo; } typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo); + if (codec != null) { + registerObjectTypeInfo(typeInfo, codec); + } return typeInfo; } @@ -518,7 +556,7 @@ public StringWriterCodec stringWriter(ObjectCodec codec) { if (installed == owner && codegen != null && codegen.canCompileWriter(owner)) { jitContext.registerJITCallback( () -> owner.getClass(), - () -> codegen.compileStringWriter(owner), + () -> codegen.compileStringWriter(owner, this), new JsonJITContext.JITCallback>() { @Override public void onSuccess(Class generated) { @@ -550,7 +588,7 @@ public Utf8WriterCodec utf8Writer(ObjectCodec codec) { if (installed == owner && codegen != null && codegen.canCompileWriter(owner)) { jitContext.registerJITCallback( () -> owner.getClass(), - () -> codegen.compileUtf8Writer(owner), + () -> codegen.compileUtf8Writer(owner, this), new JsonJITContext.JITCallback>() { @Override public void onSuccess(Class generated) { @@ -582,7 +620,7 @@ public Latin1ReaderCodec latin1Reader(ObjectCodec codec) { if (installed == owner && codegen != null && codegen.canCompileReader(owner)) { jitContext.registerJITCallback( () -> owner.getClass(), - () -> codegen.compileLatin1Reader(owner), + () -> codegen.compileLatin1Reader(owner, this), new JsonJITContext.JITCallback>() { @Override public void onSuccess(Class generated) { @@ -614,7 +652,7 @@ public Utf16ReaderCodec utf16Reader(ObjectCodec codec) { if (installed == owner && codegen != null && codegen.canCompileReader(owner)) { jitContext.registerJITCallback( () -> owner.getClass(), - () -> codegen.compileUtf16Reader(owner), + () -> codegen.compileUtf16Reader(owner, this), new JsonJITContext.JITCallback>() { @Override public void onSuccess(Class generated) { @@ -646,7 +684,7 @@ public Utf8ReaderCodec utf8Reader(ObjectCodec codec) { if (installed == owner && codegen != null && codegen.canCompileReader(owner)) { jitContext.registerJITCallback( () -> owner.getClass(), - () -> codegen.compileUtf8Reader(owner), + () -> codegen.compileUtf8Reader(owner, this), new JsonJITContext.JITCallback>() { @Override public void onSuccess(Class generated) { @@ -795,7 +833,7 @@ private StringWriterCodec newStringWriter(ObjectCodec owner, Class if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); StringWriterCodec codec = typeInfo.stringWriter(); - if (JsonCodegen.writeNestedType(field) != null + if (JsonCodegen.writeNestedType(field, this) != null && typeInfo.rawType() != owner.type() && codec instanceof ObjectCodec) { codec = stringWriter((ObjectCodec) codec); @@ -812,7 +850,7 @@ private StringWriterCodec newStringWriter(ObjectCodec owner, Class generatedClass, owner, fields, codecs); } StringWriterCodec anyCodec = any.valueTypeInfo().stringWriter(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = stringWriter((ObjectCodec) anyCodec); @@ -834,7 +872,7 @@ private Utf8WriterCodec newUtf8Writer(ObjectCodec owner, Class gen if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); Utf8WriterCodec codec = typeInfo.utf8Writer(); - if (JsonCodegen.writeNestedType(field) != null + if (JsonCodegen.writeNestedType(field, this) != null && typeInfo.rawType() != owner.type() && codec instanceof ObjectCodec) { codec = utf8Writer((ObjectCodec) codec); @@ -851,7 +889,7 @@ private Utf8WriterCodec newUtf8Writer(ObjectCodec owner, Class gen generatedClass, owner, fields, codecs); } Utf8WriterCodec anyCodec = any.valueTypeInfo().utf8Writer(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = utf8Writer((ObjectCodec) anyCodec); @@ -871,7 +909,7 @@ private StringWriterCodec newUnwrappedStringWriter( if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo child = field.writeTypeInfo(); StringWriterCodec codec = child.stringWriter(); - if (JsonCodegen.writeNestedType(field) != null + if (JsonCodegen.writeNestedType(field, this) != null && child.rawType() != owner.type() && codec instanceof ObjectCodec) { codec = stringWriter((ObjectCodec) codec); @@ -888,7 +926,7 @@ private StringWriterCodec newUnwrappedStringWriter( generatedClass, owner, fields, codecs); } StringWriterCodec anyCodec = any.valueTypeInfo().stringWriter(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = stringWriter((ObjectCodec) anyCodec); @@ -908,7 +946,7 @@ private Utf8WriterCodec newUnwrappedUtf8Writer( if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo child = field.writeTypeInfo(); Utf8WriterCodec codec = child.utf8Writer(); - if (JsonCodegen.writeNestedType(field) != null + if (JsonCodegen.writeNestedType(field, this) != null && child.rawType() != owner.type() && codec instanceof ObjectCodec) { codec = utf8Writer((ObjectCodec) codec); @@ -925,7 +963,7 @@ private Utf8WriterCodec newUnwrappedUtf8Writer( generatedClass, owner, fields, codecs); } Utf8WriterCodec anyCodec = any.valueTypeInfo().utf8Writer(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = utf8Writer((ObjectCodec) anyCodec); @@ -968,7 +1006,7 @@ private Latin1ReaderCodec newLatin1Reader( generatedClass, owner, readTable, fields, codecs); } Latin1ReaderCodec anyCodec = any.valueTypeInfo().latin1Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = latin1Reader((ObjectCodec) anyCodec); @@ -981,9 +1019,10 @@ private Latin1ReaderCodec newLatin1Reader( for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (JsonCodegen.usesReadCodec(field)) { + if (JsonCodegen.usesReadCodec(field, this)) { codecs[i] = typeInfo.latin1Reader(); - } else if (JsonCodegen.readNestedType(field) != null && field.readRawType() != owner.type()) { + } else if (JsonCodegen.readNestedType(field, this) != null + && field.readRawType() != owner.type()) { Latin1ReaderCodec codec = typeInfo.latin1Reader(); codecs[i] = codec instanceof ObjectCodec ? latin1Reader((ObjectCodec) codec) : codec; @@ -999,7 +1038,7 @@ private Latin1ReaderCodec newLatin1Reader( generatedClass, owner, readTable, fields, codecs); } Latin1ReaderCodec anyCodec = any.valueTypeInfo().latin1Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = latin1Reader((ObjectCodec) anyCodec); @@ -1038,7 +1077,7 @@ private Utf16ReaderCodec newUtf16Reader( generatedClass, owner, readTable, fields, codecs); } Utf16ReaderCodec anyCodec = any.valueTypeInfo().utf16Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = utf16Reader((ObjectCodec) anyCodec); @@ -1051,9 +1090,10 @@ private Utf16ReaderCodec newUtf16Reader( for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (JsonCodegen.usesReadCodec(field)) { + if (JsonCodegen.usesReadCodec(field, this)) { codecs[i] = typeInfo.utf16Reader(); - } else if (JsonCodegen.readNestedType(field) != null && field.readRawType() != owner.type()) { + } else if (JsonCodegen.readNestedType(field, this) != null + && field.readRawType() != owner.type()) { Utf16ReaderCodec codec = typeInfo.utf16Reader(); codecs[i] = codec instanceof ObjectCodec ? utf16Reader((ObjectCodec) codec) : codec; } @@ -1068,7 +1108,7 @@ private Utf16ReaderCodec newUtf16Reader( generatedClass, owner, readTable, fields, codecs); } Utf16ReaderCodec anyCodec = any.valueTypeInfo().utf16Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = utf16Reader((ObjectCodec) anyCodec); @@ -1107,7 +1147,7 @@ private Utf8ReaderCodec newUtf8Reader( generatedClass, owner, readTable, fields, codecs); } Utf8ReaderCodec anyCodec = any.valueTypeInfo().utf8Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = utf8Reader((ObjectCodec) anyCodec); @@ -1120,9 +1160,10 @@ private Utf8ReaderCodec newUtf8Reader( for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (JsonCodegen.usesReadCodec(field)) { + if (JsonCodegen.usesReadCodec(field, this)) { codecs[i] = typeInfo.utf8Reader(); - } else if (JsonCodegen.readNestedType(field) != null && field.readRawType() != owner.type()) { + } else if (JsonCodegen.readNestedType(field, this) != null + && field.readRawType() != owner.type()) { Utf8ReaderCodec codec = typeInfo.utf8Reader(); codecs[i] = codec instanceof ObjectCodec ? utf8Reader((ObjectCodec) codec) : codec; } @@ -1137,7 +1178,7 @@ private Utf8ReaderCodec newUtf8Reader( generatedClass, owner, readTable, fields, codecs); } Utf8ReaderCodec anyCodec = any.valueTypeInfo().utf8Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = utf8Reader((ObjectCodec) anyCodec); @@ -1156,7 +1197,7 @@ private Latin1ReaderCodec newUnwrappedLatin1Reader( for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; Latin1ReaderCodec codec = child.latin1Reader(); - if (child.usesDefaultObjectCodec() + if (canonicalObjectCodec(child) != null && child.rawType() != owner.type() && codec instanceof ObjectCodec) { codec = latin1Reader((ObjectCodec) codec); @@ -1173,7 +1214,7 @@ private Latin1ReaderCodec newUnwrappedLatin1Reader( generatedClass, owner, readTable, fields, codecs); } Latin1ReaderCodec anyCodec = any.valueTypeInfo().latin1Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = latin1Reader((ObjectCodec) anyCodec); @@ -1192,7 +1233,7 @@ private Utf16ReaderCodec newUnwrappedUtf16Reader( for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; Utf16ReaderCodec codec = child.utf16Reader(); - if (child.usesDefaultObjectCodec() + if (canonicalObjectCodec(child) != null && child.rawType() != owner.type() && codec instanceof ObjectCodec) { codec = utf16Reader((ObjectCodec) codec); @@ -1209,7 +1250,7 @@ private Utf16ReaderCodec newUnwrappedUtf16Reader( generatedClass, owner, readTable, fields, codecs); } Utf16ReaderCodec anyCodec = any.valueTypeInfo().utf16Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = utf16Reader((ObjectCodec) anyCodec); @@ -1228,7 +1269,7 @@ private Utf8ReaderCodec newUnwrappedUtf8Reader( for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; Utf8ReaderCodec codec = child.utf8Reader(); - if (child.usesDefaultObjectCodec() + if (canonicalObjectCodec(child) != null && child.rawType() != owner.type() && codec instanceof ObjectCodec) { codec = utf8Reader((ObjectCodec) codec); @@ -1245,7 +1286,7 @@ private Utf8ReaderCodec newUnwrappedUtf8Reader( generatedClass, owner, readTable, fields, codecs); } Utf8ReaderCodec anyCodec = any.valueTypeInfo().utf8Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() + if (canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && anyCodec instanceof ObjectCodec) { anyCodec = utf8Reader((ObjectCodec) anyCodec); @@ -1329,16 +1370,15 @@ private Utf8ReaderCodec newInlineUtf8Reader( return codec; } - private static void setInlineSelfReader( - ObjectCodec owner, T inlineReader, T ordinaryReader) { - if (JsonCodegen.storesSelfReader(owner)) { + private void setInlineSelfReader(ObjectCodec owner, T inlineReader, T ordinaryReader) { + if (JsonCodegen.storesSelfReader(owner, this)) { Field field = ReflectionUtils.getField(inlineReader.getClass(), "selfReader"); ReflectionUtils.setObjectFieldValue(inlineReader, field, ordinaryReader); } } - private static boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { - return !any.valueTypeInfo().usesDefaultObjectCodec() || any.valueRawType() != owner.type(); + private boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { + return canonicalObjectCodec(any.valueTypeInfo()) == null || any.valueRawType() != owner.type(); } private Field[] writerChildFields(Object parent, ObjectCodec owner) { @@ -1346,7 +1386,7 @@ private Field[] writerChildFields(Object parent, ObjectCodec owner) { JsonFieldInfo[] fields = owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); for (int i = 0; i < fields.length; i++) { - Class nestedType = JsonCodegen.writeNestedType(fields[i]); + Class nestedType = JsonCodegen.writeNestedType(fields[i], this); JsonTypeInfo child = fields[i].writeTypeInfo(); if (nestedType != null && nestedType != owner.type() @@ -1370,7 +1410,7 @@ private Field[] readerChildFields(Object parent, ObjectCodec owner) { JsonCreatorFieldInfo[] fields = creator.fields(); for (int i = 0; i < fields.length; i++) { JsonTypeInfo child = fields[i].typeInfo(); - if (child.usesDefaultObjectCodec() + if (canonicalObjectCodec(child) != null && child.rawType() != owner.type() && rawObjectTypeInfos.get(child.rawType()) == child) { if (childFields == null) { @@ -1383,7 +1423,7 @@ private Field[] readerChildFields(Object parent, ObjectCodec owner) { } JsonFieldInfo[] fields = owner.readFields(); for (int i = 0; i < fields.length; i++) { - Class nestedType = JsonCodegen.readNestedType(fields[i]); + Class nestedType = JsonCodegen.readNestedType(fields[i], this); JsonTypeInfo child = fields[i].readTypeInfo(); if (nestedType != null && nestedType != owner.type() @@ -1405,20 +1445,20 @@ private Field[] unwrappedReaderChildFields(Object parent, ObjectCodec owner) Field[] childFields = null; for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; - if (!child.usesDefaultObjectCodec() + if (canonicalObjectCodec(child) == null || child.rawType() == owner.type() || rawObjectTypeInfos.get(child.rawType()) != child) { continue; } String fieldName; if (i < directCount) { - if (creator == null && JsonCodegen.readNestedType(owner.readFields()[i]) == null) { + if (creator == null && JsonCodegen.readNestedType(owner.readFields()[i], this) == null) { continue; } fieldName = creator == null ? "o" + i : "r" + i; } else { JsonUnwrappedInfo.ReadRoute route = routes[i - directCount]; - if (route.field() != null && JsonCodegen.readNestedType(route.field()) == null) { + if (route.field() != null && JsonCodegen.readNestedType(route.field(), this) == null) { continue; } fieldName = (route.field() == null ? "r" : "o") + i; @@ -1764,7 +1804,7 @@ public void onNotifyMissed() { private boolean hasGeneratedAnyWriteChild(ObjectCodec owner, AnyInfo any) { return any != null && (any.writeField() != null || any.writeGetter() != null) - && any.valueTypeInfo().usesDefaultObjectCodec() + && canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && rawObjectTypeInfos.get(any.valueRawType()) == any.valueTypeInfo(); } @@ -1772,7 +1812,7 @@ private boolean hasGeneratedAnyWriteChild(ObjectCodec owner, AnyInfo any) { private boolean hasGeneratedAnyReadChild(ObjectCodec owner, AnyInfo any) { return any != null && (any.readField() != null || any.readSetter() != null) - && any.valueTypeInfo().usesDefaultObjectCodec() + && canonicalObjectCodec(any.valueTypeInfo()) != null && any.valueRawType() != owner.type() && rawObjectTypeInfos.get(any.valueRawType()) == any.valueTypeInfo(); } @@ -1836,7 +1876,7 @@ private JsonTypeInfo buildTypeInfo(Class rawType, Type declaredType, Object k } JsonTypeInfo typeInfo = newTypeInfo(declaredType, rawType, codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo); + registerObjectTypeInfo(typeInfo, codec); return typeInfo; } @@ -1854,7 +1894,7 @@ private JsonTypeInfo buildObjectTypeInfo(TypeRef ownerType, Object key) { // capability slots. The outer cold-resolution transaction removes both on failure. objectCodecs.put(key, codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo); + registerObjectTypeInfo(typeInfo, codec); codec.resolveTypes(this); return typeInfo; } @@ -1862,27 +1902,10 @@ private JsonTypeInfo buildObjectTypeInfo(TypeRef ownerType, Object key) { // now; the outer owner finishes field resolution before returning the codec to its caller. typeInfo = newTypeInfo(ownerType.getType(), ownerType.getRawType(), codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo); + registerObjectTypeInfo(typeInfo, codec); return typeInfo; } - private JsonTypeInfo buildRuntimeTypeInfo(Class rawType) { - JsonTypeInfo custom = customTypeInfo(rawType, rawType); - if (custom != null) { - return custom; - } - sharedRegistry.checkSecure(rawType); - TypeRef typeRef = TypeRef.of(rawType); - JsonValueCodec codec = - rawType == Object.class - ? getObjectCodec(typeRef) - : sharedRegistry.createCodec(rawType, typeRef, this); - if (codec == null) { - codec = getObjectCodec(typeRef); - } - return newTypeInfo(rawType, rawType, codec); - } - private JsonTypeInfo newTypeInfo(Type type, Class rawType, JsonValueCodec codec) { return new JsonTypeInfo(type, rawType, sharedRegistry.kind(rawType), bindCodec(codec)); } @@ -1896,11 +1919,11 @@ private JsonTypeInfo newTypeInfo( return new JsonTypeInfo(type, rawType, kind, bindCodec(codec), annotationCodec); } - private void registerObjectTypeInfo(JsonTypeInfo typeInfo) { - if (typeInfo.usesDefaultObjectCodec() + private void registerObjectTypeInfo(JsonTypeInfo typeInfo, JsonValueCodec initialCodec) { + if (initialCodec.getClass() == ObjectCodec.class && typeInfo.type() instanceof Class && typeInfo.rawType() != Object.class) { - ObjectCodec owner = (ObjectCodec) typeInfo.stringWriter(); + ObjectCodec owner = (ObjectCodec) initialCodec; rawObjectTypeInfos.put(typeInfo.rawType(), typeInfo); canonicalObjectTypeInfos.put(owner, typeInfo); } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java index c3e8b9d731..d3d540c051 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java @@ -25,6 +25,7 @@ import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; import static org.testng.Assert.expectThrows; @@ -624,7 +625,7 @@ public void semanticBindingsRemainOwners() throws Exception { try { parameterized = resolver.getTypeInfo(declaredType.getType(), GenericAsyncBox.class); parameterizedReader = parameterized.utf8Reader(); - assertFalse(parameterized.usesDefaultObjectCodec()); + assertNull(resolver.canonicalObjectCodec(parameterized)); } finally { resolver.unlockJIT(); } @@ -633,6 +634,14 @@ public void semanticBindingsRemainOwners() throws Exception { "{\"value\":\"raw\"}".getBytes(StandardCharsets.UTF_8), GenericAsyncBox.class); controlled.executor.runNext(); assertSame(parameterized.utf8Reader(), parameterizedReader); + resolver.lockJIT(); + try { + JsonTypeInfo raw = resolver.getTypeInfo(GenericAsyncBox.class, GenericAsyncBox.class); + assertSame( + resolver.canonicalObjectCodec(raw), resolver.getObjectCodec(GenericAsyncBox.class)); + } finally { + resolver.unlockJIT(); + } JsonValueCodec codec = nullCodec(); CodecRegistry codecs = new CodecRegistry(); @@ -646,9 +655,33 @@ public void semanticBindingsRemainOwners() throws Exception { JsonTypeInfo customInfo = primaryTypeResolver(custom.json).getTypeInfo(AsyncChild.class, AsyncChild.class); assertCapabilities(customInfo, codec); + assertNull(primaryTypeResolver(custom.json).canonicalObjectCodec(customInfo)); assertEquals(custom.executor.submittedTasks(), 0); } + @Test + public void sourceShapeIgnoresPublicationOrder() { + ForyJson parentFirstJson = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeResolver parentFirstResolver = primaryTypeResolver(parentFirstJson); + ObjectCodec parentFirstOwner = + parentFirstResolver.getObjectCodec(AsyncParent.class); + parentFirstResolver.getTypeInfo(AsyncParent.class, AsyncParent.class); + Object parentFirst = parentFirstResolver.utf8Writer(parentFirstOwner); + + ForyJson childFirstJson = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeResolver childFirstResolver = primaryTypeResolver(childFirstJson); + ObjectCodec childFirstOwner = childFirstResolver.getObjectCodec(AsyncChild.class); + childFirstResolver.getTypeInfo(AsyncChild.class, AsyncChild.class); + childFirstResolver.utf8Writer(childFirstOwner); + ObjectCodec childFirstParent = + childFirstResolver.getObjectCodec(AsyncParent.class); + childFirstResolver.getTypeInfo(AsyncParent.class, AsyncParent.class); + Object childFirst = childFirstResolver.utf8Writer(childFirstParent); + + assertEquals(declaredFieldTypes(parentFirst), declaredFieldTypes(childFirst)); + assertTrue(declaredFieldTypes(parentFirst).contains(Utf8WriterCodec.class.getName())); + } + @Test public void nestedAndRecursiveTypes() throws Exception { ControlledJson controlled = controlledJson(); @@ -839,6 +872,15 @@ private static void assertCapabilities(JsonTypeInfo info, Object expected) { assertSame(info.utf8Reader(), expected); } + private static List declaredFieldTypes(Object capability) { + List types = new ArrayList<>(); + for (Field field : capability.getClass().getDeclaredFields()) { + types.add(field.getType().getName()); + } + Collections.sort(types); + return types; + } + private static StringWriterCodec stringWriter( JsonTypeResolver resolver, ObjectCodec owner) { resolver.lockJIT(); From ca69c724e5a144ded28b0cb85677c89bf615dc38 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 04:35:41 +0800 Subject: [PATCH 02/20] perf(json): build atomic UTF-8 reader graphs Compile object and exact declared collection classes independently, construct resolver-local capabilities bottom-up, and publish complete acyclic UTF-8 graphs atomically. Retain interpreted fallback behavior for unsupported graphs. --- .../fory/json/codec/CollectionCodec.java | 7 +- .../apache/fory/json/codegen/JsonCodegen.java | 235 ++++++++-- .../fory/json/codegen/JsonJITContext.java | 87 +++- .../fory/json/codegen/JsonReaderCodegen.java | 123 +++--- .../json/codegen/Latin1ReaderCodegen.java | 4 + .../fory/json/codegen/Utf16ReaderCodegen.java | 4 + .../codegen/Utf8CollectionReaderCodegen.java | 88 ++++ .../fory/json/codegen/Utf8ReaderCodegen.java | 14 +- .../resolver/GeneratedCodecInstantiator.java | 18 + .../json/resolver/JsonSharedRegistry.java | 67 +++ .../fory/json/resolver/JsonTypeResolver.java | 406 ++++++++++++++++-- .../fory/json/JsonAsyncCompilationTest.java | 257 ++++++++++- 12 files changed, 1167 insertions(+), 143 deletions(-) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java 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 777a31f6b0..ef046b9676 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 @@ -201,7 +201,7 @@ final Collection finishCollection(Collection collection) { } @Internal - final boolean createsArrayList() { + public final boolean createsArrayList() { return createsArrayList; } @@ -839,6 +839,11 @@ private ObjectCollectionCodec(CollectionFactory factory, JsonTypeInfo elementTyp this.elementTypeInfo = elementTypeInfo; } + @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/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index 4fc2df460d..6c3b687155 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 @@ -22,13 +22,20 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import org.apache.fory.annotation.Internal; import org.apache.fory.codegen.CodeGenerator; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.CompileUnit; +import org.apache.fory.codegen.JaninoUtils; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; import org.apache.fory.json.codec.JsonUnwrappedInfo; import org.apache.fory.json.codec.Latin1ReaderCodec; @@ -46,13 +53,12 @@ import org.apache.fory.json.resolver.JsonTypeResolver; /** - * Shared generated-class owner for the five concrete object-codec capabilities. + * Generates concrete object and exact-collection capability classes. * - *

One instance belongs to one {@link org.apache.fory.json.resolver.JsonSharedRegistry}. Separate - * concurrent caches single-flight one generated class per Java type and capability, so using only - * one input or output representation never generates the other paths. A resolver is passed only to - * the active source-generation call for short canonical metadata lookups; this shared owner and its - * class caches never retain it. + *

One instance belongs to one {@link org.apache.fory.json.resolver.JsonSharedRegistry}. The + * registry owns UTF-8 reader class futures; the representation caches here own the other concrete + * object capabilities. A resolver is passed only to the active source-generation call for short + * canonical metadata lookups; neither owner retains it. * *

This class owns classes only. Resolver-local generated instances, child capability capture, * {@link JsonTypeInfo} slot installation, and generated parent-field updates belong to {@link @@ -60,6 +66,9 @@ * generated source and constructor boundary; handwritten runtime capability APIs remain generic. */ public final class JsonCodegen { + // HotSpot's ordinary hot-callsite ceiling. Generated methods cross it with real schema work; + // padding or compiler directives would make the boundary dependent on deployment flags. + private static final int HOT_INLINE_LIMIT = 325; private static final Map> ID_GENERATOR = new ConcurrentHashMap<>(); private final int codegenHash; @@ -69,7 +78,6 @@ public final class JsonCodegen { private final Map, Class> utf8WriterClasses = new ConcurrentHashMap<>(); private final Map, Class> latin1ReaderClasses = new ConcurrentHashMap<>(); private final Map, Class> utf16ReaderClasses = new ConcurrentHashMap<>(); - private final Map, Class> utf8ReaderClasses = new ConcurrentHashMap<>(); static String generatedCodecType(CodegenContext ctx, Class codecType) { // Janino-generated serializers use erased types, matching Fory core code generation. Runtime @@ -139,12 +147,26 @@ public Class compileUtf16Reader(ObjectCodec codec, JsonTypeResolver resolv } @Internal - public Class compileUtf8Reader(ObjectCodec codec, JsonTypeResolver resolver) { + public Class compileUtf8Reader( + ObjectCodec codec, JsonTypeResolver resolver, boolean finalDependencies) { if (!canCompileReader(codec)) { return null; } - return utf8ReaderClasses.computeIfAbsent( - codec.type(), ignored -> buildUtf8Reader(codec, resolver)); + return buildUtf8Reader(codec, resolver, finalDependencies); + } + + @Internal + public Class compileUtf8CollectionReader(Type declaredType, CollectionCodec owner) { + if (!owner.createsArrayList()) { + throw new IllegalArgumentException( + "Generated UTF-8 collection requires an ArrayList binding"); + } + Class rawType = CodecUtils.rawType(declaredType, Collection.class); + Class elementType = CodecUtils.rawType(CodecUtils.elementType(declaredType), Object.class); + String generatedPackage = CodeGenerator.getPackage(elementType); + String className = className(elementType, simpleClassName(rawType) + "Utf8CollectionReader"); + String code = new Utf8CollectionReaderCodegen().genCode(generatedPackage, className); + return compileCodecClass(generatedPackage, className, code); } @Internal @@ -226,69 +248,194 @@ private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolv Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "Latin1Reader"); - JsonGeneratedCodecBuilder builder = - new JsonGeneratedCodecBuilder(generatedPackage, className, type); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); String code = new Latin1ReaderCodegen(this, resolver) .genUnwrappedReaderCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); - String code = - any == null || any.readField() == null && any.readSetter() == null - ? new Latin1ReaderCodegen(this, resolver) - .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) - : new Latin1ReaderCodegen(this, resolver) - .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); - return compileCodecClass(generatedPackage, className, code); + JsonFieldInfo[] properties = codec.readFields(); + Function source = + groupEnds -> { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); + Latin1ReaderCodegen reader = new Latin1ReaderCodegen(this, resolver, groupEnds); + return any == null || any.readField() == null && any.readSetter() == null + ? reader.genReaderCode(builder, type, properties, codec.creatorInfo()) + : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any); + }; + return compileReaderClass( + generatedPackage, + className, + properties.length, + "readLatin1", + codec.creatorInfo() == null, + source); } private Class buildUtf16Reader(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "Utf16Reader"); - JsonGeneratedCodecBuilder builder = - new JsonGeneratedCodecBuilder(generatedPackage, className, type); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); String code = new Utf16ReaderCodegen(this, resolver) .genUnwrappedReaderCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); - String code = - any == null || any.readField() == null && any.readSetter() == null - ? new Utf16ReaderCodegen(this, resolver) - .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) - : new Utf16ReaderCodegen(this, resolver) - .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); - return compileCodecClass(generatedPackage, className, code); - } - - private Class buildUtf8Reader(ObjectCodec codec, JsonTypeResolver resolver) { + JsonFieldInfo[] properties = codec.readFields(); + Function source = + groupEnds -> { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); + Utf16ReaderCodegen reader = new Utf16ReaderCodegen(this, resolver, groupEnds); + return any == null || any.readField() == null && any.readSetter() == null + ? reader.genReaderCode(builder, type, properties, codec.creatorInfo()) + : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any); + }; + return compileReaderClass( + generatedPackage, + className, + properties.length, + "readUtf16", + codec.creatorInfo() == null, + source); + } + + private Class buildUtf8Reader( + ObjectCodec codec, JsonTypeResolver resolver, boolean finalDependencies) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "Utf8Reader"); - JsonGeneratedCodecBuilder builder = - new JsonGeneratedCodecBuilder(generatedPackage, className, type); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); String code = - new Utf8ReaderCodegen(this, resolver) + new Utf8ReaderCodegen(this, resolver, finalDependencies) .genUnwrappedReaderCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); - String code = - any == null || any.readField() == null && any.readSetter() == null - ? new Utf8ReaderCodegen(this, resolver) - .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) - : new Utf8ReaderCodegen(this, resolver) - .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); - return compileCodecClass(generatedPackage, className, code); + JsonFieldInfo[] properties = codec.readFields(); + Function source = + groupEnds -> { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); + Utf8ReaderCodegen reader = + new Utf8ReaderCodegen(this, resolver, finalDependencies, groupEnds); + return any == null || any.readField() == null && any.readSetter() == null + ? reader.genReaderCode(builder, type, properties, codec.creatorInfo()) + : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any); + }; + return compileReaderClass( + generatedPackage, + className, + properties.length, + "readUtf8", + codec.creatorInfo() == null, + source); + } + + private Class compileReaderClass( + String generatedPackage, + String className, + int propertyCount, + String readMethod, + boolean groupable, + Function source) { + int[] groupEnds = + groupable + ? readerGroupEnds(generatedPackage, className, propertyCount, readMethod, source) + : oneReaderGroup(propertyCount); + return compileCodecClass(generatedPackage, className, source.apply(groupEnds)); + } + + private int[] readerGroupEnds( + String generatedPackage, + String className, + int propertyCount, + String readMethod, + Function source) { + if (propertyCount < 2) { + return oneReaderGroup(propertyCount); + } + // Child method bodies do not belong to their generated caller's bytecode budget. Compile the + // exact caller shape on this class-owned cold path, then merge declaration-order ranges until + // every emitted helper and the root that owns the final range naturally cross the hot-inline + // ceiling. Probe classes are never defined or dumped, so class publication and source shape + // remain independent of capability-slot timing. + List ends = new ArrayList<>(propertyCount); + for (int end = 1; end <= propertyCount; end++) { + ends.add(end); + } + while (ends.size() > 1) { + int[] candidate = toIntArray(ends); + JaninoUtils.CodeStats stats = + readerCodeStats(generatedPackage, className, source.apply(candidate)); + int start = 0; + boolean merged = false; + for (int group = 0; group < ends.size() - 1; group++) { + if (methodSize(stats, readMethod + "Group" + start) <= HOT_INLINE_LIMIT) { + ends.remove(group); + merged = true; + break; + } + start = ends.get(group); + } + if (merged) { + continue; + } + if (methodSize(stats, readMethod) <= HOT_INLINE_LIMIT) { + ends.remove(ends.size() - 2); + continue; + } + return candidate; + } + return toIntArray(ends); + } + + private JaninoUtils.CodeStats readerCodeStats( + String generatedPackage, String className, String code) { + CompileUnit unit = new CompileUnit(generatedPackage, className, code); + Map classes = JaninoUtils.toBytecode(jsonLoader, "", unit); + String classFile = + (generatedPackage.isEmpty() ? "" : generatedPackage.replace('.', '/') + "/") + + className + + ".class"; + byte[] bytecode = classes.get(classFile); + if (bytecode == null) { + throw new ForyJsonException("Missing generated JSON reader bytecode " + classFile); + } + return JaninoUtils.getClassStats(bytecode); + } + + private int methodSize(JaninoUtils.CodeStats stats, String method) { + Integer size = stats.methodsSize.get(method); + if (size == null) { + throw new ForyJsonException("Missing generated JSON reader method " + method); + } + return size; + } + + private int[] oneReaderGroup(int propertyCount) { + return new int[] {propertyCount}; + } + + private int[] toIntArray(List values) { + int[] result = new int[values.size()]; + for (int i = 0; i < values.size(); i++) { + result[i] = values.get(i); + } + return result; } private Class compileCodecClass(String generatedPackage, String className, String code) { @@ -506,10 +653,14 @@ Class utf16ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) return Utf16ReaderCodec.class; } - Class utf8ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + Class utf8ReaderFieldType( + JsonTypeInfo typeInfo, JsonTypeResolver resolver, boolean finalDependencies) { if (typeInfo.usesAnnotationCodec()) { return Utf8ReaderCodec.class; } + if (finalDependencies && resolver.exactUtf8Collection(typeInfo) != null) { + return Utf8ReaderCodec.class; + } if (resolver.canonicalObjectCodec(typeInfo) != null) { return Utf8ReaderCodec.class; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java index 1ddcdc9d20..15dbd9313e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java @@ -24,6 +24,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.locks.ReentrantLock; import org.apache.fory.annotation.Internal; @@ -41,12 +43,11 @@ * parent notifications. Synchronous mode uses the callback map only to break recursive compilation * of the same identifier. * - *

{@link JITCallback#id()} correlates active child notifications; it is not a task-request or - * deduplication key. Every asynchronous registration submits its action and owns its completion - * callback. Duplicate actions converge on {@link JsonCodegen}'s shared generated-class cache, then - * may construct and install equivalent resolver-local instances. This is intentional: generated - * class compilation is single-flight, while resolver-local subscribers, construction, publication, - * and generated parent-field updates are not deduplicated. + *

{@link JITCallback#id()} correlates active child notifications. Ordinary callback + * registrations retain independent resolver-local publication callbacks; future registrations use + * the identifier to reuse one active graph publication request. Generated-class single-flight + * belongs to the shared registry, while resolver-local construction, publication, and generated + * parent-field updates remain local. */ @Internal public final class JsonJITContext { @@ -91,8 +92,8 @@ public T registerJITCallback( callbacks.remove(id); } } - // Do not skip registration when this ID is already present. JsonCodegen single-flights class - // compilation, while every local registration must retain its own publication callback. + // Do not skip ordinary registrations when this ID is already present. The shared class owner + // single-flights compilation, while every local registration retains its own callback. callbacks.computeIfAbsent(callback.id(), ignored -> new ArrayList<>()); numRunningTasks++; ExecutorService service = compilationService; @@ -115,6 +116,76 @@ public T registerJITCallback( } } + /** + * Registers one resolver-local publication against independently scheduled class futures. + * + *

The future supplier must submit every independent compilation before composing its result. + * No compilation worker is used to wait for another worker. Repeated requests with the same ID + * reuse the active resolver-local request and return the interpreted capability until the + * complete result is published. + */ + public T registerJITFuture( + Callable interpretedAction, + Callable> futureAction, + JITCallback callback) { + try { + lock(); + Object id = callback.id(); + if (callbacks.containsKey(id)) { + return interpretedAction.call(); + } + callbacks.put(id, new ArrayList<>()); + if (!asyncCompilationEnabled) { + try { + T result = futureAction.call().join(); + callback.onSuccess(result); + List notifyCallbacks = callbacks.get(id); + for (int i = 0; i < notifyCallbacks.size(); i++) { + notifyCallbacks.get(i).onNotifyResult(result); + } + return result; + } catch (Throwable t) { + Throwable failure = unwrapFutureFailure(t); + callback.onFailure(failure); + ExceptionUtils.throwException(failure); + throw new IllegalStateException("unreachable"); + } finally { + callbacks.remove(id); + } + } + CompletableFuture future; + try { + future = futureAction.call(); + } catch (Throwable t) { + callbacks.remove(id); + callback.onFailure(t); + ExceptionUtils.throwException(t); + throw new IllegalStateException("unreachable"); + } + numRunningTasks++; + future.whenComplete( + (result, failure) -> { + if (failure == null) { + completeSuccess(callback, result); + } else { + completeFailure(callback, unwrapFutureFailure(failure)); + } + }); + return interpretedAction.call(); + } catch (Exception e) { + ExceptionUtils.throwException(e); + throw new IllegalStateException("unreachable"); + } finally { + unlock(); + } + } + + private static Throwable unwrapFutureFailure(Throwable failure) { + return failure instanceof CompletionException && failure.getCause() != null + ? failure.getCause() + : failure; + } + private void runJITAction(Callable jitAction, JITCallback callback) { T result; try { 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 2e4ff5d4ba..3ab5e70f32 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 @@ -60,8 +60,6 @@ * entry for each representation. */ abstract class JsonReaderCodegen { - private static final int MIN_SPLIT_READ_FIELDS = 8; - 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; private static final long UTF16_PAIR_MASK = 0x0000FFFF0000FFFFL; @@ -69,13 +67,29 @@ abstract class JsonReaderCodegen { final JsonCodegen codegen; final JsonTypeResolver resolver; + private final boolean finalDependencies; + private final int[] fastReadGroupEnds; private AnyInfo any; private Class ownerType; private boolean storesSelfReader; JsonReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { + this(codegen, resolver, false, null); + } + + JsonReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, boolean finalDependencies) { + this(codegen, resolver, finalDependencies, null); + } + + JsonReaderCodegen( + JsonCodegen codegen, + JsonTypeResolver resolver, + boolean finalDependencies, + int[] fastReadGroupEnds) { this.codegen = codegen; this.resolver = resolver; + this.finalDependencies = finalDependencies; + this.fastReadGroupEnds = fastReadGroupEnds; } abstract Class codecFieldType(JsonFieldInfo property); @@ -119,6 +133,10 @@ final Class readNestedType(JsonFieldInfo property) { return JsonCodegen.readNestedType(property, resolver); } + final boolean finalDependencies() { + return finalDependencies; + } + String genReaderCode( JsonGeneratedCodecBuilder builder, Class type, @@ -128,6 +146,7 @@ String genReaderCode( if (creatorInfo != null) { return genCreatorReaderCode(builder, type, creatorInfo); } + validateFastReadGroups(properties); Class readerType = readerType(); String readMethod = readMethod(); String slowMethod = readMethod + "Slow"; @@ -143,10 +162,10 @@ String genReaderCode( ctx.addField(JsonFieldInfo.class, "rp" + i); } if (JsonCodegen.usesReadCodec(properties[i], resolver)) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(properties[i])), "r" + i); + addCapabilityField(ctx, codecFieldType(properties[i]), "r" + i); } if (storesReadObjectCodec(type, properties[i])) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "o" + i); + addCapabilityField(ctx, readerCapabilityType(), "o" + i); } } addGeneratedConstructor( @@ -176,6 +195,10 @@ String genReaderCode( return ctx.genCode(); } + private void addCapabilityField(CodegenContext ctx, Class type, String name) { + ctx.addField(finalDependencies, JsonCodegen.generatedCodecType(ctx, type), name, null); + } + String genAnyReaderCode( JsonGeneratedCodecBuilder builder, Class type, @@ -189,6 +212,7 @@ String genAnyReaderCode( if (creatorInfo != null) { return genAnyCreatorReaderCode(builder, type, creatorInfo); } + validateFastReadGroups(properties); Class concreteReaderType = readerType(); String readMethod = readMethod(); String anyReadMethod = readMethod + "Any"; @@ -1083,11 +1107,12 @@ private void addFastReadGroupMethods( Class readerType, Class type, JsonFieldInfo[] properties) { - if (!shouldSplitFastRead(properties)) { + if (!shouldSplitFastRead()) { return; } - for (int start = 0; start < properties.length; ) { - int end = readGroupEnd(properties, start); + int start = 0; + for (int group = 0; group < fastReadGroupEnds.length - 1; group++) { + int end = fastReadGroupEnds[group]; if (any == null) { addGeneratedMethod( ctx, @@ -2102,7 +2127,7 @@ private Expression fastReadExpression( String slowMethod, Class type, JsonFieldInfo[] properties) { - if (shouldSplitFastRead(properties)) { + if (shouldSplitFastRead()) { return splitFastReadExpression(builder, readMethod, slowMethod, type, properties); } Expression object = objectExpression(builder); @@ -2158,12 +2183,23 @@ private Expression splitFastReadExpression( Expression hashes = new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); expressions.add(hashes); - for (int start = 0; start < properties.length; ) { - int end = readGroupEnd(properties, start); + int start = 0; + for (int group = 0; group < fastReadGroupEnds.length - 1; group++) { + int end = fastReadGroupEnds[group]; Expression groupCall = inline(readGroupCall(readMethod, start, object, hashes)); expressions.add(new Expression.If(not(groupCall), returnObject(object))); start = end; } + Expression[] skips = new Expression[properties.length]; + for (int i = start + 1; i < properties.length; i++) { + skips[i] = new Expression.Variable("skip" + i, Expression.Literal.False); + expressions.add(skips[i]); + } + for (int i = start; i < properties.length; i++) { + Expression read = + fastReadField(builder, slowMethod, type, properties, i, object, hashes, skips); + expressions.add(i == start ? read : new Expression.If(not(skips[i]), read)); + } expressions.add(returnObject(object)); return expressions; } @@ -2188,11 +2224,7 @@ private Expression fastReadGroupExpression( fastReadField(builder, slowMethod, type, properties, i, end, true, object, hashes, skips); expressions.add(i == start ? read : new Expression.If(not(skips[i]), read)); } - if (end < properties.length) { - expressions.add(returnTrue()); - } else { - expressions.add(returnFalse()); - } + expressions.add(returnTrue()); return expressions; } @@ -2461,47 +2493,34 @@ final long packedNameMask(int length) { return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L; } - final boolean shouldSplitFastRead(JsonFieldInfo[] properties) { - return properties.length >= MIN_SPLIT_READ_FIELDS; - } - - final String readGroupMethod(String readMethod, int start) { - return readMethod + "Group" + start; - } - - final int readGroupEnd(JsonFieldInfo[] properties, int start) { - int end = start + 1; - while (end < properties.length - && end - start < READ_FIELD_GROUP_SIZE - && canPairReadFields(properties[end - 1], properties[end])) { - end++; + private void validateFastReadGroups(JsonFieldInfo[] properties) { + if (fastReadGroupEnds == null || fastReadGroupEnds.length == 0) { + throw new IllegalArgumentException("Fast-read group boundaries are required"); } - return end; - } - - final boolean canPairReadFields(JsonFieldInfo left, JsonFieldInfo right) { - JsonFieldKind leftKind = left.readKind(); - JsonFieldKind rightKind = right.readKind(); - if (leftKind == null || rightKind == null) { - return false; - } - // Fast-read fallback branches duplicate some field reads in each group. Keep method-size - // estimation conservative so generated helpers stay close to the C2-friendly target. - if (leftKind == JsonFieldKind.ENUM - || rightKind == JsonFieldKind.ENUM - || leftKind == JsonFieldKind.COLLECTION - || rightKind == JsonFieldKind.COLLECTION - || leftKind == JsonFieldKind.MAP - || rightKind == JsonFieldKind.MAP) { - return false; + if (properties.length == 0) { + if (fastReadGroupEnds.length != 1 || fastReadGroupEnds[0] != 0) { + throw new IllegalArgumentException("Empty readers require one empty group"); + } + return; } - if (leftKind == JsonFieldKind.ARRAY || rightKind == JsonFieldKind.ARRAY) { - return false; + int previous = 0; + for (int end : fastReadGroupEnds) { + if (end <= previous || end > properties.length) { + throw new IllegalArgumentException("Invalid fast-read group boundary " + end); + } + previous = end; } - if (leftKind == JsonFieldKind.OBJECT || rightKind == JsonFieldKind.OBJECT) { - return leftKind == JsonFieldKind.BOOLEAN || rightKind == JsonFieldKind.BOOLEAN; + if (previous != properties.length) { + throw new IllegalArgumentException("Fast-read groups must cover every property"); } - return true; + } + + final boolean shouldSplitFastRead() { + return fastReadGroupEnds.length > 1; + } + + final String readGroupMethod(String readMethod, int start) { + return readMethod + "Group" + start; } final boolean shouldSplitFieldSwitch(JsonFieldInfo[] properties) { 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 8f6f403dd9..fa81dea225 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 @@ -33,6 +33,10 @@ final class Latin1ReaderCodegen extends JsonReaderCodegen { super(codegen, resolver); } + Latin1ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, int[] fastReadGroupEnds) { + super(codegen, resolver, false, fastReadGroupEnds); + } + @Override Class codecFieldType(JsonFieldInfo property) { return codegen.latin1ReaderFieldType(property.readTypeInfo(), resolver); 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 10e5427aae..a3288ec582 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 @@ -32,6 +32,10 @@ final class Utf16ReaderCodegen extends JsonReaderCodegen { super(codegen, resolver); } + Utf16ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, int[] fastReadGroupEnds) { + super(codegen, resolver, false, fastReadGroupEnds); + } + @Override Class codecFieldType(JsonFieldInfo property) { return codegen.utf16ReaderFieldType(property.readTypeInfo(), resolver); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java new file mode 100644 index 0000000000..603dd4807c --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java @@ -0,0 +1,88 @@ +/* + * 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.codegen; + +import java.util.ArrayList; +import org.apache.fory.codegen.CodegenContext; +import org.apache.fory.json.codec.Utf8ReaderCodec; +import org.apache.fory.json.reader.Utf8JsonReader; + +/** Generates one exact declared ArrayList-backed UTF-8 collection capability. */ +final class Utf8CollectionReaderCodegen { + String genCode(String generatedPackage, String className) { + CodegenContext ctx = new CodegenContext(); + ctx.setPackage(generatedPackage); + ctx.setClassName(className); + ctx.setClassModifiers("final"); + ctx.addImports(ArrayList.class, Utf8JsonReader.class, Utf8ReaderCodec.class); + ctx.implementsInterfaces(ctx.type(Utf8ReaderCodec.class)); + ctx.addField(true, ctx.type(Utf8ReaderCodec.class), "elementReader", null); + ctx.addConstructor( + "this.elementReader = elementReader;", Utf8ReaderCodec.class, "elementReader"); + ctx.addMethod( + "@Override public final", + "readUtf8", + readBody(), + Object.class, + Utf8JsonReader.class, + "reader"); + return ctx.genCode(); + } + + private static String readBody() { + StringBuilder code = new StringBuilder(); + code.append("if (reader.tryReadNullToken()) {\n return null;\n}\n"); + code.append("reader.enterDepth();\n"); + code.append("reader.expectNextToken('[');\n"); + code.append("if (reader.consumeNextToken(']')) {\n"); + code.append(" reader.exitDepth();\n return new ArrayList(0);\n}\n"); + for (int i = 0; i < 8; i++) { + code.append("Object e").append(i).append(" = null;\n"); + } + code.append("ArrayList list = null;\nint size = 0;\n"); + code.append("do {\n"); + code.append(" Object element = elementReader.readUtf8(reader);\n"); + code.append(" if (list == null) {\n switch (size) {\n"); + for (int i = 0; i < 8; i++) { + code.append(" case ").append(i).append(": e").append(i).append(" = element; break;\n"); + } + code.append(" default:\n list = new ArrayList(9);\n"); + for (int i = 0; i < 8; i++) { + code.append(" list.add(e").append(i).append(");\n"); + } + code.append(" list.add(element);\n }\n"); + code.append(" } else {\n list.add(element);\n }\n"); + code.append(" size++;\n} while (reader.consumeNextCommaOrEndArray());\n"); + code.append("reader.exitDepth();\n"); + code.append("if (list != null) {\n return list;\n}\n"); + code.append("list = new ArrayList(size);\n"); + code.append("switch (size) {\n"); + for (int size = 1; size <= 8; size++) { + code.append(" case ").append(size).append(":\n"); + for (int i = 0; i < size; i++) { + code.append(" list.add(e").append(i).append(");\n"); + } + code.append(" break;\n"); + } + code.append(" default: throw new IllegalStateException();\n}\n"); + code.append("return list;\n"); + return code.toString(); + } +} 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 b2842044fd..0caa4591a7 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 @@ -29,13 +29,21 @@ import org.apache.fory.reflect.TypeRef; final class Utf8ReaderCodegen extends JsonReaderCodegen { - Utf8ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { - super(codegen, resolver); + Utf8ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, boolean finalDependencies) { + super(codegen, resolver, finalDependencies); + } + + Utf8ReaderCodegen( + JsonCodegen codegen, + JsonTypeResolver resolver, + boolean finalDependencies, + int[] fastReadGroupEnds) { + super(codegen, resolver, finalDependencies, fastReadGroupEnds); } @Override Class codecFieldType(JsonFieldInfo property) { - return codegen.utf8ReaderFieldType(property.readTypeInfo(), resolver); + return codegen.utf8ReaderFieldType(property.readTypeInfo(), resolver, finalDependencies()); } @Override diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java index 237e669d86..342bc9447e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java @@ -442,6 +442,24 @@ static Utf8ReaderCodec instantiateUtf8Reader( } } + @SuppressWarnings("unchecked") + static Utf8ReaderCodec instantiateUtf8CollectionReader( + Class type, Utf8ReaderCodec elementReader) { + try { + if (AndroidSupport.IS_ANDROID) { + Constructor constructor = type.getDeclaredConstructor(Utf8ReaderCodec.class); + constructor.setAccessible(true); + return (Utf8ReaderCodec) constructor.newInstance(elementReader); + } + MethodHandle constructor = + _JDKAccess._trustedLookup(type) + .findConstructor(type, MethodType.methodType(void.class, Utf8ReaderCodec.class)); + return (Utf8ReaderCodec) constructor.invoke(elementReader); + } catch (Throwable e) { + throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection reader", e); + } + } + @SuppressWarnings("unchecked") static Utf8ReaderCodec instantiateAnyUtf8Reader( Class type, diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index cf359827b3..e449963cdb 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -30,6 +30,7 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; @@ -80,6 +81,8 @@ import java.util.Set; import java.util.TimeZone; import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; @@ -91,6 +94,7 @@ import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Pattern; import org.apache.fory.annotation.Internal; +import org.apache.fory.codegen.CodeGenerator; import org.apache.fory.codegen.GeneratedClassNames; import org.apache.fory.exception.InsecureException; import org.apache.fory.json.ForyJsonException; @@ -112,6 +116,7 @@ import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.codec.MapCodec; import org.apache.fory.json.codec.MapKeyCodec; +import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.SqlJsonCodecs; import org.apache.fory.json.codegen.JsonCodegen; @@ -185,6 +190,9 @@ public int compare(DeclarationCandidate left, DeclarationCandidate right) { private final ConcurrentHashMap, MapKeyCodec> mapKeyCodecs; private final ConcurrentHashMap, GeneratedJsonCodec> generatedCodecs; private final Set> typesWithoutGeneratedCodec; + private final ConcurrentHashMap, CompletableFuture>> finalUtf8ReaderClasses; + private final ConcurrentHashMap, CompletableFuture>> mutableUtf8ReaderClasses; + private final ConcurrentHashMap>> utf8CollectionReaderClasses; private final ConcurrentHashMap cachedFieldNames; public JsonSharedRegistry(JsonConfig config) { @@ -214,6 +222,9 @@ public JsonSharedRegistry(JsonConfig config) { mapKeyCodecs = new ConcurrentHashMap<>(); generatedCodecs = new ConcurrentHashMap<>(); typesWithoutGeneratedCodec = ConcurrentHashMap.newKeySet(); + finalUtf8ReaderClasses = new ConcurrentHashMap<>(); + mutableUtf8ReaderClasses = new ConcurrentHashMap<>(); + utf8CollectionReaderClasses = new ConcurrentHashMap<>(); cachedFieldNames = new ConcurrentHashMap<>(); boolean codegenEnabled = config.codegenEnabled(); codegen = codegenEnabled ? new JsonCodegen(config.getCodegenHash(), classLoader) : null; @@ -222,6 +233,62 @@ public JsonSharedRegistry(JsonConfig config) { registerExactCodecs(); } + CompletableFuture> utf8ReaderClass( + ObjectCodec owner, JsonTypeResolver resolver, boolean finalDependencies) { + return generatedClassFuture( + finalDependencies ? finalUtf8ReaderClasses : mutableUtf8ReaderClasses, + owner.type(), + () -> codegen.compileUtf8Reader(owner, resolver, finalDependencies)); + } + + CompletableFuture> utf8CollectionReaderClass( + Type declaredType, CollectionCodec owner) { + return generatedClassFuture( + utf8CollectionReaderClasses, + declaredType, + () -> codegen.compileUtf8CollectionReader(declaredType, owner)); + } + + private CompletableFuture> generatedClassFuture( + ConcurrentHashMap>> classes, + K key, + Callable> compiler) { + CompletableFuture> existing = classes.get(key); + if (existing != null) { + return existing; + } + CompletableFuture> candidate = new CompletableFuture<>(); + existing = classes.putIfAbsent(key, candidate); + if (existing != null) { + return existing; + } + Runnable task = + () -> { + try { + candidate.complete(compiler.call()); + } catch (Throwable failure) { + classes.remove(key, candidate); + candidate.completeExceptionally(failure); + } + }; + if (!asyncCompilationEnabled) { + task.run(); + return candidate; + } + ExecutorService service = compilationService; + if (service == null) { + service = CodeGenerator.getCompilationService(); + } + try { + service.execute(task); + } catch (RuntimeException | Error failure) { + classes.remove(key, candidate); + candidate.completeExceptionally(failure); + throw failure; + } + return candidate; + } + /** Returns the immutable cached entry for {@code hash}, or null when none was published. */ @Internal public CachedFieldName cachedFieldName(long hash) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 74855f3e80..83f055cbc6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -26,6 +26,7 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -33,6 +34,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; import org.apache.fory.annotation.Internal; @@ -94,6 +96,8 @@ public final class JsonTypeResolver { private final JsonJITContext jitContext; private final IdentityMap, JsonTypeInfo> rawObjectTypeInfos; private final IdentityMap, JsonTypeInfo> canonicalObjectTypeInfos; + private final IdentityMap> collectionCodecs; + private final IdentityMap, Utf8ReaderGraph> pendingUtf8Readers; private int resolutionDepth; private enum RuntimeObjectKey { @@ -108,6 +112,8 @@ public JsonTypeResolver(JsonSharedRegistry sharedRegistry) { jitContext = sharedRegistry.newJITContext(); rawObjectTypeInfos = new IdentityMap<>(); canonicalObjectTypeInfos = new IdentityMap<>(); + collectionCodecs = new IdentityMap<>(); + pendingUtf8Readers = new IdentityMap<>(); } /** Returns the shared registry that owns this resolver and its reader cache domain. */ @@ -178,7 +184,7 @@ public ObjectCodec getUnwrappedObjectCodec(Class rawType) { } typeInfo = newTypeInfo(rawType, rawType, codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo, codec); + registerTypeInfoOwner(typeInfo, codec); return codec; } @@ -206,6 +212,41 @@ private ObjectCodec canonicalObjectOwner(JsonTypeInfo typeInfo) { return objectCodecs.get(typeInfo.rawType()); } + /** Returns an exact declared ArrayList-backed UTF-8 collection owner, or {@code null}. */ + @Internal + public CollectionCodec exactUtf8Collection(JsonTypeInfo typeInfo) { + jitContext.lock(); + try { + return exactUtf8CollectionOwner(typeInfo); + } finally { + jitContext.unlock(); + } + } + + private CollectionCodec exactUtf8CollectionOwner(JsonTypeInfo typeInfo) { + CollectionCodec owner = collectionCodecs.get(typeInfo); + if (owner == null + || !owner.createsArrayList() + || !(typeInfo.type() instanceof ParameterizedType)) { + return null; + } + Type declaredElement = CodecUtils.elementType(typeInfo.type()); + JsonTypeInfo element = declaredCollectionElement(typeInfo); + if (element == null + || !declaredElement.equals(element.type()) + || canonicalObjectOwner(element) == null + && !(owner instanceof CollectionCodec.DirectCollectionCodec)) { + return null; + } + return owner; + } + + private JsonTypeInfo declaredCollectionElement(JsonTypeInfo collection) { + Type elementType = CodecUtils.elementType(collection.type()); + Class rawType = CodecUtils.rawType(elementType, Object.class); + return typeInfos.get(typeInfoKey(elementType, rawType)); + } + public JsonTypeInfo getTypeInfo(Type declaredType, Class fallback) { Class rawType = CodecUtils.rawType(declaredType, fallback); Object key = typeInfoKey(declaredType, rawType); @@ -471,6 +512,7 @@ private void rollbackResolution(ResolutionSnapshot snapshot) { canonicalObjectTypeInfos.remove(owner); } } + collectionCodecs.remove(value); typeIterator.remove(); } } @@ -530,7 +572,7 @@ private JsonTypeInfo resolveRuntimeTypeInfo(Class runtimeType, Object key) { } typeInfos.put(key, typeInfo); if (codec != null) { - registerObjectTypeInfo(typeInfo, codec); + registerTypeInfoOwner(typeInfo, codec); } return typeInfo; } @@ -682,23 +724,16 @@ public Utf8ReaderCodec utf8Reader(ObjectCodec codec) { } Utf8ReaderCodec installed = typeInfo.utf8Reader(); if (installed == owner && codegen != null && codegen.canCompileReader(owner)) { - jitContext.registerJITCallback( - () -> owner.getClass(), - () -> codegen.compileUtf8Reader(owner, this), - new JsonJITContext.JITCallback>() { - @Override - public void onSuccess(Class generated) { - publishUtf8Reader(owner, typeInfo, generated); - } - - @Override - public void onFailure(Throwable failure) {} - - @Override - public Object id() { - return codegen.utf8ReaderJITId(owner.type()); - } - }); + if (pendingUtf8Readers.get(owner) != null) { + return (Utf8ReaderCodec) installed; + } + Utf8ReaderGraph graph = new Utf8ReaderGraph(codegen.utf8ReaderJITId(owner.type())); + if (!graph.addObject(owner, typeInfo)) { + graph = null; + } else { + graph.markPending(); + } + requestUtf8Reader(owner, typeInfo, graph); installed = typeInfo.utf8Reader(); } return (Utf8ReaderCodec) installed; @@ -799,14 +834,15 @@ private void resolveInlineUtf8Reader( return; } jitContext.registerJITNotifyCallback( - codegen.utf8ReaderJITId(owner.type()), + utf8ReaderRequestId(typeInfo), new JsonJITContext.NotifyCallback() { @Override public void onNotifyResult(Object result) { Utf8ReaderCodec installed = typeInfo.utf8Reader(); - checkGeneratedClass(result, installed); - parent.setInlineUtf8Reader( - index, newInlineUtf8Reader(owner, (Class) result, readTable, installed)); + if (installed != owner) { + parent.setInlineUtf8Reader( + index, newInlineUtf8Reader(owner, installed.getClass(), readTable, installed)); + } } @Override @@ -1471,6 +1507,305 @@ private Field[] unwrappedReaderChildFields(Object parent, ObjectCodec owner) return childFields; } + private void requestUtf8Reader( + ObjectCodec owner, JsonTypeInfo typeInfo, Utf8ReaderGraph graph) { + jitContext.registerJITFuture( + () -> null, + () -> utf8ReaderInstall(owner, typeInfo, graph), + new JsonJITContext.JITCallback() { + @Override + public void onSuccess(Utf8ReaderInstall result) { + try { + result.publish(); + } finally { + if (graph != null) { + graph.clearPending(); + } + } + } + + @Override + public void onFailure(Throwable failure) { + if (graph != null) { + graph.clearPending(); + } + } + + @Override + public Object id() { + return graph == null ? codegen.utf8ReaderJITId(owner.type()) : graph.requestId; + } + }); + } + + private Object utf8ReaderRequestId(JsonTypeInfo typeInfo) { + ObjectCodec owner = canonicalObjectOwner(typeInfo); + if (owner != null) { + Utf8ReaderGraph graph = pendingUtf8Readers.get(erase(owner)); + if (graph != null) { + return graph.requestId; + } + } + return codegen.utf8ReaderJITId(typeInfo.rawType()); + } + + private CompletableFuture utf8ReaderInstall( + ObjectCodec owner, JsonTypeInfo typeInfo, Utf8ReaderGraph graph) { + if (graph != null) { + return graph.classesReady().thenApply(ignored -> new Utf8ReaderInstall(graph)); + } + return sharedRegistry + .utf8ReaderClass(owner, this, false) + .thenApply(generated -> new Utf8ReaderInstall(owner, typeInfo, generated)); + } + + @SuppressWarnings("unchecked") + private Utf8ReaderCodec newGraphUtf8Reader( + ObjectCodec owner, + Class generatedClass, + IdentityMap> capabilities) { + JsonFieldInfo[] fields = owner.readFields(); + Utf8ReaderCodec[] codecs = + (Utf8ReaderCodec[]) new Utf8ReaderCodec[fields.length]; + for (int i = 0; i < fields.length; i++) { + JsonFieldInfo field = fields[i]; + if (JsonCodegen.usesReadCodec(field, this) + || JsonCodegen.readNestedType(field, this) != null) { + JsonTypeInfo child = field.readTypeInfo(); + Utf8ReaderCodec capability = capabilities.get(child); + codecs[i] = capability == null ? child.utf8Reader() : capability; + } + } + return GeneratedCodecInstantiator.instantiateUtf8Reader(generatedClass, owner, fields, codecs); + } + + private final class Utf8ReaderInstall { + private final Utf8ReaderGraph graph; + private final ObjectCodec owner; + private final JsonTypeInfo typeInfo; + private final Class generatedClass; + + private Utf8ReaderInstall(Utf8ReaderGraph graph) { + this.graph = graph; + owner = null; + typeInfo = null; + generatedClass = null; + } + + private Utf8ReaderInstall( + ObjectCodec owner, JsonTypeInfo typeInfo, Class generatedClass) { + graph = null; + this.owner = owner; + this.typeInfo = typeInfo; + this.generatedClass = generatedClass; + } + + private void publish() { + if (graph == null) { + publishUtf8Reader(owner, typeInfo, generatedClass); + } else { + graph.publish(); + } + } + } + + /** + * One acyclic UTF-8 graph. Class futures have no dependency edges; this resolver applies + * dependency order only after every class is ready, then publishes the complete local graph in + * one lock-held loop. + */ + private final class Utf8ReaderGraph { + private final Object requestId; + private final IdentityMap nodes = new IdentityMap<>(); + private final ArrayList ordered = new ArrayList<>(); + + private Utf8ReaderGraph(Object requestId) { + this.requestId = requestId; + } + + private void markPending() { + for (int i = 0; i < ordered.size(); i++) { + Utf8ReaderNode node = ordered.get(i); + ObjectCodec owner = node.objectOwner; + if (owner != null + && node.typeInfo.utf8Reader() == node.initial + && pendingUtf8Readers.get(owner) == null) { + pendingUtf8Readers.put(owner, this); + } + } + } + + private void clearPending() { + for (int i = 0; i < ordered.size(); i++) { + ObjectCodec owner = ordered.get(i).objectOwner; + if (owner != null && pendingUtf8Readers.get(owner) == this) { + pendingUtf8Readers.remove(owner); + } + } + } + + private boolean addObject(ObjectCodec rawOwner, JsonTypeInfo typeInfo) { + ObjectCodec owner = erase(rawOwner); + Utf8ReaderNode existing = nodes.get(typeInfo); + if (existing != null) { + return existing.complete; + } + AnyInfo any = owner.anyInfo(); + if (owner.creatorInfo() != null + || owner.unwrappedInfo() != null + || any != null && (any.readField() != null || any.readSetter() != null) + || !codegen.canCompileReader(owner)) { + return false; + } + Utf8ReaderNode node = new Utf8ReaderNode(typeInfo, owner); + nodes.put(typeInfo, node); + JsonFieldInfo[] fields = owner.readFields(); + for (int i = 0; i < fields.length; i++) { + JsonFieldInfo field = fields[i]; + JsonTypeInfo child = field.readTypeInfo(); + ObjectCodec childOwner = canonicalObjectOwner(child); + if (childOwner != null) { + // Eligibility and generated source depend only on the complete declared graph. An + // already-published child may be reused during construction, but must never hide schema + // edges here or make parent source depend on publication order. + if (!addObject(childOwner, child)) { + return false; + } + continue; + } + CollectionCodec collection = exactUtf8CollectionOwner(child); + if (collection != null) { + if (!addCollection(collection, child)) { + return false; + } + continue; + } + if (collectionCodecs.get(child) != null + || field.readKind() == JsonFieldKind.ARRAY + || field.readKind() == JsonFieldKind.MAP + || field.readKind() == JsonFieldKind.OBJECT + && (field.readRawType() == Object.class + || child.utf8Reader() instanceof ClosedSubtypeCodec)) { + return false; + } + } + node.complete = true; + ordered.add(node); + return true; + } + + private boolean addCollection(CollectionCodec owner, JsonTypeInfo typeInfo) { + Utf8ReaderNode existing = nodes.get(typeInfo); + if (existing != null) { + return existing.complete; + } + Utf8ReaderNode node = new Utf8ReaderNode(typeInfo, owner); + nodes.put(typeInfo, node); + JsonTypeInfo element = declaredCollectionElement(typeInfo); + ObjectCodec elementOwner = canonicalObjectOwner(element); + if (elementOwner != null && !addObject(elementOwner, element)) { + return false; + } + node.complete = true; + ordered.add(node); + return true; + } + + private CompletableFuture classesReady() { + CompletableFuture[] futures = new CompletableFuture[ordered.size()]; + for (int i = 0; i < ordered.size(); i++) { + Utf8ReaderNode node = ordered.get(i); + node.classFuture = + node.objectOwner == null + ? sharedRegistry.utf8CollectionReaderClass( + node.typeInfo.type(), node.collectionOwner) + : sharedRegistry.utf8ReaderClass(node.objectOwner, JsonTypeResolver.this, true); + futures[i] = node.classFuture; + } + return CompletableFuture.allOf(futures); + } + + private void publish() { + requireJITLock(); + IdentityMap> capabilities = new IdentityMap<>(); + ArrayList unpublished = new ArrayList<>(); + for (int i = 0; i < ordered.size(); i++) { + Utf8ReaderNode node = ordered.get(i); + if (!node.metadataMatches()) { + return; + } + Utf8ReaderCodec current = node.typeInfo.utf8Reader(); + if (current != node.initial) { + capabilities.put(node.typeInfo, current); + continue; + } + Class generatedClass = node.classFuture.getNow(null); + if (generatedClass == null) { + throw new IllegalStateException("Generated UTF-8 class is not ready"); + } + Utf8ReaderCodec capability; + if (node.objectOwner != null) { + capability = newGraphUtf8Reader(node.objectOwner, generatedClass, capabilities); + } else { + JsonTypeInfo element = declaredCollectionElement(node.typeInfo); + Utf8ReaderCodec elementReader = capabilities.get(element); + if (elementReader == null) { + elementReader = element.utf8Reader(); + } + capability = + GeneratedCodecInstantiator.instantiateUtf8CollectionReader( + generatedClass, elementReader); + } + node.instance = capability; + capabilities.put(node.typeInfo, capability); + unpublished.add(node); + } + for (int i = 0; i < unpublished.size(); i++) { + Utf8ReaderNode node = unpublished.get(i); + if (!node.metadataMatches() || node.typeInfo.utf8Reader() != node.initial) { + return; + } + } + for (int i = 0; i < unpublished.size(); i++) { + Utf8ReaderNode node = unpublished.get(i); + node.typeInfo.setUtf8Reader(node.instance); + } + } + } + + private final class Utf8ReaderNode { + private final JsonTypeInfo typeInfo; + private final ObjectCodec objectOwner; + private final CollectionCodec collectionOwner; + private final Utf8ReaderCodec initial; + private boolean complete; + private CompletableFuture> classFuture; + private Utf8ReaderCodec instance; + + private Utf8ReaderNode(JsonTypeInfo typeInfo, ObjectCodec owner) { + this.typeInfo = typeInfo; + objectOwner = owner; + collectionOwner = null; + initial = owner; + } + + @SuppressWarnings("unchecked") + private Utf8ReaderNode(JsonTypeInfo typeInfo, CollectionCodec owner) { + this.typeInfo = typeInfo; + objectOwner = null; + collectionOwner = owner; + initial = (Utf8ReaderCodec) (Utf8ReaderCodec) owner; + } + + private boolean metadataMatches() { + if (objectOwner != null) { + return canonicalObjectOwner(typeInfo) == objectOwner; + } + return collectionCodecs.get(typeInfo) == collectionOwner + && typeInfos.get(typeInfoKey(typeInfo.type(), typeInfo.rawType())) == typeInfo; + } + } + // Publication runs under the local JIT lock: construct the resolver-local instance, resolve // every replaceable child Field, register child notifications, then write the canonical // JsonTypeInfo slot captured when the compilation request is created. Capturing that exact slot @@ -1660,13 +1995,14 @@ private void registerUtf8ReaderCallbacks( if (field != null) { JsonTypeInfo child = readerChildTypeInfo(owner, i); jitContext.registerJITNotifyCallback( - codegen.utf8ReaderJITId(child.rawType()), + utf8ReaderRequestId(child), new JsonJITContext.NotifyCallback() { @Override public void onNotifyResult(Object result) { Utf8ReaderCodec codec = child.utf8Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); + if (!(codec instanceof ObjectCodec)) { + ReflectionUtils.setObjectFieldValue(parent, field, codec); + } } @Override @@ -1785,13 +2121,14 @@ private void registerUtf8AnyReaderCallback(Utf8ReaderCodec parent, Objec Field field = ReflectionUtils.getField(parent.getClass(), "anyReader"); JsonTypeInfo child = any.valueTypeInfo(); jitContext.registerJITNotifyCallback( - codegen.utf8ReaderJITId(child.rawType()), + utf8ReaderRequestId(child), new JsonJITContext.NotifyCallback() { @Override public void onNotifyResult(Object result) { Utf8ReaderCodec codec = child.utf8Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); + if (!(codec instanceof ObjectCodec)) { + ReflectionUtils.setObjectFieldValue(parent, field, codec); + } } @Override @@ -1876,7 +2213,7 @@ private JsonTypeInfo buildTypeInfo(Class rawType, Type declaredType, Object k } JsonTypeInfo typeInfo = newTypeInfo(declaredType, rawType, codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo, codec); + registerTypeInfoOwner(typeInfo, codec); return typeInfo; } @@ -1894,7 +2231,7 @@ private JsonTypeInfo buildObjectTypeInfo(TypeRef ownerType, Object key) { // capability slots. The outer cold-resolution transaction removes both on failure. objectCodecs.put(key, codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo, codec); + registerTypeInfoOwner(typeInfo, codec); codec.resolveTypes(this); return typeInfo; } @@ -1902,7 +2239,7 @@ private JsonTypeInfo buildObjectTypeInfo(TypeRef ownerType, Object key) { // now; the outer owner finishes field resolution before returning the codec to its caller. typeInfo = newTypeInfo(ownerType.getType(), ownerType.getRawType(), codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo, codec); + registerTypeInfoOwner(typeInfo, codec); return typeInfo; } @@ -1919,7 +2256,10 @@ private JsonTypeInfo newTypeInfo( return new JsonTypeInfo(type, rawType, kind, bindCodec(codec), annotationCodec); } - private void registerObjectTypeInfo(JsonTypeInfo typeInfo, JsonValueCodec initialCodec) { + private void registerTypeInfoOwner(JsonTypeInfo typeInfo, JsonValueCodec initialCodec) { + if (initialCodec instanceof CollectionCodec) { + collectionCodecs.put(typeInfo, (CollectionCodec) initialCodec); + } if (initialCodec.getClass() == ObjectCodec.class && typeInfo.type() instanceof Class && typeInfo.rawType() != Object.class) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java index d3d540c051..243395131d 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java @@ -35,6 +35,7 @@ import java.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -59,6 +60,7 @@ import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.codec.Latin1ReaderCodec; import org.apache.fory.json.codec.ObjectCodec; +import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.StringWriterCodec; import org.apache.fory.json.codec.Utf16ReaderCodec; import org.apache.fory.json.codec.Utf8ReaderCodec; @@ -682,6 +684,180 @@ public void sourceShapeIgnoresPublicationOrder() { assertTrue(declaredFieldTypes(parentFirst).contains(Utf8WriterCodec.class.getName())); } + @Test + public void utf8GraphPublishesAtomically() throws Exception { + ControlledJson controlled = controlledJson(); + String input = + "{\"children\":[{\"id\":1,\"name\":\"a\"},{\"id\":2,\"name\":\"b\"}," + + "{\"id\":3,\"name\":\"c\"},{\"id\":4,\"name\":\"d\"}," + + "{\"id\":5,\"name\":\"e\"},{\"id\":6,\"name\":\"f\"}," + + "{\"id\":7,\"name\":\"g\"},{\"id\":8,\"name\":\"h\"}," + + "{\"id\":9,\"name\":\"i\"}],\"friends\":[{\"id\":10}]}"; + AsyncCollections initial = + controlled.json.fromJson(input.getBytes(StandardCharsets.UTF_8), AsyncCollections.class); + assertEquals(initial.children.size(), 9); + assertEquals(initial.friends.get(0).id, 10); + + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + ObjectCodec rootOwner = resolver.getObjectCodec(AsyncCollections.class); + ObjectCodec childOwner = resolver.getObjectCodec(AsyncChild.class); + ObjectCodec friendOwner = resolver.getObjectCodec(AsyncFriend.class); + JsonTypeInfo rootInfo = resolver.getTypeInfo(AsyncCollections.class, AsyncCollections.class); + JsonTypeInfo childInfo = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); + JsonTypeInfo friendInfo = resolver.getTypeInfo(AsyncFriend.class, AsyncFriend.class); + JsonFieldInfo[] fields = rootOwner.readFields(); + JsonTypeInfo childrenInfo = fields[0].readTypeInfo(); + JsonTypeInfo friendsInfo = fields[1].readTypeInfo(); + Object initialChildren = childrenInfo.utf8Reader(); + Object initialFriends = friendsInfo.utf8Reader(); + + assertEquals(controlled.executor.pendingTasks(), 5); + for (int i = 0; i < 4; i++) { + controlled.executor.runNext(); + assertSame(rootInfo.utf8Reader(), rootOwner); + assertSame(childInfo.utf8Reader(), childOwner); + assertSame(friendInfo.utf8Reader(), friendOwner); + assertSame(childrenInfo.utf8Reader(), initialChildren); + assertSame(friendsInfo.utf8Reader(), initialFriends); + } + controlled.executor.runNext(); + + assertNotSame(rootInfo.utf8Reader(), rootOwner); + assertNotSame(childInfo.utf8Reader(), childOwner); + assertNotSame(friendInfo.utf8Reader(), friendOwner); + assertNotSame(childrenInfo.utf8Reader(), initialChildren); + assertNotSame(friendsInfo.utf8Reader(), initialFriends); + assertTrue(childrenInfo.utf8Reader().getClass() != friendsInfo.utf8Reader().getClass()); + assertFinalElementReader(childrenInfo.utf8Reader(), childInfo.utf8Reader()); + assertFinalElementReader(friendsInfo.utf8Reader(), friendInfo.utf8Reader()); + assertFinalCollectionFields( + rootInfo.utf8Reader(), childrenInfo.utf8Reader(), friendsInfo.utf8Reader()); + + AsyncCollections generated = + controlled.json.fromJson(input.getBytes(StandardCharsets.UTF_8), AsyncCollections.class); + assertEquals(generated.children.size(), 9); + assertEquals(generated.children.get(8).id, 9); + assertEquals(generated.friends.get(0).id, 10); + AsyncCollections empty = + controlled.json.fromJson( + "{\"children\":[],\"friends\":null}".getBytes(StandardCharsets.UTF_8), + AsyncCollections.class); + assertTrue(empty.children.isEmpty()); + assertNull(empty.friends); + } + + @Test + public void utf8GraphNotifiesFallbackParent() throws Exception { + ControlledJson controlled = controlledJson(); + byte[] creatorInput = + "{\"child\":{\"id\":1,\"name\":\"creator\"}}".getBytes(StandardCharsets.UTF_8); + + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + ObjectCodec creatorOwner = + resolver.getObjectCodec(AsyncCreatorParent.class); + ObjectCodec graphOwner = resolver.getObjectCodec(AsyncGraphParent.class); + ObjectCodec childOwner = resolver.getObjectCodec(AsyncChild.class); + JsonTypeInfo creatorInfo = + resolver.getTypeInfo(AsyncCreatorParent.class, AsyncCreatorParent.class); + resolver.getTypeInfo(AsyncGraphParent.class, AsyncGraphParent.class); + JsonTypeInfo childInfo = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); + resolver.lockJIT(); + try { + assertSame(resolver.utf8Reader(creatorOwner), creatorOwner); + assertSame(resolver.utf8Reader(graphOwner), graphOwner); + } finally { + resolver.unlockJIT(); + } + + assertEquals(controlled.executor.pendingTasks(), 3); + controlled.executor.runNext(); + assertNotSame(creatorInfo.utf8Reader(), creatorOwner); + assertCapabilityFields(creatorInfo.utf8Reader(), Utf8ReaderCodec.class, childOwner, 1); + controlled.executor.runNext(); + assertSame(childInfo.utf8Reader(), childOwner); + controlled.executor.runNext(); + + assertNotSame(childInfo.utf8Reader(), childOwner); + assertCapabilityFields( + creatorInfo.utf8Reader(), Utf8ReaderCodec.class, childInfo.utf8Reader(), 1); + assertEquals( + controlled.json.fromJson(creatorInput, AsyncCreatorParent.class).child.name, "creator"); + } + + @Test + public void utf8ScalarCollectionCapability() throws Exception { + ControlledJson controlled = controlledJson(); + String input = + "{\"values\":[null,\"\",\"short\",\"abcdefghijklmnopqrstuvwxyz0123456789\"," + + "\"quote\\\"\",\"slash\\\\\",\"line\\n\",\"你好\"]}"; + List expected = + Arrays.asList( + null, + "", + "short", + "abcdefghijklmnopqrstuvwxyz0123456789", + "quote\"", + "slash\\", + "line\n", + "你好"); + AsyncStringCollections initial = + controlled.json.fromJson( + input.getBytes(StandardCharsets.UTF_8), AsyncStringCollections.class); + assertEquals(initial.values, expected); + + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + ObjectCodec rootOwner = + resolver.getObjectCodec(AsyncStringCollections.class); + JsonTypeInfo rootInfo = + resolver.getTypeInfo(AsyncStringCollections.class, AsyncStringCollections.class); + JsonTypeInfo valuesInfo = rootOwner.readFields()[0].readTypeInfo(); + Object initialValues = valuesInfo.utf8Reader(); + + assertEquals(controlled.executor.pendingTasks(), 2); + controlled.executor.runNext(); + assertSame(rootInfo.utf8Reader(), rootOwner); + assertSame(valuesInfo.utf8Reader(), initialValues); + controlled.executor.runNext(); + + assertNotSame(rootInfo.utf8Reader(), rootOwner); + assertNotSame(valuesInfo.utf8Reader(), initialValues); + assertFinalElementReader(valuesInfo.utf8Reader(), ScalarCodecs.StringCodec.INSTANCE); + assertFinalCollectionFields(rootInfo.utf8Reader(), valuesInfo.utf8Reader()); + AsyncStringCollections generated = + controlled.json.fromJson( + input.getBytes(StandardCharsets.UTF_8), AsyncStringCollections.class); + assertEquals(generated.values, expected); + } + + @Test + public void utf8ShapeIgnoresPublishedChild() throws Exception { + String input = "{\"creator\":{\"id\":1,\"name\":\"a\"},\"friends\":[{\"id\":2}]}"; + + ControlledJson parentFirst = controlledJson(); + AsyncMixedParent first = + parentFirst.json.fromJson(input.getBytes(StandardCharsets.UTF_8), AsyncMixedParent.class); + parentFirst.executor.runAll(); + assertEquals(first.creator.id, 1); + JsonTypeResolver firstResolver = primaryTypeResolver(parentFirst.json); + JsonTypeInfo firstInfo = + firstResolver.getTypeInfo(AsyncMixedParent.class, AsyncMixedParent.class); + + ControlledJson childFirst = controlledJson(); + childFirst.json.fromJson( + "{\"id\":3,\"name\":\"child\"}".getBytes(StandardCharsets.UTF_8), AsyncCreator.class); + childFirst.executor.runAll(); + AsyncMixedParent second = + childFirst.json.fromJson(input.getBytes(StandardCharsets.UTF_8), AsyncMixedParent.class); + childFirst.executor.runAll(); + assertEquals(second.friends.get(0).id, 2); + JsonTypeResolver secondResolver = primaryTypeResolver(childFirst.json); + JsonTypeInfo secondInfo = + secondResolver.getTypeInfo(AsyncMixedParent.class, AsyncMixedParent.class); + + assertEquals( + declaredFieldShape(firstInfo.utf8Reader()), declaredFieldShape(secondInfo.utf8Reader())); + } + @Test public void nestedAndRecursiveTypes() throws Exception { ControlledJson controlled = controlledJson(); @@ -857,8 +1033,9 @@ private static void assertCapabilityFields( for (Field field : owner.getClass().getDeclaredFields()) { if (field.getType() == fieldType) { field.setAccessible(true); - assertSame(field.get(owner), expected, field.toString()); - count++; + if (field.get(owner) == expected) { + count++; + } } } assertEquals(count, expectedFields, owner.getClass().getName()); @@ -881,6 +1058,46 @@ private static List declaredFieldTypes(Object capability) { return types; } + private static List declaredFieldShape(Object capability) { + List shape = new ArrayList<>(); + for (Field field : capability.getClass().getDeclaredFields()) { + shape.add( + field.getName() + + ":" + + field.getType().getName() + + ":" + + Modifier.isFinal(field.getModifiers())); + } + Collections.sort(shape); + return shape; + } + + private static void assertFinalElementReader(Object collection, Object element) throws Exception { + Field field = collection.getClass().getDeclaredField("elementReader"); + field.setAccessible(true); + assertTrue(Modifier.isFinal(field.getModifiers())); + assertSame(field.get(collection), element); + } + + private static void assertFinalCollectionFields(Object root, Object... collections) + throws Exception { + int count = 0; + for (Field field : root.getClass().getDeclaredFields()) { + if (field.getType() == Utf8ReaderCodec.class) { + field.setAccessible(true); + Object value = field.get(root); + for (Object collection : collections) { + if (value == collection) { + assertTrue(Modifier.isFinal(field.getModifiers())); + count++; + break; + } + } + } + } + assertEquals(count, collections.length); + } + private static StringWriterCodec stringWriter( JsonTypeResolver resolver, ObjectCodec owner) { resolver.lockJIT(); @@ -899,8 +1116,9 @@ private static void assertNestedUtf8Readers(ForyJson json) throws Exception { for (Field field : parentReader.getClass().getDeclaredFields()) { if (field.getType() == Utf8ReaderCodec.class) { field.setAccessible(true); - assertSame(field.get(parentReader), childReader); - nestedReaders++; + if (field.get(parentReader) == childReader) { + nestedReaders++; + } } } assertEquals(nestedReaders, 1); @@ -1165,6 +1383,37 @@ public static final class AsyncParent { public List list; } + public static final class AsyncCollections { + public List children; + public List friends; + } + + public static final class AsyncMixedParent { + public AsyncCreator creator; + public List friends; + } + + public static final class AsyncStringCollections { + public List values; + } + + public static final class AsyncCreatorParent { + public final AsyncChild child; + + @JsonCreator({"child"}) + public AsyncCreatorParent(AsyncChild child) { + this.child = child; + } + } + + public static final class AsyncGraphParent { + public AsyncChild child; + } + + public static final class AsyncFriend { + public int id; + } + public static final class AsyncCreator { public final int id; public final String name; From 1f0e17f3b181fec5a3fdda39b879927dfc7ff451 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 04:43:08 +0800 Subject: [PATCH 03/20] perf(json): bound positive int tail Validate the only possible tenth digit directly after nine positive digits and reject a following digit as overflow. --- .../fory/json/reader/Utf8JsonReader.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) 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..0ecfc53aed 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 @@ -64,6 +64,8 @@ public final class Utf8JsonReader extends JsonReader { private static final int INT_CONTROL_LIMIT_BYTES = 0x20202020; private static final long QUOTE_BYTES = 0x2222222222222222L; private static final int INT_QUOTE_BYTES = 0x22222222; + private static final int INT_MAX_DIV_10 = Integer.MAX_VALUE / 10; + private static final int INT_MAX_MOD_10 = Integer.MAX_VALUE % 10; private static final long LONG_MAX_DIV_10 = Long.MAX_VALUE / 10; private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); private static final long LONG_MAX_DIV_100 = Long.MAX_VALUE / 100; @@ -636,18 +638,21 @@ private int readIntToken() { } private int readPositiveIntTail(byte[] bytes, int offset, int inputLength, int result) { - while (offset < inputLength) { + // The caller has consumed exactly nine positive digits. A Java int can contain only one more; + // any following digit is necessarily overflow rather than another loop iteration. + int digit = bytes[offset] - '0'; + if (result > INT_MAX_DIV_10 || (result == INT_MAX_DIV_10 && digit > INT_MAX_MOD_10)) { + position = offset; + throw error("Integer overflow"); + } + result = result * 10 + digit; + offset++; + if (offset < inputLength) { int ch = bytes[offset]; - if (ch < '0' || ch > '9') { - break; - } - long value = (long) result * 10 + (ch - '0'); - if (value > Integer.MAX_VALUE) { + if (ch >= '0' && ch <= '9') { position = offset; throw error("Integer overflow"); } - result = (int) value; - offset++; } position = offset; rejectFractionOrExponentFast(); From 30340e4cb488ffe72f4d300407732fcc28553dae Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 06:08:07 +0800 Subject: [PATCH 04/20] perf(json): accelerate decimal double parsing --- .../apache/fory/json/reader/DecimalMath.java | 36 ++++ .../apache/fory/json/reader/JsonReader.java | 71 +++++-- .../fory/json/reader/Utf8JsonReader.java | 185 +++++++++++++----- .../apache/fory/json/reader/DecimalMath.java | 29 +++ .../fory/json/reader/DecimalMathTest.java | 56 ++++++ .../writer/Jdk25MultiReleaseJarVerifier.java | 16 ++ 6 files changed, 324 insertions(+), 69 deletions(-) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalMath.java create mode 100644 java/fory-json/src/main/java25/org/apache/fory/json/reader/DecimalMath.java create mode 100644 java/fory-json/src/test/java/org/apache/fory/json/reader/DecimalMathTest.java diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalMath.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalMath.java new file mode 100644 index 0000000000..d66b3ea6a8 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalMath.java @@ -0,0 +1,36 @@ +/* + * 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.reader; + +/** Exact integer arithmetic used by decimal readers. */ +final class DecimalMath { + private DecimalMath() {} + + static long unsignedMultiplyHigh(long x, long y) { + long xlo = x & 0xffff_ffffL; + long xhi = x >>> 32; + long ylo = y & 0xffff_ffffL; + long yhi = y >>> 32; + long lowProduct = xlo * ylo; + long carryProduct = xhi * ylo + (lowProduct >>> 32); + long middle = (carryProduct & 0xffff_ffffL) + xlo * yhi; + return xhi * yhi + (carryProduct >>> 32) + (middle >>> 32); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java index 387b226a0b..516c7c2682 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java @@ -98,6 +98,27 @@ public abstract class JsonReader { private static final long DOUBLE_FRACTION_MASK = (1L << DOUBLE_FRACTION_BITS) - 1; private static final long DOUBLE_INFINITY_BITS = 0x7ff0_0000_0000_0000L; private static final long DOUBLE_MAX_FINITE_BITS = 0x7fef_ffff_ffff_ffffL; + private static final long[] COMPACT_DOUBLE_MANTISSAS = { + 0x8000_0000_0000_0000L, + 0xcccc_cccc_cccc_ccccL, + 0xa3d7_0a3d_70a3_d70aL, + 0x8312_6e97_8d4f_df3bL, + 0xd1b7_1758_e219_652bL, + 0xa7c5_ac47_1b47_8423L, + 0x8637_bd05_af6c_69b5L, + 0xd6bf_94d5_e57a_42bcL, + 0xabcc_7711_8461_cefcL, + 0x8970_5f41_36b4_a597L, + 0xdbe6_fece_bded_d5beL, + 0xafeb_ff0b_cb24_aafeL, + 0x8cbc_cc09_6f50_88cbL, + 0xe12e_1342_4bb4_0e13L, + 0xb424_dc35_095c_d80fL, + 0x901d_7cf7_3ab0_acd9L, + 0xe695_94be_c44d_e15bL, + 0xb877_aa32_36a4_b449L, + 0x9392_ee8e_921d_5d07L + }; private static final int DECIMAL_BOUNDARY_DIGITS = 768; private static final double[] DOUBLE_POWERS_OF_TEN = { 1.0d, @@ -1176,15 +1197,46 @@ protected static double compactDoubleValue(boolean negative, long unscaled, int if (unscaled == 0) { return negative ? -0.0d : 0.0d; } - long divisor = LONG_POWERS_OF_TEN[scale]; - double estimate = (double) unscaled / (double) divisor; - long bits = correctCompactDouble(unscaled, divisor, Double.doubleToRawLongBits(estimate)); + long bits = tryCompactDoubleBits(unscaled, scale); + if (bits == 0) { + long divisor = LONG_POWERS_OF_TEN[scale]; + double estimate = (double) unscaled / (double) divisor; + bits = correctCompactDouble(unscaled, divisor, Double.doubleToRawLongBits(estimate)); + } if (negative) { bits |= DOUBLE_SIGN_BIT; } return Double.longBitsToDouble(bits); } + // The caller supplies a nonzero positive long and scale 0..18. A zero result is therefore not a + // value; it asks the caller to use the exact midpoint path when the high product cannot prove the + // rounding direction. The common path is the Eisel-Lemire decimal conversion specialized to + // this bounded compact domain. + private static long tryCompactDoubleBits(long unscaled, int scale) { + int leadingZeros = Long.numberOfLeadingZeros(unscaled); + long upper = + DecimalMath.unsignedMultiplyHigh(unscaled << leadingZeros, COMPACT_DOUBLE_MANTISSAS[scale]); + int upperBit = (int) (upper >>> 63); + long mantissa = upper >>> (upperBit + 9); + leadingZeros += 1 ^ upperBit; + long roundBits = upper & 0x1ff; + if (roundBits == 0x1ff || (roundBits == 0 && (mantissa & 3) == 1)) { + return 0; + } + mantissa = (mantissa + 1) >>> 1; + if (mantissa >= (1L << (DOUBLE_FRACTION_BITS + 1))) { + mantissa = 1L << DOUBLE_FRACTION_BITS; + leadingZeros--; + } + mantissa &= DOUBLE_FRACTION_MASK; + long exponent = ((217706L * -scale) >> 16) + 1023 + 64 - leadingZeros; + if (exponent < 1 || exponent > 2046) { + return 0; + } + return mantissa | (exponent << DOUBLE_FRACTION_BITS); + } + private static long correctCompactDouble(long unscaled, long divisor, long bits) { // Rounded long operands plus one hardware division stay close to the exact decimal rational. // Exact midpoint comparisons repair the candidate; the old full division remains the cold @@ -1298,7 +1350,7 @@ private static int compareDecimalToFloatBoundary( private static int compareDecimalToBinary( long unscaled, long divisor, long numerator, int binaryExponent) { long productLow = divisor * numerator; - long productHigh = multiplyHigh(divisor, numerator); + long productHigh = DecimalMath.unsignedMultiplyHigh(divisor, numerator); int productBits = bitLength(productHigh, productLow); int unscaledBits = Long.SIZE - Long.numberOfLeadingZeros(unscaled); if (binaryExponent < 0) { @@ -1333,17 +1385,6 @@ private static int doubleBinaryExponent(long bits) { return exponent == 0 ? -1074 : exponent - 1075; } - private static long multiplyHigh(long x, long y) { - long xlo = x & 0xffff_ffffL; - long xhi = x >>> 32; - long ylo = y & 0xffff_ffffL; - long yhi = y >>> 32; - long lowProduct = xlo * ylo; - long carryProduct = xhi * ylo + (lowProduct >>> 32); - long middle = (carryProduct & 0xffff_ffffL) + xlo * yhi; - return xhi * yhi + (carryProduct >>> 32) + (middle >>> 32); - } - protected final double readDoubleFallbackValue(int start) { position = start; if (start < length() && charAt(start) == '"') { 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 0ecfc53aed..e86045bd85 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 @@ -70,9 +70,13 @@ public final class Utf8JsonReader extends JsonReader { private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); private static final long LONG_MAX_DIV_100 = Long.MAX_VALUE / 100; private static final int LONG_MAX_MOD_100 = (int) (Long.MAX_VALUE % 100); + private static final long FOUR_DIGITS = 10_000L; + private static final long LONG_MAX_DIV_FOUR_DIGITS = Long.MAX_VALUE / FOUR_DIGITS; private static final long LONG_MIN_DIV_10 = Long.MIN_VALUE / 10; private static final int LONG_MIN_LAST_DIGIT = (int) -(Long.MIN_VALUE % 10); private static final long EIGHT_DIGITS = 100_000_000L; + private static final long LONG_MAX_DIV_EIGHT_DIGITS = Long.MAX_VALUE / EIGHT_DIGITS; + private static final int LONG_MAX_MOD_EIGHT_DIGITS = (int) (Long.MAX_VALUE % EIGHT_DIGITS); private static final long ASCII_ZEROES = 0x3030_3030_3030_3030L; private static final long ASCII_NINES = 0x3939_3939_3939_3939L; private static final long ASCII_HIGH_BITS = 0x8080_8080_8080_8080L; @@ -933,6 +937,65 @@ private static int parseEightDigits(byte[] bytes, int offset, int safeEnd) { return (int) ((quads & 0xFFFF) * 10_000 + (quads >>> 32)); } + private static int parseFourDigits(byte[] bytes, int offset, int safeEnd) { + if (offset + 4 > safeEnd) { + return -1; + } + int chunk = LittleEndian.getInt32(bytes, offset); + int digits = chunk - (int) ASCII_ZEROES; + if (((digits | ((int) ASCII_NINES - chunk)) & INT_BYTE_HIGH_BITS) != 0) { + return -1; + } + int pairs = (digits * 10 + (digits >>> 8)) & 0x00FF_00FF; + return (pairs & 0xFFFF) * 100 + (pairs >>> 16); + } + + private static long appendEightDigits(byte[] bytes, int offset, int safeEnd, long unscaled) { + int block = parseEightDigits(bytes, offset, safeEnd); + if (block < 0 || !canAppendEightDigits(unscaled, block)) { + return -1; + } + return unscaled * EIGHT_DIGITS + block; + } + + private static long appendFourDigits(byte[] bytes, int offset, int safeEnd, long unscaled) { + int block = parseFourDigits(bytes, offset, safeEnd); + if (block < 0) { + return -1; + } + // Callers use a strict divisor bound, so every validated four-digit block is safe here. The + // one equality boundary stays on the pair path because its final block determines overflow. + return unscaled * FOUR_DIGITS + block; + } + + // Positive magnitudes below these power-of-two bounds can append the full decimal chunk without + // overflowing a signed long. On the high branch, adding the remainder carry converts the exact + // boundary into one unsigned divisor comparison; unsigned order also rejects MAX_VALUE + 1 after + // it wraps. The validated digit and pair ranges make the shifts exact zero-or-one carries. + private static boolean canAppendDigit(long unscaled, int digit) { + if ((unscaled >>> 59) == 0) { + return true; + } + long adjusted = unscaled + (digit >>> 3); + return Long.compareUnsigned(adjusted, LONG_MAX_DIV_10) <= 0; + } + + private static boolean canAppendTwoDigits(long unscaled, int pair) { + if ((unscaled >>> 56) == 0) { + return true; + } + long adjusted = unscaled + ((pair + 120) >>> 7); + return Long.compareUnsigned(adjusted, LONG_MAX_DIV_100) <= 0; + } + + private static boolean canAppendEightDigits(long unscaled, int block) { + if ((unscaled >>> 36) == 0) { + return true; + } + long adjusted = unscaled + (block > LONG_MAX_MOD_EIGHT_DIGITS ? 1 : 0); + return Long.compareUnsigned(adjusted, LONG_MAX_DIV_EIGHT_DIGITS) <= 0; + } + private BigDecimal readBigDecimalToken() { byte[] bytes = input; int offset = position; @@ -1358,6 +1421,8 @@ private float readFloatFallback(int start) { return readFloatFallbackValue(start); } + // Keep the complete integer and fraction scan in one token owner. A separate inline-sized + // fraction tail makes generated callers depend on which method C2 compiles first. private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength, int ch) { int start = offset; long unscaled = 0; @@ -1379,8 +1444,7 @@ private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength break; } int pair = high * 10 + low; - if (unscaled > LONG_MAX_DIV_100 - || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { + if (!canAppendTwoDigits(unscaled, pair)) { return readDoubleFallback(start); } unscaled = unscaled * 100 + pair; @@ -1389,8 +1453,7 @@ private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength if (offset < inputLength) { int digit = bytes[offset] - '0'; if (digit >= 0 && digit <= 9) { - if (unscaled > LONG_MAX_DIV_10 - || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + if (!canAppendDigit(unscaled, digit)) { return readDoubleFallback(start); } unscaled = unscaled * 10 + digit; @@ -1400,29 +1463,25 @@ private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength } else { return readDoubleFallback(start); } - return readPositiveDoubleTail(bytes, offset, inputLength, start, unscaled); - } - - private double readSignedDoubleToken(int start) { - byte[] bytes = input; - int offset = start + 1; - int inputLength = bytes.length; - if (offset >= inputLength) { - return readDoubleFallback(start); - } - int ch = bytes[offset]; - long unscaled = 0; - if (ch == '0') { + int scale = 0; + if (offset < inputLength && bytes[offset] == '.') { offset++; - if (offset < inputLength) { - ch = bytes[offset]; - if (ch >= '0' && ch <= '9') { - return readDoubleFallback(start); + int fractionStart = offset; + long appended = appendEightDigits(bytes, offset, inputLength, unscaled); + while (appended >= 0) { + unscaled = appended; + scale += 8; + offset += 8; + appended = appendEightDigits(bytes, offset, inputLength, unscaled); + } + if (scale != 0 && unscaled < LONG_MAX_DIV_FOUR_DIGITS) { + appended = appendFourDigits(bytes, offset, inputLength, unscaled); + if (appended >= 0) { + unscaled = appended; + scale += 4; + offset += 4; } } - } else if (ch >= '1' && ch <= '9') { - unscaled = ch - '0'; - offset++; while (offset + 1 < inputLength) { int high = bytes[offset] - '0'; int low = bytes[offset + 1] - '0'; @@ -1430,36 +1489,51 @@ private double readSignedDoubleToken(int start) { break; } int pair = high * 10 + low; - if (unscaled > LONG_MAX_DIV_100 - || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { + if (!canAppendTwoDigits(unscaled, pair)) { return readDoubleFallback(start); } unscaled = unscaled * 100 + pair; + scale += 2; offset += 2; } if (offset < inputLength) { int digit = bytes[offset] - '0'; if (digit >= 0 && digit <= 9) { - if (unscaled > LONG_MAX_DIV_10 - || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + if (!canAppendDigit(unscaled, digit)) { return readDoubleFallback(start); } unscaled = unscaled * 10 + digit; + scale++; offset++; } } - } else { - return readDoubleFallback(start); + if (offset == fractionStart) { + return readDoubleFallback(start); + } } - return readSignedDoubleTail(bytes, offset, inputLength, start, unscaled); + return finishDoubleToken(bytes, offset, inputLength, start, unscaled, scale); } - private double readPositiveDoubleTail( - byte[] bytes, int offset, int inputLength, int start, long unscaled) { - int scale = 0; - if (offset < inputLength && bytes[offset] == '.') { + private double readSignedDoubleToken(int start) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + int ch = bytes[offset]; + long unscaled = 0; + if (ch == '0') { + offset++; + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readDoubleFallback(start); + } + } + } else if (ch >= '1' && ch <= '9') { + unscaled = ch - '0'; offset++; - int fractionStart = offset; while (offset + 1 < inputLength) { int high = bytes[offset] - '0'; int low = bytes[offset + 1] - '0'; @@ -1467,39 +1541,44 @@ private double readPositiveDoubleTail( break; } int pair = high * 10 + low; - if (unscaled > LONG_MAX_DIV_100 - || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { + if (!canAppendTwoDigits(unscaled, pair)) { return readDoubleFallback(start); } unscaled = unscaled * 100 + pair; - scale += 2; offset += 2; } if (offset < inputLength) { int digit = bytes[offset] - '0'; if (digit >= 0 && digit <= 9) { - if (unscaled > LONG_MAX_DIV_10 - || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + if (!canAppendDigit(unscaled, digit)) { return readDoubleFallback(start); } unscaled = unscaled * 10 + digit; - scale++; offset++; } } - if (offset == fractionStart) { - return readDoubleFallback(start); - } + } else { + return readDoubleFallback(start); } - return finishDoubleToken(bytes, offset, inputLength, start, unscaled, scale); - } - - private double readSignedDoubleTail( - byte[] bytes, int offset, int inputLength, int start, long unscaled) { int scale = 0; if (offset < inputLength && bytes[offset] == '.') { offset++; int fractionStart = offset; + long appended = appendEightDigits(bytes, offset, inputLength, unscaled); + while (appended >= 0) { + unscaled = appended; + scale += 8; + offset += 8; + appended = appendEightDigits(bytes, offset, inputLength, unscaled); + } + if (scale != 0 && unscaled < LONG_MAX_DIV_FOUR_DIGITS) { + appended = appendFourDigits(bytes, offset, inputLength, unscaled); + if (appended >= 0) { + unscaled = appended; + scale += 4; + offset += 4; + } + } while (offset + 1 < inputLength) { int high = bytes[offset] - '0'; int low = bytes[offset + 1] - '0'; @@ -1507,8 +1586,7 @@ private double readSignedDoubleTail( break; } int pair = high * 10 + low; - if (unscaled > LONG_MAX_DIV_100 - || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { + if (!canAppendTwoDigits(unscaled, pair)) { return readDoubleFallback(start); } unscaled = unscaled * 100 + pair; @@ -1518,8 +1596,7 @@ private double readSignedDoubleTail( if (offset < inputLength) { int digit = bytes[offset] - '0'; if (digit >= 0 && digit <= 9) { - if (unscaled > LONG_MAX_DIV_10 - || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + if (!canAppendDigit(unscaled, digit)) { return readDoubleFallback(start); } unscaled = unscaled * 10 + digit; diff --git a/java/fory-json/src/main/java25/org/apache/fory/json/reader/DecimalMath.java b/java/fory-json/src/main/java25/org/apache/fory/json/reader/DecimalMath.java new file mode 100644 index 0000000000..5a75dd0779 --- /dev/null +++ b/java/fory-json/src/main/java25/org/apache/fory/json/reader/DecimalMath.java @@ -0,0 +1,29 @@ +/* + * 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.reader; + +/** JDK 25 exact integer arithmetic used by decimal readers. */ +final class DecimalMath { + private DecimalMath() {} + + static long unsignedMultiplyHigh(long x, long y) { + return Math.unsignedMultiplyHigh(x, y); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/reader/DecimalMathTest.java b/java/fory-json/src/test/java/org/apache/fory/json/reader/DecimalMathTest.java new file mode 100644 index 0000000000..be897bb33d --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/reader/DecimalMathTest.java @@ -0,0 +1,56 @@ +/* + * 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.reader; + +import static org.testng.Assert.assertEquals; + +import java.math.BigInteger; +import java.util.SplittableRandom; +import org.testng.annotations.Test; + +public class DecimalMathTest { + private static final BigInteger UNSIGNED_MASK = + BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE); + + @Test + public void unsignedHighHalf() { + long[] boundaries = { + 0, 1, 2, 0xffff_ffffL, 0x1_0000_0000L, Long.MAX_VALUE, Long.MIN_VALUE, -2, -1 + }; + for (long x : boundaries) { + for (long y : boundaries) { + assertProduct(x, y); + } + } + SplittableRandom random = new SplittableRandom(0x5f3759dfL); + for (int i = 0; i < 10_000; i++) { + assertProduct(random.nextLong(), random.nextLong()); + } + } + + private static void assertProduct(long x, long y) { + BigInteger product = unsigned(x).multiply(unsigned(y)); + assertEquals(DecimalMath.unsignedMultiplyHigh(x, y), product.shiftRight(64).longValue()); + } + + private static BigInteger unsigned(long value) { + return BigInteger.valueOf(value).and(UNSIGNED_MASK); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/writer/Jdk25MultiReleaseJarVerifier.java b/java/fory-json/src/test/java/org/apache/fory/json/writer/Jdk25MultiReleaseJarVerifier.java index b6755ae4d2..ab03a23df4 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/writer/Jdk25MultiReleaseJarVerifier.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/writer/Jdk25MultiReleaseJarVerifier.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -43,6 +44,9 @@ public final class Jdk25MultiReleaseJarVerifier { private static final String CLASS_NAME = "org.apache.fory.json.writer.BigDecimalFields"; private static final String CLASS_PATH = CLASS_NAME.replace('.', '/') + ".class"; private static final String SOURCE_PATH = CLASS_NAME.replace('.', '/') + ".java"; + private static final String DECIMAL_MATH_NAME = "org.apache.fory.json.reader.DecimalMath"; + private static final String DECIMAL_MATH_PATH = DECIMAL_MATH_NAME.replace('.', '/') + ".class"; + private static final String DECIMAL_MATH_SOURCE = DECIMAL_MATH_NAME.replace('.', '/') + ".java"; private static final String VERSION_PREFIX = "META-INF/versions/25/"; private Jdk25MultiReleaseJarVerifier() {} @@ -63,6 +67,7 @@ public static void main(String[] args) throws Exception { static void verify(Path jarPath, Path sourcesPath) throws Exception { byte[] versionClass; + byte[] decimalMathClass; try (JarFile jar = new JarFile(jarPath.toFile())) { Manifest manifest = jar.getManifest(); require(manifest != null, "missing manifest"); @@ -70,11 +75,16 @@ static void verify(Path jarPath, Path sourcesPath) throws Exception { require("true".equalsIgnoreCase(attributes.getValue("Multi-Release")), "missing manifest"); require(jar.getJarEntry(CLASS_PATH) != null, "missing root BigDecimalFields class"); versionClass = read(jar, VERSION_PREFIX + CLASS_PATH); + require(jar.getJarEntry(DECIMAL_MATH_PATH) != null, "missing root DecimalMath class"); + decimalMathClass = read(jar, VERSION_PREFIX + DECIMAL_MATH_PATH); } try (JarFile sources = new JarFile(sourcesPath.toFile())) { require( sources.getJarEntry(VERSION_PREFIX + SOURCE_PATH) != null, "missing JDK25 BigDecimalFields source"); + require( + sources.getJarEntry(VERSION_PREFIX + DECIMAL_MATH_SOURCE) != null, + "missing JDK25 DecimalMath source"); } Class type = new VersionClassLoader().define(versionClass); @@ -82,6 +92,12 @@ static void verify(Path jarPath, Path sourcesPath) throws Exception { requireVarHandle(type, "INT_COMPACT"); requireVarHandle(type, "INT_VAL"); requireVarHandle(type, "SCALE"); + + Class decimalMath = new VersionClassLoader().define(decimalMathClass); + require(DECIMAL_MATH_NAME.equals(decimalMath.getName()), "wrong JDK25 DecimalMath name"); + Method multiply = decimalMath.getDeclaredMethod("unsignedMultiplyHigh", long.class, long.class); + require(Modifier.isStatic(multiply.getModifiers()), "unsignedMultiplyHigh must be static"); + require(multiply.getReturnType() == long.class, "unsignedMultiplyHigh return type"); } private static void requireVarHandle(Class type, String name) throws NoSuchFieldException { From b43a624d2a90c747351afce8ab70d41e7ac1beac Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 06:26:52 +0800 Subject: [PATCH 05/20] perf(json): combine UTF-8 string stop checks --- .../fory/json/reader/Utf8JsonReader.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) 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 e86045bd85..5e4fa3c8cd 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 @@ -56,14 +56,14 @@ public final class Utf8JsonReader extends JsonReader { private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; private static final long BYTE_ONES = 0x0101010101010101L; private static final int INT_BYTE_ONES = 0x01010101; + private static final long BYTE_TWOS = 0x0202020202020202L; + private static final int INT_BYTE_TWOS = 0x02020202; private static final long BYTE_HIGH_BITS = 0x8080808080808080L; private static final int INT_BYTE_HIGH_BITS = 0x80808080; private static final long BACKSLASH_BYTES = 0x5c5c5c5c5c5c5c5cL; private static final int INT_BACKSLASH_BYTES = 0x5c5c5c5c; - private static final long CONTROL_LIMIT_BYTES = 0x2020202020202020L; - private static final int INT_CONTROL_LIMIT_BYTES = 0x20202020; - private static final long QUOTE_BYTES = 0x2222222222222222L; - private static final int INT_QUOTE_BYTES = 0x22222222; + private static final long QUOTE_CONTROL_LIMIT_BYTES = 0x2121212121212121L; + private static final int INT_QUOTE_CONTROL_LIMIT_BYTES = 0x21212121; private static final int INT_MAX_DIV_10 = Integer.MAX_VALUE / 10; private static final int INT_MAX_MOD_10 = Integer.MAX_VALUE % 10; private static final long LONG_MAX_DIV_10 = Long.MAX_VALUE / 10; @@ -2317,14 +2317,18 @@ private static long stringStopMask(long word) { // Subtraction borrow may only create later high bits after an earlier real stop, so the // compact syntax/range expression preserves the first-stop position. Latin1JsonReader cannot // use this shortcut because high-bit Latin-1 bytes are valid string payload. - long syntaxStop = ((word ^ QUOTE_BYTES) - BYTE_ONES) | ((word ^ BACKSLASH_BYTES) - BYTE_ONES); - return (syntaxStop | word | (word - CONTROL_LIMIT_BYTES)) & BYTE_HIGH_BITS; + // XOR by 2 preserves control bytes below 0x20 and maps quote 0x22 to 0x20. One relaxed + // byte-lane comparison against 0x21 can therefore cover both cases without a separate quote + // zero detector; printable bytes before the first stop remain at or above the limit. + long quoteOrControl = (word ^ BYTE_TWOS) - QUOTE_CONTROL_LIMIT_BYTES; + long backslash = (word ^ BACKSLASH_BYTES) - BYTE_ONES; + return (quoteOrControl | backslash | word) & BYTE_HIGH_BITS; } private static int stringStopMask(int word) { - int syntaxStop = - ((word ^ INT_QUOTE_BYTES) - INT_BYTE_ONES) | ((word ^ INT_BACKSLASH_BYTES) - INT_BYTE_ONES); - return (syntaxStop | word | (word - INT_CONTROL_LIMIT_BYTES)) & INT_BYTE_HIGH_BITS; + int quoteOrControl = (word ^ INT_BYTE_TWOS) - INT_QUOTE_CONTROL_LIMIT_BYTES; + int backslash = (word ^ INT_BACKSLASH_BYTES) - INT_BYTE_ONES; + return (quoteOrControl | backslash | word) & INT_BYTE_HIGH_BITS; } @Override From 51b5fe944b3360a76129dc9e9980b5eced752949 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 06:37:46 +0800 Subject: [PATCH 06/20] perf(json): accelerate short 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 5e4fa3c8cd..db3aaaa63d 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 @@ -2608,6 +2608,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 860951214c0f73e0e9aabeb04e68cc16d1337058 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 08:22:23 +0800 Subject: [PATCH 07/20] perf(json): publish generated capability graphs atomically Compile representation classes independently, construct resolver-local capabilities bottom-up, and publish only complete dependency graphs. Keep exact collection bindings, cyclic slot ownership, and parent-local closed-subtype readers explicit. --- .../fory/json/codec/ClosedSubtypeCodec.java | 114 +- .../apache/fory/json/codegen/JsonCodegen.java | 34 +- .../fory/json/codegen/JsonJITContext.java | 169 +- .../fory/json/codegen/JsonReaderCodegen.java | 311 +-- .../fory/json/codegen/JsonWriterCodegen.java | 114 +- .../json/codegen/Latin1ReaderCodegen.java | 9 +- .../json/codegen/StringWriterCodegen.java | 5 + .../fory/json/codegen/Utf16ReaderCodegen.java | 9 +- .../fory/json/codegen/Utf8ReaderCodegen.java | 5 + .../fory/json/codegen/Utf8WriterCodegen.java | 5 + .../apache/fory/json/meta/JsonFieldInfo.java | 7 +- .../resolver/GeneratedCodecInstantiator.java | 65 +- .../json/resolver/JsonSharedRegistry.java | 49 +- .../fory/json/resolver/JsonTypeResolver.java | 1770 +++++++---------- .../fory/json/JsonAsyncCompilationTest.java | 274 ++- 15 files changed, 1322 insertions(+), 1618 deletions(-) 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 8efdf25770..0e2e3a9b7c 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 @@ -39,10 +39,9 @@ * Resolver-local closed subtype dispatcher whose branch slots follow child JsonTypeInfo updates. * *

Inline discriminator state belongs to this parent. Any-readable children use parent-local - * field tables and generated reader instances so one child can be shared by parents with different - * discriminator names without changing the child's canonical metadata. Nested values of the same - * child type still use its canonical reader; the derived skip table applies only to the outer - * inline object. + * field tables and complete generated-reader arrays so one child can be shared by parents with + * different discriminator names without changing canonical child metadata. The derived skip table + * applies only to the outer inline object. * *

Writing rejects fixed-schema discriminator collisions when branches are resolved, but never * queries an Any Map. Runtime dynamic-key conflicts are owned by the application; probing here @@ -56,9 +55,9 @@ public final class ClosedSubtypeCodec implements JsonValueCodec { private final JsonTypeInfo[] children; private final ObjectCodec[] objectCodecs; private JsonFieldTable[] inlineReadTables; - private Latin1ReaderCodec[] inlineLatin1Readers; - private Utf16ReaderCodec[] inlineUtf16Readers; - private Utf8ReaderCodec[] inlineUtf8Readers; + private volatile Latin1ReaderCodec[] inlineLatin1Readers; + private volatile Utf16ReaderCodec[] inlineUtf16Readers; + private volatile Utf8ReaderCodec[] inlineUtf8Readers; /** Creates an unresolved resolver-local dispatcher shell for a validated subtype definition. */ @Internal @@ -96,16 +95,15 @@ public void resolve(JsonTypeResolver resolver) { if (any != null && (any.readField() != null || any.readSetter() != null)) { if (inlineReadTables == null) { inlineReadTables = new JsonFieldTable[children.length]; - inlineLatin1Readers = new Latin1ReaderCodec[children.length]; - inlineUtf16Readers = new Utf16ReaderCodec[children.length]; - inlineUtf8Readers = new Utf8ReaderCodec[children.length]; } JsonFieldTable table = objectCodec.readTable().withSkippedName(definition.scanInfo.property()); inlineReadTables[i] = table; // The subtype scan restores the cursor, so the outer child rereads the discriminator and - // needs this parent-local skip table. Nested child values must use the canonical table. - resolver.resolveInlineAnyReaders(this, i, objectCodec, table); + // needs this parent-local skip table. The resolver constructs a complete immutable array + // of generated readers for each representation and publishes that array in one volatile + // write; never publish its elements independently. Nested child values keep the canonical + // table and canonical capability. } } children[i] = child; @@ -178,10 +176,11 @@ public Object readLatin1(Latin1JsonReader reader) { int index = reader.scanObjectStringField(definition.scanInfo); JsonFieldTable[] tables = inlineReadTables; if (tables != null && tables[index] != null) { - Latin1ReaderCodec codec = inlineLatin1Readers[index]; - return codec == null - ? objectCodecs[index].readLatin1Object(reader, tables[index]) - : codec.readLatin1(reader); + Latin1ReaderCodec[] readers = inlineLatin1Readers; + if (readers != null) { + return readers[index].readLatin1(reader); + } + return objectCodecs[index].readLatin1Object(reader, tables[index]); } return children[index].latin1Reader().readLatin1(reader); } @@ -213,10 +212,11 @@ public Object readUtf16(Utf16JsonReader reader) { int index = reader.scanObjectStringField(definition.scanInfo); JsonFieldTable[] tables = inlineReadTables; if (tables != null && tables[index] != null) { - Utf16ReaderCodec codec = inlineUtf16Readers[index]; - return codec == null - ? objectCodecs[index].readUtf16Object(reader, tables[index]) - : codec.readUtf16(reader); + Utf16ReaderCodec[] readers = inlineUtf16Readers; + if (readers != null) { + return readers[index].readUtf16(reader); + } + return objectCodecs[index].readUtf16Object(reader, tables[index]); } return children[index].utf16Reader().readUtf16(reader); } @@ -248,10 +248,11 @@ public Object readUtf8(Utf8JsonReader reader) { int index = reader.scanObjectStringField(definition.scanInfo); JsonFieldTable[] tables = inlineReadTables; if (tables != null && tables[index] != null) { - Utf8ReaderCodec codec = inlineUtf8Readers[index]; - return codec == null - ? objectCodecs[index].readUtf8Object(reader, tables[index]) - : codec.readUtf8(reader); + Utf8ReaderCodec[] readers = inlineUtf8Readers; + if (readers != null) { + return readers[index].readUtf8(reader); + } + return objectCodecs[index].readUtf8Object(reader, tables[index]); } return children[index].utf8Reader().readUtf8(reader); } @@ -284,18 +285,71 @@ private int requireSubtype(Class runtimeType) { } @Internal - public void setInlineLatin1Reader(int index, Latin1ReaderCodec reader) { - inlineLatin1Readers[index] = reader; + public int childCount() { + return children.length; + } + + @Internal + public JsonTypeInfo child(int index) { + return children[index]; + } + + @Internal + public JsonFieldTable inlineReadTable(int index) { + return inlineReadTables == null ? null : inlineReadTables[index]; + } + + @Internal + public Latin1ReaderCodec[] inlineLatin1Readers() { + return inlineLatin1Readers; + } + + @Internal + public Utf16ReaderCodec[] inlineUtf16Readers() { + return inlineUtf16Readers; } @Internal - public void setInlineUtf16Reader(int index, Utf16ReaderCodec reader) { - inlineUtf16Readers[index] = reader; + public Utf8ReaderCodec[] inlineUtf8Readers() { + return inlineUtf8Readers; } @Internal - public void setInlineUtf8Reader(int index, Utf8ReaderCodec reader) { - inlineUtf8Readers[index] = reader; + public void installInlineLatin1Readers(Latin1ReaderCodec[] readers) { + validateInlineReaders(readers); + if (inlineLatin1Readers != null) { + throw new IllegalStateException("Inline Latin1 readers are already installed"); + } + inlineLatin1Readers = readers; + } + + @Internal + public void installInlineUtf16Readers(Utf16ReaderCodec[] readers) { + validateInlineReaders(readers); + if (inlineUtf16Readers != null) { + throw new IllegalStateException("Inline UTF16 readers are already installed"); + } + inlineUtf16Readers = readers; + } + + @Internal + public void installInlineUtf8Readers(Utf8ReaderCodec[] readers) { + validateInlineReaders(readers); + if (inlineUtf8Readers != null) { + throw new IllegalStateException("Inline UTF8 readers are already installed"); + } + inlineUtf8Readers = readers; + } + + private void validateInlineReaders(Object[] readers) { + if (inlineReadTables == null || readers == null || readers.length != children.length) { + throw new IllegalArgumentException("Inline reader array does not match subtype branches"); + } + for (int i = 0; i < children.length; i++) { + if (inlineReadTables[i] != null && readers[i] == null) { + throw new IllegalArgumentException("Missing inline reader for subtype branch " + i); + } + } } private static void rejectDiscriminatorCollision(ObjectCodec codec, String property) { 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 6c3b687155..7cf1936515 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 @@ -56,12 +56,12 @@ * Generates concrete object and exact-collection capability classes. * *

One instance belongs to one {@link org.apache.fory.json.resolver.JsonSharedRegistry}. The - * registry owns UTF-8 reader class futures; the representation caches here own the other concrete - * object capabilities. A resolver is passed only to the active source-generation call for short - * canonical metadata lookups; neither owner retains it. + * registry owns every generated-class future and single-flight decision. A resolver is passed only + * to the active source-generation call for short canonical metadata lookups; neither owner retains + * it. * - *

This class owns classes only. Resolver-local generated instances, child capability capture, - * {@link JsonTypeInfo} slot installation, and generated parent-field updates belong to {@link + *

This class owns class generation only. Resolver-local generated instances, final direct-child + * capture, canonical cycle slots, and {@link JsonTypeInfo} slot installation belong to {@link * org.apache.fory.json.resolver.JsonTypeResolver}. The raw types emitted for Janino stop at the * generated source and constructor boundary; handwritten runtime capability APIs remain generic. */ @@ -74,10 +74,6 @@ public final class JsonCodegen { private final int codegenHash; private final CodeGenerator codeGenerator; private final ClassLoader jsonLoader; - private final Map, Class> stringWriterClasses = new ConcurrentHashMap<>(); - private final Map, Class> utf8WriterClasses = new ConcurrentHashMap<>(); - private final Map, Class> latin1ReaderClasses = new ConcurrentHashMap<>(); - private final Map, Class> utf16ReaderClasses = new ConcurrentHashMap<>(); static String generatedCodecType(CodegenContext ctx, Class codecType) { // Janino-generated serializers use erased types, matching Fory core code generation. Runtime @@ -104,19 +100,16 @@ public JsonCodegen(int codegenHash, ClassLoader jsonLoader) { * depends on mutable capability slots. Active codec classes are inspected only for non-canonical * bindings, whose capability fields are never replaced by generated raw-object codecs. * - *

Generated classes are cached here because this object is shared by every pooled resolver of - * one Fory JSON instance. Concurrent map computation provides generated-class single-flight; - * resolver-local construction and capability publication belong to {@link - * org.apache.fory.json.resolver.JsonTypeResolver} and are ordered by its generic {@link - * JsonJITContext} callbacks. + *

The shared registry caches the resulting class future for every pooled resolver of one Fory + * JSON instance. Resolver-local construction and capability publication belong to {@link + * org.apache.fory.json.resolver.JsonTypeResolver} and are ordered by its {@link JsonJITContext}. */ @Internal public Class compileStringWriter(ObjectCodec codec, JsonTypeResolver resolver) { if (!canCompileWriter(codec)) { return null; } - return stringWriterClasses.computeIfAbsent( - codec.type(), ignored -> buildStringWriter(codec, resolver)); + return buildStringWriter(codec, resolver); } @Internal @@ -124,8 +117,7 @@ public Class compileUtf8Writer(ObjectCodec codec, JsonTypeResolver resolve if (!canCompileWriter(codec)) { return null; } - return utf8WriterClasses.computeIfAbsent( - codec.type(), ignored -> buildUtf8Writer(codec, resolver)); + return buildUtf8Writer(codec, resolver); } @Internal @@ -133,8 +125,7 @@ public Class compileLatin1Reader(ObjectCodec codec, JsonTypeResolver resol if (!canCompileReader(codec)) { return null; } - return latin1ReaderClasses.computeIfAbsent( - codec.type(), ignored -> buildLatin1Reader(codec, resolver)); + return buildLatin1Reader(codec, resolver); } @Internal @@ -142,8 +133,7 @@ public Class compileUtf16Reader(ObjectCodec codec, JsonTypeResolver resolv if (!canCompileReader(codec)) { return null; } - return utf16ReaderClasses.computeIfAbsent( - codec.type(), ignored -> buildUtf16Reader(codec, resolver)); + return buildUtf16Reader(codec, resolver); } @Internal diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java index 15dbd9313e..ba1fd65a56 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java @@ -19,101 +19,38 @@ package org.apache.fory.json.codegen; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutorService; import java.util.concurrent.locks.ReentrantLock; import org.apache.fory.annotation.Internal; -import org.apache.fory.codegen.CodeGenerator; import org.apache.fory.util.ExceptionUtils; -import org.apache.fory.util.Preconditions; /** - * Generic resolver-local lock and completion-notification context for generated code. + * Resolver-local lock and completion context for generated code. * - *

This class deliberately has the same owner model as Fory core's JIT context. It knows nothing - * about JSON readers, writers, capabilities, generated classes, or resolver metadata. A root graph - * and its completion callbacks use the same local lock. The JIT action runs outside that lock; - * success reacquires it before invoking the resolver-owned publication callback and then registered - * parent notifications. Synchronous mode uses the callback map only to break recursive compilation - * of the same identifier. + *

This class knows nothing about JSON readers, writers, capabilities, generated classes, or + * resolver metadata. The shared registry owns compilation and returns futures; this context only + * prevents a duplicate resolver-local publication request and invokes its completion while holding + * the same lock as root codec execution. * - *

{@link JITCallback#id()} correlates active child notifications. Ordinary callback - * registrations retain independent resolver-local publication callbacks; future registrations use - * the identifier to reuse one active graph publication request. Generated-class single-flight - * belongs to the shared registry, while resolver-local construction, publication, and generated - * parent-field updates remain local. + *

{@link JITCallback#id()} identifies one active resolver-local graph request. Generated-class + * single-flight belongs to the shared registry; resolver-local construction and atomic capability + * publication belong to the resolver callback. */ @Internal public final class JsonJITContext { private final boolean asyncCompilationEnabled; - private final ExecutorService compilationService; private final ReentrantLock jitLock; - private final Map> callbacks; - private int numRunningTasks; + private final Set activeTasks; @Internal - public JsonJITContext(boolean asyncCompilationEnabled, ExecutorService compilationService) { + public JsonJITContext(boolean asyncCompilationEnabled) { this.asyncCompilationEnabled = asyncCompilationEnabled; - this.compilationService = compilationService; jitLock = new ReentrantLock(true); - callbacks = new HashMap<>(); - } - - @Internal - public T registerJITCallback( - Callable interpretedAction, Callable jitAction, JITCallback callback) { - try { - lock(); - if (!asyncCompilationEnabled) { - Object id = callback.id(); - if (callbacks.containsKey(id)) { - return interpretedAction.call(); - } - callbacks.put(id, new ArrayList<>()); - try { - T result = jitAction.call(); - callback.onSuccess(result); - List notifyCallbacks = callbacks.get(id); - for (int i = 0; i < notifyCallbacks.size(); i++) { - notifyCallbacks.get(i).onNotifyResult(result); - } - return result; - } catch (Throwable t) { - callback.onFailure(t); - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); - } finally { - callbacks.remove(id); - } - } - // Do not skip ordinary registrations when this ID is already present. The shared class owner - // single-flights compilation, while every local registration retains its own callback. - callbacks.computeIfAbsent(callback.id(), ignored -> new ArrayList<>()); - numRunningTasks++; - ExecutorService service = compilationService; - if (service == null) { - service = CodeGenerator.getCompilationService(); - } - try { - service.execute(() -> runJITAction(jitAction, callback)); - } catch (Throwable t) { - finishTask(); - callback.onFailure(t); - ExceptionUtils.throwException(t); - } - return interpretedAction.call(); - } catch (Exception e) { - ExceptionUtils.throwException(e); - throw new IllegalStateException("unreachable"); - } finally { - unlock(); - } + activeTasks = new HashSet<>(); } /** @@ -124,45 +61,35 @@ public T registerJITCallback( * reuse the active resolver-local request and return the interpreted capability until the * complete result is published. */ - public T registerJITFuture( - Callable interpretedAction, - Callable> futureAction, - JITCallback callback) { + public void registerJITFuture( + Callable> futureAction, JITCallback callback) { try { lock(); Object id = callback.id(); - if (callbacks.containsKey(id)) { - return interpretedAction.call(); + if (!activeTasks.add(id)) { + return; } - callbacks.put(id, new ArrayList<>()); if (!asyncCompilationEnabled) { try { T result = futureAction.call().join(); callback.onSuccess(result); - List notifyCallbacks = callbacks.get(id); - for (int i = 0; i < notifyCallbacks.size(); i++) { - notifyCallbacks.get(i).onNotifyResult(result); - } - return result; } catch (Throwable t) { Throwable failure = unwrapFutureFailure(t); callback.onFailure(failure); ExceptionUtils.throwException(failure); - throw new IllegalStateException("unreachable"); } finally { - callbacks.remove(id); + activeTasks.remove(id); } + return; } CompletableFuture future; try { future = futureAction.call(); } catch (Throwable t) { - callbacks.remove(id); + activeTasks.remove(id); callback.onFailure(t); - ExceptionUtils.throwException(t); - throw new IllegalStateException("unreachable"); + return; } - numRunningTasks++; future.whenComplete( (result, failure) -> { if (failure == null) { @@ -171,10 +98,8 @@ public T registerJITFuture( completeFailure(callback, unwrapFutureFailure(failure)); } }); - return interpretedAction.call(); } catch (Exception e) { ExceptionUtils.throwException(e); - throw new IllegalStateException("unreachable"); } finally { unlock(); } @@ -186,29 +111,12 @@ private static Throwable unwrapFutureFailure(Throwable failure) { : failure; } - private void runJITAction(Callable jitAction, JITCallback callback) { - T result; - try { - result = jitAction.call(); - } catch (Throwable t) { - completeFailure(callback, t); - return; - } - completeSuccess(callback, result); - } - private void completeSuccess(JITCallback callback, T result) { try { lock(); callback.onSuccess(result); - List notifyCallbacks = callbacks.get(callback.id()); - if (notifyCallbacks != null) { - for (int i = 0; i < notifyCallbacks.size(); i++) { - notifyCallbacks.get(i).onNotifyResult(result); - } - } } finally { - finishTask(); + activeTasks.remove(callback.id()); unlock(); } } @@ -220,29 +128,7 @@ private void completeFailure(JITCallback callback, Throwable failure) { lock(); callback.onFailure(failure); } finally { - finishTask(); - unlock(); - } - } - - private void finishTask() { - numRunningTasks--; - if (numRunningTasks == 0) { - callbacks.clear(); - } - } - - public void registerJITNotifyCallback(Object id, NotifyCallback callback) { - Preconditions.checkNotNull(id); - try { - lock(); - List notifyCallbacks = callbacks.get(id); - if (notifyCallbacks == null) { - callback.onNotifyMissed(); - } else { - notifyCallbacks.add(callback); - } - } finally { + activeTasks.remove(callback.id()); unlock(); } } @@ -276,13 +162,4 @@ default void onFailure(Throwable failure) { Object id(); } - - @Internal - public interface NotifyCallback { - default void onNotifyResult(Object result) { - onNotifyMissed(); - } - - void onNotifyMissed(); - } } 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 3ab5e70f32..184f231b7c 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 @@ -44,6 +44,7 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; import org.apache.fory.json.meta.JsonFieldTable; +import org.apache.fory.json.resolver.JsonTypeInfo; import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.reflect.TypeRef; @@ -55,9 +56,10 @@ *

The three concrete generators own representation-specific token, field-name, enum, and direct * value expressions. This base shares only source-construction algorithms after the concrete reader * is selected; it is not a runtime reader mode. Generated readers retain immutable field lookup - * metadata and concrete child capability fields, avoiding per-field resolver lookup. Wide objects - * split generated methods to bound compiler size while preserving a single nullable capability - * entry for each representation. + * metadata and final direct-child capabilities. Canonical multi-object cycles retain the final + * child type-info slot owner instead of a later-mutated capability field. Wide objects split + * generated methods to bound compiler size while preserving one capability entry per + * representation. */ abstract class JsonReaderCodegen { private static final int READ_FIELD_SWITCH_SIZE = 8; @@ -102,6 +104,8 @@ abstract class JsonReaderCodegen { abstract String readMethod(); + abstract String readerSlotMethod(); + abstract String readEnumMethod(boolean tokenValueRead, boolean hashFallback); final String readEnumMethod(boolean tokenValueRead) { @@ -153,8 +157,8 @@ String genReaderCode( CodegenContext ctx = builder.context(); ctx.addImports(ObjectCodec.class, readerType, JsonFieldTable.class); ctx.implementsInterfaces(JsonCodegen.generatedCodecType(ctx, readerCapabilityType())); - // Generated mutable readers retain immutable lookup metadata directly. Creator-backed readers - // are selected above and consume the exact executable owned by JsonCreatorInfo. + // Generated readers retain immutable lookup metadata directly. Creator-backed readers are + // selected above and consume the exact executable owned by JsonCreatorInfo. ctx.addField(JsonFieldTable.class, "readTable"); ctx.addField(long[].class, "fieldHashes"); for (int i = 0; i < properties.length; i++) { @@ -165,7 +169,7 @@ String genReaderCode( addCapabilityField(ctx, codecFieldType(properties[i]), "r" + i); } if (storesReadObjectCodec(type, properties[i])) { - addCapabilityField(ctx, readerCapabilityType(), "o" + i); + addObjectReaderField(ctx, properties[i], "o" + i); } } addGeneratedConstructor( @@ -199,6 +203,30 @@ private void addCapabilityField(CodegenContext ctx, Class type, String name) ctx.addField(finalDependencies, JsonCodegen.generatedCodecType(ctx, type), name, null); } + private void addObjectReaderField(CodegenContext ctx, JsonFieldInfo field, String name) { + if (usesReaderSlot(field.readTypeInfo())) { + ctx.addField(true, ctx.type(JsonTypeInfo.class), name, null); + } else { + addCapabilityField(ctx, readerCapabilityType(), name); + } + } + + private void addCreatorReaderField(CodegenContext ctx, JsonCreatorFieldInfo field, String name) { + if (usesReaderSlot(field.typeInfo())) { + ctx.addField(true, ctx.type(JsonTypeInfo.class), name, null); + } else { + addCapabilityField(ctx, readerCapabilityType(), name); + } + } + + private void addAnyReaderField(CodegenContext ctx, AnyInfo any) { + if (usesReaderSlot(any.valueTypeInfo())) { + ctx.addField(true, ctx.type(JsonTypeInfo.class), "anyReader", null); + } else { + addCapabilityField(ctx, readerCapabilityType(), "anyReader"); + } + } + String genAnyReaderCode( JsonGeneratedCodecBuilder builder, Class type, @@ -226,7 +254,7 @@ String genAnyReaderCode( addSelfReaderField(ctx); boolean storesAnyReader = storesAnyReader(type); if (storesAnyReader) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "anyReader"); + addAnyReaderField(ctx, any); } if (any.readField() != null && Modifier.isFinal(any.readField().getModifiers())) { addFinalAnyMapMethod(ctx); @@ -235,33 +263,7 @@ String genAnyReaderCode( addAnySetterMethod(ctx, type, any); } addReaderFields(ctx, type, properties); - if (storesAnyReader) { - addGeneratedConstructor( - ctx, - readerConstructorExpression(type, properties), - ObjectCodec.class, - "owner", - JsonFieldTable.class, - "readTable", - JsonFieldInfo[].class, - "properties", - JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), - "codecs", - JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), - "anyReader"); - } else { - addGeneratedConstructor( - ctx, - readerConstructorExpression(type, properties), - ObjectCodec.class, - "owner", - JsonFieldTable.class, - "readTable", - JsonFieldInfo[].class, - "properties", - JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), - "codecs"); - } + addAnyReaderConstructor(ctx, readerConstructorExpression(type, properties), storesAnyReader); addGeneratedMethod( ctx, "private final", @@ -324,7 +326,7 @@ String genUnwrappedReaderCode( addSelfReaderField(ctx); boolean storesAnyReader = any != null && storesAnyReader(type); if (storesAnyReader) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "anyReader"); + addAnyReaderField(ctx, any); } if (any != null && any.readField() != null @@ -339,7 +341,7 @@ String genUnwrappedReaderCode( } else { for (int i = 0; i < directCreatorFields.length; i++) { if (!isDirectCreatorPrimitive(directCreatorFields[i])) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "r" + i); + addCreatorReaderField(ctx, directCreatorFields[i], "r" + i); } } } @@ -351,13 +353,13 @@ String genUnwrappedReaderCode( ctx.addField(JsonFieldInfo.class, "rp" + id); } if (JsonCodegen.usesReadCodec(field, resolver)) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(field)), "r" + id); + addCapabilityField(ctx, codecFieldType(field), "r" + id); } if (storesReadObjectCodec(type, field)) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "o" + id); + addObjectReaderField(ctx, field, "o" + id); } } else if (!isDirectCreatorPrimitive(routes[i].creatorField())) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "r" + id); + addCreatorReaderField(ctx, routes[i].creatorField(), "r" + id); } } if (groups.length != 0) { @@ -375,32 +377,8 @@ String genUnwrappedReaderCode( directCount, storesAnyReader, any != null); - if (storesAnyReader) { - addGeneratedConstructor( - ctx, - constructor, - ObjectCodec.class, - "owner", - JsonFieldTable.class, - "readTable", - JsonFieldInfo[].class, - "properties", - JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), - "codecs", - JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), - "anyReader"); - } else if (any != null) { - addGeneratedConstructor( - ctx, - constructor, - ObjectCodec.class, - "owner", - JsonFieldTable.class, - "readTable", - JsonFieldInfo[].class, - "properties", - JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), - "codecs"); + if (any != null) { + addAnyReaderConstructor(ctx, constructor, storesAnyReader); } else { addGeneratedConstructor( ctx, @@ -512,10 +490,10 @@ private void addReaderFields(CodegenContext ctx, Class type, JsonFieldInfo[] ctx.addField(JsonFieldInfo.class, "rp" + i); } if (JsonCodegen.usesReadCodec(properties[i], resolver)) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(properties[i])), "r" + i); + addCapabilityField(ctx, codecFieldType(properties[i]), "r" + i); } if (storesReadObjectCodec(type, properties[i])) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "o" + i); + addObjectReaderField(ctx, properties[i], "o" + i); } } } @@ -583,7 +561,7 @@ private String genCreatorReaderCode( ctx.addField(JsonCreatorInfo.class, "creator"); for (int i = 0; i < fields.length; i++) { if (!isDirectCreatorPrimitive(fields[i])) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "r" + i); + addCreatorReaderField(ctx, fields[i], "r" + i); } } addGeneratedConstructor( @@ -630,40 +608,14 @@ private String genAnyCreatorReaderCode( addSelfReaderField(ctx); boolean storesAnyReader = storesAnyReader(type); if (storesAnyReader) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "anyReader"); + addAnyReaderField(ctx, any); } for (int i = 0; i < fields.length; i++) { if (!isDirectCreatorPrimitive(fields[i])) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "r" + i); + addCreatorReaderField(ctx, fields[i], "r" + i); } } - if (storesAnyReader) { - addGeneratedConstructor( - ctx, - creatorConstructorExpression(fields), - ObjectCodec.class, - "owner", - JsonFieldTable.class, - "readTable", - JsonFieldInfo[].class, - "properties", - JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), - "codecs", - JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), - "anyReader"); - } else { - addGeneratedConstructor( - ctx, - creatorConstructorExpression(fields), - ObjectCodec.class, - "owner", - JsonFieldTable.class, - "readTable", - JsonFieldInfo[].class, - "properties", - JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), - "codecs"); - } + addAnyReaderConstructor(ctx, creatorConstructorExpression(fields), storesAnyReader); addCreatorMethod(ctx, type, creatorInfo.executable()); addGeneratedMethod( ctx, @@ -846,24 +798,74 @@ private Expression creatorConstructorExpression(JsonCreatorFieldInfo[] fields) { new Reference("this.readTable", TypeRef.of(JsonFieldTable.class)), new Reference("readTable", TypeRef.of(JsonFieldTable.class)))); if (storesAnyReader(ownerType)) { - expressions.add( - new Expression.Assign( - new Reference("this.anyReader", TypeRef.of(readerCapabilityType())), - new Reference("anyReader", TypeRef.of(readerCapabilityType())))); + addAnyReaderAssignment(expressions, owner); } } + addSelfReaderAssignment(expressions); Reference codecs = new Reference("codecs", TypeRef.of(readerArrayType())); for (int i = 0; i < fields.length; i++) { if (!isDirectCreatorPrimitive(fields[i])) { - expressions.add( - new Expression.Assign( - new Reference("this.r" + i, TypeRef.of(readerCapabilityType())), - new Expression.ArrayValue(codecs, Expression.Literal.ofInt(i)))); + if (usesReaderSlot(fields[i].typeInfo())) { + Expression creator = + new Expression.Invoke(owner, "creatorInfo", TypeRef.of(JsonCreatorInfo.class)) + .inline(); + Expression creatorFields = + new Expression.Invoke(creator, "fields", TypeRef.of(JsonCreatorFieldInfo[].class)) + .inline(); + Expression field = new Expression.ArrayValue(creatorFields, Expression.Literal.ofInt(i)); + expressions.add( + new Expression.Assign( + new Reference("this.r" + i, TypeRef.of(JsonTypeInfo.class)), + new Expression.Invoke(field, "typeInfo", TypeRef.of(JsonTypeInfo.class)) + .inline())); + } else { + expressions.add( + new Expression.Assign( + new Reference("this.r" + i, TypeRef.of(readerCapabilityType())), + new Expression.ArrayValue(codecs, Expression.Literal.ofInt(i)))); + } } } return expressions; } + private void addAnyReaderAssignment(Expression.ListExpression expressions, Expression owner) { + if (usesReaderSlot(any.valueTypeInfo())) { + Expression anyInfo = + new Expression.Invoke(owner, "anyInfo", TypeRef.of(AnyInfo.class)).inline(); + expressions.add( + new Expression.Assign( + new Reference("this.anyReader", TypeRef.of(JsonTypeInfo.class)), + new Expression.Invoke(anyInfo, "valueTypeInfo", TypeRef.of(JsonTypeInfo.class)) + .inline())); + } else { + expressions.add( + new Expression.Assign( + new Reference("this.anyReader", TypeRef.of(readerCapabilityType())), + new Reference("anyReader", TypeRef.of(readerCapabilityType())))); + } + } + + private void addSelfReaderAssignment(Expression.ListExpression expressions) { + if (!storesSelfReader) { + return; + } + Expression nestedReader = selfRef(); + if (any != null) { + Reference supplied = new Reference("selfReader", TypeRef.of(readerCapabilityType())); + nestedReader = + new Expression.Ternary( + eq(supplied, new Expression.Null(TypeRef.of(readerCapabilityType()), false)), + selfRef(), + supplied, + true, + TypeRef.of(readerCapabilityType())); + } + expressions.add( + new Expression.Assign( + new Reference("this.selfReader", TypeRef.of(readerCapabilityType())), nestedReader)); + } + private Expression creatorReadExpression(Class type, JsonCreatorInfo creatorInfo) { JsonCreatorFieldInfo[] fields = creatorInfo.fields(); Expression.ListExpression expressions = new Expression.ListExpression(); @@ -1037,12 +1039,12 @@ private Expression creatorDefault(Class type) { private Expression readCreatorValue(JsonCreatorFieldInfo field, int id) { Class type = field.rawType(); if (!isDirectCreatorPrimitive(field)) { + Expression codec = + usesReaderSlot(field.typeInfo()) + ? readerFromSlot(fieldRef("r" + id, JsonTypeInfo.class)) + : fieldRef("r" + id, readerCapabilityType()); Expression value = - new Expression.Invoke( - fieldRef("r" + id, readerCapabilityType()), - readMethod(), - TypeRef.of(Object.class), - readerRef()); + new Expression.Invoke(codec, readMethod(), TypeRef.of(Object.class), readerRef()); if (type.isPrimitive()) { value = new Expression.StaticInvoke( @@ -1278,6 +1280,41 @@ private void addGeneratedConstructor( ctx.addConstructor(code, params); } + private void addAnyReaderConstructor( + CodegenContext ctx, Expression expression, boolean storesAnyReader) { + if (storesAnyReader) { + addGeneratedConstructor( + ctx, + expression, + ObjectCodec.class, + "owner", + JsonFieldTable.class, + "readTable", + JsonFieldInfo[].class, + "properties", + JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), + "codecs", + JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), + "selfReader", + JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), + "anyReader"); + } else { + addGeneratedConstructor( + ctx, + expression, + ObjectCodec.class, + "owner", + JsonFieldTable.class, + "readTable", + JsonFieldInfo[].class, + "properties", + JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), + "codecs", + JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), + "selfReader"); + } + } + private Expression readerConstructorExpression(Class type, JsonFieldInfo[] properties) { Expression.ListExpression expressions = new Expression.ListExpression(); Reference propertiesRef = new Reference("properties", TypeRef.of(JsonFieldInfo[].class)); @@ -1294,12 +1331,10 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr expressions.add( new Expression.Assign(new Reference("this.owner", TypeRef.of(ObjectCodec.class)), owner)); } + addSelfReaderAssignment(expressions); if (any != null) { if (storesAnyReader(ownerType)) { - expressions.add( - new Expression.Assign( - new Reference("this.anyReader", TypeRef.of(readerCapabilityType())), - new Reference("anyReader", TypeRef.of(readerCapabilityType())))); + addAnyReaderAssignment(expressions, owner); } } Reference hashes = new Reference("this.fieldHashes", TypeRef.of(long[].class)); @@ -1332,10 +1367,18 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr new Expression.Cast( new Expression.ArrayValue(codecsRef, id), TypeRef.of(codecType)))); } else if (storesReadObjectCodec(type, properties[i])) { - expressions.add( - new Expression.Assign( - new Reference("this.o" + i, TypeRef.of(readerCapabilityType())), - new Expression.ArrayValue(codecsRef, id))); + if (usesReaderSlot(properties[i].readTypeInfo())) { + expressions.add( + new Expression.Assign( + new Reference("this.o" + i, TypeRef.of(JsonTypeInfo.class)), + new Expression.Invoke(property, "readTypeInfo", TypeRef.of(JsonTypeInfo.class)) + .inline())); + } else { + expressions.add( + new Expression.Assign( + new Reference("this.o" + i, TypeRef.of(readerCapabilityType())), + new Expression.ArrayValue(codecsRef, id))); + } } } return expressions; @@ -1367,6 +1410,7 @@ private Expression unwrappedReaderConstructor( unwrapped, new Expression.Invoke(owner, "unwrappedInfo", TypeRef.of(JsonUnwrappedInfo.class)) .inline())); + addSelfReaderAssignment(expressions); if (creatorFields != null) { expressions.add( new Expression.Assign( @@ -3056,13 +3100,10 @@ final Reference selfRef() { private void addSelfReaderField(CodegenContext ctx) { if (storesSelfReader) { - // Canonical readers keep this self-reference. A parent-local inline instance is rewired to - // the canonical reader before publication so its derived skip table cannot reach children. + // Canonical construction supplies null and stores this. A parent-local inline construction + // supplies the final canonical reader so its derived skip table cannot reach nested values. ctx.addField( - false, - JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), - "selfReader", - selfRef()); + true, JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "selfReader", null); } } @@ -3076,9 +3117,21 @@ private boolean storesAnyReader(Class type) { } private Expression anyReaderRef() { - return storesAnyReader(ownerType) - ? fieldRef("anyReader", readerCapabilityType()) - : nestedSelfReaderRef(); + if (!storesAnyReader(ownerType)) { + return nestedSelfReaderRef(); + } + return usesReaderSlot(any.valueTypeInfo()) + ? readerFromSlot(fieldRef("anyReader", JsonTypeInfo.class)) + : fieldRef("anyReader", readerCapabilityType()); + } + + private boolean usesReaderSlot(JsonTypeInfo child) { + return resolver.usesReaderSlot(ownerType, child); + } + + private Expression readerFromSlot(Expression slot) { + return new Expression.Invoke(slot, readerSlotMethod(), TypeRef.of(readerCapabilityType())) + .inline(); } final Reference fieldRef(String name, Class type) { @@ -3739,7 +3792,9 @@ final Expression readObjectValue(Class type, JsonFieldInfo property, int id) Expression codec = property.readRawType() == type ? nestedSelfReaderRef() - : fieldRef("o" + id, readerCapabilityType()); + : usesReaderSlot(property.readTypeInfo()) + ? readerFromSlot(fieldRef("o" + id, JsonTypeInfo.class)) + : fieldRef("o" + id, readerCapabilityType()); return new Expression.Cast( inline( new Expression.Invoke( 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 a47f8e5987..c6024c2285 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 @@ -43,6 +43,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.json.resolver.JsonTypeResolver; import org.apache.fory.reflect.TypeRef; @@ -52,8 +53,9 @@ *

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 final direct-child capabilities; canonical multi-object cycles retain the final child + * type-info slot owner. They fuse safe object prefixes and split wide objects into bounded methods + * to protect compiler and inlining budgets without adding per-field resolver lookup. */ abstract class JsonWriterCodegen { // Bound field logic in independently compiled generated methods. The entry method keeps a small @@ -80,6 +82,8 @@ abstract class JsonWriterCodegen { abstract String writeMethod(); + abstract String writerSlotMethod(); + // 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. abstract String memberGroupMethod(); @@ -194,7 +198,7 @@ String genWriterCode( ctx.addField(JsonFieldInfo.class, "wp" + i); } if (storesWriteCodec(property)) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(property)), "w" + i); + addWriterCodecField(ctx, property, "w" + i); } if (usesPrefix(property)) { addPrefixFields(ctx, property, i, prefixFields); @@ -264,10 +268,10 @@ String genAnyWriterCode( } boolean storesAnyWriter = storesAnyWriter(any); if (storesAnyWriter) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, completeWriterType()), "anyWriter"); + addAnyWriterField(ctx, any); addGeneratedConstructor( ctx, - anyWriterConstructorExpression(properties, prefixFields, true), + anyWriterConstructorExpression(properties, prefixFields, any, true), ObjectCodec.class, "owner", JsonFieldInfo[].class, @@ -279,7 +283,7 @@ String genAnyWriterCode( } else { addGeneratedConstructor( ctx, - anyWriterConstructorExpression(properties, prefixFields, false), + anyWriterConstructorExpression(properties, prefixFields, any, false), ObjectCodec.class, "owner", JsonFieldInfo[].class, @@ -372,10 +376,10 @@ String genUnwrappedWriterCode( addAnyGetterMethod(ctx, type, any); } if (storesAnyWriter(any)) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, completeWriterType()), "anyWriter"); + addAnyWriterField(ctx, any); addGeneratedConstructor( ctx, - anyWriterConstructorExpression(leaves, prefixFields, true), + anyWriterConstructorExpression(leaves, prefixFields, any, true), ObjectCodec.class, "owner", JsonFieldInfo[].class, @@ -387,7 +391,7 @@ String genUnwrappedWriterCode( } else { addGeneratedConstructor( ctx, - anyWriterConstructorExpression(leaves, prefixFields, false), + anyWriterConstructorExpression(leaves, prefixFields, any, false), ObjectCodec.class, "owner", JsonFieldInfo[].class, @@ -721,7 +725,7 @@ private void addWriterFields( ctx.addField(JsonFieldInfo.class, "wp" + i); } if (storesWriteCodec(property)) { - ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(property)), "w" + i); + addWriterCodecField(ctx, property, "w" + i); } if (usesPrefix(property)) { addPrefixFields(ctx, property, i, prefixFields); @@ -730,7 +734,7 @@ private void addWriterFields( } private Expression anyWriterConstructorExpression( - JsonFieldInfo[] properties, PrefixFields prefixFields, boolean storesAnyWriter) { + JsonFieldInfo[] properties, PrefixFields prefixFields, AnyInfo any, boolean storesAnyWriter) { Expression.ListExpression expressions = new Expression.ListExpression( writerConstructorExpression(properties, prefixFields), @@ -738,14 +742,45 @@ private Expression anyWriterConstructorExpression( new Reference("this.owner", TypeRef.of(ObjectCodec.class)), new Reference("owner", TypeRef.of(ObjectCodec.class)))); if (storesAnyWriter) { - expressions.add( - new Expression.Assign( - new Reference("this.anyWriter", TypeRef.of(completeWriterType())), - new Reference("anyWriter", TypeRef.of(completeWriterType())))); + if (usesAnyWriterSlot(any)) { + Expression owner = new Reference("owner", TypeRef.of(ObjectCodec.class)); + Expression anyInfo = + new Expression.Invoke(owner, "anyInfo", TypeRef.of(AnyInfo.class)).inline(); + expressions.add( + new Expression.Assign( + new Reference("this.anyWriter", TypeRef.of(JsonTypeInfo.class)), + new Expression.Invoke(anyInfo, "valueTypeInfo", TypeRef.of(JsonTypeInfo.class)) + .inline())); + } else { + expressions.add( + new Expression.Assign( + new Reference("this.anyWriter", TypeRef.of(completeWriterType())), + new Reference("anyWriter", TypeRef.of(completeWriterType())))); + } } return expressions; } + private void addWriterCodecField(CodegenContext ctx, JsonFieldInfo property, String name) { + if (usesWriterSlot(property)) { + ctx.addField(true, ctx.type(JsonTypeInfo.class), name, null); + } else { + addCapabilityField(ctx, codecFieldType(property), name); + } + } + + private void addAnyWriterField(CodegenContext ctx, AnyInfo any) { + if (usesAnyWriterSlot(any)) { + ctx.addField(true, ctx.type(JsonTypeInfo.class), "anyWriter", null); + } else { + addCapabilityField(ctx, completeWriterType(), "anyWriter"); + } + } + + private static void addCapabilityField(CodegenContext ctx, Class type, String name) { + ctx.addField(true, JsonCodegen.generatedCodecType(ctx, type), name, null); + } + private boolean storesAnyWriter(AnyInfo any) { return resolver.canonicalObjectCodec(any.valueTypeInfo()) == null || any.valueRawType() != ownerType; @@ -786,12 +821,20 @@ private Expression writerConstructorExpression( new Reference("this.wp" + i, TypeRef.of(JsonFieldInfo.class)), property)); } if (storesWriteCodec(properties[i])) { - Class codecType = codecFieldType(properties[i]); - expressions.add( - new Expression.Assign( - new Reference("this.w" + i, TypeRef.of(codecType)), - new Expression.Cast( - new Expression.ArrayValue(codecsRef, id), TypeRef.of(codecType)))); + if (usesWriterSlot(properties[i])) { + expressions.add( + new Expression.Assign( + new Reference("this.w" + i, TypeRef.of(JsonTypeInfo.class)), + new Expression.Invoke(property, "writeTypeInfo", TypeRef.of(JsonTypeInfo.class)) + .inline())); + } else { + Class codecType = codecFieldType(properties[i]); + expressions.add( + new Expression.Assign( + new Reference("this.w" + i, TypeRef.of(codecType)), + new Expression.Cast( + new Expression.ArrayValue(codecsRef, id), TypeRef.of(codecType)))); + } } if (usesPrefix(properties[i])) { addPrefixAssignments(expressions, property, properties[i], i, prefixFields); @@ -989,6 +1032,13 @@ private Expression writeAny( } Expression map = new Expression.Variable("anyMap", cast(inline(mapValue), TypeRef.of(Map.class))); + Expression anyCodec = new Reference("this", TypeRef.of(completeWriterType())); + if (storesAnyWriter(any)) { + anyCodec = + usesAnyWriterSlot(any) + ? writerFromSlot(fieldRef("anyWriter", JsonTypeInfo.class)) + : fieldRef("anyWriter", completeWriterType()); + } return new Expression.ListExpression( map, new Expression.Invoke( @@ -998,9 +1048,7 @@ private Expression writeAny( false, writerRef(), map, - storesAnyWriter(any) - ? fieldRef("anyWriter", completeWriterType()) - : new Reference("this", TypeRef.of(completeWriterType())), + anyCodec, written)); } @@ -1259,16 +1307,32 @@ private Expression writeCodec( Expression codec = object && property.writeRawType() == ownerType ? new Reference("this", TypeRef.of(completeWriterType())) - : fieldRef("w" + id, codecFieldType(property)); + : usesWriterSlot(property) + ? writerFromSlot(fieldRef("w" + id, JsonTypeInfo.class)) + : fieldRef("w" + id, codecFieldType(property)); return new Expression.Invoke(codec, writeMethod(), writer, value); } + private Expression writerFromSlot(Expression slot) { + return new Expression.Invoke(slot, writerSlotMethod(), TypeRef.of(completeWriterType())) + .inline(); + } + private boolean storesWriteCodec(JsonFieldInfo property) { return usesWriteCodec(property) && (resolver.canonicalObjectCodec(property.writeTypeInfo()) == null || property.writeRawType() != ownerType); } + private boolean usesWriterSlot(JsonFieldInfo property) { + return storesWriteCodec(property) + && resolver.usesWriterSlot(ownerType, property.writeTypeInfo()); + } + + private boolean usesAnyWriterSlot(AnyInfo any) { + return storesAnyWriter(any) && resolver.usesWriterSlot(ownerType, any.valueTypeInfo()); + } + private static Expression writeStringCollection(Expression value, Expression writer) { return new Expression.Invoke(writer, "writeStringCollection", value); } 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 fa81dea225..25426eaf74 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 @@ -30,11 +30,11 @@ final class Latin1ReaderCodegen extends JsonReaderCodegen { Latin1ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { - super(codegen, resolver); + super(codegen, resolver, true); } Latin1ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, int[] fastReadGroupEnds) { - super(codegen, resolver, false, fastReadGroupEnds); + super(codegen, resolver, true, fastReadGroupEnds); } @Override @@ -62,6 +62,11 @@ String readMethod() { return "readLatin1"; } + @Override + String readerSlotMethod() { + return "latin1Reader"; + } + @Override String readEnumMethod(boolean tokenValueRead, boolean hashFallback) { return tokenValueRead 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 6d489cc34d..52b394b763 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 @@ -61,6 +61,11 @@ String writeMethod() { return "writeString"; } + @Override + String writerSlotMethod() { + return "stringWriter"; + } + @Override String memberGroupMethod() { return "writeStringMembers"; 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 a3288ec582..8dfa82985d 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 @@ -29,11 +29,11 @@ final class Utf16ReaderCodegen extends JsonReaderCodegen { Utf16ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) { - super(codegen, resolver); + super(codegen, resolver, true); } Utf16ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, int[] fastReadGroupEnds) { - super(codegen, resolver, false, fastReadGroupEnds); + super(codegen, resolver, true, fastReadGroupEnds); } @Override @@ -61,6 +61,11 @@ String readMethod() { return "readUtf16"; } + @Override + String readerSlotMethod() { + return "utf16Reader"; + } + @Override String readEnumMethod(boolean tokenValueRead, boolean hashFallback) { return "readNextUtf16Enum"; 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 0caa4591a7..1767f005ed 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 @@ -66,6 +66,11 @@ String readMethod() { return "readUtf8"; } + @Override + String readerSlotMethod() { + return "utf8Reader"; + } + @Override String readEnumMethod(boolean tokenValueRead, boolean hashFallback) { return tokenValueRead 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 e599dae870..3161a85f52 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 @@ -70,6 +70,11 @@ String writeMethod() { return "writeUtf8"; } + @Override + String writerSlotMethod() { + return "utf8Writer"; + } + @Override String memberGroupMethod() { return "writeUtf8Members"; 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..176db70e7f 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 @@ -52,9 +52,10 @@ * *

{@link #resolveTypes(JsonTypeResolver)} installs the resolved read and write {@link * JsonTypeInfo} bindings after recursive object metadata has been published. Those bindings are the - * only mutable lifecycle phase; generated instances capture the current concrete child capability - * under the resolver-local JIT lock and receive later child replacements through resolver - * callbacks. + * only mutable lifecycle phase. Generated classes are compiled independently from this stable + * metadata; generated instances are constructed bottom-up with final direct child capabilities. A + * canonical multi-object cycle instead captures the final child type-info slot owner. The complete + * graph is published under the resolver-local JIT lock. */ public final class JsonFieldInfo { private static final int KIND_BOOLEAN = 1; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java index 342bc9447e..aea0a26b2c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java @@ -243,7 +243,8 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( ObjectCodec owner, JsonFieldTable readTable, JsonFieldInfo[] fields, - Latin1ReaderCodec[] codecs) { + Latin1ReaderCodec[] codecs, + Latin1ReaderCodec selfReader) { try { if (AndroidSupport.IS_ANDROID) { Constructor constructor = @@ -251,10 +252,11 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( ObjectCodec.class, JsonFieldTable.class, JsonFieldInfo[].class, - Latin1ReaderCodec[].class); + Latin1ReaderCodec[].class, + Latin1ReaderCodec.class); constructor.setAccessible(true); return (Latin1ReaderCodec) - constructor.newInstance(owner, readTable, fields, codecs); + constructor.newInstance(owner, readTable, fields, codecs, selfReader); } MethodHandle constructor = _JDKAccess._trustedLookup(type) @@ -265,8 +267,10 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( ObjectCodec.class, JsonFieldTable.class, JsonFieldInfo[].class, - Latin1ReaderCodec[].class)); - return (Latin1ReaderCodec) constructor.invoke(owner, readTable, fields, codecs); + Latin1ReaderCodec[].class, + Latin1ReaderCodec.class)); + return (Latin1ReaderCodec) + constructor.invoke(owner, readTable, fields, codecs, selfReader); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any Latin1 reader", e); } @@ -279,6 +283,7 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( JsonFieldTable readTable, JsonFieldInfo[] fields, Latin1ReaderCodec[] codecs, + Latin1ReaderCodec selfReader, Latin1ReaderCodec anyCodec) { try { if (AndroidSupport.IS_ANDROID) { @@ -288,10 +293,11 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( JsonFieldTable.class, JsonFieldInfo[].class, Latin1ReaderCodec[].class, + Latin1ReaderCodec.class, Latin1ReaderCodec.class); constructor.setAccessible(true); return (Latin1ReaderCodec) - constructor.newInstance(owner, readTable, fields, codecs, anyCodec); + constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec); } MethodHandle constructor = _JDKAccess._trustedLookup(type) @@ -303,9 +309,10 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( JsonFieldTable.class, JsonFieldInfo[].class, Latin1ReaderCodec[].class, + Latin1ReaderCodec.class, Latin1ReaderCodec.class)); return (Latin1ReaderCodec) - constructor.invoke(owner, readTable, fields, codecs, anyCodec); + constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any Latin1 reader", e); } @@ -346,7 +353,8 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( ObjectCodec owner, JsonFieldTable readTable, JsonFieldInfo[] fields, - Utf16ReaderCodec[] codecs) { + Utf16ReaderCodec[] codecs, + Utf16ReaderCodec selfReader) { try { if (AndroidSupport.IS_ANDROID) { Constructor constructor = @@ -354,9 +362,11 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( ObjectCodec.class, JsonFieldTable.class, JsonFieldInfo[].class, - Utf16ReaderCodec[].class); + Utf16ReaderCodec[].class, + Utf16ReaderCodec.class); constructor.setAccessible(true); - return (Utf16ReaderCodec) constructor.newInstance(owner, readTable, fields, codecs); + return (Utf16ReaderCodec) + constructor.newInstance(owner, readTable, fields, codecs, selfReader); } MethodHandle constructor = _JDKAccess._trustedLookup(type) @@ -367,8 +377,10 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( ObjectCodec.class, JsonFieldTable.class, JsonFieldInfo[].class, - Utf16ReaderCodec[].class)); - return (Utf16ReaderCodec) constructor.invoke(owner, readTable, fields, codecs); + Utf16ReaderCodec[].class, + Utf16ReaderCodec.class)); + return (Utf16ReaderCodec) + constructor.invoke(owner, readTable, fields, codecs, selfReader); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any UTF16 reader", e); } @@ -381,6 +393,7 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( JsonFieldTable readTable, JsonFieldInfo[] fields, Utf16ReaderCodec[] codecs, + Utf16ReaderCodec selfReader, Utf16ReaderCodec anyCodec) { try { if (AndroidSupport.IS_ANDROID) { @@ -390,10 +403,11 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( JsonFieldTable.class, JsonFieldInfo[].class, Utf16ReaderCodec[].class, + Utf16ReaderCodec.class, Utf16ReaderCodec.class); constructor.setAccessible(true); return (Utf16ReaderCodec) - constructor.newInstance(owner, readTable, fields, codecs, anyCodec); + constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec); } MethodHandle constructor = _JDKAccess._trustedLookup(type) @@ -405,9 +419,10 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( JsonFieldTable.class, JsonFieldInfo[].class, Utf16ReaderCodec[].class, + Utf16ReaderCodec.class, Utf16ReaderCodec.class)); return (Utf16ReaderCodec) - constructor.invoke(owner, readTable, fields, codecs, anyCodec); + constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any UTF16 reader", e); } @@ -466,7 +481,8 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( ObjectCodec owner, JsonFieldTable readTable, JsonFieldInfo[] fields, - Utf8ReaderCodec[] codecs) { + Utf8ReaderCodec[] codecs, + Utf8ReaderCodec selfReader) { try { if (AndroidSupport.IS_ANDROID) { Constructor constructor = @@ -474,9 +490,11 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( ObjectCodec.class, JsonFieldTable.class, JsonFieldInfo[].class, - Utf8ReaderCodec[].class); + Utf8ReaderCodec[].class, + Utf8ReaderCodec.class); constructor.setAccessible(true); - return (Utf8ReaderCodec) constructor.newInstance(owner, readTable, fields, codecs); + return (Utf8ReaderCodec) + constructor.newInstance(owner, readTable, fields, codecs, selfReader); } MethodHandle constructor = _JDKAccess._trustedLookup(type) @@ -487,8 +505,10 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( ObjectCodec.class, JsonFieldTable.class, JsonFieldInfo[].class, - Utf8ReaderCodec[].class)); - return (Utf8ReaderCodec) constructor.invoke(owner, readTable, fields, codecs); + Utf8ReaderCodec[].class, + Utf8ReaderCodec.class)); + return (Utf8ReaderCodec) + constructor.invoke(owner, readTable, fields, codecs, selfReader); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any UTF8 reader", e); } @@ -501,6 +521,7 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( JsonFieldTable readTable, JsonFieldInfo[] fields, Utf8ReaderCodec[] codecs, + Utf8ReaderCodec selfReader, Utf8ReaderCodec anyCodec) { try { if (AndroidSupport.IS_ANDROID) { @@ -510,10 +531,11 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( JsonFieldTable.class, JsonFieldInfo[].class, Utf8ReaderCodec[].class, + Utf8ReaderCodec.class, Utf8ReaderCodec.class); constructor.setAccessible(true); return (Utf8ReaderCodec) - constructor.newInstance(owner, readTable, fields, codecs, anyCodec); + constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec); } MethodHandle constructor = _JDKAccess._trustedLookup(type) @@ -525,9 +547,10 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( JsonFieldTable.class, JsonFieldInfo[].class, Utf8ReaderCodec[].class, + Utf8ReaderCodec.class, Utf8ReaderCodec.class)); return (Utf8ReaderCodec) - constructor.invoke(owner, readTable, fields, codecs, anyCodec); + constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any UTF8 reader", e); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index e449963cdb..9e77489f1d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -148,9 +148,9 @@ * state. Common short field names admitted by reader-local caches are published here for * best-effort String reference reuse across readers. Reader-local admission is the only field-name * capacity gate; the shared field-name map has no explicit limit. Source-generated model companions - * and JIT-generated classes are shared here; concrete JIT codec instances, ordinary type bindings, - * JIT locks, and callbacks remain resolver-local. A fresh generic {@link JsonJITContext} is - * therefore created for every pooled JSON state. + * and JIT-generated class futures are shared here; concrete JIT codec instances, ordinary type + * bindings, graph construction, JIT locks, and publication remain resolver-local. A fresh {@link + * JsonJITContext} is therefore created for every pooled JSON state. */ public final class JsonSharedRegistry { private static final int TYPE_CHECK_CACHE_LIMIT = 8192; @@ -190,8 +190,11 @@ public int compare(DeclarationCandidate left, DeclarationCandidate right) { private final ConcurrentHashMap, MapKeyCodec> mapKeyCodecs; private final ConcurrentHashMap, GeneratedJsonCodec> generatedCodecs; private final Set> typesWithoutGeneratedCodec; - private final ConcurrentHashMap, CompletableFuture>> finalUtf8ReaderClasses; - private final ConcurrentHashMap, CompletableFuture>> mutableUtf8ReaderClasses; + private final ConcurrentHashMap, CompletableFuture>> stringWriterClasses; + private final ConcurrentHashMap, CompletableFuture>> utf8WriterClasses; + private final ConcurrentHashMap, CompletableFuture>> latin1ReaderClasses; + private final ConcurrentHashMap, CompletableFuture>> utf16ReaderClasses; + private final ConcurrentHashMap, CompletableFuture>> utf8ReaderClasses; private final ConcurrentHashMap>> utf8CollectionReaderClasses; private final ConcurrentHashMap cachedFieldNames; @@ -222,8 +225,11 @@ public JsonSharedRegistry(JsonConfig config) { mapKeyCodecs = new ConcurrentHashMap<>(); generatedCodecs = new ConcurrentHashMap<>(); typesWithoutGeneratedCodec = ConcurrentHashMap.newKeySet(); - finalUtf8ReaderClasses = new ConcurrentHashMap<>(); - mutableUtf8ReaderClasses = new ConcurrentHashMap<>(); + stringWriterClasses = new ConcurrentHashMap<>(); + utf8WriterClasses = new ConcurrentHashMap<>(); + latin1ReaderClasses = new ConcurrentHashMap<>(); + utf16ReaderClasses = new ConcurrentHashMap<>(); + utf8ReaderClasses = new ConcurrentHashMap<>(); utf8CollectionReaderClasses = new ConcurrentHashMap<>(); cachedFieldNames = new ConcurrentHashMap<>(); boolean codegenEnabled = config.codegenEnabled(); @@ -233,12 +239,29 @@ public JsonSharedRegistry(JsonConfig config) { registerExactCodecs(); } - CompletableFuture> utf8ReaderClass( - ObjectCodec owner, JsonTypeResolver resolver, boolean finalDependencies) { + CompletableFuture> stringWriterClass(ObjectCodec owner, JsonTypeResolver resolver) { return generatedClassFuture( - finalDependencies ? finalUtf8ReaderClasses : mutableUtf8ReaderClasses, - owner.type(), - () -> codegen.compileUtf8Reader(owner, resolver, finalDependencies)); + stringWriterClasses, owner.type(), () -> codegen.compileStringWriter(owner, resolver)); + } + + CompletableFuture> utf8WriterClass(ObjectCodec owner, JsonTypeResolver resolver) { + return generatedClassFuture( + utf8WriterClasses, owner.type(), () -> codegen.compileUtf8Writer(owner, resolver)); + } + + CompletableFuture> latin1ReaderClass(ObjectCodec owner, JsonTypeResolver resolver) { + return generatedClassFuture( + latin1ReaderClasses, owner.type(), () -> codegen.compileLatin1Reader(owner, resolver)); + } + + CompletableFuture> utf16ReaderClass(ObjectCodec owner, JsonTypeResolver resolver) { + return generatedClassFuture( + utf16ReaderClasses, owner.type(), () -> codegen.compileUtf16Reader(owner, resolver)); + } + + CompletableFuture> utf8ReaderClass(ObjectCodec owner, JsonTypeResolver resolver) { + return generatedClassFuture( + utf8ReaderClasses, owner.type(), () -> codegen.compileUtf8Reader(owner, resolver, true)); } CompletableFuture> utf8CollectionReaderClass( @@ -823,7 +846,7 @@ public JsonFieldKind kind(Class type) { } JsonJITContext newJITContext() { - return new JsonJITContext(asyncCompilationEnabled, compilationService); + return new JsonJITContext(asyncCompilationEnabled); } JsonCodegen codegen() { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 83f055cbc6..9583a62cd3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -19,7 +19,6 @@ package org.apache.fory.json.resolver; -import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; @@ -67,19 +66,18 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; import org.apache.fory.json.meta.JsonFieldTable; -import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; /** * Local JSON type dispatcher used exclusively by one borrowed {@code ForyJson} state at a time. * - *

This class corresponds to Fory core's {@code ClassResolver}: it owns terminal capabilities, - * generated codec construction, capability-slot publication, and generated parent child-field - * callbacks. Root codec execution and completion callbacks use the same resolver-local JIT lock. - * {@link JsonJITContext} only orders generic JIT and notify callbacks under that lock; it does not - * know any JSON capability, codec, generated class, or field metadata. Compilation failure leaves - * the interpreted capability in its {@link JsonTypeInfo} slot; no parallel requested or failure - * state is retained, so a later operation may retry compilation. + *

This class corresponds to Fory core's {@code ClassResolver}: it owns terminal capability + * construction, final child wiring, and capability-slot publication. After an outer metadata + * resolution succeeds, it registers every eligible representation graph once. Root codec execution + * and graph completion use the same resolver-local JIT lock. {@link JsonJITContext} only orders the + * completion under that lock; it does not know any JSON capability, generated class, or field + * metadata. Compilation failure leaves the interpreted capability in its {@link JsonTypeInfo} slot + * without adding failure or request state to the hot codec. * *

{@code typeInfos} owns declared and parameterized bindings. {@code objectCodecs} breaks * recursive object-metadata construction by publishing the complete object owner before resolving @@ -97,7 +95,6 @@ public final class JsonTypeResolver { private final IdentityMap, JsonTypeInfo> rawObjectTypeInfos; private final IdentityMap, JsonTypeInfo> canonicalObjectTypeInfos; private final IdentityMap> collectionCodecs; - private final IdentityMap, Utf8ReaderGraph> pendingUtf8Readers; private int resolutionDepth; private enum RuntimeObjectKey { @@ -113,7 +110,6 @@ public JsonTypeResolver(JsonSharedRegistry sharedRegistry) { rawObjectTypeInfos = new IdentityMap<>(); canonicalObjectTypeInfos = new IdentityMap<>(); collectionCodecs = new IdentityMap<>(); - pendingUtf8Readers = new IdentityMap<>(); } /** Returns the shared registry that owns this resolver and its reader cache domain. */ @@ -151,7 +147,9 @@ private ObjectCodec getObjectCodec(TypeRef ownerType, Object key) { } ResolutionSnapshot snapshot = beginResolution(); try { - return buildObjectCodec(ownerType, key); + ObjectCodec result = buildObjectCodec(ownerType, key); + completeResolution(snapshot); + return result; } catch (RuntimeException | Error e) { rollbackResolution(snapshot); throw e; @@ -256,7 +254,9 @@ public JsonTypeInfo getTypeInfo(Type declaredType, Class fallback) { } ResolutionSnapshot snapshot = beginResolution(); try { - return resolveTypeInfo(declaredType, rawType, key); + JsonTypeInfo result = resolveTypeInfo(declaredType, rawType, key); + completeResolution(snapshot); + return result; } catch (RuntimeException | Error e) { rollbackResolution(snapshot); throw e; @@ -273,7 +273,9 @@ public JsonTypeInfo getTypeInfo(Type declaredType, Class fallback, JsonCodec Class rawType = CodecUtils.rawType(declaredType, fallback); ResolutionSnapshot snapshot = beginResolution(); try { - return resolveTypeInfo(declaredType, rawType, annotation); + JsonTypeInfo result = resolveTypeInfo(declaredType, rawType, annotation); + completeResolution(snapshot); + return result; } catch (RuntimeException | Error e) { rollbackResolution(snapshot); throw e; @@ -493,6 +495,21 @@ private void endResolution() { resolutionDepth--; } + private void completeResolution(ResolutionSnapshot snapshot) { + if (snapshot == null || codegen == null) { + return; + } + ArrayList roots = new ArrayList<>(); + for (Map.Entry entry : typeInfos.entrySet()) { + if (!snapshot.typeKeys.contains(entry.getKey())) { + roots.add(entry.getValue()); + } + } + if (!roots.isEmpty()) { + requestCapabilities(roots); + } + } + private void rollbackResolution(ResolutionSnapshot snapshot) { if (snapshot == null) { return; @@ -542,7 +559,9 @@ public JsonTypeInfo getRuntimeTypeInfo(Class runtimeType) { } ResolutionSnapshot snapshot = beginResolution(); try { - return resolveRuntimeTypeInfo(runtimeType, key); + JsonTypeInfo result = resolveRuntimeTypeInfo(runtimeType, key); + completeResolution(snapshot); + return result; } catch (RuntimeException | Error e) { rollbackResolution(snapshot); throw e; @@ -594,28 +613,7 @@ public StringWriterCodec stringWriter(ObjectCodec codec) { if (typeInfo == null) { return codec; } - StringWriterCodec installed = typeInfo.stringWriter(); - if (installed == owner && codegen != null && codegen.canCompileWriter(owner)) { - jitContext.registerJITCallback( - () -> owner.getClass(), - () -> codegen.compileStringWriter(owner, this), - new JsonJITContext.JITCallback>() { - @Override - public void onSuccess(Class generated) { - publishStringWriter(owner, typeInfo, generated); - } - - @Override - public void onFailure(Throwable failure) {} - - @Override - public Object id() { - return codegen.stringWriterJITId(owner.type()); - } - }); - installed = typeInfo.stringWriter(); - } - return (StringWriterCodec) installed; + return (StringWriterCodec) typeInfo.stringWriter(); } @SuppressWarnings("unchecked") @@ -626,28 +624,7 @@ public Utf8WriterCodec utf8Writer(ObjectCodec codec) { if (typeInfo == null) { return codec; } - Utf8WriterCodec installed = typeInfo.utf8Writer(); - if (installed == owner && codegen != null && codegen.canCompileWriter(owner)) { - jitContext.registerJITCallback( - () -> owner.getClass(), - () -> codegen.compileUtf8Writer(owner, this), - new JsonJITContext.JITCallback>() { - @Override - public void onSuccess(Class generated) { - publishUtf8Writer(owner, typeInfo, generated); - } - - @Override - public void onFailure(Throwable failure) {} - - @Override - public Object id() { - return codegen.utf8WriterJITId(owner.type()); - } - }); - installed = typeInfo.utf8Writer(); - } - return (Utf8WriterCodec) installed; + return (Utf8WriterCodec) typeInfo.utf8Writer(); } @SuppressWarnings("unchecked") @@ -658,28 +635,7 @@ public Latin1ReaderCodec latin1Reader(ObjectCodec codec) { if (typeInfo == null) { return codec; } - Latin1ReaderCodec installed = typeInfo.latin1Reader(); - if (installed == owner && codegen != null && codegen.canCompileReader(owner)) { - jitContext.registerJITCallback( - () -> owner.getClass(), - () -> codegen.compileLatin1Reader(owner, this), - new JsonJITContext.JITCallback>() { - @Override - public void onSuccess(Class generated) { - publishLatin1Reader(owner, typeInfo, generated); - } - - @Override - public void onFailure(Throwable failure) {} - - @Override - public Object id() { - return codegen.latin1ReaderJITId(owner.type()); - } - }); - installed = typeInfo.latin1Reader(); - } - return (Latin1ReaderCodec) installed; + return (Latin1ReaderCodec) typeInfo.latin1Reader(); } @SuppressWarnings("unchecked") @@ -690,28 +646,7 @@ public Utf16ReaderCodec utf16Reader(ObjectCodec codec) { if (typeInfo == null) { return codec; } - Utf16ReaderCodec installed = typeInfo.utf16Reader(); - if (installed == owner && codegen != null && codegen.canCompileReader(owner)) { - jitContext.registerJITCallback( - () -> owner.getClass(), - () -> codegen.compileUtf16Reader(owner, this), - new JsonJITContext.JITCallback>() { - @Override - public void onSuccess(Class generated) { - publishUtf16Reader(owner, typeInfo, generated); - } - - @Override - public void onFailure(Throwable failure) {} - - @Override - public Object id() { - return codegen.utf16ReaderJITId(owner.type()); - } - }); - installed = typeInfo.utf16Reader(); - } - return (Utf16ReaderCodec) installed; + return (Utf16ReaderCodec) typeInfo.utf16Reader(); } @SuppressWarnings("unchecked") @@ -722,144 +657,16 @@ public Utf8ReaderCodec utf8Reader(ObjectCodec codec) { if (typeInfo == null) { return codec; } - Utf8ReaderCodec installed = typeInfo.utf8Reader(); - if (installed == owner && codegen != null && codegen.canCompileReader(owner)) { - if (pendingUtf8Readers.get(owner) != null) { - return (Utf8ReaderCodec) installed; - } - Utf8ReaderGraph graph = new Utf8ReaderGraph(codegen.utf8ReaderJITId(owner.type())); - if (!graph.addObject(owner, typeInfo)) { - graph = null; - } else { - graph.markPending(); - } - requestUtf8Reader(owner, typeInfo, graph); - installed = typeInfo.utf8Reader(); - } - return (Utf8ReaderCodec) installed; - } - - @Internal - public void resolveInlineAnyReaders( - ClosedSubtypeCodec parent, int index, ObjectCodec codec, JsonFieldTable readTable) { - requireJITLock(); - ObjectCodec owner = erase(codec); - JsonTypeInfo typeInfo = canonicalObjectTypeInfos.get(owner); - if (typeInfo == null || codegen == null || !codegen.canCompileReader(owner)) { - return; - } - resolveInlineLatin1Reader(parent, index, owner, typeInfo, readTable); - resolveInlineUtf16Reader(parent, index, owner, typeInfo, readTable); - resolveInlineUtf8Reader(parent, index, owner, typeInfo, readTable); - } - - private void resolveInlineLatin1Reader( - ClosedSubtypeCodec parent, - int index, - ObjectCodec owner, - JsonTypeInfo typeInfo, - JsonFieldTable readTable) { - Latin1ReaderCodec current = latin1Reader(owner); - if (current != owner) { - parent.setInlineLatin1Reader( - index, newInlineLatin1Reader(owner, current.getClass(), readTable, current)); - return; - } - jitContext.registerJITNotifyCallback( - codegen.latin1ReaderJITId(owner.type()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Latin1ReaderCodec installed = typeInfo.latin1Reader(); - checkGeneratedClass(result, installed); - parent.setInlineLatin1Reader( - index, newInlineLatin1Reader(owner, (Class) result, readTable, installed)); - } - - @Override - public void onNotifyMissed() { - Latin1ReaderCodec installed = typeInfo.latin1Reader(); - if (installed != owner) { - parent.setInlineLatin1Reader( - index, newInlineLatin1Reader(owner, installed.getClass(), readTable, installed)); - } - } - }); - } - - private void resolveInlineUtf16Reader( - ClosedSubtypeCodec parent, - int index, - ObjectCodec owner, - JsonTypeInfo typeInfo, - JsonFieldTable readTable) { - Utf16ReaderCodec current = utf16Reader(owner); - if (current != owner) { - parent.setInlineUtf16Reader( - index, newInlineUtf16Reader(owner, current.getClass(), readTable, current)); - return; - } - jitContext.registerJITNotifyCallback( - codegen.utf16ReaderJITId(owner.type()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf16ReaderCodec installed = typeInfo.utf16Reader(); - checkGeneratedClass(result, installed); - parent.setInlineUtf16Reader( - index, newInlineUtf16Reader(owner, (Class) result, readTable, installed)); - } - - @Override - public void onNotifyMissed() { - Utf16ReaderCodec installed = typeInfo.utf16Reader(); - if (installed != owner) { - parent.setInlineUtf16Reader( - index, newInlineUtf16Reader(owner, installed.getClass(), readTable, installed)); - } - } - }); - } - - private void resolveInlineUtf8Reader( - ClosedSubtypeCodec parent, - int index, - ObjectCodec owner, - JsonTypeInfo typeInfo, - JsonFieldTable readTable) { - Utf8ReaderCodec current = utf8Reader(owner); - if (current != owner) { - parent.setInlineUtf8Reader( - index, newInlineUtf8Reader(owner, current.getClass(), readTable, current)); - return; - } - jitContext.registerJITNotifyCallback( - utf8ReaderRequestId(typeInfo), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf8ReaderCodec installed = typeInfo.utf8Reader(); - if (installed != owner) { - parent.setInlineUtf8Reader( - index, newInlineUtf8Reader(owner, installed.getClass(), readTable, installed)); - } - } - - @Override - public void onNotifyMissed() { - Utf8ReaderCodec installed = typeInfo.utf8Reader(); - if (installed != owner) { - parent.setInlineUtf8Reader( - index, newInlineUtf8Reader(owner, installed.getClass(), readTable, installed)); - } - } - }); + return (Utf8ReaderCodec) typeInfo.utf8Reader(); } @SuppressWarnings("unchecked") - private StringWriterCodec newStringWriter(ObjectCodec owner, Class generatedClass) { + private StringWriterCodec newStringWriter( + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { if (owner.unwrappedInfo() != null) { - return newUnwrappedStringWriter(owner, generatedClass); + return newUnwrappedStringWriter(owner, generatedClass, capabilities); } JsonFieldInfo[] fields = owner.writeFields(); StringWriterCodec[] codecs = @@ -868,13 +675,7 @@ private StringWriterCodec newStringWriter(ObjectCodec owner, Class JsonFieldInfo field = fields[i]; if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); - StringWriterCodec codec = typeInfo.stringWriter(); - if (JsonCodegen.writeNestedType(field, this) != null - && typeInfo.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = stringWriter((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.STRING_WRITER); } } AnyInfo any = owner.anyInfo(); @@ -885,20 +686,19 @@ private StringWriterCodec newStringWriter(ObjectCodec owner, Class return GeneratedCodecInstantiator.instantiateAnyStringWriter( generatedClass, owner, fields, codecs); } - StringWriterCodec anyCodec = any.valueTypeInfo().stringWriter(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = stringWriter((ObjectCodec) anyCodec); - } + StringWriterCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.STRING_WRITER); return GeneratedCodecInstantiator.instantiateAnyStringWriter( generatedClass, owner, fields, codecs, anyCodec); } @SuppressWarnings("unchecked") - private Utf8WriterCodec newUtf8Writer(ObjectCodec owner, Class generatedClass) { + private Utf8WriterCodec newUtf8Writer( + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { if (owner.unwrappedInfo() != null) { - return newUnwrappedUtf8Writer(owner, generatedClass); + return newUnwrappedUtf8Writer(owner, generatedClass, capabilities); } JsonFieldInfo[] fields = owner.writeFields(); Utf8WriterCodec[] codecs = @@ -907,13 +707,7 @@ private Utf8WriterCodec newUtf8Writer(ObjectCodec owner, Class gen JsonFieldInfo field = fields[i]; if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); - Utf8WriterCodec codec = typeInfo.utf8Writer(); - if (JsonCodegen.writeNestedType(field, this) != null - && typeInfo.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = utf8Writer((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_WRITER); } } AnyInfo any = owner.anyInfo(); @@ -924,19 +718,17 @@ private Utf8WriterCodec newUtf8Writer(ObjectCodec owner, Class gen return GeneratedCodecInstantiator.instantiateAnyUtf8Writer( generatedClass, owner, fields, codecs); } - Utf8WriterCodec anyCodec = any.valueTypeInfo().utf8Writer(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Writer((ObjectCodec) anyCodec); - } + Utf8WriterCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_WRITER); return GeneratedCodecInstantiator.instantiateAnyUtf8Writer( generatedClass, owner, fields, codecs, anyCodec); } @SuppressWarnings("unchecked") private StringWriterCodec newUnwrappedStringWriter( - ObjectCodec owner, Class generatedClass) { + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { JsonFieldInfo[] fields = unwrappedWriteFields(owner); StringWriterCodec[] codecs = (StringWriterCodec[]) new StringWriterCodec[fields.length]; @@ -944,13 +736,7 @@ private StringWriterCodec newUnwrappedStringWriter( JsonFieldInfo field = fields[i]; if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo child = field.writeTypeInfo(); - StringWriterCodec codec = child.stringWriter(); - if (JsonCodegen.writeNestedType(field, this) != null - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = stringWriter((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.STRING_WRITER); } } AnyInfo any = owner.anyInfo(); @@ -961,19 +747,17 @@ private StringWriterCodec newUnwrappedStringWriter( return GeneratedCodecInstantiator.instantiateAnyStringWriter( generatedClass, owner, fields, codecs); } - StringWriterCodec anyCodec = any.valueTypeInfo().stringWriter(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = stringWriter((ObjectCodec) anyCodec); - } + StringWriterCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.STRING_WRITER); return GeneratedCodecInstantiator.instantiateAnyStringWriter( generatedClass, owner, fields, codecs, anyCodec); } @SuppressWarnings("unchecked") private Utf8WriterCodec newUnwrappedUtf8Writer( - ObjectCodec owner, Class generatedClass) { + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { JsonFieldInfo[] fields = unwrappedWriteFields(owner); Utf8WriterCodec[] codecs = (Utf8WriterCodec[]) new Utf8WriterCodec[fields.length]; @@ -981,13 +765,7 @@ private Utf8WriterCodec newUnwrappedUtf8Writer( JsonFieldInfo field = fields[i]; if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo child = field.writeTypeInfo(); - Utf8WriterCodec codec = child.utf8Writer(); - if (JsonCodegen.writeNestedType(field, this) != null - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = utf8Writer((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.UTF8_WRITER); } } AnyInfo any = owner.anyInfo(); @@ -998,12 +776,8 @@ private Utf8WriterCodec newUnwrappedUtf8Writer( return GeneratedCodecInstantiator.instantiateAnyUtf8Writer( generatedClass, owner, fields, codecs); } - Utf8WriterCodec anyCodec = any.valueTypeInfo().utf8Writer(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Writer((ObjectCodec) anyCodec); - } + Utf8WriterCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_WRITER); return GeneratedCodecInstantiator.instantiateAnyUtf8Writer( generatedClass, owner, fields, codecs, anyCodec); } @@ -1013,15 +787,22 @@ private static JsonFieldInfo[] unwrappedWriteFields(ObjectCodec owner) { } @SuppressWarnings("unchecked") - private Latin1ReaderCodec newLatin1Reader(ObjectCodec owner, Class generatedClass) { - return newLatin1Reader(owner, generatedClass, owner.readTable()); + private Latin1ReaderCodec newLatin1Reader( + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { + return newLatin1Reader(owner, generatedClass, owner.readTable(), capabilities, null); } @SuppressWarnings("unchecked") private Latin1ReaderCodec newLatin1Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Latin1ReaderCodec selfReader) { if (owner.unwrappedInfo() != null) { - return newUnwrappedLatin1Reader(owner, generatedClass, readTable); + return newUnwrappedLatin1Reader(owner, generatedClass, readTable, capabilities, selfReader); } JsonFieldInfo[] fields = owner.readFields(); JsonCreatorInfo creator = owner.creatorInfo(); @@ -1030,7 +811,9 @@ private Latin1ReaderCodec newLatin1Reader( Latin1ReaderCodec[] codecs = (Latin1ReaderCodec[]) new Latin1ReaderCodec[creatorFields.length]; for (int i = 0; i < creatorFields.length; i++) { - codecs[i] = creatorFields[i].typeInfo().latin1Reader(); + codecs[i] = + resolvedCapability( + creatorFields[i].typeInfo(), capabilities, CapabilityKind.LATIN1_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1039,16 +822,12 @@ private Latin1ReaderCodec newLatin1Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs); - } - Latin1ReaderCodec anyCodec = any.valueTypeInfo().latin1Reader(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = latin1Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Latin1ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.LATIN1_READER); return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } Latin1ReaderCodec[] codecs = (Latin1ReaderCodec[]) new Latin1ReaderCodec[fields.length]; @@ -1056,12 +835,10 @@ private Latin1ReaderCodec newLatin1Reader( JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); if (JsonCodegen.usesReadCodec(field, this)) { - codecs[i] = typeInfo.latin1Reader(); + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.LATIN1_READER); } else if (JsonCodegen.readNestedType(field, this) != null && field.readRawType() != owner.type()) { - Latin1ReaderCodec codec = typeInfo.latin1Reader(); - codecs[i] = - codec instanceof ObjectCodec ? latin1Reader((ObjectCodec) codec) : codec; + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.LATIN1_READER); } } AnyInfo any = owner.anyInfo(); @@ -1071,28 +848,31 @@ private Latin1ReaderCodec newLatin1Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs); - } - Latin1ReaderCodec anyCodec = any.valueTypeInfo().latin1Reader(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = latin1Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Latin1ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.LATIN1_READER); return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") - private Utf16ReaderCodec newUtf16Reader(ObjectCodec owner, Class generatedClass) { - return newUtf16Reader(owner, generatedClass, owner.readTable()); + private Utf16ReaderCodec newUtf16Reader( + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { + return newUtf16Reader(owner, generatedClass, owner.readTable(), capabilities, null); } @SuppressWarnings("unchecked") private Utf16ReaderCodec newUtf16Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Utf16ReaderCodec selfReader) { if (owner.unwrappedInfo() != null) { - return newUnwrappedUtf16Reader(owner, generatedClass, readTable); + return newUnwrappedUtf16Reader(owner, generatedClass, readTable, capabilities, selfReader); } JsonFieldInfo[] fields = owner.readFields(); JsonCreatorInfo creator = owner.creatorInfo(); @@ -1101,7 +881,9 @@ private Utf16ReaderCodec newUtf16Reader( Utf16ReaderCodec[] codecs = (Utf16ReaderCodec[]) new Utf16ReaderCodec[creatorFields.length]; for (int i = 0; i < creatorFields.length; i++) { - codecs[i] = creatorFields[i].typeInfo().utf16Reader(); + codecs[i] = + resolvedCapability( + creatorFields[i].typeInfo(), capabilities, CapabilityKind.UTF16_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1110,16 +892,12 @@ private Utf16ReaderCodec newUtf16Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf16ReaderCodec anyCodec = any.valueTypeInfo().utf16Reader(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf16Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf16ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF16_READER); return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } Utf16ReaderCodec[] codecs = (Utf16ReaderCodec[]) new Utf16ReaderCodec[fields.length]; @@ -1127,11 +905,10 @@ private Utf16ReaderCodec newUtf16Reader( JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); if (JsonCodegen.usesReadCodec(field, this)) { - codecs[i] = typeInfo.utf16Reader(); + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF16_READER); } else if (JsonCodegen.readNestedType(field, this) != null && field.readRawType() != owner.type()) { - Utf16ReaderCodec codec = typeInfo.utf16Reader(); - codecs[i] = codec instanceof ObjectCodec ? utf16Reader((ObjectCodec) codec) : codec; + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF16_READER); } } AnyInfo any = owner.anyInfo(); @@ -1141,28 +918,31 @@ private Utf16ReaderCodec newUtf16Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf16ReaderCodec anyCodec = any.valueTypeInfo().utf16Reader(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf16Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf16ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF16_READER); return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") - private Utf8ReaderCodec newUtf8Reader(ObjectCodec owner, Class generatedClass) { - return newUtf8Reader(owner, generatedClass, owner.readTable()); + private Utf8ReaderCodec newUtf8Reader( + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { + return newUtf8Reader(owner, generatedClass, owner.readTable(), capabilities, null); } @SuppressWarnings("unchecked") private Utf8ReaderCodec newUtf8Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Utf8ReaderCodec selfReader) { if (owner.unwrappedInfo() != null) { - return newUnwrappedUtf8Reader(owner, generatedClass, readTable); + return newUnwrappedUtf8Reader(owner, generatedClass, readTable, capabilities, selfReader); } JsonFieldInfo[] fields = owner.readFields(); JsonCreatorInfo creator = owner.creatorInfo(); @@ -1171,7 +951,9 @@ private Utf8ReaderCodec newUtf8Reader( Utf8ReaderCodec[] codecs = (Utf8ReaderCodec[]) new Utf8ReaderCodec[creatorFields.length]; for (int i = 0; i < creatorFields.length; i++) { - codecs[i] = creatorFields[i].typeInfo().utf8Reader(); + codecs[i] = + resolvedCapability( + creatorFields[i].typeInfo(), capabilities, CapabilityKind.UTF8_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1180,16 +962,12 @@ private Utf8ReaderCodec newUtf8Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf8ReaderCodec anyCodec = any.valueTypeInfo().utf8Reader(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf8ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_READER); return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } Utf8ReaderCodec[] codecs = (Utf8ReaderCodec[]) new Utf8ReaderCodec[fields.length]; @@ -1197,11 +975,10 @@ private Utf8ReaderCodec newUtf8Reader( JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); if (JsonCodegen.usesReadCodec(field, this)) { - codecs[i] = typeInfo.utf8Reader(); + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_READER); } else if (JsonCodegen.readNestedType(field, this) != null && field.readRawType() != owner.type()) { - Utf8ReaderCodec codec = typeInfo.utf8Reader(); - codecs[i] = codec instanceof ObjectCodec ? utf8Reader((ObjectCodec) codec) : codec; + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_READER); } } AnyInfo any = owner.anyInfo(); @@ -1211,34 +988,28 @@ private Utf8ReaderCodec newUtf8Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf8ReaderCodec anyCodec = any.valueTypeInfo().utf8Reader(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf8ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_READER); return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") private Latin1ReaderCodec newUnwrappedLatin1Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Latin1ReaderCodec selfReader) { JsonFieldInfo[] fields = unwrappedReadFields(owner); JsonTypeInfo[] children = unwrappedReadTypeInfos(owner); Latin1ReaderCodec[] codecs = (Latin1ReaderCodec[]) new Latin1ReaderCodec[children.length]; for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; - Latin1ReaderCodec codec = child.latin1Reader(); - if (canonicalObjectCodec(child) != null - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = latin1Reader((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.LATIN1_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1247,34 +1018,28 @@ private Latin1ReaderCodec newUnwrappedLatin1Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs); - } - Latin1ReaderCodec anyCodec = any.valueTypeInfo().latin1Reader(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = latin1Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Latin1ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.LATIN1_READER); return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") private Utf16ReaderCodec newUnwrappedUtf16Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Utf16ReaderCodec selfReader) { JsonFieldInfo[] fields = unwrappedReadFields(owner); JsonTypeInfo[] children = unwrappedReadTypeInfos(owner); Utf16ReaderCodec[] codecs = (Utf16ReaderCodec[]) new Utf16ReaderCodec[children.length]; for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; - Utf16ReaderCodec codec = child.utf16Reader(); - if (canonicalObjectCodec(child) != null - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = utf16Reader((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.UTF16_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1283,34 +1048,28 @@ private Utf16ReaderCodec newUnwrappedUtf16Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf16ReaderCodec anyCodec = any.valueTypeInfo().utf16Reader(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf16Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf16ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF16_READER); return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") private Utf8ReaderCodec newUnwrappedUtf8Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Utf8ReaderCodec selfReader) { JsonFieldInfo[] fields = unwrappedReadFields(owner); JsonTypeInfo[] children = unwrappedReadTypeInfos(owner); Utf8ReaderCodec[] codecs = (Utf8ReaderCodec[]) new Utf8ReaderCodec[children.length]; for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; - Utf8ReaderCodec codec = child.utf8Reader(); - if (canonicalObjectCodec(child) != null - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = utf8Reader((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.UTF8_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1319,16 +1078,12 @@ private Utf8ReaderCodec newUnwrappedUtf8Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf8ReaderCodec anyCodec = any.valueTypeInfo().utf8Reader(); - if (canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf8ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_READER); return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } private static JsonFieldInfo[] unwrappedReadFields(ObjectCodec owner) { @@ -1367,325 +1122,530 @@ private static JsonTypeInfo[] unwrappedReadTypeInfos(ObjectCodec owner) { return children; } - private Latin1ReaderCodec newInlineLatin1Reader( - ObjectCodec owner, - Class generatedClass, - JsonFieldTable readTable, - Latin1ReaderCodec ordinaryReader) { - Latin1ReaderCodec codec = newLatin1Reader(owner, generatedClass, readTable); - setInlineSelfReader(owner, codec, ordinaryReader); - Field[] childFields = readerChildFields(codec, owner); - registerLatin1ReaderCallbacks(codec, owner, childFields); - registerLatin1AnyReaderCallback(codec, owner); - return codec; + private boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { + return canonicalObjectCodec(any.valueTypeInfo()) == null || any.valueRawType() != owner.type(); } - private Utf16ReaderCodec newInlineUtf16Reader( - ObjectCodec owner, - Class generatedClass, - JsonFieldTable readTable, - Utf16ReaderCodec ordinaryReader) { - Utf16ReaderCodec codec = newUtf16Reader(owner, generatedClass, readTable); - setInlineSelfReader(owner, codec, ordinaryReader); - Field[] childFields = readerChildFields(codec, owner); - registerUtf16ReaderCallbacks(codec, owner, childFields); - registerUtf16AnyReaderCallback(codec, owner); - return codec; + private enum CapabilityKind { + STRING_WRITER, + UTF8_WRITER, + LATIN1_READER, + UTF16_READER, + UTF8_READER } - private Utf8ReaderCodec newInlineUtf8Reader( - ObjectCodec owner, - Class generatedClass, - JsonFieldTable readTable, - Utf8ReaderCodec ordinaryReader) { - Utf8ReaderCodec codec = newUtf8Reader(owner, generatedClass, readTable); - setInlineSelfReader(owner, codec, ordinaryReader); - Field[] childFields = readerChildFields(codec, owner); - registerUtf8ReaderCallbacks(codec, owner, childFields); - registerUtf8AnyReaderCallback(codec, owner); - return codec; + private ArrayList capabilityChildren(ObjectCodec owner, CapabilityKind kind) { + ArrayList children = new ArrayList<>(); + AnyInfo any = owner.anyInfo(); + boolean writer = kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER; + if (writer) { + JsonFieldInfo[] fields = + owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); + for (int i = 0; i < fields.length; i++) { + JsonFieldInfo field = fields[i]; + if (JsonCodegen.usesWriteCodec(field) + && (field.writeRawType() != owner.type() + || canonicalObjectOwner(field.writeTypeInfo()) == null)) { + children.add(field.writeTypeInfo()); + } + } + if (any != null + && (any.writeField() != null || any.writeGetter() != null) + && storesAnyCodec(owner, any)) { + children.add(any.valueTypeInfo()); + } + return children; + } + if (owner.unwrappedInfo() != null) { + JsonCreatorInfo creator = owner.creatorInfo(); + if (creator == null) { + JsonFieldInfo[] fields = owner.readFields(); + for (int i = 0; i < fields.length; i++) { + addReadDependency(children, owner, fields[i]); + } + } else { + JsonCreatorFieldInfo[] fields = creator.fields(); + for (int i = 0; i < fields.length; i++) { + children.add(fields[i].typeInfo()); + } + } + JsonUnwrappedInfo.ReadRoute[] routes = owner.unwrappedInfo().readRoutes(); + for (int i = 0; i < routes.length; i++) { + JsonUnwrappedInfo.ReadRoute route = routes[i]; + if (route.field() == null) { + children.add(route.creatorField().typeInfo()); + } else { + addReadDependency(children, owner, route.field()); + } + } + } else if (owner.creatorInfo() == null) { + JsonFieldInfo[] fields = owner.readFields(); + for (int i = 0; i < fields.length; i++) { + addReadDependency(children, owner, fields[i]); + } + } else { + JsonCreatorFieldInfo[] fields = owner.creatorInfo().fields(); + for (int i = 0; i < fields.length; i++) { + children.add(fields[i].typeInfo()); + } + } + if (any != null + && (any.readField() != null || any.readSetter() != null) + && storesAnyCodec(owner, any)) { + children.add(any.valueTypeInfo()); + } + return children; } - private void setInlineSelfReader(ObjectCodec owner, T inlineReader, T ordinaryReader) { - if (JsonCodegen.storesSelfReader(owner, this)) { - Field field = ReflectionUtils.getField(inlineReader.getClass(), "selfReader"); - ReflectionUtils.setObjectFieldValue(inlineReader, field, ordinaryReader); + private void addReadDependency( + ArrayList children, ObjectCodec owner, JsonFieldInfo field) { + if (JsonCodegen.usesReadCodec(field, this) + || JsonCodegen.readNestedType(field, this) != null && field.readRawType() != owner.type()) { + children.add(field.readTypeInfo()); } } - private boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { - return canonicalObjectCodec(any.valueTypeInfo()) == null || any.valueRawType() != owner.type(); + /** Returns whether a generated writer must traverse this cyclic edge through its type slot. */ + @Internal + public boolean usesWriterSlot(Class ownerType, JsonTypeInfo child) { + jitContext.lock(); + try { + JsonTypeInfo owner = rawObjectTypeInfos.get(ownerType); + return owner != null + && child != owner + && canonicalObjectOwner(child) != null + && reachesWriter(child, owner, new IdentityMap<>()); + } finally { + jitContext.unlock(); + } } - private Field[] writerChildFields(Object parent, ObjectCodec owner) { - Field[] childFields = null; - JsonFieldInfo[] fields = - owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); - for (int i = 0; i < fields.length; i++) { - Class nestedType = JsonCodegen.writeNestedType(fields[i], this); - JsonTypeInfo child = fields[i].writeTypeInfo(); - if (nestedType != null - && nestedType != owner.type() - && rawObjectTypeInfos.get(child.rawType()) == child) { - if (childFields == null) { - childFields = new Field[fields.length]; - } - childFields[i] = ReflectionUtils.getField(parent.getClass(), "w" + i); - } + /** Returns whether a generated reader must traverse this cyclic edge through its type slot. */ + @Internal + public boolean usesReaderSlot(Class ownerType, JsonTypeInfo child) { + jitContext.lock(); + try { + JsonTypeInfo ownerInfo = rawObjectTypeInfos.get(ownerType); + ObjectCodec owner = ownerInfo == null ? null : canonicalObjectOwner(ownerInfo); + return owner != null + && owner.unwrappedInfo() == null + && child != ownerInfo + && canonicalObjectOwner(child) != null + && reachesReader(child, ownerInfo, new IdentityMap<>()); + } finally { + jitContext.unlock(); } - return childFields; } - private Field[] readerChildFields(Object parent, ObjectCodec owner) { - if (owner.unwrappedInfo() != null) { - return unwrappedReaderChildFields(parent, owner); + private boolean reachesWriter( + JsonTypeInfo current, JsonTypeInfo target, IdentityMap visited) { + if (current == target) { + return true; } - Field[] childFields = null; - JsonCreatorInfo creator = owner.creatorInfo(); - if (creator != null) { - JsonCreatorFieldInfo[] fields = creator.fields(); - for (int i = 0; i < fields.length; i++) { - JsonTypeInfo child = fields[i].typeInfo(); - if (canonicalObjectCodec(child) != null - && child.rawType() != owner.type() - && rawObjectTypeInfos.get(child.rawType()) == child) { - if (childFields == null) { - childFields = new Field[fields.length]; - } - childFields[i] = ReflectionUtils.getField(parent.getClass(), "r" + i); - } - } - return childFields; + if (visited.put(current, Boolean.TRUE) != null) { + return false; } - JsonFieldInfo[] fields = owner.readFields(); - for (int i = 0; i < fields.length; i++) { - Class nestedType = JsonCodegen.readNestedType(fields[i], this); - JsonTypeInfo child = fields[i].readTypeInfo(); - if (nestedType != null - && nestedType != owner.type() - && rawObjectTypeInfos.get(child.rawType()) == child) { - if (childFields == null) { - childFields = new Field[fields.length]; + ObjectCodec owner = canonicalObjectOwner(current); + if (owner != null) { + ArrayList children = capabilityChildren(owner, CapabilityKind.STRING_WRITER); + for (int i = 0; i < children.size(); i++) { + JsonTypeInfo child = children.get(i); + if (child == target + || canonicalObjectOwner(child) != null && reachesWriter(child, target, visited)) { + return true; + } + Object capability = child.stringWriter(); + if (capability instanceof ClosedSubtypeCodec + && reachesWriter((ClosedSubtypeCodec) capability, target, visited)) { + return true; } - childFields[i] = ReflectionUtils.getField(parent.getClass(), "o" + i); } } - return childFields; + return false; } - private Field[] unwrappedReaderChildFields(Object parent, ObjectCodec owner) { - JsonTypeInfo[] children = unwrappedReadTypeInfos(owner); - JsonCreatorInfo creator = owner.creatorInfo(); - int directCount = creator == null ? owner.readFields().length : creator.fields().length; - JsonUnwrappedInfo.ReadRoute[] routes = owner.unwrappedInfo().readRoutes(); - Field[] childFields = null; - for (int i = 0; i < children.length; i++) { - JsonTypeInfo child = children[i]; - if (canonicalObjectCodec(child) == null - || child.rawType() == owner.type() - || rawObjectTypeInfos.get(child.rawType()) != child) { - continue; + private boolean reachesWriter( + ClosedSubtypeCodec subtype, JsonTypeInfo target, IdentityMap visited) { + for (int i = 0; i < subtype.childCount(); i++) { + JsonTypeInfo child = subtype.child(i); + if (child == target || reachesWriter(child, target, visited)) { + return true; } - String fieldName; - if (i < directCount) { - if (creator == null && JsonCodegen.readNestedType(owner.readFields()[i], this) == null) { - continue; + } + return false; + } + + private boolean reachesReader( + JsonTypeInfo current, JsonTypeInfo target, IdentityMap visited) { + if (current == target) { + return true; + } + if (visited.put(current, Boolean.TRUE) != null) { + return false; + } + ObjectCodec owner = canonicalObjectOwner(current); + if (owner != null && owner.unwrappedInfo() == null) { + ArrayList children = capabilityChildren(owner, CapabilityKind.UTF8_READER); + for (int i = 0; i < children.size(); i++) { + JsonTypeInfo child = children.get(i); + if (child == target + || canonicalObjectOwner(child) != null && reachesReader(child, target, visited)) { + return true; } - fieldName = creator == null ? "o" + i : "r" + i; - } else { - JsonUnwrappedInfo.ReadRoute route = routes[i - directCount]; - if (route.field() != null && JsonCodegen.readNestedType(route.field(), this) == null) { - continue; + Object capability = child.utf8Reader(); + if (capability instanceof ClosedSubtypeCodec + && reachesReader((ClosedSubtypeCodec) capability, target, visited)) { + return true; } - fieldName = (route.field() == null ? "r" : "o") + i; } - if (childFields == null) { - childFields = new Field[children.length]; + } + return false; + } + + private boolean reachesReader( + ClosedSubtypeCodec subtype, JsonTypeInfo target, IdentityMap visited) { + for (int i = 0; i < subtype.childCount(); i++) { + JsonTypeInfo child = subtype.child(i); + if (child == target || reachesReader(child, target, visited)) { + return true; } - childFields[i] = ReflectionUtils.getField(parent.getClass(), fieldName); } - return childFields; + return false; } - private void requestUtf8Reader( - ObjectCodec owner, JsonTypeInfo typeInfo, Utf8ReaderGraph graph) { - jitContext.registerJITFuture( - () -> null, - () -> utf8ReaderInstall(owner, typeInfo, graph), - new JsonJITContext.JITCallback() { - @Override - public void onSuccess(Utf8ReaderInstall result) { - try { - result.publish(); - } finally { - if (graph != null) { - graph.clearPending(); - } - } - } + private boolean canCompile(ObjectCodec owner, CapabilityKind kind) { + return kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER + ? codegen.canCompileWriter(owner) + : codegen.canCompileReader(owner); + } - @Override - public void onFailure(Throwable failure) { - if (graph != null) { - graph.clearPending(); - } - } + private static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) { + switch (kind) { + case STRING_WRITER: + return typeInfo.stringWriter(); + case UTF8_WRITER: + return typeInfo.utf8Writer(); + case LATIN1_READER: + return typeInfo.latin1Reader(); + case UTF16_READER: + return typeInfo.utf16Reader(); + case UTF8_READER: + return typeInfo.utf8Reader(); + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); + } + } - @Override - public Object id() { - return graph == null ? codegen.utf8ReaderJITId(owner.type()) : graph.requestId; - } - }); + private CompletableFuture> generatedClass(CapabilityNode node, CapabilityKind kind) { + if (node.subtypeOwner != null) { + throw new IllegalStateException("Inline subtype readers reuse child generated classes"); + } + if (node.collectionOwner != null) { + return sharedRegistry.utf8CollectionReaderClass(node.typeInfo.type(), node.collectionOwner); + } + switch (kind) { + case STRING_WRITER: + return sharedRegistry.stringWriterClass(node.objectOwner, this); + case UTF8_WRITER: + return sharedRegistry.utf8WriterClass(node.objectOwner, this); + case LATIN1_READER: + return sharedRegistry.latin1ReaderClass(node.objectOwner, this); + case UTF16_READER: + return sharedRegistry.utf16ReaderClass(node.objectOwner, this); + case UTF8_READER: + return sharedRegistry.utf8ReaderClass(node.objectOwner, this); + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); + } } - private Object utf8ReaderRequestId(JsonTypeInfo typeInfo) { - ObjectCodec owner = canonicalObjectOwner(typeInfo); - if (owner != null) { - Utf8ReaderGraph graph = pendingUtf8Readers.get(erase(owner)); - if (graph != null) { - return graph.requestId; - } + private Object newCapability( + CapabilityNode node, + Class generatedClass, + IdentityMap capabilities, + CapabilityKind kind) { + if (node.subtypeOwner != null) { + return newSubtypeReaders(node.subtypeOwner, capabilities, kind); + } + if (node.collectionOwner != null) { + JsonTypeInfo element = declaredCollectionElement(node.typeInfo); + Utf8ReaderCodec elementReader = resolvedCapability(element, capabilities, kind); + return GeneratedCodecInstantiator.instantiateUtf8CollectionReader( + generatedClass, elementReader); + } + switch (kind) { + case STRING_WRITER: + return newStringWriter(node.objectOwner, generatedClass, capabilities); + case UTF8_WRITER: + return newUtf8Writer(node.objectOwner, generatedClass, capabilities); + case LATIN1_READER: + return newLatin1Reader(node.objectOwner, generatedClass, capabilities); + case UTF16_READER: + return newUtf16Reader(node.objectOwner, generatedClass, capabilities); + case UTF8_READER: + return newUtf8Reader(node.objectOwner, generatedClass, capabilities); + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); } - return codegen.utf8ReaderJITId(typeInfo.rawType()); } - private CompletableFuture utf8ReaderInstall( - ObjectCodec owner, JsonTypeInfo typeInfo, Utf8ReaderGraph graph) { - if (graph != null) { - return graph.classesReady().thenApply(ignored -> new Utf8ReaderInstall(graph)); + @SuppressWarnings("unchecked") + private static T resolvedCapability( + JsonTypeInfo typeInfo, IdentityMap capabilities, CapabilityKind kind) { + Object capability = capabilities.get(typeInfo); + if (capability != null) { + return (T) capability; + } + return (T) currentCapability(typeInfo, kind); + } + + private static void installCapability( + JsonTypeInfo typeInfo, Object capability, CapabilityKind kind) { + switch (kind) { + case STRING_WRITER: + typeInfo.setStringWriter((StringWriterCodec) capability); + return; + case UTF8_WRITER: + typeInfo.setUtf8Writer((Utf8WriterCodec) capability); + return; + case LATIN1_READER: + typeInfo.setLatin1Reader((Latin1ReaderCodec) capability); + return; + case UTF16_READER: + typeInfo.setUtf16Reader((Utf16ReaderCodec) capability); + return; + case UTF8_READER: + typeInfo.setUtf8Reader((Utf8ReaderCodec) capability); + return; + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); } - return sharedRegistry - .utf8ReaderClass(owner, this, false) - .thenApply(generated -> new Utf8ReaderInstall(owner, typeInfo, generated)); } @SuppressWarnings("unchecked") - private Utf8ReaderCodec newGraphUtf8Reader( - ObjectCodec owner, - Class generatedClass, - IdentityMap> capabilities) { - JsonFieldInfo[] fields = owner.readFields(); - Utf8ReaderCodec[] codecs = - (Utf8ReaderCodec[]) new Utf8ReaderCodec[fields.length]; - for (int i = 0; i < fields.length; i++) { - JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesReadCodec(field, this) - || JsonCodegen.readNestedType(field, this) != null) { - JsonTypeInfo child = field.readTypeInfo(); - Utf8ReaderCodec capability = capabilities.get(child); - codecs[i] = capability == null ? child.utf8Reader() : capability; - } + private Object newSubtypeReaders( + ClosedSubtypeCodec subtype, + IdentityMap capabilities, + CapabilityKind kind) { + int childCount = subtype.childCount(); + switch (kind) { + case LATIN1_READER: + Latin1ReaderCodec[] latin1Readers = + (Latin1ReaderCodec[]) new Latin1ReaderCodec[childCount]; + for (int i = 0; i < childCount; i++) { + JsonFieldTable table = subtype.inlineReadTable(i); + if (table != null) { + JsonTypeInfo child = subtype.child(i); + ObjectCodec owner = erase(requireObjectOwner(child)); + Latin1ReaderCodec canonical = resolvedCapability(child, capabilities, kind); + latin1Readers[i] = + newLatin1Reader(owner, canonical.getClass(), table, capabilities, canonical); + } + } + return latin1Readers; + case UTF16_READER: + Utf16ReaderCodec[] utf16Readers = + (Utf16ReaderCodec[]) new Utf16ReaderCodec[childCount]; + for (int i = 0; i < childCount; i++) { + JsonFieldTable table = subtype.inlineReadTable(i); + if (table != null) { + JsonTypeInfo child = subtype.child(i); + ObjectCodec owner = erase(requireObjectOwner(child)); + Utf16ReaderCodec canonical = resolvedCapability(child, capabilities, kind); + utf16Readers[i] = + newUtf16Reader(owner, canonical.getClass(), table, capabilities, canonical); + } + } + return utf16Readers; + case UTF8_READER: + Utf8ReaderCodec[] utf8Readers = + (Utf8ReaderCodec[]) new Utf8ReaderCodec[childCount]; + for (int i = 0; i < childCount; i++) { + JsonFieldTable table = subtype.inlineReadTable(i); + if (table != null) { + JsonTypeInfo child = subtype.child(i); + ObjectCodec owner = erase(requireObjectOwner(child)); + Utf8ReaderCodec canonical = resolvedCapability(child, capabilities, kind); + utf8Readers[i] = + newUtf8Reader(owner, canonical.getClass(), table, capabilities, canonical); + } + } + return utf8Readers; + default: + throw new IllegalStateException("Writer graph cannot construct inline subtype readers"); } - return GeneratedCodecInstantiator.instantiateUtf8Reader(generatedClass, owner, fields, codecs); } - private final class Utf8ReaderInstall { - private final Utf8ReaderGraph graph; - private final ObjectCodec owner; - private final JsonTypeInfo typeInfo; - private final Class generatedClass; + private ObjectCodec requireObjectOwner(JsonTypeInfo typeInfo) { + ObjectCodec owner = canonicalObjectOwner(typeInfo); + if (owner == null) { + throw new IllegalStateException( + "Inline subtype lost its canonical object owner: " + typeInfo.rawType().getName()); + } + return owner; + } - private Utf8ReaderInstall(Utf8ReaderGraph graph) { - this.graph = graph; - owner = null; - typeInfo = null; - generatedClass = null; + private static boolean readerKind(CapabilityKind kind) { + return kind == CapabilityKind.LATIN1_READER + || kind == CapabilityKind.UTF16_READER + || kind == CapabilityKind.UTF8_READER; + } + + private static boolean hasInlineReadTable(ClosedSubtypeCodec subtype) { + for (int i = 0; i < subtype.childCount(); i++) { + if (subtype.inlineReadTable(i) != null) { + return true; + } } + return false; + } - private Utf8ReaderInstall( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generatedClass) { - graph = null; - this.owner = owner; - this.typeInfo = typeInfo; - this.generatedClass = generatedClass; + private static Object currentSubtypeReaders(ClosedSubtypeCodec subtype, CapabilityKind kind) { + switch (kind) { + case LATIN1_READER: + return subtype.inlineLatin1Readers(); + case UTF16_READER: + return subtype.inlineUtf16Readers(); + case UTF8_READER: + return subtype.inlineUtf8Readers(); + default: + throw new IllegalStateException("Writer graph has no inline subtype readers"); } + } - private void publish() { - if (graph == null) { - publishUtf8Reader(owner, typeInfo, generatedClass); - } else { - graph.publish(); + @SuppressWarnings("unchecked") + private static void installSubtypeReaders( + ClosedSubtypeCodec subtype, Object readers, CapabilityKind kind) { + switch (kind) { + case LATIN1_READER: + subtype.installInlineLatin1Readers((Latin1ReaderCodec[]) readers); + return; + case UTF16_READER: + subtype.installInlineUtf16Readers((Utf16ReaderCodec[]) readers); + return; + case UTF8_READER: + subtype.installInlineUtf8Readers((Utf8ReaderCodec[]) readers); + return; + default: + throw new IllegalStateException("Writer graph has no inline subtype readers"); + } + } + + private void requestCapabilities(ArrayList roots) { + for (CapabilityKind kind : CapabilityKind.values()) { + CapabilityGraph graph = new CapabilityGraph(kind); + if (graph.addRoots(roots) && !graph.ordered.isEmpty()) { + requestGraph(graph); } } } + private void requestGraph(CapabilityGraph graph) { + jitContext.registerJITFuture( + () -> graph.classesReady().thenApply(ignored -> graph), + new JsonJITContext.JITCallback() { + @Override + public void onSuccess(CapabilityGraph result) { + result.publish(); + } + + @Override + public void onFailure(Throwable failure) {} + + @Override + public Object id() { + return graph; + } + }); + } + /** - * One acyclic UTF-8 graph. Class futures have no dependency edges; this resolver applies - * dependency order only after every class is ready, then publishes the complete local graph in - * one lock-held loop. + * One representation graph whose constructor dependencies are acyclic after canonical + * multi-object cycles become slot edges. Every class future is submitted before dependency order + * is applied to resolver-local instance construction and one lock-held publication loop. */ - private final class Utf8ReaderGraph { - private final Object requestId; - private final IdentityMap nodes = new IdentityMap<>(); - private final ArrayList ordered = new ArrayList<>(); + private final class CapabilityGraph { + private final CapabilityKind kind; + private final IdentityMap nodes = new IdentityMap<>(); + private final IdentityMap subtypes = new IdentityMap<>(); + private final ArrayList ordered = new ArrayList<>(); - private Utf8ReaderGraph(Object requestId) { - this.requestId = requestId; + private CapabilityGraph(CapabilityKind kind) { + this.kind = kind; } - private void markPending() { - for (int i = 0; i < ordered.size(); i++) { - Utf8ReaderNode node = ordered.get(i); - ObjectCodec owner = node.objectOwner; - if (owner != null - && node.typeInfo.utf8Reader() == node.initial - && pendingUtf8Readers.get(owner) == null) { - pendingUtf8Readers.put(owner, this); + private boolean addRoots(ArrayList roots) { + for (int i = 0; i < roots.size(); i++) { + if (!addDependency(roots.get(i))) { + return false; } } + return true; } - private void clearPending() { - for (int i = 0; i < ordered.size(); i++) { - ObjectCodec owner = ordered.get(i).objectOwner; - if (owner != null && pendingUtf8Readers.get(owner) == this) { - pendingUtf8Readers.remove(owner); + private boolean addDependency(JsonTypeInfo typeInfo) { + return addDependency(typeInfo, false); + } + + private boolean addDependency(JsonTypeInfo typeInfo, boolean slotEdge) { + ObjectCodec objectOwner = canonicalObjectOwner(typeInfo); + if (objectOwner != null) { + return addObject(objectOwner, typeInfo, slotEdge); + } + if (kind == CapabilityKind.UTF8_READER) { + CollectionCodec collectionOwner = exactUtf8CollectionOwner(typeInfo); + if (collectionOwner != null) { + return addCollection(collectionOwner, typeInfo); + } + } + Object capability = currentCapability(typeInfo, kind); + return !(capability instanceof ClosedSubtypeCodec) + || addSubtype(typeInfo, (ClosedSubtypeCodec) capability); + } + + private boolean addSubtype(JsonTypeInfo typeInfo, ClosedSubtypeCodec subtype) { + Boolean complete = subtypes.get(subtype); + if (complete != null) { + return complete; + } + subtypes.put(subtype, Boolean.FALSE); + for (int i = 0; i < subtype.childCount(); i++) { + if (!addDependency(subtype.child(i))) { + return false; + } + } + subtypes.put(subtype, Boolean.TRUE); + if (readerKind(kind) && hasInlineReadTable(subtype)) { + Object initial = currentSubtypeReaders(subtype, kind); + if (initial == null) { + ordered.add(new CapabilityNode(typeInfo, subtype, initial)); } } + return true; } - private boolean addObject(ObjectCodec rawOwner, JsonTypeInfo typeInfo) { + private boolean addObject(ObjectCodec rawOwner, JsonTypeInfo typeInfo, boolean slotEdge) { ObjectCodec owner = erase(rawOwner); - Utf8ReaderNode existing = nodes.get(typeInfo); + Object initial = currentCapability(typeInfo, kind); + if (initial != owner) { + return true; + } + CapabilityNode existing = nodes.get(typeInfo); if (existing != null) { - return existing.complete; + return existing.complete || slotEdge; } - AnyInfo any = owner.anyInfo(); - if (owner.creatorInfo() != null - || owner.unwrappedInfo() != null - || any != null && (any.readField() != null || any.readSetter() != null) - || !codegen.canCompileReader(owner)) { + if (!canCompile(owner, kind)) { return false; } - Utf8ReaderNode node = new Utf8ReaderNode(typeInfo, owner); + CapabilityNode node = new CapabilityNode(typeInfo, owner, initial); nodes.put(typeInfo, node); - JsonFieldInfo[] fields = owner.readFields(); - for (int i = 0; i < fields.length; i++) { - JsonFieldInfo field = fields[i]; - JsonTypeInfo child = field.readTypeInfo(); - ObjectCodec childOwner = canonicalObjectOwner(child); - if (childOwner != null) { - // Eligibility and generated source depend only on the complete declared graph. An - // already-published child may be reused during construction, but must never hide schema - // edges here or make parent source depend on publication order. - if (!addObject(childOwner, child)) { - return false; - } - continue; - } - CollectionCodec collection = exactUtf8CollectionOwner(child); - if (collection != null) { - if (!addCollection(collection, child)) { - return false; - } - continue; - } - if (collectionCodecs.get(child) != null - || field.readKind() == JsonFieldKind.ARRAY - || field.readKind() == JsonFieldKind.MAP - || field.readKind() == JsonFieldKind.OBJECT - && (field.readRawType() == Object.class - || child.utf8Reader() instanceof ClosedSubtypeCodec)) { + ArrayList children = capabilityChildren(owner, kind); + for (int i = 0; i < children.size(); i++) { + JsonTypeInfo child = children.get(i); + boolean writer = kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER; + boolean childSlot = + writer ? usesWriterSlot(owner.type(), child) : usesReaderSlot(owner.type(), child); + if (!addDependency(child, childSlot)) { return false; } } @@ -1695,15 +1655,26 @@ private boolean addObject(ObjectCodec rawOwner, JsonTypeInfo typeInfo) { } private boolean addCollection(CollectionCodec owner, JsonTypeInfo typeInfo) { - Utf8ReaderNode existing = nodes.get(typeInfo); + Object initial = currentCapability(typeInfo, kind); + if (initial != owner) { + return true; + } + CapabilityNode existing = nodes.get(typeInfo); if (existing != null) { return existing.complete; } - Utf8ReaderNode node = new Utf8ReaderNode(typeInfo, owner); - nodes.put(typeInfo, node); JsonTypeInfo element = declaredCollectionElement(typeInfo); - ObjectCodec elementOwner = canonicalObjectOwner(element); - if (elementOwner != null && !addObject(elementOwner, element)) { + if (element == null + || element.rawType() == Object.class + || element.usesAnnotationCodec() + || element.kind() == JsonFieldKind.OBJECT + && canonicalObjectOwner(element) == null + && !(element.utf8Reader() instanceof ClosedSubtypeCodec)) { + return false; + } + CapabilityNode node = new CapabilityNode(typeInfo, owner, initial); + nodes.put(typeInfo, node); + if (!addDependency(element)) { return false; } node.complete = true; @@ -1712,458 +1683,120 @@ private boolean addCollection(CollectionCodec owner, JsonTypeInfo typeInfo) { } private CompletableFuture classesReady() { - CompletableFuture[] futures = new CompletableFuture[ordered.size()]; + ArrayList> futures = new ArrayList<>(); for (int i = 0; i < ordered.size(); i++) { - Utf8ReaderNode node = ordered.get(i); - node.classFuture = - node.objectOwner == null - ? sharedRegistry.utf8CollectionReaderClass( - node.typeInfo.type(), node.collectionOwner) - : sharedRegistry.utf8ReaderClass(node.objectOwner, JsonTypeResolver.this, true); - futures[i] = node.classFuture; + CapabilityNode node = ordered.get(i); + if (node.subtypeOwner != null) { + continue; + } + node.classFuture = generatedClass(node, kind); + futures.add(node.classFuture); } - return CompletableFuture.allOf(futures); + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); } private void publish() { requireJITLock(); - IdentityMap> capabilities = new IdentityMap<>(); - ArrayList unpublished = new ArrayList<>(); + IdentityMap capabilities = new IdentityMap<>(); + ArrayList unpublished = new ArrayList<>(); for (int i = 0; i < ordered.size(); i++) { - Utf8ReaderNode node = ordered.get(i); - if (!node.metadataMatches()) { + CapabilityNode node = ordered.get(i); + if (!node.metadataMatches(kind)) { return; } - Utf8ReaderCodec current = node.typeInfo.utf8Reader(); + Object current = node.current(kind); if (current != node.initial) { - capabilities.put(node.typeInfo, current); + if (node.subtypeOwner == null) { + capabilities.put(node.typeInfo, current); + } continue; } - Class generatedClass = node.classFuture.getNow(null); - if (generatedClass == null) { - throw new IllegalStateException("Generated UTF-8 class is not ready"); - } - Utf8ReaderCodec capability; - if (node.objectOwner != null) { - capability = newGraphUtf8Reader(node.objectOwner, generatedClass, capabilities); - } else { - JsonTypeInfo element = declaredCollectionElement(node.typeInfo); - Utf8ReaderCodec elementReader = capabilities.get(element); - if (elementReader == null) { - elementReader = element.utf8Reader(); + Class generatedClass = null; + if (node.subtypeOwner == null) { + generatedClass = node.classFuture.getNow(null); + if (generatedClass == null) { + throw new IllegalStateException("Generated JSON class is not ready"); } - capability = - GeneratedCodecInstantiator.instantiateUtf8CollectionReader( - generatedClass, elementReader); } - node.instance = capability; - capabilities.put(node.typeInfo, capability); + node.instance = newCapability(node, generatedClass, capabilities, kind); + if (node.subtypeOwner == null) { + capabilities.put(node.typeInfo, node.instance); + } unpublished.add(node); } for (int i = 0; i < unpublished.size(); i++) { - Utf8ReaderNode node = unpublished.get(i); - if (!node.metadataMatches() || node.typeInfo.utf8Reader() != node.initial) { + CapabilityNode node = unpublished.get(i); + if (!node.metadataMatches(kind) || node.current(kind) != node.initial) { return; } } for (int i = 0; i < unpublished.size(); i++) { - Utf8ReaderNode node = unpublished.get(i); - node.typeInfo.setUtf8Reader(node.instance); + CapabilityNode node = unpublished.get(i); + node.install(kind); } } } - private final class Utf8ReaderNode { + private final class CapabilityNode { private final JsonTypeInfo typeInfo; private final ObjectCodec objectOwner; private final CollectionCodec collectionOwner; - private final Utf8ReaderCodec initial; + private final ClosedSubtypeCodec subtypeOwner; + private final Object initial; private boolean complete; private CompletableFuture> classFuture; - private Utf8ReaderCodec instance; + private Object instance; - private Utf8ReaderNode(JsonTypeInfo typeInfo, ObjectCodec owner) { + private CapabilityNode(JsonTypeInfo typeInfo, ObjectCodec owner, Object initial) { this.typeInfo = typeInfo; objectOwner = owner; collectionOwner = null; - initial = owner; + subtypeOwner = null; + this.initial = initial; } - @SuppressWarnings("unchecked") - private Utf8ReaderNode(JsonTypeInfo typeInfo, CollectionCodec owner) { + private CapabilityNode(JsonTypeInfo typeInfo, CollectionCodec owner, Object initial) { this.typeInfo = typeInfo; objectOwner = null; collectionOwner = owner; - initial = (Utf8ReaderCodec) (Utf8ReaderCodec) owner; + subtypeOwner = null; + this.initial = initial; + } + + private CapabilityNode(JsonTypeInfo typeInfo, ClosedSubtypeCodec owner, Object initial) { + this.typeInfo = typeInfo; + objectOwner = null; + collectionOwner = null; + subtypeOwner = owner; + this.initial = initial; } - private boolean metadataMatches() { + private boolean metadataMatches(CapabilityKind kind) { + if (subtypeOwner != null) { + return currentCapability(typeInfo, kind) == subtypeOwner; + } if (objectOwner != null) { return canonicalObjectOwner(typeInfo) == objectOwner; } return collectionCodecs.get(typeInfo) == collectionOwner && typeInfos.get(typeInfoKey(typeInfo.type(), typeInfo.rawType())) == typeInfo; } - } - - // Publication runs under the local JIT lock: construct the resolver-local instance, resolve - // every replaceable child Field, register child notifications, then write the canonical - // JsonTypeInfo slot captured when the compilation request is created. Capturing that exact slot - // is required because a failed closed-subtype transaction can remove its provisional metadata - // and later build a new canonical slot for the same class before an old async task completes. - // The old task may refine only its now-unreachable slot; a type lookup here would let it corrupt - // the replacement generation. Construction and field lookup are the fallible phase. Publication - // is deterministic ordinary field assignment and is never modeled as a transaction or rolled - // back; a failure there is a generated-code invariant violation. - private void publishStringWriter( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - StringWriterCodec codec = newStringWriter(owner, generated); - Field[] childFields = writerChildFields(codec, owner); - registerStringWriterCallbacks(codec, owner, childFields); - registerStringAnyWriterCallback(codec, owner); - typeInfo.setStringWriter(codec); - } - - private void publishUtf8Writer( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - Utf8WriterCodec codec = newUtf8Writer(owner, generated); - Field[] childFields = writerChildFields(codec, owner); - registerUtf8WriterCallbacks(codec, owner, childFields); - registerUtf8AnyWriterCallback(codec, owner); - typeInfo.setUtf8Writer(codec); - } - - private void publishLatin1Reader( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - Latin1ReaderCodec codec = newLatin1Reader(owner, generated); - Field[] childFields = readerChildFields(codec, owner); - registerLatin1ReaderCallbacks(codec, owner, childFields); - registerLatin1AnyReaderCallback(codec, owner); - typeInfo.setLatin1Reader(codec); - } - - private void publishUtf16Reader( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - Utf16ReaderCodec codec = newUtf16Reader(owner, generated); - Field[] childFields = readerChildFields(codec, owner); - registerUtf16ReaderCallbacks(codec, owner, childFields); - registerUtf16AnyReaderCallback(codec, owner); - typeInfo.setUtf16Reader(codec); - } - - private void publishUtf8Reader( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - Utf8ReaderCodec codec = newUtf8Reader(owner, generated); - Field[] childFields = readerChildFields(codec, owner); - registerUtf8ReaderCallbacks(codec, owner, childFields); - registerUtf8AnyReaderCallback(codec, owner); - typeInfo.setUtf8Reader(codec); - } - - // A generated parent captures the current child slot during construction. If a child task is - // active, notification updates only the matching concrete field after the child slot is - // published. If no task is active, onNotifyMissed installs the already-current slot immediately. - // The callback list is notification state, not a resolver dependency graph or task-dedup map. - private void registerStringWriterCallbacks( - StringWriterCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; - } - JsonFieldInfo[] properties = - owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = properties[i].writeTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.stringWriterJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - StringWriterCodec codec = child.stringWriter(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.stringWriter()); - } - }); - } - } - } - private void registerUtf8WriterCallbacks( - Utf8WriterCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; + private Object current(CapabilityKind kind) { + return subtypeOwner == null + ? currentCapability(typeInfo, kind) + : currentSubtypeReaders(subtypeOwner, kind); } - JsonFieldInfo[] properties = - owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = properties[i].writeTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.utf8WriterJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf8WriterCodec codec = child.utf8Writer(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf8Writer()); - } - }); - } - } - } - private void registerLatin1ReaderCallbacks( - Latin1ReaderCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; - } - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = readerChildTypeInfo(owner, i); - jitContext.registerJITNotifyCallback( - codegen.latin1ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Latin1ReaderCodec codec = child.latin1Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.latin1Reader()); - } - }); - } - } - } - - private void registerUtf16ReaderCallbacks( - Utf16ReaderCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; - } - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = readerChildTypeInfo(owner, i); - jitContext.registerJITNotifyCallback( - codegen.utf16ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf16ReaderCodec codec = child.utf16Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf16Reader()); - } - }); - } - } - } - - private void registerUtf8ReaderCallbacks( - Utf8ReaderCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; - } - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = readerChildTypeInfo(owner, i); - jitContext.registerJITNotifyCallback( - utf8ReaderRequestId(child), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf8ReaderCodec codec = child.utf8Reader(); - if (!(codec instanceof ObjectCodec)) { - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf8Reader()); - } - }); + private void install(CapabilityKind kind) { + if (subtypeOwner == null) { + installCapability(typeInfo, instance, kind); + } else { + installSubtypeReaders(subtypeOwner, instance, kind); } } } - private void registerStringAnyWriterCallback( - StringWriterCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyWriteChild(owner, any)) { - return; - } - Field field = ReflectionUtils.getField(parent.getClass(), "anyWriter"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.stringWriterJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - StringWriterCodec codec = child.stringWriter(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.stringWriter()); - } - }); - } - - private void registerUtf8AnyWriterCallback(Utf8WriterCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyWriteChild(owner, any)) { - return; - } - Field field = ReflectionUtils.getField(parent.getClass(), "anyWriter"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.utf8WriterJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf8WriterCodec codec = child.utf8Writer(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf8Writer()); - } - }); - } - - private void registerLatin1AnyReaderCallback( - Latin1ReaderCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyReadChild(owner, any)) { - return; - } - Field field = ReflectionUtils.getField(parent.getClass(), "anyReader"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.latin1ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Latin1ReaderCodec codec = child.latin1Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.latin1Reader()); - } - }); - } - - private void registerUtf16AnyReaderCallback( - Utf16ReaderCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyReadChild(owner, any)) { - return; - } - Field field = ReflectionUtils.getField(parent.getClass(), "anyReader"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.utf16ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf16ReaderCodec codec = child.utf16Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf16Reader()); - } - }); - } - - private void registerUtf8AnyReaderCallback(Utf8ReaderCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyReadChild(owner, any)) { - return; - } - Field field = ReflectionUtils.getField(parent.getClass(), "anyReader"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - utf8ReaderRequestId(child), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf8ReaderCodec codec = child.utf8Reader(); - if (!(codec instanceof ObjectCodec)) { - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf8Reader()); - } - }); - } - - private boolean hasGeneratedAnyWriteChild(ObjectCodec owner, AnyInfo any) { - return any != null - && (any.writeField() != null || any.writeGetter() != null) - && canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && rawObjectTypeInfos.get(any.valueRawType()) == any.valueTypeInfo(); - } - - private boolean hasGeneratedAnyReadChild(ObjectCodec owner, AnyInfo any) { - return any != null - && (any.readField() != null || any.readSetter() != null) - && canonicalObjectCodec(any.valueTypeInfo()) != null - && any.valueRawType() != owner.type() - && rawObjectTypeInfos.get(any.valueRawType()) == any.valueTypeInfo(); - } - - private static JsonTypeInfo readerChildTypeInfo(ObjectCodec owner, int index) { - if (owner.unwrappedInfo() != null) { - return unwrappedReadTypeInfos(owner)[index]; - } - JsonCreatorInfo creator = owner.creatorInfo(); - return creator == null - ? owner.readFields()[index].readTypeInfo() - : creator.fields()[index].typeInfo(); - } - @SuppressWarnings("unchecked") private ObjectCodec buildObjectCodec(TypeRef ownerType, Object key) { ObjectCodec cached = objectCodecs.get(key); @@ -2269,13 +1902,6 @@ private void registerTypeInfoOwner(JsonTypeInfo typeInfo, JsonValueCodec init } } - private static void checkGeneratedClass(Object result, Object codec) { - if (codec.getClass() != result) { - throw new IllegalStateException( - "Generated JSON callback does not match installed capability"); - } - } - private static Object typeInfoKey(Type declaredType, Class rawType) { return declaredType instanceof Class ? rawType : declaredType; } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java index 243395131d..9b470dec35 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -78,7 +79,6 @@ import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.reflect.TypeRef; -import org.apache.fory.serializer.StringSerializer; import org.testng.annotations.Test; public class JsonAsyncCompilationTest { @@ -90,48 +90,34 @@ public void defaultBuilderEnablesAsync() throws Exception { } @Test - public void capabilitiesCompileLazily() throws Exception { + public void capabilitiesRegisterAfterResolution() throws Exception { ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); JsonTypeResolver resolver = primaryTypeResolver(json); ObjectCodec owner = resolver.getObjectCodec(AsyncChild.class); JsonTypeInfo info = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); - assertCapabilities(info, owner); + assertNotSame(info.stringWriter(), owner); + assertNotSame(info.utf8Writer(), owner); + assertNotSame(info.latin1Reader(), owner); + assertNotSame(info.utf16Reader(), owner); + assertNotSame(info.utf8Reader(), owner); AsyncChild value = child("root", 1); assertEquals(json.toJson(value), "{\"id\":1,\"name\":\"root\"}"); - assertNotSame(info.stringWriter(), owner); - assertSame(info.utf8Writer(), owner); - assertSame(info.latin1Reader(), owner); - assertSame(info.utf16Reader(), owner); - assertSame(info.utf8Reader(), owner); - assertEquals( new String(json.toJsonBytes(value), StandardCharsets.UTF_8), "{\"id\":1,\"name\":\"root\"}"); - assertNotSame(info.utf8Writer(), owner); assertEquals(json.fromJson("{\"id\":2,\"name\":\"latin\"}", AsyncChild.class).id, 2); - if (StringSerializer.isBytesBackedString()) { - assertNotSame(info.latin1Reader(), owner); - assertSame(info.utf16Reader(), owner); - } else { - assertSame(info.latin1Reader(), owner); - assertNotSame(info.utf16Reader(), owner); - resolver.latin1Reader(owner); - assertNotSame(info.latin1Reader(), owner); - } assertEquals(json.fromJson("{\"id\":3,\"name\":\"\u4f60\"}", AsyncChild.class).id, 3); - assertNotSame(info.utf16Reader(), owner); assertEquals( json.fromJson( "{\"id\":4,\"name\":\"utf8\"}".getBytes(StandardCharsets.UTF_8), AsyncChild.class) .id, 4); - assertNotSame(info.utf8Reader(), owner); assertSame(resolver.getObjectCodec(AsyncChild.class), owner); } @Test - public void inlineAnyReadersInstall() throws Exception { + public void closedSubtypeBranchesInstall() throws Exception { ControlledJson controlled = controlledJson(); ForyJson json = controlled.json; AsyncInlineChild initial = @@ -140,12 +126,17 @@ public void inlineAnyReadersInstall() throws Exception { controlled.executor.runAll(); JsonTypeResolver resolver = primaryTypeResolver(json); - ClosedSubtypeCodec codec = - (ClosedSubtypeCodec) - resolver.getTypeInfo(AsyncInlineShape.class, AsyncInlineShape.class).latin1Reader(); - assertNotNull(((Object[]) field(codec, "inlineLatin1Readers"))[0]); - assertNotNull(((Object[]) field(codec, "inlineUtf16Readers"))[0]); - assertNotNull(((Object[]) field(codec, "inlineUtf8Readers"))[0]); + JsonTypeInfo shapeInfo = resolver.getTypeInfo(AsyncInlineShape.class, AsyncInlineShape.class); + assertTrue(shapeInfo.latin1Reader() instanceof ClosedSubtypeCodec); + ClosedSubtypeCodec subtype = (ClosedSubtypeCodec) shapeInfo.latin1Reader(); + JsonTypeInfo childInfo = resolver.getTypeInfo(AsyncInlineChild.class, AsyncInlineChild.class); + ObjectCodec childOwner = resolver.getObjectCodec(AsyncInlineChild.class); + assertNotSame(childInfo.latin1Reader(), childOwner); + assertNotSame(childInfo.utf16Reader(), childOwner); + assertNotSame(childInfo.utf8Reader(), childOwner); + assertInlineReader(subtype.inlineLatin1Readers(), childInfo.latin1Reader()); + assertInlineReader(subtype.inlineUtf16Readers(), childInfo.utf16Reader()); + assertInlineReader(subtype.inlineUtf8Readers(), childInfo.utf8Reader()); AsyncInlineChild latin1 = (AsyncInlineChild) json.fromJson("{\"x\":2,\"kind\":\"child\"}", AsyncInlineShape.class); @@ -179,17 +170,6 @@ public void creatorCapabilitiesInstall() throws Exception { 4); JsonTypeResolver resolver = primaryTypeResolver(json); ObjectCodec owner = resolver.getObjectCodec(AsyncCreator.class); - if (!StringSerializer.isBytesBackedString()) { - // Java 8 strings are char-backed, so public String reads select the UTF-16 reader and cannot - // request Latin-1 compilation. Explicitly request that cold capability to keep this test's - // coverage of every asynchronously installed codec independent of the JDK string layout. - resolver.lockJIT(); - try { - assertSame(resolver.latin1Reader(owner), owner); - } finally { - resolver.unlockJIT(); - } - } controlled.executor.runAll(); JsonTypeInfo info = resolver.getTypeInfo(AsyncCreator.class, AsyncCreator.class); assertNotSame(info.stringWriter(), owner); @@ -224,7 +204,7 @@ public void asyncInstancesAreResolverLocal() throws Exception { } finally { second.unlockJIT(); } - assertEquals(controlled.executor.pendingTasks(), 2); + assertEquals(controlled.executor.pendingTasks(), 5); controlled.executor.runAll(); StringWriterCodec firstWriter = stringWriter(first, firstOwner); @@ -304,7 +284,7 @@ public void asyncCompletionPublishesAllPaths() throws Exception { } @Test - public void duplicateCallbacksPublish() throws Exception { + public void accessorsDoNotReregisterCapabilities() throws Exception { ControlledJson controlled = controlledJson(); JsonTypeResolver resolver = new JsonTypeResolver(controlled.registry); resolver.lockJIT(); @@ -318,8 +298,13 @@ public void duplicateCallbacksPublish() throws Exception { resolver.unlockJIT(); } - assertEquals(controlled.executor.submittedTasks(), 2); - assertEquals(controlled.executor.pendingTasks(), 2); + assertEquals(controlled.executor.submittedTasks(), 5); + assertEquals(controlled.executor.pendingTasks(), 5); + for (int i = 0; i < 100; i++) { + assertSame(stringWriter(resolver, owner), owner); + } + assertEquals(controlled.executor.submittedTasks(), 5); + assertEquals(controlled.executor.pendingTasks(), 5); controlled.executor.runAll(); assertNotSame(stringWriter(resolver, owner), owner); } @@ -335,22 +320,23 @@ public void rolledBackReaderTasksStayIsolated() throws Exception { expectThrows( ForyJsonException.class, () -> resolver.getTypeInfo(BrokenShape.class, BrokenShape.class)); - assertEquals(controlled.executor.pendingTasks(), 3); + assertEquals(controlled.executor.pendingTasks(), 0); replacementOwner = resolver.getObjectCodec(RollbackCircle.class); replacement = resolver.getTypeInfo(RollbackCircle.class, RollbackCircle.class); assertSame(replacement.latin1Reader(), replacementOwner); assertSame(replacement.utf16Reader(), replacementOwner); assertSame(replacement.utf8Reader(), replacementOwner); + assertEquals(controlled.executor.pendingTasks(), 5); } finally { resolver.unlockJIT(); } - // The failed subtype transaction queued parent-local Any readers against its provisional - // slots. Completing those tasks must not mutate the replacement generation for the same class. + // A failed metadata transaction cannot register a capability graph. The later independent + // binding registers exactly its own five representations and cannot be mutated by stale work. controlled.executor.runAll(); - assertSame(replacement.latin1Reader(), replacementOwner); - assertSame(replacement.utf16Reader(), replacementOwner); - assertSame(replacement.utf8Reader(), replacementOwner); + assertNotSame(replacement.latin1Reader(), replacementOwner); + assertNotSame(replacement.utf16Reader(), replacementOwner); + assertNotSame(replacement.utf8Reader(), replacementOwner); } @Test @@ -374,33 +360,34 @@ public void rollbackClearsCanonicalOwners() throws Exception { @Test public void asyncFailureKeepsInterpretedResult() { - ControlledExecutor executor = new ControlledExecutor(); - JsonJITContext context = new JsonJITContext(true, executor); + JsonJITContext context = new JsonJITContext(true); + CompletableFuture future = new CompletableFuture<>(); AtomicReference failure = new AtomicReference<>(); - int result = - context.registerJITCallback( - () -> 1, - () -> { - throw new IllegalStateException("compile failure"); - }, - new JsonJITContext.JITCallback() { - @Override - public void onSuccess(Integer generated) { - fail("Unexpected generated result"); - } + JsonJITContext.JITCallback callback = + new JsonJITContext.JITCallback() { + @Override + public void onSuccess(Integer generated) { + fail("Unexpected generated result"); + } - @Override - public void onFailure(Throwable throwable) { - failure.set(throwable); - } + @Override + public void onFailure(Throwable throwable) { + failure.set(throwable); + } - @Override - public Object id() { - return "failure"; - } - }); - assertEquals(result, 1); - executor.runNext(); + @Override + public Object id() { + return "failure"; + } + }; + context.registerJITFuture(() -> future, callback); + context.registerJITFuture( + () -> { + fail("Duplicate active request"); + return CompletableFuture.completedFuture(2); + }, + callback); + future.completeExceptionally(new IllegalStateException("compile failure")); assertTrue(failure.get() instanceof IllegalStateException); } @@ -411,17 +398,6 @@ public void rootAndCompletionUseLocalLock() throws Exception { CodecRegistry codecs = new CodecRegistry(); codecs.register(BlockingValue.class, new BlockingCodec(rootEntered, releaseRoot)); ControlledJson controlled = controlledJson(codecs); - JsonTypeResolver compilerResolver = new JsonTypeResolver(controlled.registry); - compilerResolver.lockJIT(); - try { - ObjectCodec owner = compilerResolver.getObjectCodec(AsyncChild.class); - compilerResolver.getTypeInfo(AsyncChild.class, AsyncChild.class); - assertSame(compilerResolver.stringWriter(owner), owner); - } finally { - compilerResolver.unlockJIT(); - } - controlled.executor.runNext(); - JsonTypeResolver resolver = primaryTypeResolver(controlled.json); resolver.lockJIT(); ObjectCodec owner; @@ -465,6 +441,7 @@ public void rootAndCompletionUseLocalLock() throws Exception { installer.start(); await(installStarted); assertSame(info.stringWriter(), owner); + assertFalse(installFinished.await(100, TimeUnit.MILLISECONDS)); releaseRoot.countDown(); root.join(); await(installFinished); @@ -578,32 +555,24 @@ public void pooledStatesRemainConcurrent() throws Exception { second.start(); await(secondFinished); assertFailure(secondFailure.get()); - assertEquals(controlled.executor.pendingTasks(), 1); + assertEquals(controlled.executor.pendingTasks(), 5); releaseRoot.countDown(); first.join(); assertFailure(firstFailure.get()); - controlled.executor.runNext(); + controlled.executor.runAll(); } @Test public void capabilityFailureIsIndependent() throws Exception { ControlledJson controlled = controlledJson(); - JsonTypeResolver resolver = primaryTypeResolver(controlled.json); - resolver.lockJIT(); - ObjectCodec owner; - JsonTypeInfo info; - try { - owner = resolver.getObjectCodec(AsyncChild.class); - info = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); - } finally { - resolver.unlockJIT(); - } controlled.executor.rejectNext(); - expectThrows(RejectedExecutionException.class, () -> controlled.json.toJson(child("x", 1))); + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + assertEquals(controlled.json.toJson(child("x", 1)), "{\"id\":1,\"name\":\"x\"}"); + ObjectCodec owner = resolver.getObjectCodec(AsyncChild.class); + JsonTypeInfo info = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); assertSame(info.stringWriter(), owner); - - controlled.json.toJsonBytes(child("x", 1)); - controlled.executor.runNext(); + assertEquals(controlled.executor.pendingTasks(), 4); + controlled.executor.runAll(); assertNotSame(info.utf8Writer(), owner); assertSame(info.stringWriter(), owner); } @@ -711,8 +680,8 @@ public void utf8GraphPublishesAtomically() throws Exception { Object initialChildren = childrenInfo.utf8Reader(); Object initialFriends = friendsInfo.utf8Reader(); - assertEquals(controlled.executor.pendingTasks(), 5); - for (int i = 0; i < 4; i++) { + assertEquals(controlled.executor.pendingTasks(), 17); + for (int i = 0; i < 16; i++) { controlled.executor.runNext(); assertSame(rootInfo.utf8Reader(), rootOwner); assertSame(childInfo.utf8Reader(), childOwner); @@ -747,7 +716,7 @@ public void utf8GraphPublishesAtomically() throws Exception { } @Test - public void utf8GraphNotifiesFallbackParent() throws Exception { + public void utf8ParentsCaptureFinalChild() throws Exception { ControlledJson controlled = controlledJson(); byte[] creatorInput = "{\"child\":{\"id\":1,\"name\":\"creator\"}}".getBytes(StandardCharsets.UTF_8); @@ -759,7 +728,7 @@ public void utf8GraphNotifiesFallbackParent() throws Exception { ObjectCodec childOwner = resolver.getObjectCodec(AsyncChild.class); JsonTypeInfo creatorInfo = resolver.getTypeInfo(AsyncCreatorParent.class, AsyncCreatorParent.class); - resolver.getTypeInfo(AsyncGraphParent.class, AsyncGraphParent.class); + JsonTypeInfo graphInfo = resolver.getTypeInfo(AsyncGraphParent.class, AsyncGraphParent.class); JsonTypeInfo childInfo = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); resolver.lockJIT(); try { @@ -769,17 +738,15 @@ public void utf8GraphNotifiesFallbackParent() throws Exception { resolver.unlockJIT(); } - assertEquals(controlled.executor.pendingTasks(), 3); - controlled.executor.runNext(); + assertEquals(controlled.executor.pendingTasks(), 15); + controlled.executor.runAll(); assertNotSame(creatorInfo.utf8Reader(), creatorOwner); - assertCapabilityFields(creatorInfo.utf8Reader(), Utf8ReaderCodec.class, childOwner, 1); - controlled.executor.runNext(); - assertSame(childInfo.utf8Reader(), childOwner); - controlled.executor.runNext(); - + assertNotSame(graphInfo.utf8Reader(), graphOwner); assertNotSame(childInfo.utf8Reader(), childOwner); assertCapabilityFields( creatorInfo.utf8Reader(), Utf8ReaderCodec.class, childInfo.utf8Reader(), 1); + assertCapabilityFields( + graphInfo.utf8Reader(), Utf8ReaderCodec.class, childInfo.utf8Reader(), 1); assertEquals( controlled.json.fromJson(creatorInput, AsyncCreatorParent.class).child.name, "creator"); } @@ -813,10 +780,12 @@ public void utf8ScalarCollectionCapability() throws Exception { JsonTypeInfo valuesInfo = rootOwner.readFields()[0].readTypeInfo(); Object initialValues = valuesInfo.utf8Reader(); - assertEquals(controlled.executor.pendingTasks(), 2); - controlled.executor.runNext(); - assertSame(rootInfo.utf8Reader(), rootOwner); - assertSame(valuesInfo.utf8Reader(), initialValues); + assertEquals(controlled.executor.pendingTasks(), 6); + for (int i = 0; i < 5; i++) { + controlled.executor.runNext(); + assertSame(rootInfo.utf8Reader(), rootOwner); + assertSame(valuesInfo.utf8Reader(), initialValues); + } controlled.executor.runNext(); assertNotSame(rootInfo.utf8Reader(), rootOwner); @@ -921,7 +890,7 @@ public void selfRecursiveWriterUsesThis() throws Exception { assertSame(recursiveField.writeTypeInfo(), typeInfo); assertSame(recursiveField.readTypeInfo(), typeInfo); StringWriterCodec writer = resolver.stringWriter(owner); - assertTrue(writer != owner); + assertNotSame(writer, owner); for (Field field : writer.getClass().getDeclaredFields()) { assertFalse(field.getType() == StringWriterCodec.class, field.toString()); } @@ -968,7 +937,7 @@ public void nestedPublication() throws Exception { } @Test - public void mutualPublication() throws Exception { + public void mutualTypesUseCanonicalSlots() throws Exception { ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); JsonTypeResolver resolver = primaryTypeResolver(json); ObjectCodec firstOwner = resolver.getObjectCodec(MutualFirst.class); @@ -976,37 +945,16 @@ public void mutualPublication() throws Exception { JsonTypeInfo firstInfo = resolver.getTypeInfo(MutualFirst.class, MutualFirst.class); JsonTypeInfo secondInfo = resolver.getTypeInfo(MutualSecond.class, MutualSecond.class); - Object first = resolver.stringWriter(firstOwner); - Object second = secondInfo.stringWriter(); - assertSame(resolver.stringWriter(secondOwner), second); - assertSame(secondInfo.stringWriter(), second); - assertMutualFields( - firstInfo.stringWriter(), secondInfo.stringWriter(), StringWriterCodec.class); - - first = resolver.utf8Writer(firstOwner); - second = secondInfo.utf8Writer(); - assertSame(resolver.utf8Writer(secondOwner), second); - assertSame(secondInfo.utf8Writer(), second); - assertMutualFields(firstInfo.utf8Writer(), secondInfo.utf8Writer(), Utf8WriterCodec.class); - - first = resolver.latin1Reader(firstOwner); - second = secondInfo.latin1Reader(); - assertSame(resolver.latin1Reader(secondOwner), second); - assertSame(secondInfo.latin1Reader(), second); - assertMutualFields( - firstInfo.latin1Reader(), secondInfo.latin1Reader(), Latin1ReaderCodec.class); - - first = resolver.utf16Reader(firstOwner); - second = secondInfo.utf16Reader(); - assertSame(resolver.utf16Reader(secondOwner), second); - assertSame(secondInfo.utf16Reader(), second); - assertMutualFields(firstInfo.utf16Reader(), secondInfo.utf16Reader(), Utf16ReaderCodec.class); - - first = resolver.utf8Reader(firstOwner); - second = secondInfo.utf8Reader(); - assertSame(resolver.utf8Reader(secondOwner), second); - assertSame(secondInfo.utf8Reader(), second); - assertMutualFields(firstInfo.utf8Reader(), secondInfo.utf8Reader(), Utf8ReaderCodec.class); + assertCyclicSlot(firstInfo.stringWriter(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.stringWriter(), secondOwner, firstInfo); + assertCyclicSlot(firstInfo.utf8Writer(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.utf8Writer(), secondOwner, firstInfo); + assertCyclicSlot(firstInfo.latin1Reader(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.latin1Reader(), secondOwner, firstInfo); + assertCyclicSlot(firstInfo.utf16Reader(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.utf16Reader(), secondOwner, firstInfo); + assertCyclicSlot(firstInfo.utf8Reader(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.utf8Reader(), secondOwner, firstInfo); } private static void assertPublishedChild( @@ -1021,12 +969,6 @@ private static void assertPublishedChild( assertCapabilityFields(parent, fieldType, child, expectedFields); } - private static void assertMutualFields(Object first, Object second, Class fieldType) - throws Exception { - assertCapabilityFields(first, fieldType, second, 1); - assertCapabilityFields(second, fieldType, first, 1); - } - private static void assertCapabilityFields( Object owner, Class fieldType, Object expected, int expectedFields) throws Exception { int count = 0; @@ -1041,6 +983,22 @@ private static void assertCapabilityFields( assertEquals(count, expectedFields, owner.getClass().getName()); } + private static void assertCyclicSlot(Object capability, Object owner, JsonTypeInfo target) + throws Exception { + assertNotSame(capability, owner); + int count = 0; + for (Field field : capability.getClass().getDeclaredFields()) { + if (field.getType() == JsonTypeInfo.class) { + field.setAccessible(true); + if (field.get(capability) == target) { + assertTrue(Modifier.isFinal(field.getModifiers())); + count++; + } + } + } + assertEquals(count, 1, capability.getClass().getName()); + } + private static void assertCapabilities(JsonTypeInfo info, Object expected) { assertSame(info.stringWriter(), expected); assertSame(info.utf8Writer(), expected); @@ -1079,6 +1037,14 @@ private static void assertFinalElementReader(Object collection, Object element) assertSame(field.get(collection), element); } + private static void assertInlineReader(Object[] readers, Object canonical) { + assertNotNull(readers); + assertEquals(readers.length, 1); + assertNotNull(readers[0]); + assertNotSame(readers[0], canonical); + assertSame(readers[0].getClass(), canonical.getClass()); + } + private static void assertFinalCollectionFields(Object root, Object... collections) throws Exception { int count = 0; From 1a9dc3345e3aa41c9c5cf05730bb97b8ff5b2998 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 10:15:40 +0800 Subject: [PATCH 08/20] perf(json): generate UTF-8 string collection readers Generate exact declared String collection readers that fuse separator transitions with bounded UTF-8 string probes while preserving collection ownership and generic fallback behavior. --- .../apache/fory/json/codegen/JsonCodegen.java | 4 +- .../codegen/Utf8CollectionReaderCodegen.java | 304 +++++++++++++++++- .../fory/json/codegen/Utf8ReaderCodegen.java | 109 +++++++ .../fory/json/reader/Utf8JsonReader.java | 100 ++++++ .../fory/json/JsonAsyncCompilationTest.java | 44 ++- 5 files changed, 553 insertions(+), 8 deletions(-) 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 7cf1936515..e5967b0807 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 @@ -155,7 +155,9 @@ public Class compileUtf8CollectionReader(Type declaredType, CollectionCodec elementType = CodecUtils.rawType(CodecUtils.elementType(declaredType), Object.class); String generatedPackage = CodeGenerator.getPackage(elementType); String className = className(elementType, simpleClassName(rawType) + "Utf8CollectionReader"); - String code = new Utf8CollectionReaderCodegen().genCode(generatedPackage, className); + boolean stringElements = owner instanceof CollectionCodec.StringCollectionCodec; + String code = + new Utf8CollectionReaderCodegen().genCode(generatedPackage, className, stringElements); return compileCodecClass(generatedPackage, className, code); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java index 603dd4807c..b61b1eb26b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java @@ -20,13 +20,22 @@ package org.apache.fory.json.codegen; import java.util.ArrayList; +import org.apache.fory.codegen.Code; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.json.codec.Utf8ReaderCodec; import org.apache.fory.json.reader.Utf8JsonReader; /** Generates one exact declared ArrayList-backed UTF-8 collection capability. */ final class Utf8CollectionReaderCodegen { - String genCode(String generatedPackage, String className) { + // The ninth value remains scalar until the following separator proves whether the list ends or + // continues. Exact size nine therefore allocates nine slots, while a continuation starts at the + // same capacity 13 that ArrayList growth would have selected, without creating and copying a + // discarded nine-slot array. + private static final int ARRAY_LIST_PREFIX_SIZE = 9; + private static final int ARRAY_LIST_FIRST_GROWTH = + ARRAY_LIST_PREFIX_SIZE + (ARRAY_LIST_PREFIX_SIZE >> 1); + + String genCode(String generatedPackage, String className, boolean stringElements) { CodegenContext ctx = new CodegenContext(); ctx.setPackage(generatedPackage); ctx.setClassName(className); @@ -36,14 +45,303 @@ String genCode(String generatedPackage, String className) { ctx.addField(true, ctx.type(Utf8ReaderCodec.class), "elementReader", null); ctx.addConstructor( "this.elementReader = elementReader;", Utf8ReaderCodec.class, "elementReader"); + if (stringElements) { + addStringArrayListMethods(ctx); + } else { + ctx.addMethod( + "@Override public final", + "readUtf8", + readBody(), + Object.class, + Utf8JsonReader.class, + "reader"); + } + return ctx.genCode(); + } + + private static void addStringArrayListMethods(CodegenContext ctx) { ctx.addMethod( "@Override public final", "readUtf8", - readBody(), + readStringArrayListCode(ctx), Object.class, Utf8JsonReader.class, "reader"); - return ctx.genCode(); + ctx.addMethod( + "private final", + "readArrayListBody", + readStringArrayListBodyCode(ctx), + ArrayList.class, + Utf8JsonReader.class, + "reader", + String.class, + "e0", + String.class, + "e1"); + ctx.addMethod( + "private final", + "readArrayListTail", + readStringArrayListTailCode(ctx), + ArrayList.class, + Utf8JsonReader.class, + "reader", + String.class, + "e0", + String.class, + "e1", + String.class, + "e2", + String.class, + "e3"); + ctx.addMethod( + "private final", + "readArrayListLongTail", + readStringArrayListLongTailCode(ctx), + ArrayList.class, + Utf8JsonReader.class, + "reader", + String.class, + "e0", + String.class, + "e1", + String.class, + "e2", + String.class, + "e3", + String.class, + "e4", + String.class, + "e5"); + ctx.addMethod( + "private final", + "readArrayListLoop", + readStringArrayListLoopCode(ctx), + ArrayList.class, + Utf8JsonReader.class, + "reader", + String.class, + "e0", + String.class, + "e1", + String.class, + "e2", + String.class, + "e3", + String.class, + "e4", + String.class, + "e5", + String.class, + "e6", + String.class, + "e7"); + } + + private static String readStringArrayListCode(CodegenContext ctx) { + return "if (reader.tryReadNullToken()) {\n" + + " return null;\n" + + "}\n" + + "reader.enterDepth();\n" + + "reader.expectNextToken('[');\n" + + "if (reader.consumeNextToken(']')) {\n" + + " reader.exitDepth();\n" + + " return new ArrayList(0);\n" + + "}\n" + + readStringElement(ctx, "e0", "E0") + + "int nextElement = reader.consumeNextStringArrayElement();\n" + + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n" + + " reader.exitDepth();\n" + + " ArrayList list = new ArrayList(1);\n" + + " list.add(e0);\n" + + " return list;\n" + + "}\n" + + readStringArrayElement(ctx, "e1", "E1") + + "return readArrayListBody(reader, e0, e1);"; + } + + private static String readStringArrayListBodyCode(CodegenContext ctx) { + // Each prefix method consumes its incoming separator state locally. This keeps cursor + // publication and the following quote probe in one generated compilation unit. + return "int nextElement = reader.consumeNextStringArrayElement();\n" + + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n" + + " reader.exitDepth();\n" + + " ArrayList list = new ArrayList(2);\n" + + " list.add(e0);\n" + + " list.add(e1);\n" + + " return list;\n" + + "}\n" + + readStringArrayElement(ctx, "e2", "B2") + + "nextElement = reader.consumeNextStringArrayElement();\n" + + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n" + + " reader.exitDepth();\n" + + " ArrayList list = new ArrayList(3);\n" + + " list.add(e0);\n" + + " list.add(e1);\n" + + " list.add(e2);\n" + + " return list;\n" + + "}\n" + + readStringArrayElement(ctx, "e3", "B3") + + "return readArrayListTail(reader, e0, e1, e2, e3);"; + } + + private static String readStringArrayListTailCode(CodegenContext ctx) { + return "int nextElement = reader.consumeNextStringArrayElement();\n" + + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n" + + " reader.exitDepth();\n" + + " ArrayList list = new ArrayList(4);\n" + + " list.add(e0);\n" + + " list.add(e1);\n" + + " list.add(e2);\n" + + " list.add(e3);\n" + + " return list;\n" + + "}\n" + + readStringArrayElement(ctx, "e4", "T4") + + "nextElement = reader.consumeNextStringArrayElement();\n" + + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n" + + " reader.exitDepth();\n" + + " ArrayList list = new ArrayList(5);\n" + + " list.add(e0);\n" + + " list.add(e1);\n" + + " list.add(e2);\n" + + " list.add(e3);\n" + + " list.add(e4);\n" + + " return list;\n" + + "}\n" + + readStringArrayElement(ctx, "e5", "T5") + + "return readArrayListLongTail(reader, e0, e1, e2, e3, e4, e5);"; + } + + private static String readStringArrayListLongTailCode(CodegenContext ctx) { + return "int nextElement = reader.consumeNextStringArrayElement();\n" + + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n" + + " reader.exitDepth();\n" + + " ArrayList list = new ArrayList(6);\n" + + " list.add(e0);\n" + + " list.add(e1);\n" + + " list.add(e2);\n" + + " list.add(e3);\n" + + " list.add(e4);\n" + + " list.add(e5);\n" + + " return list;\n" + + "}\n" + + readStringArrayElement(ctx, "e6", "L6") + + "nextElement = reader.consumeNextStringArrayElement();\n" + + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n" + + " reader.exitDepth();\n" + + " ArrayList list = new ArrayList(7);\n" + + " list.add(e0);\n" + + " list.add(e1);\n" + + " list.add(e2);\n" + + " list.add(e3);\n" + + " list.add(e4);\n" + + " list.add(e5);\n" + + " list.add(e6);\n" + + " return list;\n" + + "}\n" + + readStringArrayElement(ctx, "e7", "L7") + + "if (!reader.consumeNextCommaOrEndArray()) {\n" + + " reader.exitDepth();\n" + + " ArrayList list = new ArrayList(8);\n" + + " list.add(e0);\n" + + " list.add(e1);\n" + + " list.add(e2);\n" + + " list.add(e3);\n" + + " list.add(e4);\n" + + " list.add(e5);\n" + + " list.add(e6);\n" + + " list.add(e7);\n" + + " return list;\n" + + "}\n" + + "return readArrayListLoop(reader, e0, e1, e2, e3, e4, e5, e6, e7);"; + } + + private static String readStringArrayListLoopCode(CodegenContext ctx) { + return "String e8 = (String) elementReader.readUtf8(reader);\n" + + "int nextElement = reader.consumeNextStringArrayElement();\n" + + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n" + + " reader.exitDepth();\n" + + " ArrayList list = new ArrayList(" + + ARRAY_LIST_PREFIX_SIZE + + ");\n" + + " list.add(e0);\n" + + " list.add(e1);\n" + + " list.add(e2);\n" + + " list.add(e3);\n" + + " list.add(e4);\n" + + " list.add(e5);\n" + + " list.add(e6);\n" + + " list.add(e7);\n" + + " list.add(e8);\n" + + " return list;\n" + + "}\n" + + "ArrayList list = new ArrayList(" + + ARRAY_LIST_FIRST_GROWTH + + ");\n" + + "list.add(e0);\n" + + "list.add(e1);\n" + + "list.add(e2);\n" + + "list.add(e3);\n" + + "list.add(e4);\n" + + "list.add(e5);\n" + + "list.add(e6);\n" + + "list.add(e7);\n" + + "list.add(e8);\n" + + "do {\n" + + " String element;\n" + + " if (nextElement == Utf8JsonReader.STRING_ARRAY_QUOTED) {\n" + + readQuotedStringElement(ctx, "quotedElement", "LoopQuoted") + + " element = quotedElement;\n" + + " } else {\n" + + " element = (String) elementReader.readUtf8(reader);\n" + + " }\n" + + " list.add(element);\n" + + " nextElement = reader.consumeNextStringArrayElement();\n" + + "} while (nextElement != Utf8JsonReader.STRING_ARRAY_END);\n" + + "reader.exitDepth();\n" + + "return list;"; + } + + private static String readStringElement(CodegenContext ctx, String variable, String id) { + ctx.clearExprState(); + Code.ExprCode expression = Utf8ReaderCodegen.readStringElement(id).genCode(ctx); + String expressionCode = expression.code(); + return (expressionCode == null ? "" : expressionCode + "\n") + + "String " + + variable + + " = " + + expression.value() + + ";\n"; + } + + private static String readStringArrayElement(CodegenContext ctx, String variable, String id) { + String quoted = variable + "Quoted"; + return "String " + + variable + + ";\n" + + "if (nextElement == Utf8JsonReader.STRING_ARRAY_QUOTED) {\n" + + readQuotedStringElement(ctx, quoted, id) + + " " + + variable + + " = " + + quoted + + ";\n" + + "} else {\n" + + " " + + variable + + " = (String) elementReader.readUtf8(reader);\n" + + "}\n"; + } + + private static String readQuotedStringElement(CodegenContext ctx, String variable, String id) { + ctx.clearExprState(); + Code.ExprCode expression = Utf8ReaderCodegen.readQuotedStringElement(id).genCode(ctx); + String expressionCode = expression.code(); + return (expressionCode == null ? "" : expressionCode + "\n") + + "String " + + variable + + " = " + + expression.value() + + ";\n"; } private static String readBody() { 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 1767f005ed..6d0de5f4fb 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.codegen.ExpressionUtils; import org.apache.fory.json.codec.Utf8ReaderCodec; import org.apache.fory.json.meta.JsonAsciiToken; import org.apache.fory.json.meta.JsonFieldInfo; @@ -29,6 +30,11 @@ import org.apache.fory.reflect.TypeRef; final class Utf8ReaderCodegen extends JsonReaderCodegen { + // Generated String collections own three bounded word probes. Longer inputs continue in the + // reader-owned scanner, keeping escape, Unicode, malformed-input, and arbitrary-length work out + // of each generated prefix method. + private static final int DIRECT_STRING_WORDS = 3; + Utf8ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, boolean finalDependencies) { super(codegen, resolver, finalDependencies); } @@ -88,6 +94,109 @@ String readFieldMethod() { return "readUtf8"; } + static Expression readStringElement(String id) { + Reference reader = new Reference("reader", TypeRef.of(Utf8JsonReader.class)); + Expression.Variable value = + new Expression.Variable("stringElement" + id, TypeRef.of(String.class)); + Expression fallback = + new Expression.If( + new Expression.Invoke(reader, "tryReadNextNullToken", TypeRef.of(boolean.class)) + .inline(), + new Expression.Assign(value, ExpressionUtils.nullValue(String.class)), + new Expression.Assign( + value, + new Expression.Invoke( + reader, "readNullableStringToken", TypeRef.of(String.class), true) + .inline())); + return new Expression.ListExpression( + value, + new Expression.If( + tryConsumeStringQuote(reader), + new Expression.Assign(value, readStringToken(id, reader)), + fallback), + value); + } + + static Expression readQuotedStringElement(String id) { + Reference reader = new Reference("reader", TypeRef.of(Utf8JsonReader.class)); + return readStringToken(id, reader); + } + + private static Expression readStringToken(String id, Expression reader) { + Expression start = + new Expression.Variable( + "stringStart" + id, + new Expression.Invoke(reader, "position", TypeRef.of(int.class)).inline()); + Expression offset = new Expression.Variable("stringOffset" + id, start); + Expression inputLength = + new Expression.Variable( + "stringInputLength" + id, + new Expression.Invoke(reader, "inputLength", TypeRef.of(int.class)).inline()); + Expression directWordEnd = + new Expression.Variable( + "stringDirectWordEnd" + id, + new Expression.Subtract( + true, inputLength, Expression.Literal.ofInt(Long.BYTES * DIRECT_STRING_WORDS))); + Expression state = new Expression.Variable("stringState" + id, Expression.Literal.ofLong(0L)); + Expression value = new Expression.Variable("stringValue" + id, TypeRef.of(String.class)); + Expression zero = Expression.Literal.ofLong(0L); + Expression directScans = new Expression.Empty(); + for (int i = 0; i < DIRECT_STRING_WORDS; i++) { + directScans = + new Expression.ListExpression( + new Expression.Assign(state, scanStringWord(reader, offset)), + new Expression.If( + equal(state, zero), + new Expression.ListExpression(advanceStringOffset(offset), directScans))); + } + directScans = + new Expression.If( + new Expression.Comparator("<=", offset, directWordEnd, true), directScans); + Expression finish = + new Expression.If( + notEqual(state, zero), + new Expression.Assign(value, finishStringWord(reader, start, offset, state)), + new Expression.Assign(value, readStringLongTail(reader, start, offset))); + return new Expression.ListExpression( + start, offset, inputLength, directWordEnd, state, value, directScans, finish, value); + } + + private static Expression tryConsumeStringQuote(Expression reader) { + return new Expression.Invoke(reader, "tryConsumeStringQuote", TypeRef.of(boolean.class)) + .inline(); + } + + private static Expression readStringLongTail( + Expression reader, Expression start, Expression offset) { + return new Expression.Invoke( + reader, "readStringTokenLongTail", TypeRef.of(String.class), start, offset) + .inline(); + } + + private static Expression finishStringWord( + Expression reader, Expression start, Expression offset, Expression state) { + return new Expression.Invoke( + reader, "finishStringWord", TypeRef.of(String.class), start, offset, state) + .inline(); + } + + private static Expression advanceStringOffset(Expression offset) { + return new Expression.Assign( + offset, new Expression.Add(true, offset, Expression.Literal.ofInt(Long.BYTES))); + } + + private static Expression equal(Expression left, Expression right) { + return new Expression.Comparator("==", left, right, true); + } + + private static Expression notEqual(Expression left, Expression right) { + return new Expression.Comparator("!=", left, right, true); + } + + private static Expression scanStringWord(Expression reader, Expression offset) { + return new Expression.Invoke(reader, "scanStringWord", TypeRef.of(long.class), offset).inline(); + } + @Override boolean isDirectName(String name, boolean tokenValueRead) { return JsonAsciiToken.isLongPackable(fieldNameToken(name)); 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 db3aaaa63d..fba9bcf0e3 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 @@ -25,6 +25,7 @@ import java.time.ZoneOffset; import java.util.Arrays; import java.util.UUID; +import org.apache.fory.annotation.Internal; import org.apache.fory.json.JsonConfig; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; @@ -83,6 +84,15 @@ public final class Utf8JsonReader extends JsonReader { // Little-endian packed ASCII bytes for "null". private static final int NULL_LITERAL = 0x6C6C756E; + /** The generated String-array loop consumed the closing bracket. */ + @Internal public static final int STRING_ARRAY_END = 0; + + /** The generated String-array loop consumed both a comma and the next opening quote. */ + @Internal public static final int STRING_ARRAY_QUOTED = 1; + + /** The generated String-array loop consumed a comma and left the next value unread. */ + @Internal public static final int STRING_ARRAY_VALUE = 2; + // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. // UTF-8 string decoding must keep unsigned byte conversion for non-ASCII content. private byte[] input; @@ -417,6 +427,18 @@ public boolean consumeNextToken(char expected) { return consumeToken(expected); } + /** Consumes a string quote without classifying whitespace, null, or malformed input. */ + @Internal + public boolean tryConsumeStringQuote() { + byte[] bytes = input; + int offset = position; + if (offset < bytes.length && bytes[offset] == '"') { + position = offset + 1; + return true; + } + return false; + } + public void expectToken(char expected) { if (!consumeToken(expected)) { throw error("Expected '" + expected + "'"); @@ -483,6 +505,60 @@ public boolean consumeNextCommaOrEndArray() { return consumeNextCommaOrEndArraySlow(); } + /** + * Consumes an array separator and, when adjacent, the next String's opening quote. + * + *

The concrete reader owns syntax and cursor publication. Generated exact String collections + * own value decoding and can therefore continue directly from the returned state without a second + * token probe. + */ + @Internal + public int consumeNextStringArrayElement() { + byte[] bytes = input; + int offset = position; + if (offset < bytes.length) { + int ch = bytes[offset]; + if (ch == ']') { + position = offset + 1; + return STRING_ARRAY_END; + } + if (ch == ',') { + offset++; + if (offset < bytes.length && bytes[offset] == '"') { + position = offset + 1; + return STRING_ARRAY_QUOTED; + } + position = offset; + return STRING_ARRAY_VALUE; + } + } + return consumeNextStringArrayElementSlow(); + } + + private int consumeNextStringArrayElementSlow() { + skipWhitespaceFast(); + byte[] bytes = input; + int offset = position; + if (offset < bytes.length) { + int ch = bytes[offset]; + if (ch == ']') { + position = offset + 1; + return STRING_ARRAY_END; + } + if (ch == ',') { + position = offset + 1; + skipWhitespaceFast(); + offset = position; + if (offset < bytes.length && bytes[offset] == '"') { + position = offset + 1; + return STRING_ARRAY_QUOTED; + } + return STRING_ARRAY_VALUE; + } + } + throw error("Expected ',' or ']'"); + } + private boolean consumeNextArrayEndOrSlow(int ch) { if (ch == ']') { position++; @@ -2016,6 +2092,30 @@ private String readStringWordStop(int start, int offset, long stopMask) { return readStringStop(start, stop, b); } + /** Returns the current UTF-8 input length to generated bounded String probes. */ + @Internal + public int inputLength() { + return input.length; + } + + /** Scans one in-bounds generated String word without publishing the reader cursor. */ + @Internal + public long scanStringWord(int offset) { + return stringStopMask(LittleEndian.getInt64(input, offset)); + } + + /** Finishes a generated String after a bounded word probe finds its first stop byte. */ + @Internal + public String finishStringWord(int start, int offset, long stopMask) { + return readStringWordStop(start, offset, stopMask); + } + + /** Continues a String after generated bounded word probes found no stop byte. */ + @Internal + public String readStringTokenLongTail(int start, int offset) { + return readStringTokenLongTail(start, offset, input.length); + } + private String readStringTokenLongTail(int start, int offset, int inputLength) { byte[] bytes = input; int doubleWordEnd = inputLength - (Long.BYTES << 1); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java index 9b470dec35..900b5ba6c5 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java @@ -754,9 +754,21 @@ public void utf8ParentsCaptureFinalChild() throws Exception { @Test public void utf8ScalarCollectionCapability() throws Exception { ControlledJson controlled = controlledJson(); - String input = - "{\"values\":[null,\"\",\"short\",\"abcdefghijklmnopqrstuvwxyz0123456789\"," - + "\"quote\\\"\",\"slash\\\\\",\"line\\n\",\"你好\"]}"; + String[] tokens = { + "null", + "\"\"", + "\"short\"", + "\"abcdefghijklmnopqrstuvwxyz0123456789\"", + "\"quote\\\"\"", + "\"slash\\\\\"", + "\"line\\n\"", + "\"你好\"", + "\"eight\"", + "\"nine\"", + "\"ten\"", + "null", + "\"tail\"" + }; List expected = Arrays.asList( null, @@ -766,7 +778,13 @@ public void utf8ScalarCollectionCapability() throws Exception { "quote\"", "slash\\", "line\n", - "你好"); + "你好", + "eight", + "nine", + "ten", + null, + "tail"); + String input = stringCollectionInput(tokens, tokens.length); AsyncStringCollections initial = controlled.json.fromJson( input.getBytes(StandardCharsets.UTF_8), AsyncStringCollections.class); @@ -796,6 +814,24 @@ public void utf8ScalarCollectionCapability() throws Exception { controlled.json.fromJson( input.getBytes(StandardCharsets.UTF_8), AsyncStringCollections.class); assertEquals(generated.values, expected); + for (int size = 0; size <= tokens.length; size++) { + AsyncStringCollections prefix = + controlled.json.fromJson( + stringCollectionInput(tokens, size).getBytes(StandardCharsets.UTF_8), + AsyncStringCollections.class); + assertEquals(prefix.values, expected.subList(0, size)); + } + } + + private static String stringCollectionInput(String[] tokens, int size) { + StringBuilder input = new StringBuilder("{\"values\":["); + for (int i = 0; i < size; i++) { + if (i != 0) { + input.append(i == 10 ? " \n, \t" : ","); + } + input.append(tokens[i]); + } + return input.append("]}").toString(); } @Test From b777929abb812bd8637e8ac490dfa2aa56158088 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 12:23:36 +0800 Subject: [PATCH 09/20] perf(json): pack dynamic UTF-8 field prefixes Keep schema prefix constants in generated codecs and buffer mutation in the UTF-8 writer. --- .../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 3161a85f52..8de9874b16 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 @@ -107,10 +107,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; } @@ -125,22 +122,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]) { @@ -234,7 +215,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)); @@ -262,7 +243,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)), @@ -287,6 +268,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) @@ -384,13 +370,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; } @@ -398,9 +386,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..a07f751313 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); } } @@ -599,6 +601,22 @@ public void writeLongField(long prefix0, long prefix1, int prefixLength, long va writeLongNoEnsure(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 writeObjectStartWithLongField(byte[] namePrefix, long value) { enterDepth(); ensure(namePrefix.length + 21); @@ -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 16bbea06c1a2e5df1dd1713842628d94673e5d3c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 13:46:32 +0800 Subject: [PATCH 10/20] perf(json): use current-token checks in generated readers Generated object entries delegate whitespace fallback to their concrete representation reader. Generated collection entries retain their existing complete-token ownership. --- .../apache/fory/json/codegen/JsonReaderCodegen.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 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 184f231b7c..1fe645a63c 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 @@ -186,10 +186,12 @@ String genReaderCode( fastReadExpression(builder, readMethod, slowMethod, type, properties).genCode(ctx); String bodyCode = body.code(); bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode); + // Generated object entries use the current-token fast path; concrete readers own whitespace + // fallback. 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"); @@ -275,7 +277,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);", @@ -477,7 +479,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"); @@ -581,7 +583,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"); @@ -628,7 +630,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);", From 84c74de3eb6321515602081b7f3fe5a2faddb8a8 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 18:01:16 +0800 Subject: [PATCH 11/20] perf(json): stabilize generated UTF-8 writer ownership Generate exact UTF-8 collection writers alongside their object dependencies, then construct and publish the complete graph bottom-up. Keep common string and UTC date work in concrete representation owners and derive writer groups from actual generated bytecode so C2 sees stable natural call boundaries. --- .../apache/fory/json/codegen/JsonCodegen.java | 162 ++++++-- .../fory/json/codegen/JsonWriterCodegen.java | 123 ++++-- .../json/codegen/StringWriterCodegen.java | 5 + .../codegen/Utf8CollectionWriterCodegen.java | 88 ++++ .../fory/json/codegen/Utf8WriterCodegen.java | 52 +-- .../resolver/GeneratedCodecInstantiator.java | 39 ++ .../json/resolver/JsonSharedRegistry.java | 10 + .../fory/json/resolver/JsonTypeResolver.java | 110 ++++- .../fory/json/writer/Utf8JsonWriter.java | 383 ++++++------------ .../fory/json/JsonAsyncCompilationTest.java | 196 +++++++-- 10 files changed, 751 insertions(+), 417 deletions(-) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java 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 e5967b0807..957560c98e 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 @@ -145,6 +145,18 @@ public Class compileUtf8Reader( return buildUtf8Reader(codec, resolver, finalDependencies); } + @Internal + public Class compileUtf8CollectionWriter(Type declaredType, CollectionCodec owner) { + Class rawType = CodecUtils.rawType(declaredType, Collection.class); + Class elementType = CodecUtils.rawType(CodecUtils.elementType(declaredType), Object.class); + String generatedPackage = CodeGenerator.getPackage(elementType); + String className = className(elementType, simpleClassName(rawType) + "Utf8CollectionWriter"); + boolean stringElements = owner instanceof CollectionCodec.StringCollectionCodec; + String code = + new Utf8CollectionWriterCodegen().genCode(generatedPackage, className, stringElements); + return compileCodecClass(generatedPackage, className, code); + } + @Internal public Class compileUtf8CollectionReader(Type declaredType, CollectionCodec owner) { if (!owner.createsArrayList()) { @@ -194,46 +206,66 @@ private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolv Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "StringWriter"); - JsonGeneratedCodecBuilder builder = - new JsonGeneratedCodecBuilder(generatedPackage, className, type); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); String code = new StringWriterCodegen(this, resolver) .genUnwrappedWriterCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); - String code = - any == null || any.writeField() == null && any.writeGetter() == null - ? new StringWriterCodegen(this, resolver) - .genWriterCode(builder, type, codec.writeFields()) - : new StringWriterCodegen(this, resolver) - .genAnyWriterCode(builder, type, codec.writeFields(), any); - return compileCodecClass(generatedPackage, className, code); + JsonFieldInfo[] properties = codec.writeFields(); + if (any != null && (any.writeField() != null || any.writeGetter() != null)) { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); + String code = + new StringWriterCodegen(this, resolver).genAnyWriterCode(builder, type, properties, any); + return compileCodecClass(generatedPackage, className, code); + } + Function source = + groupEnds -> { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); + return new StringWriterCodegen(this, resolver) + .genWriterCode(builder, type, properties, groupEnds); + }; + return compileWriterClass( + generatedPackage, className, properties, "writeString", "writeStringMembers", source); } private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); String className = className(type, "Utf8Writer"); - JsonGeneratedCodecBuilder builder = - new JsonGeneratedCodecBuilder(generatedPackage, className, type); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); String code = new Utf8WriterCodegen(this, resolver) .genUnwrappedWriterCode(builder, type, codec, unwrapped); return compileCodecClass(generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); - String code = - any == null || any.writeField() == null && any.writeGetter() == null - ? new Utf8WriterCodegen(this, resolver) - .genWriterCode(builder, type, codec.writeFields()) - : new Utf8WriterCodegen(this, resolver) - .genAnyWriterCode(builder, type, codec.writeFields(), any); - return compileCodecClass(generatedPackage, className, code); + JsonFieldInfo[] properties = codec.writeFields(); + if (any != null && (any.writeField() != null || any.writeGetter() != null)) { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); + String code = + new Utf8WriterCodegen(this, resolver).genAnyWriterCode(builder, type, properties, any); + return compileCodecClass(generatedPackage, className, code); + } + Function source = + groupEnds -> { + JsonGeneratedCodecBuilder builder = + new JsonGeneratedCodecBuilder(generatedPackage, className, type); + return new Utf8WriterCodegen(this, resolver) + .genWriterCode(builder, type, properties, groupEnds); + }; + return compileWriterClass( + generatedPackage, className, properties, "writeUtf8", "writeUtf8Members", source); } private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolver) { @@ -347,10 +379,73 @@ private Class compileReaderClass( int[] groupEnds = groupable ? readerGroupEnds(generatedPackage, className, propertyCount, readMethod, source) - : oneReaderGroup(propertyCount); + : oneGroup(propertyCount); + return compileCodecClass(generatedPackage, className, source.apply(groupEnds)); + } + + private Class compileWriterClass( + String generatedPackage, + String className, + JsonFieldInfo[] properties, + String writeMethod, + String memberMethod, + Function source) { + if (properties.length < 2) { + return compileCodecClass(generatedPackage, className, source.apply(null)); + } + int[] oneGroup = new int[] {properties.length}; + JaninoUtils.CodeStats oneGroupStats = + codeStats(generatedPackage, className, source.apply(oneGroup)); + if (privateMethodSize(oneGroupStats, writeMethod + "Object") <= HOT_INLINE_LIMIT) { + return compileCodecClass(generatedPackage, className, source.apply(null)); + } + int[] groupEnds = + writerGroupEnds( + generatedPackage, + className, + properties.length, + JsonWriterCodegen.firstGroupMember(properties), + writeMethod, + memberMethod, + source); return compileCodecClass(generatedPackage, className, source.apply(groupEnds)); } + private int[] writerGroupEnds( + String generatedPackage, + String className, + int propertyCount, + int firstGroupMember, + String writeMethod, + String memberMethod, + Function source) { + List ends = new ArrayList<>(propertyCount - firstGroupMember); + for (int end = firstGroupMember + 1; end <= propertyCount; end++) { + ends.add(end); + } + if (ends.size() < 2) { + return oneGroup(propertyCount); + } + while (ends.size() > 1) { + int[] candidate = toIntArray(ends); + JaninoUtils.CodeStats stats = codeStats(generatedPackage, className, source.apply(candidate)); + boolean merged = false; + for (int group = 0; group < ends.size() - 1; group++) { + String method = group == 0 ? memberMethod : memberMethod + group; + if (privateMethodSize(stats, method) <= HOT_INLINE_LIMIT) { + ends.remove(group); + merged = true; + break; + } + } + if (merged) { + continue; + } + return candidate; + } + return toIntArray(ends); + } + private int[] readerGroupEnds( String generatedPackage, String className, @@ -358,7 +453,7 @@ private int[] readerGroupEnds( String readMethod, Function source) { if (propertyCount < 2) { - return oneReaderGroup(propertyCount); + return oneGroup(propertyCount); } // Child method bodies do not belong to their generated caller's bytecode budget. Compile the // exact caller shape on this class-owned cold path, then merge declaration-order ranges until @@ -371,8 +466,7 @@ private int[] readerGroupEnds( } while (ends.size() > 1) { int[] candidate = toIntArray(ends); - JaninoUtils.CodeStats stats = - readerCodeStats(generatedPackage, className, source.apply(candidate)); + JaninoUtils.CodeStats stats = codeStats(generatedPackage, className, source.apply(candidate)); int start = 0; boolean merged = false; for (int group = 0; group < ends.size() - 1; group++) { @@ -395,8 +489,7 @@ private int[] readerGroupEnds( return toIntArray(ends); } - private JaninoUtils.CodeStats readerCodeStats( - String generatedPackage, String className, String code) { + private JaninoUtils.CodeStats codeStats(String generatedPackage, String className, String code) { CompileUnit unit = new CompileUnit(generatedPackage, className, code); Map classes = JaninoUtils.toBytecode(jsonLoader, "", unit); String classFile = @@ -405,7 +498,7 @@ private JaninoUtils.CodeStats readerCodeStats( + ".class"; byte[] bytecode = classes.get(classFile); if (bytecode == null) { - throw new ForyJsonException("Missing generated JSON reader bytecode " + classFile); + throw new ForyJsonException("Missing generated JSON bytecode " + classFile); } return JaninoUtils.getClassStats(bytecode); } @@ -413,12 +506,17 @@ private JaninoUtils.CodeStats readerCodeStats( private int methodSize(JaninoUtils.CodeStats stats, String method) { Integer size = stats.methodsSize.get(method); if (size == null) { - throw new ForyJsonException("Missing generated JSON reader method " + method); + throw new ForyJsonException( + "Missing generated JSON method " + method + " in " + stats.methodsSize.keySet()); } return size; } - private int[] oneReaderGroup(int propertyCount) { + private int privateMethodSize(JaninoUtils.CodeStats stats, String sourceName) { + return methodSize(stats, sourceName + "$"); + } + + private int[] oneGroup(int propertyCount) { return new int[] {propertyCount}; } @@ -606,6 +704,9 @@ Class utf8WriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { if (typeInfo.usesAnnotationCodec()) { return Utf8WriterCodec.class; } + if (resolver.exactUtf8WriterCollection(typeInfo) != null) { + return Utf8WriterCodec.class; + } if (resolver.canonicalObjectCodec(typeInfo) != null) { return Utf8WriterCodec.class; } @@ -687,6 +788,13 @@ public static boolean usesWriteCodec(JsonFieldInfo property) { } } + @Internal + public static boolean usesUtf8WriteCodec(JsonFieldInfo property, JsonTypeResolver resolver) { + return usesWriteCodec(property) + || property.writeKind() == JsonFieldKind.COLLECTION + && resolver.exactUtf8WriterCollection(property.writeTypeInfo()) != null; + } + static boolean writesStringCollectionDirectly(JsonFieldInfo property) { return property.writeElementRawType() == String.class && property.writeTypeInfo().stringWriter().getClass() 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 c6024c2285..891f72c79f 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 @@ -54,8 +54,8 @@ * 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 final direct-child capabilities; canonical multi-object cycles retain the final child - * type-info slot owner. They fuse safe object prefixes and split wide objects into bounded methods - * to protect compiler and inlining budgets without adding per-field resolver lookup. + * type-info slot owner. They fuse safe object prefixes and split ordinary object writers from their + * actual emitted bytecode without adding per-field resolver lookup. */ abstract class JsonWriterCodegen { // Bound field logic in independently compiled generated methods. The entry method keeps a small @@ -145,8 +145,12 @@ abstract Expression utf16EnumFieldValue( abstract Expression writeExactArray(JsonFieldInfo property, Expression value, Expression writer); - private static boolean usesWriteCodec(JsonFieldInfo property) { - return JsonCodegen.usesWriteCodec(property); + abstract boolean writesStringCollectionDirectly(JsonFieldInfo property); + + private boolean usesWriteCodec(JsonFieldInfo property) { + return property.writeKind() == JsonFieldKind.COLLECTION + ? !writesStringCollectionDirectly(property) + : JsonCodegen.usesWriteCodec(property); } static Reference fieldRef(String name, Class type) { @@ -185,7 +189,10 @@ private static void addGeneratedConstructor( } String genWriterCode( - JsonGeneratedCodecBuilder builder, Class type, JsonFieldInfo[] properties) { + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + int[] groupEnds) { ownerType = type; CodegenContext ctx = builder.context(); ctx.addImports(writerType()); @@ -212,16 +219,21 @@ String genWriterCode( JsonCodegen.generatedCodecArrayType(ctx, codecArrayType()), "codecs"); String bodyCode; - // Keep the sole nullable capability entry small for wide POJOs. Callers can inline its null - // ownership without absorbing the independently compiled field graph into a container loop. - if (properties.length >= splitMemberThreshold()) { + // A non-null group list means the cold bytecode planner proved that the complete schema body + // naturally exceeds the ordinary hot-inline limit. Keep null ownership in the capability + // entry while the independently compiled object method owns the complete field graph. + if (groupEnds != null) { String objectMethod = writeMethod() + "Object"; addGeneratedMethod( ctx, "private final", objectMethod, writeExpression( - builder, properties, objectStartFused, new Reference("object", TypeRef.of(type))), + builder, + properties, + objectStartFused, + new Reference("object", TypeRef.of(type)), + groupEnds), void.class, writerType(), "writer", @@ -237,7 +249,7 @@ builder, properties, objectStartFused, new Reference("object", TypeRef.of(type)) Expression object = properties.length <= 1 ? castObject : new Expression.Variable("object", castObject); Code.ExprCode body = - writeExpression(builder, properties, objectStartFused, object).genCode(ctx); + writeExpression(builder, properties, objectStartFused, object, null).genCode(ctx); bodyCode = body.code(); bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode); } @@ -847,7 +859,8 @@ private Expression writeExpression( JsonGeneratedCodecBuilder builder, JsonFieldInfo[] properties, boolean objectStartFused, - Expression object) { + Expression object, + int[] groupEnds) { Reference writer = writerRef(); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(object); @@ -887,8 +900,8 @@ private Expression writeExpression( } } boolean commaKnown = objectStartFused; - boolean splitMembers = properties.length >= splitMemberThreshold(); - List memberGroup = splitMembers ? new ArrayList<>(MAX_MEMBERS_PER_METHOD) : null; + List memberGroup = groupEnds == null ? null : new ArrayList<>(); + int memberGroupIndex = 0; for (int i = firstProperty; i < properties.length; i++) { Expression member; if (objectStartFused && i == 0) { @@ -898,23 +911,29 @@ private Expression writeExpression( } else { member = writeProp(builder, properties[i], i, commaKnown, index, object, writer); } - if (splitMembers && commaKnown) { + if (memberGroup != null && commaKnown) { memberGroup.add(member); - if (memberGroup.size() == MAX_MEMBERS_PER_METHOD) { - addMemberGroup(builder, expressions, memberGroup, object, writer); + if (i + 1 == groupEnds[memberGroupIndex]) { + boolean rootGroup = memberGroupIndex == groupEnds.length - 1; + addWriterGroup(builder, expressions, memberGroup, object, writer, rootGroup); + memberGroupIndex++; } } else { - if (splitMembers) { - addMemberGroup(builder, expressions, memberGroup, object, writer); - } expressions.add(member); } if (properties[i].writeNull()) { commaKnown = true; } } - if (splitMembers) { - addMemberGroup(builder, expressions, memberGroup, object, writer, true); + if (memberGroup != null) { + boolean directRoot = + memberGroupIndex == 0 + && memberGroup.isEmpty() + && groupEnds.length == 1 + && groupEnds[0] == properties.length; + if (!directRoot && (memberGroupIndex != groupEnds.length || !memberGroup.isEmpty())) { + throw new ForyJsonException("Invalid generated writer group boundaries"); + } } expressions.add(new Expression.Invoke(writer, "writeObjectEnd")); return expressions; @@ -1052,6 +1071,40 @@ private Expression writeAny( written)); } + private void addWriterGroup( + JsonGeneratedCodecBuilder builder, + Expression.ListExpression expressions, + List memberGroup, + Expression object, + Reference writer, + boolean rootGroup) { + if (rootGroup) { + for (Expression member : memberGroup) { + expressions.add(member); + } + memberGroup.clear(); + return; + } + expressions.add(memberGroupInvoke(builder, memberGroup, object, writer)); + memberGroup.clear(); + } + + private Expression memberGroupInvoke( + JsonGeneratedCodecBuilder builder, + List memberGroup, + Expression object, + Reference writer) { + LinkedHashSet cutPoints = new LinkedHashSet<>(); + cutPoints.add(object); + cutPoints.add(writer); + return ExpressionOptimizer.invokeGenerated( + builder.context(), + cutPoints, + new Expression.ListExpression(new ArrayList<>(memberGroup)), + memberGroupMethod(), + false); + } + private void addMemberGroup( JsonGeneratedCodecBuilder builder, Expression.ListExpression expressions, @@ -1078,19 +1131,22 @@ private void addMemberGroup( memberGroup.clear(); return; } - LinkedHashSet cutPoints = new LinkedHashSet<>(); - cutPoints.add(object); - cutPoints.add(writer); - expressions.add( - ExpressionOptimizer.invokeGenerated( - builder.context(), - cutPoints, - new Expression.ListExpression(new ArrayList<>(memberGroup)), - memberGroupMethod(), - false)); + expressions.add(memberGroupInvoke(builder, memberGroup, object, writer)); memberGroup.clear(); } + static int firstGroupMember(JsonFieldInfo[] properties) { + if (canFuseObjectStart(properties)) { + return 0; + } + for (int i = 0; i < properties.length; i++) { + if (properties[i].writeNull()) { + return i + 1; + } + } + return properties.length; + } + private static boolean canFuseObjectStart(JsonFieldInfo[] properties) { if (properties.length == 0 || !properties[0].writeRawType().isPrimitive()) { return false; @@ -1136,8 +1192,7 @@ private Expression writeProp( kind == JsonFieldKind.MAP || kind == JsonFieldKind.ARRAY && writeExactArray(property, value, writer) == null || kind == JsonFieldKind.OBJECT && writeExactScalar(property, value, writer) == null - || kind == JsonFieldKind.COLLECTION - && !JsonCodegen.writesStringCollectionDirectly(property); + || kind == JsonFieldKind.COLLECTION && !writesStringCollectionDirectly(property); if (onlyCodec) { return new Expression.ListExpression( value, @@ -1258,7 +1313,7 @@ private Expression writeValue( case MAP: return writeCodec(property, id, value, writer); case COLLECTION: - if (JsonCodegen.writesStringCollectionDirectly(property)) { + if (writesStringCollectionDirectly(property)) { return writeStringCollection(value, writer); } return writeCodec(property, id, value, writer); 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 52b394b763..9edb6301cd 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 @@ -81,6 +81,11 @@ int splitMemberThreshold() { return MIN_SPLIT_MEMBERS; } + @Override + boolean writesStringCollectionDirectly(JsonFieldInfo property) { + return JsonCodegen.writesStringCollectionDirectly(property); + } + @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/Utf8CollectionWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java new file mode 100644 index 0000000000..2c279cee07 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java @@ -0,0 +1,88 @@ +/* + * 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.codegen; + +import java.util.ArrayList; +import org.apache.fory.codegen.CodegenContext; +import org.apache.fory.json.codec.Utf8WriterCodec; +import org.apache.fory.json.writer.Utf8JsonWriter; + +/** Generates one exact declared UTF-8 collection capability. */ +final class Utf8CollectionWriterCodegen { + String genCode(String generatedPackage, String className, boolean stringElements) { + CodegenContext ctx = new CodegenContext(); + ctx.setPackage(generatedPackage); + ctx.setClassName(className); + ctx.setClassModifiers("final"); + ctx.addImports(ArrayList.class, Utf8JsonWriter.class, Utf8WriterCodec.class); + ctx.implementsInterfaces(ctx.type(Utf8WriterCodec.class)); + ctx.addField(true, ctx.type(Utf8WriterCodec.class), "fallback", null); + if (stringElements) { + ctx.addConstructor("this.fallback = fallback;", Utf8WriterCodec.class, "fallback"); + } else { + ctx.addField(true, ctx.type(Utf8WriterCodec.class), "elementWriter", null); + ctx.addConstructor( + "this.fallback = fallback;\nthis.elementWriter = elementWriter;", + Utf8WriterCodec.class, + "fallback", + Utf8WriterCodec.class, + "elementWriter"); + } + ctx.addMethod( + "@Override public final", + "writeUtf8", + writeBody(stringElements), + void.class, + Utf8JsonWriter.class, + "writer", + Object.class, + "value"); + return ctx.genCode(); + } + + private static String writeBody(boolean stringElements) { + String elementWrite = + stringElements + ? "String element = (String) list.get(index);\n" + + " writer.writeComma(index);\n" + + " if (element == null) {\n" + + " writer.writeNull();\n" + + " } else {\n" + + " writer.writeString(element);\n" + + " }" + : "writer.writeComma(index);\n" + " elementWriter.writeUtf8(writer, list.get(index));"; + return "if (value == null) {\n" + + " writer.writeNull();\n" + + " return;\n" + + "}\n" + + "if (value.getClass() != ArrayList.class) {\n" + + " fallback.writeUtf8(writer, value);\n" + + " return;\n" + + "}\n" + + "ArrayList list = (ArrayList) value;\n" + + "writer.writeArrayStart();\n" + + "for (int index = 0, size = list.size(); index < size; index++) {\n" + + " " + + elementWrite + + "\n" + + "}\n" + + "writer.writeArrayEnd();"; + } +} 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 8de9874b16..ca6391364d 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 @@ -90,6 +90,12 @@ int splitMemberThreshold() { return MIN_SPLIT_MEMBERS; } + @Override + boolean writesStringCollectionDirectly(JsonFieldInfo property) { + return JsonCodegen.writesStringCollectionDirectly(property) + && resolver.exactUtf8WriterCollection(property.writeTypeInfo()) == null; + } + @Override PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused) { PrefixFields fields = new PrefixFields(properties.length); @@ -189,8 +195,10 @@ Expression tryWriteObjectStartString( if (!canPackObjectStartString(property)) { return null; } - return new Expression.Invoke( - writer, "writeObjectStartWithStringField", objectPackedPrefixArgs(property, value)); + return new Expression.ListExpression( + new Expression.Invoke(writer, "writeObjectStart"), + new Expression.Invoke(writer, "writeRawValue", packedPrefixArgs(property, false)), + new Expression.Invoke(writer, "writeString", value)); } private static boolean canPackObjectStartString(JsonFieldInfo property) { @@ -236,30 +244,9 @@ Expression writeStringField( boolean commaKnown, Expression index, Expression writer) { - if (commaKnown) { - if (canPackPrefix(property, true)) { - return new Expression.Invoke( - writer, "writeStringField", packedPrefixArgs(property, true, value)); - } - return new Expression.Invoke(writer, "writeStringField", utf8PrefixRef(true, id), value); - } - if (canPackPrefix(property, false) && canPackPrefix(property, true)) { - return new Expression.ListExpression( - new Expression.Invoke( - writer, "writeStringField", packedDynamicPrefixArgs(property, index, value)), - increment(index)); - } - Expression.ListExpression expressions = - new Expression.ListExpression( - new Expression.Invoke( - writer, - "writeStringField", - utf8PrefixRef(false, id), - utf8PrefixRef(true, id), - index, - value)); - expressions.add(increment(index)); - return expressions; + return new Expression.ListExpression( + writeFieldName(property, id, commaKnown, index, writer), + new Expression.Invoke(writer, "writeString", value)); } @Override @@ -353,19 +340,6 @@ private static Expression[] packedPrefixArgs( return args; } - private static Expression[] objectPackedPrefixArgs(JsonFieldInfo property, Expression value) { - byte[] namePrefix = property.utf8NamePrefix(); - byte[] prefix = new byte[namePrefix.length + 1]; - prefix[0] = '{'; - System.arraycopy(namePrefix, 0, prefix, 1, namePrefix.length); - return new Expression[] { - Expression.Literal.ofLong(packedPrefixWord(prefix, 0)), - Expression.Literal.ofLong(packedPrefixWord(prefix, Long.BYTES)), - Expression.Literal.ofInt(prefix.length), - value - }; - } - private static Expression[] packedDynamicPrefixArgs( JsonFieldInfo property, Expression index, Expression... extraArgs) { byte[] namePrefix = property.utf8NamePrefix(); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java index aea0a26b2c..4e382b3bda 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java @@ -457,6 +457,45 @@ static Utf8ReaderCodec instantiateUtf8Reader( } } + @SuppressWarnings("unchecked") + static Utf8WriterCodec instantiateUtf8CollectionWriter( + Class type, Utf8WriterCodec fallback) { + try { + if (AndroidSupport.IS_ANDROID) { + Constructor constructor = type.getDeclaredConstructor(Utf8WriterCodec.class); + constructor.setAccessible(true); + return (Utf8WriterCodec) constructor.newInstance(fallback); + } + MethodHandle constructor = + _JDKAccess._trustedLookup(type) + .findConstructor(type, MethodType.methodType(void.class, Utf8WriterCodec.class)); + return (Utf8WriterCodec) constructor.invoke(fallback); + } catch (Throwable e) { + throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e); + } + } + + @SuppressWarnings("unchecked") + static Utf8WriterCodec instantiateUtf8CollectionWriter( + Class type, Utf8WriterCodec fallback, Utf8WriterCodec elementWriter) { + try { + if (AndroidSupport.IS_ANDROID) { + Constructor constructor = + type.getDeclaredConstructor(Utf8WriterCodec.class, Utf8WriterCodec.class); + constructor.setAccessible(true); + return (Utf8WriterCodec) constructor.newInstance(fallback, elementWriter); + } + MethodHandle constructor = + _JDKAccess._trustedLookup(type) + .findConstructor( + type, + MethodType.methodType(void.class, Utf8WriterCodec.class, Utf8WriterCodec.class)); + return (Utf8WriterCodec) constructor.invoke(fallback, elementWriter); + } catch (Throwable e) { + throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e); + } + } + @SuppressWarnings("unchecked") static Utf8ReaderCodec instantiateUtf8CollectionReader( Class type, Utf8ReaderCodec elementReader) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index 9e77489f1d..285bf4247b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -195,6 +195,7 @@ public int compare(DeclarationCandidate left, DeclarationCandidate right) { private final ConcurrentHashMap, CompletableFuture>> latin1ReaderClasses; private final ConcurrentHashMap, CompletableFuture>> utf16ReaderClasses; private final ConcurrentHashMap, CompletableFuture>> utf8ReaderClasses; + private final ConcurrentHashMap>> utf8CollectionWriterClasses; private final ConcurrentHashMap>> utf8CollectionReaderClasses; private final ConcurrentHashMap cachedFieldNames; @@ -230,6 +231,7 @@ public JsonSharedRegistry(JsonConfig config) { latin1ReaderClasses = new ConcurrentHashMap<>(); utf16ReaderClasses = new ConcurrentHashMap<>(); utf8ReaderClasses = new ConcurrentHashMap<>(); + utf8CollectionWriterClasses = new ConcurrentHashMap<>(); utf8CollectionReaderClasses = new ConcurrentHashMap<>(); cachedFieldNames = new ConcurrentHashMap<>(); boolean codegenEnabled = config.codegenEnabled(); @@ -264,6 +266,14 @@ CompletableFuture> utf8ReaderClass(ObjectCodec owner, JsonTypeResolv utf8ReaderClasses, owner.type(), () -> codegen.compileUtf8Reader(owner, resolver, true)); } + CompletableFuture> utf8CollectionWriterClass( + Type declaredType, CollectionCodec owner) { + return generatedClassFuture( + utf8CollectionWriterClasses, + declaredType, + () -> codegen.compileUtf8CollectionWriter(declaredType, owner)); + } + CompletableFuture> utf8CollectionReaderClass( Type declaredType, CollectionCodec owner) { return generatedClassFuture( diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 9583a62cd3..8b59697c6c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -222,18 +222,58 @@ public CollectionCodec exactUtf8Collection(JsonTypeInfo typeInfo) { } private CollectionCodec exactUtf8CollectionOwner(JsonTypeInfo typeInfo) { + CollectionCodec owner = exactDeclaredCollectionOwner(typeInfo); + if (owner == null || !owner.createsArrayList()) { + return null; + } + JsonTypeInfo element = declaredCollectionElement(typeInfo); + if (canonicalObjectOwner(element) == null + && !(owner instanceof CollectionCodec.DirectCollectionCodec)) { + return null; + } + return owner; + } + + /** Returns an exact declared UTF-8 collection writer owner, or {@code null}. */ + @Internal + public CollectionCodec exactUtf8WriterCollection(JsonTypeInfo typeInfo) { + jitContext.lock(); + try { + return exactUtf8WriterCollectionOwner(typeInfo); + } finally { + jitContext.unlock(); + } + } + + private CollectionCodec exactUtf8WriterCollectionOwner(JsonTypeInfo typeInfo) { + CollectionCodec owner = exactDeclaredCollectionOwner(typeInfo); + // A generated collection writer owns only the ArrayList common loop. Keep declarations that + // cannot legally contain an ArrayList on their existing codec instead of compiling a class + // whose common branch is unreachable. + if (owner == null || !typeInfo.rawType().isAssignableFrom(ArrayList.class)) { + return null; + } + if (owner instanceof CollectionCodec.DirectCollectionCodec) { + return owner; + } + JsonTypeInfo element = declaredCollectionElement(typeInfo); + return owner instanceof CollectionCodec.ObjectCollectionCodec + && canonicalObjectOwner(element) != null + ? owner + : null; + } + + private CollectionCodec exactDeclaredCollectionOwner(JsonTypeInfo typeInfo) { CollectionCodec owner = collectionCodecs.get(typeInfo); - if (owner == null - || !owner.createsArrayList() - || !(typeInfo.type() instanceof ParameterizedType)) { + if (owner == null || !(typeInfo.type() instanceof ParameterizedType)) { return null; } - Type declaredElement = CodecUtils.elementType(typeInfo.type()); JsonTypeInfo element = declaredCollectionElement(typeInfo); + Type declaredElement = CodecUtils.elementType(typeInfo.type()); if (element == null - || !declaredElement.equals(element.type()) - || canonicalObjectOwner(element) == null - && !(owner instanceof CollectionCodec.DirectCollectionCodec)) { + || element.rawType() == Object.class + || element.usesAnnotationCodec() + || !declaredElement.equals(element.type())) { return null; } return owner; @@ -705,7 +745,7 @@ private Utf8WriterCodec newUtf8Writer( (Utf8WriterCodec[]) new Utf8WriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesWriteCodec(field)) { + if (JsonCodegen.usesUtf8WriteCodec(field, this)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_WRITER); } @@ -763,7 +803,7 @@ private Utf8WriterCodec newUnwrappedUtf8Writer( (Utf8WriterCodec[]) new Utf8WriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesWriteCodec(field)) { + if (JsonCodegen.usesUtf8WriteCodec(field, this)) { JsonTypeInfo child = field.writeTypeInfo(); codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.UTF8_WRITER); } @@ -1143,7 +1183,11 @@ private ArrayList capabilityChildren(ObjectCodec owner, Capabil owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesWriteCodec(field) + boolean usesCodec = + kind == CapabilityKind.UTF8_WRITER + ? JsonCodegen.usesUtf8WriteCodec(field, this) + : JsonCodegen.usesWriteCodec(field); + if (usesCodec && (field.writeRawType() != owner.type() || canonicalObjectOwner(field.writeTypeInfo()) == null)) { children.add(field.writeTypeInfo()); @@ -1341,7 +1385,13 @@ private CompletableFuture> generatedClass(CapabilityNode node, Capabili throw new IllegalStateException("Inline subtype readers reuse child generated classes"); } if (node.collectionOwner != null) { - return sharedRegistry.utf8CollectionReaderClass(node.typeInfo.type(), node.collectionOwner); + if (kind == CapabilityKind.UTF8_WRITER) { + return sharedRegistry.utf8CollectionWriterClass(node.typeInfo.type(), node.collectionOwner); + } + if (kind == CapabilityKind.UTF8_READER) { + return sharedRegistry.utf8CollectionReaderClass(node.typeInfo.type(), node.collectionOwner); + } + throw new IllegalStateException("Unsupported generated JSON collection capability " + kind); } switch (kind) { case STRING_WRITER: @@ -1369,9 +1419,22 @@ private Object newCapability( } if (node.collectionOwner != null) { JsonTypeInfo element = declaredCollectionElement(node.typeInfo); - Utf8ReaderCodec elementReader = resolvedCapability(element, capabilities, kind); - return GeneratedCodecInstantiator.instantiateUtf8CollectionReader( - generatedClass, elementReader); + if (kind == CapabilityKind.UTF8_WRITER) { + Utf8WriterCodec elementWriter = resolvedCapability(element, capabilities, kind); + Utf8WriterCodec fallback = eraseUtf8Writer(node.collectionOwner); + if (node.collectionOwner instanceof CollectionCodec.StringCollectionCodec) { + return GeneratedCodecInstantiator.instantiateUtf8CollectionWriter( + generatedClass, fallback); + } + return GeneratedCodecInstantiator.instantiateUtf8CollectionWriter( + generatedClass, fallback, elementWriter); + } + if (kind == CapabilityKind.UTF8_READER) { + Utf8ReaderCodec elementReader = resolvedCapability(element, capabilities, kind); + return GeneratedCodecInstantiator.instantiateUtf8CollectionReader( + generatedClass, elementReader); + } + throw new IllegalStateException("Unsupported generated JSON collection capability " + kind); } switch (kind) { case STRING_WRITER: @@ -1399,6 +1462,11 @@ private static T resolvedCapability( return (T) currentCapability(typeInfo, kind); } + @SuppressWarnings("unchecked") + private static Utf8WriterCodec eraseUtf8Writer(CollectionCodec codec) { + return (Utf8WriterCodec) (Utf8WriterCodec) codec; + } + private static void installCapability( JsonTypeInfo typeInfo, Object capability, CapabilityKind kind) { switch (kind) { @@ -1592,8 +1660,11 @@ private boolean addDependency(JsonTypeInfo typeInfo, boolean slotEdge) { if (objectOwner != null) { return addObject(objectOwner, typeInfo, slotEdge); } - if (kind == CapabilityKind.UTF8_READER) { - CollectionCodec collectionOwner = exactUtf8CollectionOwner(typeInfo); + if (kind == CapabilityKind.UTF8_WRITER || kind == CapabilityKind.UTF8_READER) { + CollectionCodec collectionOwner = + kind == CapabilityKind.UTF8_WRITER + ? exactUtf8WriterCollectionOwner(typeInfo) + : exactUtf8CollectionOwner(typeInfo); if (collectionOwner != null) { return addCollection(collectionOwner, typeInfo); } @@ -1664,12 +1735,7 @@ private boolean addCollection(CollectionCodec owner, JsonTypeInfo typeInfo) { return existing.complete; } JsonTypeInfo element = declaredCollectionElement(typeInfo); - if (element == null - || element.rawType() == Object.class - || element.usesAnnotationCodec() - || element.kind() == JsonFieldKind.OBJECT - && canonicalObjectOwner(element) == null - && !(element.utf8Reader() instanceof ClosedSubtypeCodec)) { + if (element == null) { return false; } CapabilityNode node = new CapabilityNode(typeInfo, owner, initial); 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 a07f751313..1f009622bb 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 @@ -272,13 +272,85 @@ public void writeChar(char value) { @Override public void writeString(String value) { if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - if (bytes.length == value.length()) { - if (writeLatin1String(bytes)) { - return; + byte[] stringBytes = StringSerializer.getStringBytes(value); + int length = stringBytes.length; + if (length == value.length()) { + ensure(length + 2); + int start = position; + // Keep the compact Latin1 common path in this concrete entry. Its real work makes the + // generated object and collection call sites retain a stable C2 boundary. + if (length > 31) { + if (writeLongLatin1StringNoEnsure(stringBytes, length)) { + return; + } + } else if (length > 24) { + if (writeLatin1String25To31(stringBytes, length)) { + return; + } + } else if (length > 16) { + if (writeLatin1String17To24(stringBytes, length)) { + return; + } + } else if (length < 8) { + if (writeLatin1String0To7(stringBytes, length)) { + return; + } + } else { + byte[] bytes = buffer; + int pos = start; + bytes[pos++] = (byte) '"'; + latin1: + { + long word = LittleEndian.getInt64(stringBytes, 0); + if (!isJsonAsciiWord(word)) { + break latin1; + } + LittleEndian.putInt64(bytes, pos, word); + pos += Long.BYTES; + int index = Long.BYTES; + if (index + Long.BYTES <= length) { + long tail = LittleEndian.getInt64(stringBytes, index); + if (!isJsonAsciiWord(tail)) { + break latin1; + } + LittleEndian.putInt64(bytes, pos, tail); + pos += Long.BYTES; + index += Long.BYTES; + } + if (index + Integer.BYTES <= length) { + int tail = LittleEndian.getInt32(stringBytes, index); + if (!isJsonAsciiInt(tail)) { + break latin1; + } + LittleEndian.putInt32(bytes, pos, tail); + pos += Integer.BYTES; + index += Integer.BYTES; + } + if (index + Short.BYTES <= length) { + int tail = (stringBytes[index] & 0xFF) | ((stringBytes[index + 1] & 0xFF) << 8); + if (!isJsonAsciiShort(tail)) { + break latin1; + } + bytes[pos] = (byte) tail; + bytes[pos + 1] = (byte) (tail >>> 8); + pos += Short.BYTES; + index += Short.BYTES; + } + if (index < length) { + byte tail = stringBytes[index]; + if (!isJsonAsciiByte(tail)) { + break latin1; + } + bytes[pos++] = tail; + } + bytes[pos++] = (byte) '"'; + position = pos; + return; + } } + position = start; } else { - if (writeUtf16String(bytes)) { + if (writeUtf16String(stringBytes)) { return; } } @@ -345,9 +417,35 @@ public void writeOffsetDateTime(OffsetDateTime value) { bytes[pos++] = (byte) '"'; pos = writeLocalDateBytes(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); bytes[pos++] = (byte) 'T'; - pos = - writeTime( - bytes, pos, value.getHour(), value.getMinute(), value.getSecond(), value.getNano()); + pos = writeTwoDigits(bytes, pos, value.getHour()); + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, value.getMinute()); + int second = value.getSecond(); + int nano = value.getNano(); + if (second != 0 || nano != 0) { + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, second); + if (nano != 0) { + bytes[pos++] = (byte) '.'; + if (nano % 1_000_000 == 0) { + pos = writePadded3(bytes, pos, nano / 1_000_000); + } else if (nano % 1000 == 0) { + int micros = nano / 1000; + int high = micros / 1000; + int low = micros - high * 1000; + pos = writePadded3(bytes, pos, high); + pos = writePadded3(bytes, pos, low); + } else { + int first = nano / 100000000; + int rem = nano - first * 100000000; + int middle = rem / 10000; + int low = rem - middle * 10000; + bytes[pos++] = (byte) ('0' + first); + pos = writePadded4(bytes, pos, middle); + pos = writePadded4(bytes, pos, low); + } + } + } bytes[pos++] = (byte) 'Z'; bytes[pos++] = (byte) '"'; position = pos; @@ -634,12 +732,6 @@ public void writeObjectStartWithLongField( writeLongNoEnsure(value); } - public void writeObjectStartWithStringField( - long prefix0, long prefix1, int prefixLength, String value) { - enterDepth(); - writeStringField(prefix0, prefix1, prefixLength, value); - } - public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeStringField(prefix, value); @@ -662,61 +754,13 @@ public void writeStringField( } public void writeStringField(byte[] prefix, String value) { - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - int start = position; - if (bytes.length == value.length()) { - ensure(prefix.length + bytes.length + 2); - writeRawNoEnsure(prefix); - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - position = start; - } else { - ensure(prefix.length + (bytes.length >> 1) * 3 + 2); - writeRawNoEnsure(prefix); - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - position = start; - } - } - writeStringFieldChars(prefix, value); + writeRaw(prefix); + writeString(value); } public void writeStringField(long prefix0, long prefix1, int prefixLength, String value) { - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - int start = position; - if (bytes.length == value.length()) { - ensurePackedPrefix(prefixLength, bytes.length + 2); - writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - position = start; - } else { - ensurePackedPrefix(prefixLength, (bytes.length >> 1) * 3 + 2); - writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - position = start; - } - } - writeStringFieldChars(prefix0, prefix1, prefixLength, value); - } - - private void writeStringFieldChars(byte[] prefix, String value) { - ensure(prefix.length + value.length() * 3 + 2); - writeRawNoEnsure(prefix); - writeStringNoEnsure(value); - } - - private void writeStringFieldChars(long prefix0, long prefix1, int prefixLength, String value) { - ensurePackedPrefix(prefixLength, value.length() * 3 + 2); - writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - writeStringNoEnsure(value); + writeRawValue(prefix0, prefix1, prefixLength); + writeString(value); } public void writeStringCollection(Collection values) { @@ -793,30 +837,10 @@ private void writeStringElementWithComma(int comma, String value) { writeNullStringElement(comma); return; } - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - int start = position; - if (bytes.length == value.length()) { - ensure(comma + bytes.length + 2); - if (comma != 0) { - buffer[position++] = ','; - } - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - position = start; - } else { - ensure(comma + (bytes.length >> 1) * 3 + 2); - if (comma != 0) { - buffer[position++] = ','; - } - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - position = start; - } + if (comma != 0) { + writeByteRaw((byte) ','); } - writeStringElementChars(comma, value); + writeString(value); } private void writeNullStringElement(int comma) { @@ -827,14 +851,6 @@ private void writeNullStringElement(int comma) { writeAsciiNoEnsure("null"); } - private void writeStringElementChars(int comma, String value) { - ensure(comma + value.length() * 3 + 2); - if (comma != 0) { - buffer[position++] = ','; - } - writeStringNoEnsure(value); - } - public void writeRawValue(byte[] value) { writeRaw(value); } @@ -1014,24 +1030,6 @@ public void writeComma(int index) { } } - private boolean writeLatin1String(byte[] value) { - int length = value.length; - ensure(length + 2); - return writeLatin1StringNoEnsure(value, length); - } - - private boolean writeLatin1StringNoEnsure(byte[] value) { - int length = value.length; - return writeLatin1StringNoEnsure(value, length); - } - - private boolean writeLatin1StringNoEnsure(byte[] value, int length) { - if (length < 32) { - return writeShortLatin1StringNoEnsure(value, length); - } - return writeLongLatin1StringNoEnsure(value, length); - } - private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { byte[] bytes = buffer; int start = position; @@ -1088,73 +1086,6 @@ 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); - } - 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. - // 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) { - long word = LittleEndian.getInt64(value, 0); - if (!isJsonAsciiWord(word)) { - return false; - } - LittleEndian.putInt64(bytes, pos, word); - pos += 8; - int index = Long.BYTES; - if (index + Long.BYTES <= length) { - long tail = LittleEndian.getInt64(value, index); - if (!isJsonAsciiWord(tail)) { - return false; - } - LittleEndian.putInt64(bytes, pos, tail); - pos += Long.BYTES; - index += Long.BYTES; - } - if (index + Integer.BYTES <= length) { - int tail = LittleEndian.getInt32(value, index); - if (!isJsonAsciiInt(tail)) { - return false; - } - LittleEndian.putInt32(bytes, pos, tail); - pos += Integer.BYTES; - 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] = (byte) tail; - bytes[pos + 1] = (byte) (tail >>> 8); - pos += Short.BYTES; - index += Short.BYTES; - } - if (index < length) { - byte tail = value[index]; - if (!isJsonAsciiByte(tail)) { - return false; - } - 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; @@ -1465,59 +1396,6 @@ private void writeDurationFraction(int value) { } } - private void writeStringNoEnsure(String value) { - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - if (bytes.length == value.length()) { - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - } else { - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - } - } - writeStringCharsNoEnsure(value); - } - - private void writeStringCharsNoEnsure(String value) { - int length = value.length(); - byte[] bytes = buffer; - int pos = position; - bytes[pos++] = (byte) '"'; - int i = 0; - while (i + 4 <= length) { - char c0 = value.charAt(i); - char c1 = value.charAt(i + 1); - char c2 = value.charAt(i + 2); - char c3 = value.charAt(i + 3); - if (isJsonAscii(c0) && isJsonAscii(c1) && isJsonAscii(c2) && isJsonAscii(c3)) { - bytes[pos] = (byte) c0; - bytes[pos + 1] = (byte) c1; - bytes[pos + 2] = (byte) c2; - bytes[pos + 3] = (byte) c3; - pos += 4; - i += 4; - } else { - break; - } - } - while (i < length) { - char ch = value.charAt(i); - if (isJsonAscii(ch)) { - bytes[pos++] = (byte) ch; - i++; - } else { - position = pos; - writeStringSlow(value, i, length); - return; - } - } - bytes[pos++] = (byte) '"'; - position = pos; - } - private void writeCodePoint(int codePoint) { ensure(4); buffer[position++] = (byte) (0xF0 | (codePoint >>> 18)); @@ -2015,41 +1893,6 @@ private static int writeLocalDateBytes(byte[] bytes, int pos, int year, int mont return writeTwoDigits(bytes, pos, day); } - private static int writeTime(byte[] bytes, int pos, int hour, int minute, int second, int nano) { - pos = writeTwoDigits(bytes, pos, hour); - bytes[pos++] = (byte) ':'; - pos = writeTwoDigits(bytes, pos, minute); - if (second != 0 || nano != 0) { - bytes[pos++] = (byte) ':'; - pos = writeTwoDigits(bytes, pos, second); - if (nano != 0) { - bytes[pos++] = (byte) '.'; - pos = writeNano(bytes, pos, nano); - } - } - return pos; - } - - private static int writeNano(byte[] bytes, int pos, int nano) { - if (nano % 1_000_000 == 0) { - return writePadded3(bytes, pos, nano / 1_000_000); - } - if (nano % 1000 == 0) { - int micros = nano / 1000; - int high = micros / 1000; - int low = micros - high * 1000; - pos = writePadded3(bytes, pos, high); - return writePadded3(bytes, pos, low); - } - int first = nano / 100000000; - int rem = nano - first * 100000000; - int middle = rem / 10000; - int low = rem - middle * 10000; - bytes[pos++] = (byte) ('0' + first); - pos = writePadded4(bytes, pos, middle); - return writePadded4(bytes, pos, low); - } - private static int writePadded3(byte[] bytes, int pos, int value) { int high = value / 100; int rem = value - high * 100; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java index 900b5ba6c5..a4cac821e6 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java @@ -41,8 +41,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; @@ -677,19 +680,53 @@ public void utf8GraphPublishesAtomically() throws Exception { JsonFieldInfo[] fields = rootOwner.readFields(); JsonTypeInfo childrenInfo = fields[0].readTypeInfo(); JsonTypeInfo friendsInfo = fields[1].readTypeInfo(); + Object initialRootReader = rootInfo.utf8Reader(); + Object initialChildReader = childInfo.utf8Reader(); + Object initialFriendReader = friendInfo.utf8Reader(); Object initialChildren = childrenInfo.utf8Reader(); Object initialFriends = friendsInfo.utf8Reader(); - - assertEquals(controlled.executor.pendingTasks(), 17); - for (int i = 0; i < 16; i++) { + JsonFieldInfo[] writeFields = rootOwner.writeFields(); + JsonTypeInfo writtenChildrenInfo = writeFields[0].writeTypeInfo(); + JsonTypeInfo writtenFriendsInfo = writeFields[1].writeTypeInfo(); + Object initialRootWriter = rootInfo.utf8Writer(); + Object initialChildWriter = childInfo.utf8Writer(); + Object initialFriendWriter = friendInfo.utf8Writer(); + Object initialChildrenWriter = writtenChildrenInfo.utf8Writer(); + Object initialFriendsWriter = writtenFriendsInfo.utf8Writer(); + assertEquals(new String(controlled.json.toJsonBytes(initial), StandardCharsets.UTF_8), input); + + int pendingTasks = controlled.executor.pendingTasks(); + assertEquals(pendingTasks, 19); + for (int i = 0; i < pendingTasks; i++) { controlled.executor.runNext(); - assertSame(rootInfo.utf8Reader(), rootOwner); - assertSame(childInfo.utf8Reader(), childOwner); - assertSame(friendInfo.utf8Reader(), friendOwner); - assertSame(childrenInfo.utf8Reader(), initialChildren); - assertSame(friendsInfo.utf8Reader(), initialFriends); + boolean initialReaderGraph = + rootInfo.utf8Reader() == initialRootReader + && childInfo.utf8Reader() == initialChildReader + && friendInfo.utf8Reader() == initialFriendReader + && childrenInfo.utf8Reader() == initialChildren + && friendsInfo.utf8Reader() == initialFriends; + boolean generatedReaderGraph = + rootInfo.utf8Reader() != initialRootReader + && childInfo.utf8Reader() != initialChildReader + && friendInfo.utf8Reader() != initialFriendReader + && childrenInfo.utf8Reader() != initialChildren + && friendsInfo.utf8Reader() != initialFriends; + assertTrue(initialReaderGraph || generatedReaderGraph); + + boolean initialWriterGraph = + rootInfo.utf8Writer() == initialRootWriter + && childInfo.utf8Writer() == initialChildWriter + && friendInfo.utf8Writer() == initialFriendWriter + && writtenChildrenInfo.utf8Writer() == initialChildrenWriter + && writtenFriendsInfo.utf8Writer() == initialFriendsWriter; + boolean generatedWriterGraph = + rootInfo.utf8Writer() != initialRootWriter + && childInfo.utf8Writer() != initialChildWriter + && friendInfo.utf8Writer() != initialFriendWriter + && writtenChildrenInfo.utf8Writer() != initialChildrenWriter + && writtenFriendsInfo.utf8Writer() != initialFriendsWriter; + assertTrue(initialWriterGraph || generatedWriterGraph); } - controlled.executor.runNext(); assertNotSame(rootInfo.utf8Reader(), rootOwner); assertNotSame(childInfo.utf8Reader(), childOwner); @@ -697,22 +734,54 @@ public void utf8GraphPublishesAtomically() throws Exception { assertNotSame(childrenInfo.utf8Reader(), initialChildren); assertNotSame(friendsInfo.utf8Reader(), initialFriends); assertTrue(childrenInfo.utf8Reader().getClass() != friendsInfo.utf8Reader().getClass()); - assertFinalElementReader(childrenInfo.utf8Reader(), childInfo.utf8Reader()); - assertFinalElementReader(friendsInfo.utf8Reader(), friendInfo.utf8Reader()); + assertFinalField(childrenInfo.utf8Reader(), "elementReader", childInfo.utf8Reader()); + assertFinalField(friendsInfo.utf8Reader(), "elementReader", friendInfo.utf8Reader()); assertFinalCollectionFields( - rootInfo.utf8Reader(), childrenInfo.utf8Reader(), friendsInfo.utf8Reader()); + rootInfo.utf8Reader(), + Utf8ReaderCodec.class, + childrenInfo.utf8Reader(), + friendsInfo.utf8Reader()); + + assertNotSame(rootInfo.utf8Writer(), initialRootWriter); + assertNotSame(childInfo.utf8Writer(), initialChildWriter); + assertNotSame(friendInfo.utf8Writer(), initialFriendWriter); + assertNotSame(writtenChildrenInfo.utf8Writer(), initialChildrenWriter); + assertNotSame(writtenFriendsInfo.utf8Writer(), initialFriendsWriter); + assertTrue( + writtenChildrenInfo.utf8Writer().getClass() != writtenFriendsInfo.utf8Writer().getClass()); + assertFinalField(writtenChildrenInfo.utf8Writer(), "elementWriter", childInfo.utf8Writer()); + assertFinalField(writtenFriendsInfo.utf8Writer(), "elementWriter", friendInfo.utf8Writer()); + assertFinalField(writtenChildrenInfo.utf8Writer(), "fallback", initialChildrenWriter); + assertFinalField(writtenFriendsInfo.utf8Writer(), "fallback", initialFriendsWriter); + assertFinalCollectionFields( + rootInfo.utf8Writer(), + Utf8WriterCodec.class, + writtenChildrenInfo.utf8Writer(), + writtenFriendsInfo.utf8Writer()); + assertEquals(writeUtf8(writtenChildrenInfo.utf8Writer(), null), "null"); + assertEquals(writeUtf8(writtenFriendsInfo.utf8Writer(), new ArrayList<>()), "[]"); AsyncCollections generated = controlled.json.fromJson(input.getBytes(StandardCharsets.UTF_8), AsyncCollections.class); assertEquals(generated.children.size(), 9); assertEquals(generated.children.get(8).id, 9); assertEquals(generated.friends.get(0).id, 10); + assertEquals(new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), input); + + AsyncCollections fallback = new AsyncCollections(); + fallback.children = new LinkedList<>(generated.children); + fallback.friends = new LinkedList<>(generated.friends); + assertEquals(new String(controlled.json.toJsonBytes(fallback), StandardCharsets.UTF_8), input); + AsyncCollections empty = controlled.json.fromJson( "{\"children\":[],\"friends\":null}".getBytes(StandardCharsets.UTF_8), AsyncCollections.class); assertTrue(empty.children.isEmpty()); assertNull(empty.friends); + assertEquals( + new String(controlled.json.toJsonBytes(empty), StandardCharsets.UTF_8), + "{\"children\":[]}"); } @Test @@ -796,24 +865,61 @@ public void utf8ScalarCollectionCapability() throws Exception { JsonTypeInfo rootInfo = resolver.getTypeInfo(AsyncStringCollections.class, AsyncStringCollections.class); JsonTypeInfo valuesInfo = rootOwner.readFields()[0].readTypeInfo(); + Object initialRootReader = rootInfo.utf8Reader(); Object initialValues = valuesInfo.utf8Reader(); + JsonTypeInfo writtenValuesInfo = rootOwner.writeFields()[0].writeTypeInfo(); + Object initialRootWriter = rootInfo.utf8Writer(); + Object initialValuesWriter = writtenValuesInfo.utf8Writer(); + String expectedJson = "{\"values\":[" + String.join(",", tokens) + "]}"; + assertEquals( + new String(controlled.json.toJsonBytes(initial), StandardCharsets.UTF_8), expectedJson); - assertEquals(controlled.executor.pendingTasks(), 6); - for (int i = 0; i < 5; i++) { + int pendingTasks = controlled.executor.pendingTasks(); + assertEquals(pendingTasks, 7); + for (int i = 0; i < pendingTasks; i++) { controlled.executor.runNext(); - assertSame(rootInfo.utf8Reader(), rootOwner); - assertSame(valuesInfo.utf8Reader(), initialValues); + boolean initialReaderGraph = + rootInfo.utf8Reader() == initialRootReader && valuesInfo.utf8Reader() == initialValues; + boolean generatedReaderGraph = + rootInfo.utf8Reader() != initialRootReader && valuesInfo.utf8Reader() != initialValues; + assertTrue(initialReaderGraph || generatedReaderGraph); + boolean initialWriterGraph = + rootInfo.utf8Writer() == initialRootWriter + && writtenValuesInfo.utf8Writer() == initialValuesWriter; + boolean generatedWriterGraph = + rootInfo.utf8Writer() != initialRootWriter + && writtenValuesInfo.utf8Writer() != initialValuesWriter; + assertTrue(initialWriterGraph || generatedWriterGraph); } - controlled.executor.runNext(); assertNotSame(rootInfo.utf8Reader(), rootOwner); assertNotSame(valuesInfo.utf8Reader(), initialValues); - assertFinalElementReader(valuesInfo.utf8Reader(), ScalarCodecs.StringCodec.INSTANCE); - assertFinalCollectionFields(rootInfo.utf8Reader(), valuesInfo.utf8Reader()); + assertFinalField(valuesInfo.utf8Reader(), "elementReader", ScalarCodecs.StringCodec.INSTANCE); + assertFinalCollectionFields( + rootInfo.utf8Reader(), Utf8ReaderCodec.class, valuesInfo.utf8Reader()); + assertNotSame(rootInfo.utf8Writer(), initialRootWriter); + assertNotSame(writtenValuesInfo.utf8Writer(), initialValuesWriter); + assertFinalField(writtenValuesInfo.utf8Writer(), "fallback", initialValuesWriter); + assertEquals(writtenValuesInfo.utf8Writer().getClass().getDeclaredFields().length, 1); + assertFinalCollectionFields( + rootInfo.utf8Writer(), Utf8WriterCodec.class, writtenValuesInfo.utf8Writer()); + assertEquals(writeUtf8(writtenValuesInfo.utf8Writer(), null), "null"); + AsyncStringCollections generated = controlled.json.fromJson( input.getBytes(StandardCharsets.UTF_8), AsyncStringCollections.class); assertEquals(generated.values, expected); + assertEquals( + new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), expectedJson); + generated.values = new LinkedList<>(expected); + assertEquals( + new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), expectedJson); + generated.values = new ArrayList<>(); + assertEquals( + new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), + "{\"values\":[]}"); + generated.values = null; + assertEquals(new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), "{}"); for (int size = 0; size <= tokens.length; size++) { AsyncStringCollections prefix = controlled.json.fromJson( @@ -834,6 +940,35 @@ private static String stringCollectionInput(String[] tokens, int size) { return input.append("]}").toString(); } + @Test + public void nonListCollectionStaysOnOwner() throws Exception { + ControlledJson controlled = controlledJson(); + AsyncFriend first = new AsyncFriend(); + first.id = 1; + AsyncFriend second = new AsyncFriend(); + second.id = 2; + AsyncSetCollections value = new AsyncSetCollections(); + value.values = new LinkedHashSet<>(Arrays.asList(first, second)); + String expected = "{\"values\":[{\"id\":1},{\"id\":2}]}"; + assertEquals(new String(controlled.json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + ObjectCodec rootOwner = resolver.getObjectCodec(AsyncSetCollections.class); + JsonTypeInfo rootInfo = + resolver.getTypeInfo(AsyncSetCollections.class, AsyncSetCollections.class); + JsonTypeInfo valuesInfo = rootOwner.writeFields()[0].writeTypeInfo(); + Object initialRootWriter = rootInfo.utf8Writer(); + Object initialValuesWriter = valuesInfo.utf8Writer(); + + assertEquals(controlled.executor.pendingTasks(), 10); + controlled.executor.runAll(); + assertNotSame(rootInfo.utf8Writer(), initialRootWriter); + assertSame(valuesInfo.utf8Writer(), initialValuesWriter); + assertFinalCollectionFields( + rootInfo.utf8Writer(), initialValuesWriter.getClass(), initialValuesWriter); + assertEquals(new String(controlled.json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + } + @Test public void utf8ShapeIgnoresPublishedChild() throws Exception { String input = "{\"creator\":{\"id\":1,\"name\":\"a\"},\"friends\":[{\"id\":2}]}"; @@ -1066,11 +1201,12 @@ private static List declaredFieldShape(Object capability) { return shape; } - private static void assertFinalElementReader(Object collection, Object element) throws Exception { - Field field = collection.getClass().getDeclaredField("elementReader"); + private static void assertFinalField(Object owner, String name, Object expected) + throws Exception { + Field field = owner.getClass().getDeclaredField(name); field.setAccessible(true); assertTrue(Modifier.isFinal(field.getModifiers())); - assertSame(field.get(collection), element); + assertSame(field.get(owner), expected); } private static void assertInlineReader(Object[] readers, Object canonical) { @@ -1081,11 +1217,11 @@ private static void assertInlineReader(Object[] readers, Object canonical) { assertSame(readers[0].getClass(), canonical.getClass()); } - private static void assertFinalCollectionFields(Object root, Object... collections) - throws Exception { + private static void assertFinalCollectionFields( + Object root, Class fieldType, Object... collections) throws Exception { int count = 0; for (Field field : root.getClass().getDeclaredFields()) { - if (field.getType() == Utf8ReaderCodec.class) { + if (field.getType() == fieldType) { field.setAccessible(true); Object value = field.get(root); for (Object collection : collections) { @@ -1100,6 +1236,12 @@ private static void assertFinalCollectionFields(Object root, Object... collectio assertEquals(count, collections.length); } + private static String writeUtf8(Utf8WriterCodec codec, Object value) { + Utf8JsonWriter writer = JsonTestSupport.newUtf8Writer(); + codec.writeUtf8(writer, value); + return new String(writer.toJsonBytes(), StandardCharsets.UTF_8); + } + private static StringWriterCodec stringWriter( JsonTypeResolver resolver, ObjectCodec owner) { resolver.lockJIT(); @@ -1399,6 +1541,10 @@ public static final class AsyncStringCollections { public List values; } + public static final class AsyncSetCollections { + public Set values; + } + public static final class AsyncCreatorParent { public final AsyncChild child; From 8863b4dbe17d64ff6bfa0a82b1e8e3bca746035a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 18:44:26 +0800 Subject: [PATCH 12/20] refactor(json): restore natural writer leaf boundaries --- .../fory/json/writer/Utf8JsonWriter.java | 383 ++++++++++++------ 1 file changed, 270 insertions(+), 113 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 1f009622bb..a07f751313 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 @@ -272,85 +272,13 @@ public void writeChar(char value) { @Override public void writeString(String value) { if (STRING_BYTES_BACKED) { - byte[] stringBytes = StringSerializer.getStringBytes(value); - int length = stringBytes.length; - if (length == value.length()) { - ensure(length + 2); - int start = position; - // Keep the compact Latin1 common path in this concrete entry. Its real work makes the - // generated object and collection call sites retain a stable C2 boundary. - if (length > 31) { - if (writeLongLatin1StringNoEnsure(stringBytes, length)) { - return; - } - } else if (length > 24) { - if (writeLatin1String25To31(stringBytes, length)) { - return; - } - } else if (length > 16) { - if (writeLatin1String17To24(stringBytes, length)) { - return; - } - } else if (length < 8) { - if (writeLatin1String0To7(stringBytes, length)) { - return; - } - } else { - byte[] bytes = buffer; - int pos = start; - bytes[pos++] = (byte) '"'; - latin1: - { - long word = LittleEndian.getInt64(stringBytes, 0); - if (!isJsonAsciiWord(word)) { - break latin1; - } - LittleEndian.putInt64(bytes, pos, word); - pos += Long.BYTES; - int index = Long.BYTES; - if (index + Long.BYTES <= length) { - long tail = LittleEndian.getInt64(stringBytes, index); - if (!isJsonAsciiWord(tail)) { - break latin1; - } - LittleEndian.putInt64(bytes, pos, tail); - pos += Long.BYTES; - index += Long.BYTES; - } - if (index + Integer.BYTES <= length) { - int tail = LittleEndian.getInt32(stringBytes, index); - if (!isJsonAsciiInt(tail)) { - break latin1; - } - LittleEndian.putInt32(bytes, pos, tail); - pos += Integer.BYTES; - index += Integer.BYTES; - } - if (index + Short.BYTES <= length) { - int tail = (stringBytes[index] & 0xFF) | ((stringBytes[index + 1] & 0xFF) << 8); - if (!isJsonAsciiShort(tail)) { - break latin1; - } - bytes[pos] = (byte) tail; - bytes[pos + 1] = (byte) (tail >>> 8); - pos += Short.BYTES; - index += Short.BYTES; - } - if (index < length) { - byte tail = stringBytes[index]; - if (!isJsonAsciiByte(tail)) { - break latin1; - } - bytes[pos++] = tail; - } - bytes[pos++] = (byte) '"'; - position = pos; - return; - } + byte[] bytes = StringSerializer.getStringBytes(value); + if (bytes.length == value.length()) { + if (writeLatin1String(bytes)) { + return; } - position = start; } else { - if (writeUtf16String(stringBytes)) { + if (writeUtf16String(bytes)) { return; } } @@ -417,35 +345,9 @@ public void writeOffsetDateTime(OffsetDateTime value) { bytes[pos++] = (byte) '"'; pos = writeLocalDateBytes(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); bytes[pos++] = (byte) 'T'; - pos = writeTwoDigits(bytes, pos, value.getHour()); - bytes[pos++] = (byte) ':'; - pos = writeTwoDigits(bytes, pos, value.getMinute()); - int second = value.getSecond(); - int nano = value.getNano(); - if (second != 0 || nano != 0) { - bytes[pos++] = (byte) ':'; - pos = writeTwoDigits(bytes, pos, second); - if (nano != 0) { - bytes[pos++] = (byte) '.'; - if (nano % 1_000_000 == 0) { - pos = writePadded3(bytes, pos, nano / 1_000_000); - } else if (nano % 1000 == 0) { - int micros = nano / 1000; - int high = micros / 1000; - int low = micros - high * 1000; - pos = writePadded3(bytes, pos, high); - pos = writePadded3(bytes, pos, low); - } else { - int first = nano / 100000000; - int rem = nano - first * 100000000; - int middle = rem / 10000; - int low = rem - middle * 10000; - bytes[pos++] = (byte) ('0' + first); - pos = writePadded4(bytes, pos, middle); - pos = writePadded4(bytes, pos, low); - } - } - } + pos = + writeTime( + bytes, pos, value.getHour(), value.getMinute(), value.getSecond(), value.getNano()); bytes[pos++] = (byte) 'Z'; bytes[pos++] = (byte) '"'; position = pos; @@ -732,6 +634,12 @@ public void writeObjectStartWithLongField( writeLongNoEnsure(value); } + public void writeObjectStartWithStringField( + long prefix0, long prefix1, int prefixLength, String value) { + enterDepth(); + writeStringField(prefix0, prefix1, prefixLength, value); + } + public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeStringField(prefix, value); @@ -754,13 +662,61 @@ public void writeStringField( } public void writeStringField(byte[] prefix, String value) { - writeRaw(prefix); - writeString(value); + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + int start = position; + if (bytes.length == value.length()) { + ensure(prefix.length + bytes.length + 2); + writeRawNoEnsure(prefix); + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + position = start; + } else { + ensure(prefix.length + (bytes.length >> 1) * 3 + 2); + writeRawNoEnsure(prefix); + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + position = start; + } + } + writeStringFieldChars(prefix, value); } public void writeStringField(long prefix0, long prefix1, int prefixLength, String value) { - writeRawValue(prefix0, prefix1, prefixLength); - writeString(value); + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + int start = position; + if (bytes.length == value.length()) { + ensurePackedPrefix(prefixLength, bytes.length + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + position = start; + } else { + ensurePackedPrefix(prefixLength, (bytes.length >> 1) * 3 + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + position = start; + } + } + writeStringFieldChars(prefix0, prefix1, prefixLength, value); + } + + private void writeStringFieldChars(byte[] prefix, String value) { + ensure(prefix.length + value.length() * 3 + 2); + writeRawNoEnsure(prefix); + writeStringNoEnsure(value); + } + + private void writeStringFieldChars(long prefix0, long prefix1, int prefixLength, String value) { + ensurePackedPrefix(prefixLength, value.length() * 3 + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeStringNoEnsure(value); } public void writeStringCollection(Collection values) { @@ -837,10 +793,30 @@ private void writeStringElementWithComma(int comma, String value) { writeNullStringElement(comma); return; } - if (comma != 0) { - writeByteRaw((byte) ','); + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + int start = position; + if (bytes.length == value.length()) { + ensure(comma + bytes.length + 2); + if (comma != 0) { + buffer[position++] = ','; + } + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + position = start; + } else { + ensure(comma + (bytes.length >> 1) * 3 + 2); + if (comma != 0) { + buffer[position++] = ','; + } + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + position = start; + } } - writeString(value); + writeStringElementChars(comma, value); } private void writeNullStringElement(int comma) { @@ -851,6 +827,14 @@ private void writeNullStringElement(int comma) { writeAsciiNoEnsure("null"); } + private void writeStringElementChars(int comma, String value) { + ensure(comma + value.length() * 3 + 2); + if (comma != 0) { + buffer[position++] = ','; + } + writeStringNoEnsure(value); + } + public void writeRawValue(byte[] value) { writeRaw(value); } @@ -1030,6 +1014,24 @@ public void writeComma(int index) { } } + private boolean writeLatin1String(byte[] value) { + int length = value.length; + ensure(length + 2); + return writeLatin1StringNoEnsure(value, length); + } + + private boolean writeLatin1StringNoEnsure(byte[] value) { + int length = value.length; + return writeLatin1StringNoEnsure(value, length); + } + + private boolean writeLatin1StringNoEnsure(byte[] value, int length) { + if (length < 32) { + return writeShortLatin1StringNoEnsure(value, length); + } + return writeLongLatin1StringNoEnsure(value, length); + } + private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { byte[] bytes = buffer; int start = position; @@ -1086,6 +1088,73 @@ 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); + } + 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. + // 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) { + long word = LittleEndian.getInt64(value, 0); + if (!isJsonAsciiWord(word)) { + return false; + } + LittleEndian.putInt64(bytes, pos, word); + pos += 8; + int index = Long.BYTES; + if (index + Long.BYTES <= length) { + long tail = LittleEndian.getInt64(value, index); + if (!isJsonAsciiWord(tail)) { + return false; + } + LittleEndian.putInt64(bytes, pos, tail); + pos += Long.BYTES; + index += Long.BYTES; + } + if (index + Integer.BYTES <= length) { + int tail = LittleEndian.getInt32(value, index); + if (!isJsonAsciiInt(tail)) { + return false; + } + LittleEndian.putInt32(bytes, pos, tail); + pos += Integer.BYTES; + 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] = (byte) tail; + bytes[pos + 1] = (byte) (tail >>> 8); + pos += Short.BYTES; + index += Short.BYTES; + } + if (index < length) { + byte tail = value[index]; + if (!isJsonAsciiByte(tail)) { + return false; + } + 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; @@ -1396,6 +1465,59 @@ private void writeDurationFraction(int value) { } } + private void writeStringNoEnsure(String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + if (bytes.length == value.length()) { + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + } else { + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + } + } + writeStringCharsNoEnsure(value); + } + + private void writeStringCharsNoEnsure(String value) { + int length = value.length(); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + int i = 0; + while (i + 4 <= length) { + char c0 = value.charAt(i); + char c1 = value.charAt(i + 1); + char c2 = value.charAt(i + 2); + char c3 = value.charAt(i + 3); + if (isJsonAscii(c0) && isJsonAscii(c1) && isJsonAscii(c2) && isJsonAscii(c3)) { + bytes[pos] = (byte) c0; + bytes[pos + 1] = (byte) c1; + bytes[pos + 2] = (byte) c2; + bytes[pos + 3] = (byte) c3; + pos += 4; + i += 4; + } else { + break; + } + } + while (i < length) { + char ch = value.charAt(i); + if (isJsonAscii(ch)) { + bytes[pos++] = (byte) ch; + i++; + } else { + position = pos; + writeStringSlow(value, i, length); + return; + } + } + bytes[pos++] = (byte) '"'; + position = pos; + } + private void writeCodePoint(int codePoint) { ensure(4); buffer[position++] = (byte) (0xF0 | (codePoint >>> 18)); @@ -1893,6 +2015,41 @@ private static int writeLocalDateBytes(byte[] bytes, int pos, int year, int mont return writeTwoDigits(bytes, pos, day); } + private static int writeTime(byte[] bytes, int pos, int hour, int minute, int second, int nano) { + pos = writeTwoDigits(bytes, pos, hour); + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, minute); + if (second != 0 || nano != 0) { + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, second); + if (nano != 0) { + bytes[pos++] = (byte) '.'; + pos = writeNano(bytes, pos, nano); + } + } + return pos; + } + + private static int writeNano(byte[] bytes, int pos, int nano) { + if (nano % 1_000_000 == 0) { + return writePadded3(bytes, pos, nano / 1_000_000); + } + if (nano % 1000 == 0) { + int micros = nano / 1000; + int high = micros / 1000; + int low = micros - high * 1000; + pos = writePadded3(bytes, pos, high); + return writePadded3(bytes, pos, low); + } + int first = nano / 100000000; + int rem = nano - first * 100000000; + int middle = rem / 10000; + int low = rem - middle * 10000; + bytes[pos++] = (byte) ('0' + first); + pos = writePadded4(bytes, pos, middle); + return writePadded4(bytes, pos, low); + } + private static int writePadded3(byte[] bytes, int pos, int value) { int high = value / 100; int rem = value - high * 100; From 25860e2945914d9df952c52f0691ab9d05d00b8d Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 21:53:57 +0800 Subject: [PATCH 13/20] perf(json): stabilize UTF-8 temporal writer boundary --- .../fory/json/writer/Utf8JsonWriter.java | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 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 a07f751313..47cc99a3dd 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 @@ -345,9 +345,37 @@ public void writeOffsetDateTime(OffsetDateTime value) { bytes[pos++] = (byte) '"'; pos = writeLocalDateBytes(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); bytes[pos++] = (byte) 'T'; - pos = - writeTime( - bytes, pos, value.getHour(), value.getMinute(), value.getSecond(), value.getNano()); + // Keep the complete UTC common path in its representation owner so generated callers see a + // natural C2 boundary that does not depend on compilation order. + pos = writeTwoDigits(bytes, pos, value.getHour()); + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, value.getMinute()); + int second = value.getSecond(); + int nano = value.getNano(); + if (second != 0 || nano != 0) { + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, second); + if (nano != 0) { + bytes[pos++] = (byte) '.'; + if (nano % 1_000_000 == 0) { + pos = writePadded3(bytes, pos, nano / 1_000_000); + } else if (nano % 1000 == 0) { + int micros = nano / 1000; + int high = micros / 1000; + int low = micros - high * 1000; + pos = writePadded3(bytes, pos, high); + pos = writePadded3(bytes, pos, low); + } else { + int first = nano / 100000000; + int rem = nano - first * 100000000; + int middle = rem / 10000; + int low = rem - middle * 10000; + bytes[pos++] = (byte) ('0' + first); + pos = writePadded4(bytes, pos, middle); + pos = writePadded4(bytes, pos, low); + } + } + } bytes[pos++] = (byte) 'Z'; bytes[pos++] = (byte) '"'; position = pos; @@ -2015,41 +2043,6 @@ private static int writeLocalDateBytes(byte[] bytes, int pos, int year, int mont return writeTwoDigits(bytes, pos, day); } - private static int writeTime(byte[] bytes, int pos, int hour, int minute, int second, int nano) { - pos = writeTwoDigits(bytes, pos, hour); - bytes[pos++] = (byte) ':'; - pos = writeTwoDigits(bytes, pos, minute); - if (second != 0 || nano != 0) { - bytes[pos++] = (byte) ':'; - pos = writeTwoDigits(bytes, pos, second); - if (nano != 0) { - bytes[pos++] = (byte) '.'; - pos = writeNano(bytes, pos, nano); - } - } - return pos; - } - - private static int writeNano(byte[] bytes, int pos, int nano) { - if (nano % 1_000_000 == 0) { - return writePadded3(bytes, pos, nano / 1_000_000); - } - if (nano % 1000 == 0) { - int micros = nano / 1000; - int high = micros / 1000; - int low = micros - high * 1000; - pos = writePadded3(bytes, pos, high); - return writePadded3(bytes, pos, low); - } - int first = nano / 100000000; - int rem = nano - first * 100000000; - int middle = rem / 10000; - int low = rem - middle * 10000; - bytes[pos++] = (byte) ('0' + first); - pos = writePadded4(bytes, pos, middle); - return writePadded4(bytes, pos, low); - } - private static int writePadded3(byte[] bytes, int pos, int value) { int high = value / 100; int rem = value - high * 100; From 96a44421ff8436074799d8a3fbae32360ab1dac8 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 23 Jul 2026 22:03:45 +0800 Subject: [PATCH 14/20] perf(json): stabilize UTF-8 string writer boundary --- .../fory/json/writer/Utf8JsonWriter.java | 317 +++++------------- 1 file changed, 85 insertions(+), 232 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 47cc99a3dd..cc6f96ed20 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 @@ -272,13 +272,86 @@ public void writeChar(char value) { @Override public void writeString(String value) { if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - if (bytes.length == value.length()) { - if (writeLatin1String(bytes)) { - return; + byte[] stringBytes = StringSerializer.getStringBytes(value); + int length = stringBytes.length; + if (length == value.length()) { + ensure(length + 2); + int start = position; + // This representation owner contains the complete compact Latin1 common path. Generated + // schema and collection methods therefore call one natural, compilation-order-independent + // C2 boundary without duplicating String work. + if (length > 31) { + if (writeLongLatin1StringNoEnsure(stringBytes, length)) { + return; + } + } else if (length > 24) { + if (writeLatin1String25To31(stringBytes, length)) { + return; + } + } else if (length > 16) { + if (writeLatin1String17To24(stringBytes, length)) { + return; + } + } else if (length < 8) { + if (writeLatin1String0To7(stringBytes, length)) { + return; + } + } else { + byte[] bytes = buffer; + int pos = start; + bytes[pos++] = (byte) '"'; + latin1: + { + long word = LittleEndian.getInt64(stringBytes, 0); + if (!isJsonAsciiWord(word)) { + break latin1; + } + LittleEndian.putInt64(bytes, pos, word); + pos += Long.BYTES; + int index = Long.BYTES; + if (index + Long.BYTES <= length) { + long tail = LittleEndian.getInt64(stringBytes, index); + if (!isJsonAsciiWord(tail)) { + break latin1; + } + LittleEndian.putInt64(bytes, pos, tail); + pos += Long.BYTES; + index += Long.BYTES; + } + if (index + Integer.BYTES <= length) { + int tail = LittleEndian.getInt32(stringBytes, index); + if (!isJsonAsciiInt(tail)) { + break latin1; + } + LittleEndian.putInt32(bytes, pos, tail); + pos += Integer.BYTES; + index += Integer.BYTES; + } + if (index + Short.BYTES <= length) { + int tail = (stringBytes[index] & 0xFF) | ((stringBytes[index + 1] & 0xFF) << 8); + if (!isJsonAsciiShort(tail)) { + break latin1; + } + bytes[pos] = (byte) tail; + bytes[pos + 1] = (byte) (tail >>> 8); + pos += Short.BYTES; + index += Short.BYTES; + } + if (index < length) { + byte tail = stringBytes[index]; + if (!isJsonAsciiByte(tail)) { + break latin1; + } + bytes[pos++] = tail; + } + bytes[pos++] = (byte) '"'; + position = pos; + return; + } } + position = start; } else { - if (writeUtf16String(bytes)) { + if (writeUtf16String(stringBytes)) { return; } } @@ -662,12 +735,6 @@ public void writeObjectStartWithLongField( writeLongNoEnsure(value); } - public void writeObjectStartWithStringField( - long prefix0, long prefix1, int prefixLength, String value) { - enterDepth(); - writeStringField(prefix0, prefix1, prefixLength, value); - } - public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeStringField(prefix, value); @@ -690,61 +757,13 @@ public void writeStringField( } public void writeStringField(byte[] prefix, String value) { - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - int start = position; - if (bytes.length == value.length()) { - ensure(prefix.length + bytes.length + 2); - writeRawNoEnsure(prefix); - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - position = start; - } else { - ensure(prefix.length + (bytes.length >> 1) * 3 + 2); - writeRawNoEnsure(prefix); - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - position = start; - } - } - writeStringFieldChars(prefix, value); + writeRaw(prefix); + writeString(value); } public void writeStringField(long prefix0, long prefix1, int prefixLength, String value) { - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - int start = position; - if (bytes.length == value.length()) { - ensurePackedPrefix(prefixLength, bytes.length + 2); - writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - position = start; - } else { - ensurePackedPrefix(prefixLength, (bytes.length >> 1) * 3 + 2); - writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - position = start; - } - } - writeStringFieldChars(prefix0, prefix1, prefixLength, value); - } - - private void writeStringFieldChars(byte[] prefix, String value) { - ensure(prefix.length + value.length() * 3 + 2); - writeRawNoEnsure(prefix); - writeStringNoEnsure(value); - } - - private void writeStringFieldChars(long prefix0, long prefix1, int prefixLength, String value) { - ensurePackedPrefix(prefixLength, value.length() * 3 + 2); - writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - writeStringNoEnsure(value); + writeRawValue(prefix0, prefix1, prefixLength); + writeString(value); } public void writeStringCollection(Collection values) { @@ -821,30 +840,10 @@ private void writeStringElementWithComma(int comma, String value) { writeNullStringElement(comma); return; } - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - int start = position; - if (bytes.length == value.length()) { - ensure(comma + bytes.length + 2); - if (comma != 0) { - buffer[position++] = ','; - } - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - position = start; - } else { - ensure(comma + (bytes.length >> 1) * 3 + 2); - if (comma != 0) { - buffer[position++] = ','; - } - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - position = start; - } + if (comma != 0) { + writeByteRaw((byte) ','); } - writeStringElementChars(comma, value); + writeString(value); } private void writeNullStringElement(int comma) { @@ -855,14 +854,6 @@ private void writeNullStringElement(int comma) { writeAsciiNoEnsure("null"); } - private void writeStringElementChars(int comma, String value) { - ensure(comma + value.length() * 3 + 2); - if (comma != 0) { - buffer[position++] = ','; - } - writeStringNoEnsure(value); - } - public void writeRawValue(byte[] value) { writeRaw(value); } @@ -1042,24 +1033,6 @@ public void writeComma(int index) { } } - private boolean writeLatin1String(byte[] value) { - int length = value.length; - ensure(length + 2); - return writeLatin1StringNoEnsure(value, length); - } - - private boolean writeLatin1StringNoEnsure(byte[] value) { - int length = value.length; - return writeLatin1StringNoEnsure(value, length); - } - - private boolean writeLatin1StringNoEnsure(byte[] value, int length) { - if (length < 32) { - return writeShortLatin1StringNoEnsure(value, length); - } - return writeLongLatin1StringNoEnsure(value, length); - } - private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { byte[] bytes = buffer; int start = position; @@ -1116,73 +1089,6 @@ 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); - } - 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. - // 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) { - long word = LittleEndian.getInt64(value, 0); - if (!isJsonAsciiWord(word)) { - return false; - } - LittleEndian.putInt64(bytes, pos, word); - pos += 8; - int index = Long.BYTES; - if (index + Long.BYTES <= length) { - long tail = LittleEndian.getInt64(value, index); - if (!isJsonAsciiWord(tail)) { - return false; - } - LittleEndian.putInt64(bytes, pos, tail); - pos += Long.BYTES; - index += Long.BYTES; - } - if (index + Integer.BYTES <= length) { - int tail = LittleEndian.getInt32(value, index); - if (!isJsonAsciiInt(tail)) { - return false; - } - LittleEndian.putInt32(bytes, pos, tail); - pos += Integer.BYTES; - 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] = (byte) tail; - bytes[pos + 1] = (byte) (tail >>> 8); - pos += Short.BYTES; - index += Short.BYTES; - } - if (index < length) { - byte tail = value[index]; - if (!isJsonAsciiByte(tail)) { - return false; - } - 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; @@ -1493,59 +1399,6 @@ private void writeDurationFraction(int value) { } } - private void writeStringNoEnsure(String value) { - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - if (bytes.length == value.length()) { - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - } else { - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - } - } - writeStringCharsNoEnsure(value); - } - - private void writeStringCharsNoEnsure(String value) { - int length = value.length(); - byte[] bytes = buffer; - int pos = position; - bytes[pos++] = (byte) '"'; - int i = 0; - while (i + 4 <= length) { - char c0 = value.charAt(i); - char c1 = value.charAt(i + 1); - char c2 = value.charAt(i + 2); - char c3 = value.charAt(i + 3); - if (isJsonAscii(c0) && isJsonAscii(c1) && isJsonAscii(c2) && isJsonAscii(c3)) { - bytes[pos] = (byte) c0; - bytes[pos + 1] = (byte) c1; - bytes[pos + 2] = (byte) c2; - bytes[pos + 3] = (byte) c3; - pos += 4; - i += 4; - } else { - break; - } - } - while (i < length) { - char ch = value.charAt(i); - if (isJsonAscii(ch)) { - bytes[pos++] = (byte) ch; - i++; - } else { - position = pos; - writeStringSlow(value, i, length); - return; - } - } - bytes[pos++] = (byte) '"'; - position = pos; - } - private void writeCodePoint(int codePoint) { ensure(4); buffer[position++] = (byte) (0xF0 | (codePoint >>> 18)); From d3e11b7c73095793a8bca18e046b03966daaaa04 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 24 Jul 2026 00:42:53 +0800 Subject: [PATCH 15/20] perf(json): stabilize UTF-8 long writer boundary --- .../apache/fory/json/codegen/JsonCodegen.java | 2 + .../fory/json/writer/Utf8JsonWriter.java | 97 ++++++++++++++----- 2 files changed, 73 insertions(+), 26 deletions(-) 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 957560c98e..8ca06e6d10 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 @@ -393,6 +393,8 @@ private Class compileWriterClass( if (properties.length < 2) { return compileCodecClass(generatedPackage, className, source.apply(null)); } + // Group only the bytecode emitted in this generated class. A callee with its own stable + // boundary contributes its invocation, not the body that C2 must keep in the callee. int[] oneGroup = new int[] {properties.length}; JaninoUtils.CodeStats oneGroupStats = codeStats(generatedPackage, className, source.apply(oneGroup)); 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 cc6f96ed20..fe9761da08 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 @@ -170,16 +170,8 @@ public void writeInt(int value) { @Override public void writeLong(long value) { - if (value == Long.MIN_VALUE) { - writeRaw(MIN_LONG_BYTES); - return; - } ensure(20); - if (value < 0) { - buffer[position++] = (byte) '-'; - value = -value; - } - writePositiveLongNoEnsure(value); + writeLongNoEnsure(value); } @Override @@ -1546,7 +1538,7 @@ private void writeCompactBigDecimal(long unscaled, int scale) { return; } if (precision == 1) { - writePositiveLongNoEnsure(unscaled); + writeLongNoEnsure(unscaled); } else { long divisor = BigNumberDigits.LONG_POWERS_OF_TEN[precision - 1]; int first = (int) (unscaled / divisor); @@ -1809,34 +1801,87 @@ private void writeLongNoEnsure(long value) { buffer[position++] = (byte) '-'; value = -value; } - writePositiveLongNoEnsure(value); + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + return; + } + position = writePositiveLong(buffer, position, value); } private void writePositiveIntNoEnsure(int value) { position = writePositiveInt(buffer, position, value); } - private void writePositiveLongNoEnsure(long value) { - if (value <= Integer.MAX_VALUE) { - writePositiveIntNoEnsure((int) value); - return; - } - byte[] bytes = buffer; - int pos = position; + // Keep the full-width formatter behind one natural C2 boundary. The explicit byte/cursor + // carrier lets its callers publish the returned cursor once without giving the large callee + // ownership of mutable writer state. + private static int writePositiveLong(byte[] bytes, int pos, long value) { long high = value / EIGHT_DIGITS; int low = (int) (value - high * EIGHT_DIGITS); if (high <= Integer.MAX_VALUE) { - pos = writePositiveInt(bytes, pos, (int) high); + int highValue = (int) high; + if (highValue < 10000) { + if (highValue < 1000) { + int digits = DIGIT_TRIPLES[highValue]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + pos += 3 - skip; + } else { + LittleEndian.putInt32(bytes, pos, DIGIT_QUADS[highValue]); + pos += 4; + } + } else { + int highHigh = divide10000(highValue); + int highLow = highValue - highHigh * 10000; + if (highHigh < 10000) { + if (highHigh >= 1000) { + LittleEndian.putInt64( + bytes, + pos, + (DIGIT_QUADS[highHigh] & 0xFFFFFFFFL) | ((long) DIGIT_QUADS[highLow] << 32)); + pos += 8; + } else { + int digits = DIGIT_TRIPLES[highHigh]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + pos += 3 - skip; + LittleEndian.putInt32(bytes, pos, DIGIT_QUADS[highLow]); + pos += 4; + } + } else { + int top = divide10000(highHigh); + int middle = highHigh - top * 10000; + int digits = DIGIT_TRIPLES[top]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + pos += 3 - skip; + LittleEndian.putInt64( + bytes, + pos, + (DIGIT_QUADS[middle] & 0xFFFFFFFFL) | ((long) DIGIT_QUADS[highLow] << 32)); + pos += 8; + } + } } else { long top = high / EIGHT_DIGITS; int middle = (int) (high - top * EIGHT_DIGITS); - // A positive long has at most 19 decimal digits, so removing two eight-digit chunks leaves a - // top chunk in [1, 922]. Bypass the general int branch tree for this proven three-digit - // bound. - pos = writeIntUpTo3(bytes, pos, (int) top); - pos = writePadded8Digits(bytes, pos, middle); - } - position = writePadded8Digits(bytes, pos, low); + int digits = DIGIT_TRIPLES[(int) top]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + pos += 3 - skip; + int middleHigh = divide10000(middle); + int middleLow = middle - middleHigh * 10000; + LittleEndian.putInt64( + bytes, + pos, + (DIGIT_QUADS[middleHigh] & 0xFFFFFFFFL) | ((long) DIGIT_QUADS[middleLow] << 32)); + pos += 8; + } + int lowHigh = divide10000(low); + int lowLow = low - lowHigh * 10000; + LittleEndian.putInt64( + bytes, pos, (DIGIT_QUADS[lowHigh] & 0xFFFFFFFFL) | ((long) DIGIT_QUADS[lowLow] << 32)); + return pos + 8; } private static int writePositiveInt(byte[] bytes, int pos, int value) { From 87acdafda19fbc12c4378a3aa131427a6c9e4473 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 24 Jul 2026 02:16:46 +0800 Subject: [PATCH 16/20] perf(json): isolate UTF-8 long field profiling --- .../fory/json/writer/Utf8JsonWriter.java | 27 ++++++++++++++++--- 1 file changed, 23 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 fe9761da08..4db1bd3f9e 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 @@ -685,13 +685,13 @@ public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, public void writeLongField(byte[] prefix, long value) { ensure(prefix.length + 20); writeRawNoEnsure(prefix); - writeLongNoEnsure(value); + writeLongFieldNoEnsure(value); } public void writeLongField(long prefix0, long prefix1, int prefixLength, long value) { ensurePackedPrefix(prefixLength, 20); writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - writeLongNoEnsure(value); + writeLongFieldNoEnsure(value); } public void writeLongField( @@ -715,7 +715,7 @@ public void writeObjectStartWithLongField(byte[] namePrefix, long value) { ensure(namePrefix.length + 21); buffer[position++] = (byte) '{'; writeRawNoEnsure(namePrefix); - writeLongNoEnsure(value); + writeLongFieldNoEnsure(value); } public void writeObjectStartWithLongField( @@ -724,7 +724,7 @@ public void writeObjectStartWithLongField( ensurePackedPrefix(prefixLength, 21); buffer[position++] = (byte) '{'; writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - writeLongNoEnsure(value); + writeLongFieldNoEnsure(value); } public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { @@ -1808,6 +1808,25 @@ private void writeLongNoEnsure(long value) { position = writePositiveLong(buffer, position, value); } + // Field values and long-array elements have independent range profiles. Keep their entry + // profiles separate so C2 cannot use an int-range array profile to expand the generated object + // field path; both entries still share the same positive-Int and positive-Long implementations. + private void writeLongFieldNoEnsure(long value) { + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + return; + } + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + return; + } + position = writePositiveLong(buffer, position, value); + } + private void writePositiveIntNoEnsure(int value) { position = writePositiveInt(buffer, position, value); } From e5e94985145d45c7fadb15f2e85e1987b76c4370 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 24 Jul 2026 09:27:37 +0800 Subject: [PATCH 17/20] perf(json): stabilize generated UTF-8 writer boundaries Route generated object bodies, member groups, and root dispatch through one shared interface invocation site. Derive group layout from the generated compilation units while retaining direct code for small schemas. --- .../java/org/apache/fory/json/ForyJson.java | 11 +- .../fory/json/codec/JsonTrampolineInvoke.java | 41 +++++ .../apache/fory/json/codegen/JsonCodegen.java | 107 +++++++++++- .../fory/json/codegen/JsonWriterCodegen.java | 164 +++++++++++++++--- .../codegen/Utf8CollectionWriterCodegen.java | 3 + .../fory/json/codegen/Utf8WriterCodegen.java | 5 + 6 files changed, 294 insertions(+), 37 deletions(-) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java 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..7f85952161 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; @@ -174,7 +175,7 @@ public byte[] toJsonBytes(Object value) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); - typeInfo.utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8(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.writeUtf8(typeInfo.utf8Writer(), writer, value); } } finally { state.typeResolver.unlockJIT(); @@ -288,7 +289,8 @@ private byte[] toJsonBytesDeclared(Object value, Type type, Class fallback) { try { state.typeResolver.lockJIT(); try { - state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value); + JsonTrampolineInvoke.writeUtf8( + state.rootTypeInfo(type, fallback).utf8Writer(), writer, value); } finally { state.typeResolver.unlockJIT(); } @@ -310,7 +312,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.writeUtf8( + 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/JsonTrampolineInvoke.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java new file mode 100644 index 0000000000..12c2da1674 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java @@ -0,0 +1,41 @@ +/* + * 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.Utf8JsonWriter; + +/** Shared invocation bytecode for independently compiled UTF-8 writer owners. */ +@Internal +public final class JsonTrampolineInvoke { + private JsonTrampolineInvoke() {} + + /** + * Invokes root codecs, generated UTF-8 object bodies, and field groups through one interface-call + * BCI. + * + *

Keep this method as the direct invocation only. The receivers are real codec owners from the + * current graph; this callsite must not contain type resolution, state checks, or synthetic + * receivers. + */ + public static void writeUtf8(Utf8WriterCodec codec, Utf8JsonWriter writer, Object value) { + codec.writeUtf8(writer, 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 8ca06e6d10..01ea495b61 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 @@ -25,6 +25,7 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -232,7 +233,13 @@ private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolv .genWriterCode(builder, type, properties, groupEnds); }; return compileWriterClass( - generatedPackage, className, properties, "writeString", "writeStringMembers", source); + generatedPackage, + className, + properties, + "writeString", + "writeStringMembers", + false, + source); } private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver) { @@ -265,7 +272,7 @@ private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver .genWriterCode(builder, type, properties, groupEnds); }; return compileWriterClass( - generatedPackage, className, properties, "writeUtf8", "writeUtf8Members", source); + generatedPackage, className, properties, "writeUtf8", "writeUtf8Members", true, source); } private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolver) { @@ -389,10 +396,34 @@ private Class compileWriterClass( JsonFieldInfo[] properties, String writeMethod, String memberMethod, + boolean trampolineGroups, Function source) { if (properties.length < 2) { return compileCodecClass(generatedPackage, className, source.apply(null)); } + if (trampolineGroups) { + JaninoUtils.CodeStats directStats = + codeStats(generatedPackage, className, source.apply(null)); + if (methodSize(directStats, writeMethod) <= HOT_INLINE_LIMIT) { + return compileCodecClass(generatedPackage, className, source.apply(null)); + } + int firstGroupMember = JsonWriterCodegen.firstGroupMember(properties); + if (properties.length - firstGroupMember < 2) { + return compileCodecClass(generatedPackage, className, source.apply(null)); + } + int[] groupEnds = + writerTrampolineGroupEnds( + generatedPackage, + className, + properties.length, + firstGroupMember, + writeMethod, + source); + if (groupEnds.length < 2) { + return compileCodecClass(generatedPackage, className, source.apply(null)); + } + return compileCodecClass(generatedPackage, className, source.apply(groupEnds)); + } // Group only the bytecode emitted in this generated class. A callee with its own stable // boundary contributes its invocation, not the body that C2 must keep in the callee. int[] oneGroup = new int[] {properties.length}; @@ -413,6 +444,36 @@ private Class compileWriterClass( return compileCodecClass(generatedPackage, className, source.apply(groupEnds)); } + private int[] writerTrampolineGroupEnds( + String generatedPackage, + String className, + int propertyCount, + int firstGroupMember, + String writeMethod, + Function source) { + List ends = new ArrayList<>(propertyCount - firstGroupMember); + for (int end = firstGroupMember + 1; end <= propertyCount; end++) { + ends.add(end); + } + boolean merged; + do { + merged = false; + for (int group = 0; group < ends.size() - 1; group++) { + List candidate = new ArrayList<>(ends); + candidate.remove(group); + Map stats = + codeStatsByClass(generatedPackage, className, source.apply(toIntArray(candidate))); + if (nestedMethodSize(stats, JsonWriterCodegen.memberGroupClassName(group), writeMethod) + <= HOT_INLINE_LIMIT) { + ends = candidate; + merged = true; + break; + } + } + } while (merged); + return toIntArray(ends); + } + private int[] writerGroupEnds( String generatedPackage, String className, @@ -492,17 +553,49 @@ private int[] readerGroupEnds( } private JaninoUtils.CodeStats codeStats(String generatedPackage, String className, String code) { - CompileUnit unit = new CompileUnit(generatedPackage, className, code); - Map classes = JaninoUtils.toBytecode(jsonLoader, "", unit); + Map stats = codeStatsByClass(generatedPackage, className, code); + return statsForMainClass(generatedPackage, className, stats); + } + + private JaninoUtils.CodeStats statsForMainClass( + String generatedPackage, String className, Map stats) { String classFile = (generatedPackage.isEmpty() ? "" : generatedPackage.replace('.', '/') + "/") + className + ".class"; - byte[] bytecode = classes.get(classFile); - if (bytecode == null) { + JaninoUtils.CodeStats classStats = stats.get(classFile); + if (classStats == null) { throw new ForyJsonException("Missing generated JSON bytecode " + classFile); } - return JaninoUtils.getClassStats(bytecode); + return classStats; + } + + private Map codeStatsByClass( + String generatedPackage, String className, String code) { + CompileUnit unit = new CompileUnit(generatedPackage, className, code); + Map classes = JaninoUtils.toBytecode(jsonLoader, "", unit); + Map stats = new LinkedHashMap<>(); + for (Map.Entry entry : classes.entrySet()) { + stats.put(entry.getKey(), JaninoUtils.getClassStats(entry.getValue())); + } + return stats; + } + + private int nestedMethodSize( + Map stats, String nestedClass, String method) { + JaninoUtils.CodeStats match = null; + for (Map.Entry entry : stats.entrySet()) { + if (entry.getKey().endsWith(nestedClass + ".class")) { + if (match != null) { + throw new ForyJsonException("Ambiguous generated JSON class " + nestedClass); + } + match = entry.getValue(); + } + } + if (match == null) { + throw new ForyJsonException("Missing generated JSON class " + nestedClass); + } + return methodSize(match, method); } private int methodSize(JaninoUtils.CodeStats stats, String method) { 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 891f72c79f..383f6e63e8 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 @@ -31,11 +31,13 @@ import java.util.List; import java.util.Map; import org.apache.fory.codegen.Code; +import org.apache.fory.codegen.CodeGenerator; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.Expression; 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; @@ -92,6 +94,10 @@ abstract class JsonWriterCodegen { abstract int splitMemberThreshold(); + Class memberGroupType() { + return null; + } + abstract PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused); abstract void addPrefixFields( @@ -211,35 +217,35 @@ String genWriterCode( addPrefixFields(ctx, property, i, prefixFields); } } - addGeneratedConstructor( - ctx, - writerConstructorExpression(properties, prefixFields), - JsonFieldInfo[].class, - "properties", - JsonCodegen.generatedCodecArrayType(ctx, codecArrayType()), - "codecs"); String bodyCode; - // A non-null group list means the cold bytecode planner proved that the complete schema body - // naturally exceeds the ordinary hot-inline limit. Keep null ownership in the capability - // entry while the independently compiled object method owns the complete field graph. + // A non-null group list means the exact generated field work needs multiple compilation + // units. UTF-8 bodies and groups share one real invocation BCI; other representations retain + // their natural generated-method boundaries. if (groupEnds != null) { - String objectMethod = writeMethod() + "Object"; - addGeneratedMethod( - ctx, - "private final", - objectMethod, + Expression body = writeExpression( builder, properties, objectStartFused, new Reference("object", TypeRef.of(type)), - groupEnds), - void.class, - writerType(), - "writer", - type, - "object"); - bodyCode = "this." + objectMethod + "(writer, (" + ctx.type(type) + ") value);\n"; + groupEnds); + Class groupType = memberGroupType(); + if (groupType == null) { + String objectMethod = writeMethod() + "Object"; + addGeneratedMethod( + ctx, + "private final", + objectMethod, + body, + void.class, + writerType(), + "writer", + type, + "object"); + bodyCode = "this." + objectMethod + "(writer, (" + ctx.type(type) + ") value);\n"; + } else { + bodyCode = addTrampolineBody(ctx, body, groupType); + } } else { ctx.clearExprState(); Expression castObject = @@ -253,6 +259,13 @@ String genWriterCode( 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(), @@ -914,8 +927,9 @@ private Expression writeExpression( if (memberGroup != null && commaKnown) { memberGroup.add(member); if (i + 1 == groupEnds[memberGroupIndex]) { - boolean rootGroup = memberGroupIndex == groupEnds.length - 1; - addWriterGroup(builder, expressions, memberGroup, object, writer, rootGroup); + boolean rootGroup = memberGroupType() == null && memberGroupIndex == groupEnds.length - 1; + addWriterGroup( + builder, expressions, memberGroup, object, writer, memberGroupIndex, rootGroup); memberGroupIndex++; } } else { @@ -927,7 +941,8 @@ private Expression writeExpression( } if (memberGroup != null) { boolean directRoot = - memberGroupIndex == 0 + memberGroupType() == null + && memberGroupIndex == 0 && memberGroup.isEmpty() && groupEnds.length == 1 && groupEnds[0] == properties.length; @@ -939,6 +954,45 @@ private Expression writeExpression( return expressions; } + private String addTrampolineBody(CodegenContext ctx, Expression body, Class bodyType) { + ctx.clearExprState(); + Code.ExprCode bodyExpr = body.genCode(ctx); + String bodyCode = bodyExpr.code(); + bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode); + if (!bodyCode.isEmpty()) { + bodyCode = " " + CodeGenerator.alignIndent(bodyCode, 4) + "\n"; + } + + String fieldName = "writeBody"; + String bodyClass = "WriteBody"; + ctx.addImports(JsonTrampolineInvoke.class, bodyType); + String bodyTypeName = ctx.type(bodyType); + ctx.addField(true, bodyTypeName, fieldName, null); + ctx.addInitCode( + "class " + + bodyClass + + " implements " + + bodyTypeName + + " {\n" + + " @Override public void writeUtf8(" + + ctx.type(writerType()) + + " writer, Object value) {\n" + + " " + + ctx.type(ownerType) + + " object = (" + + ctx.type(ownerType) + + ") value;\n" + + bodyCode + + " }\n" + + "}\n" + + "this." + + fieldName + + " = new " + + bodyClass + + "();"); + return ctx.type(JsonTrampolineInvoke.class) + ".writeUtf8(" + fieldName + ", writer, value);\n"; + } + private Expression writeAnyExpression( JsonGeneratedCodecBuilder builder, JsonFieldInfo[] properties, @@ -1077,7 +1131,9 @@ private void addWriterGroup( List memberGroup, Expression object, Reference writer, + int groupIndex, boolean rootGroup) { + Class groupType = memberGroupType(); if (rootGroup) { for (Expression member : memberGroup) { expressions.add(member); @@ -1085,7 +1141,10 @@ private void addWriterGroup( memberGroup.clear(); return; } - expressions.add(memberGroupInvoke(builder, memberGroup, object, writer)); + expressions.add( + groupType != null + ? trampolineGroupInvoke(builder, memberGroup, object, writer, groupIndex, groupType) + : memberGroupInvoke(builder, memberGroup, object, writer)); memberGroup.clear(); } @@ -1105,6 +1164,59 @@ private Expression memberGroupInvoke( false); } + private Expression trampolineGroupInvoke( + JsonGeneratedCodecBuilder builder, + List memberGroup, + Expression object, + Reference writer, + int groupIndex, + Class groupType) { + CodegenContext ctx = builder.context(); + ctx.clearExprState(); + Code.ExprCode body = new Expression.ListExpression(new ArrayList<>(memberGroup)).genCode(ctx); + String bodyCode = body.code(); + bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode); + if (!bodyCode.isEmpty()) { + bodyCode = " " + CodeGenerator.alignIndent(bodyCode, 4) + "\n"; + } + + String fieldName = "writeGroup" + groupIndex; + String groupClass = memberGroupClassName(groupIndex); + ctx.addImports(JsonTrampolineInvoke.class, groupType); + String groupTypeName = ctx.type(groupType); + ctx.addField(true, groupTypeName, fieldName, null); + // The receiver directly owns the field body. A forwarding method creates two valid C2 + // products depending on whether the body finishes L4 compilation before the receiver. + ctx.addInitCode( + "class " + + groupClass + + " implements " + + groupTypeName + + " {\n" + + " @Override public void writeUtf8(" + + ctx.type(writerType()) + + " writer, Object value) {\n" + + " " + + ctx.type(ownerType) + + " object = (" + + ctx.type(ownerType) + + ") value;\n" + + bodyCode + + " }\n" + + "}\n" + + "this." + + fieldName + + " = new " + + groupClass + + "();"); + return new Expression.StaticInvoke( + JsonTrampolineInvoke.class, "writeUtf8", fieldRef(fieldName, groupType), writer, object); + } + + static String memberGroupClassName(int groupIndex) { + return "WriteGroup" + groupIndex; + } + private void addMemberGroup( JsonGeneratedCodecBuilder builder, Expression.ListExpression expressions, diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java index 2c279cee07..152dc0e865 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java @@ -58,6 +58,9 @@ String genCode(String generatedPackage, String className, boolean stringElements } private static String writeBody(boolean stringElements) { + // Object-level compilation boundaries belong to the final element codec. The collection keeps + // its one ordinary element call so member-group receiver profiles cannot be skewed by element + // cardinality. String elementWrite = stringElements ? "String element = (String) list.get(index);\n" 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 ca6391364d..dbd2240ac0 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 @@ -80,6 +80,11 @@ String memberGroupMethod() { return "writeUtf8Members"; } + @Override + Class memberGroupType() { + return Utf8WriterCodec.class; + } + @Override String writeAnyMethod() { return "writeUtf8Any"; From 9b835edca277f76a3de28d3a83eaf58624ce2926 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 24 Jul 2026 09:27:53 +0800 Subject: [PATCH 18/20] perf(json): isolate UTF-8 long array writing Keep the pair-unrolled array loop as the compilation owner and retain signed Long and Int dispatch in each real loop lane. Cover boundaries, parity, and generated UTF-8 field output. --- .../fory/json/writer/Utf8JsonWriter.java | 78 +++++++++++++++++-- .../apache/fory/json/JsonContainerTest.java | 37 +++++++++ 2 files changed, 108 insertions(+), 7 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 4db1bd3f9e..838ba4849e 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 @@ -802,21 +802,86 @@ public void writeLongArray(long[] values) { buffer[position++] = '['; int length = values.length; if (length != 0) { + // Keep the signed-Long dispatch in each pair-loop lane. Extracting this real work into a + // small helper lets compilation order move the array closure into generated object methods. ensure(22); - writeLongNoEnsure(values[0]); + { + long value = values[0]; + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + } else { + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + } else { + position = writePositiveLong(buffer, position, value); + } + } + } + int i = 1; if ((length & 1) == 0) { ensure(22); buffer[position++] = ','; - writeLongNoEnsure(values[i]); + { + long value = values[i]; + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + } else { + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + } else { + position = writePositiveLong(buffer, position, value); + } + } + } i++; } + for (; i < length; i += 2) { ensure(44); buffer[position++] = ','; - writeLongNoEnsure(values[i]); + { + long value = values[i]; + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + } else { + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + } else { + position = writePositiveLong(buffer, position, value); + } + } + } + buffer[position++] = ','; - writeLongNoEnsure(values[i + 1]); + { + long value = values[i + 1]; + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + } else { + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + } else { + position = writePositiveLong(buffer, position, value); + } + } + } } } buffer[position++] = ']'; @@ -1808,9 +1873,8 @@ private void writeLongNoEnsure(long value) { position = writePositiveLong(buffer, position, value); } - // Field values and long-array elements have independent range profiles. Keep their entry - // profiles separate so C2 cannot use an int-range array profile to expand the generated object - // field path; both entries still share the same positive-Int and positive-Long implementations. + // Generated object fields and general scalar Long calls have independent range profiles. Keep + // their entries separate; array element dispatch is owned directly by writeLongArray. private void writeLongFieldNoEnsure(long value) { if (value == Long.MIN_VALUE) { writeRawNoEnsure(MIN_LONG_BYTES); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 0c966eeb82..ce64801b73 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -52,6 +52,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Random; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingDeque; @@ -567,6 +568,42 @@ public void readLongArrays() { () -> json.fromJson("[1,null]".getBytes(StandardCharsets.UTF_8), long[].class)); } + @Test + public void writeLongArrays() { + ForyJson json = newJson(); + LongArrayUtf16Root root = new LongArrayUtf16Root(); + root.text = ZH_TEXT; + long[][] boundaries = { + new long[0], + {0L}, + { + -1L, + 1L, + Integer.MIN_VALUE, + Integer.MAX_VALUE, + (long) Integer.MIN_VALUE - 1L, + (long) Integer.MAX_VALUE + 1L, + Long.MIN_VALUE, + Long.MAX_VALUE + } + }; + for (long[] values : boundaries) { + root.values = values; + assertEquals(new String(json.toJsonBytes(root), StandardCharsets.UTF_8), json.toJson(root)); + } + + Random random = new Random(0x7a11_5eedL); + for (int length = 1; length <= 33; length++) { + long[] values = new long[length]; + for (int i = 0; i < length; i++) { + values[i] = random.nextLong(); + } + root.values = values; + assertEquals(new String(json.toJsonBytes(root), StandardCharsets.UTF_8), json.toJson(root)); + } + assertGeneratedWhenSupported(json, LongArrayUtf16Root.class); + } + @Test public void writeReadBoxedPrimitiveArrays() { ForyJson json = newJson(); From aab76010d769fc9a779c2a0dc11739a91ec184dc Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 24 Jul 2026 09:51:28 +0800 Subject: [PATCH 19/20] perf(json): keep shared UTF-8 dispatch generated-only Invoke root codecs directly so startup ObjectCodec profiles cannot enter the shared generated body and member-group callsite. --- .../src/main/java/org/apache/fory/json/ForyJson.java | 11 ++++------- .../apache/fory/json/codec/JsonTrampolineInvoke.java | 10 +++++----- 2 files changed, 9 insertions(+), 12 deletions(-) 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 7f85952161..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; @@ -175,7 +174,7 @@ public byte[] toJsonBytes(Object value) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); - JsonTrampolineInvoke.writeUtf8(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.writeUtf8(typeInfo.utf8Writer(), writer, value); + typeInfo.utf8Writer().writeUtf8(writer, value); } } finally { state.typeResolver.unlockJIT(); @@ -289,8 +288,7 @@ private byte[] toJsonBytesDeclared(Object value, Type type, Class fallback) { try { state.typeResolver.lockJIT(); try { - JsonTrampolineInvoke.writeUtf8( - state.rootTypeInfo(type, fallback).utf8Writer(), writer, value); + state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value); } finally { state.typeResolver.unlockJIT(); } @@ -312,8 +310,7 @@ private void writeJsonDeclared(Object value, Type type, Class fallback, Outpu try { state.typeResolver.lockJIT(); try { - JsonTrampolineInvoke.writeUtf8( - 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/JsonTrampolineInvoke.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java index 12c2da1674..4f4b2b335d 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 @@ -28,12 +28,12 @@ public final class JsonTrampolineInvoke { private JsonTrampolineInvoke() {} /** - * Invokes root codecs, generated UTF-8 object bodies, and field groups through one interface-call - * BCI. + * Invokes generated UTF-8 object bodies and field groups through one interface-call BCI. * - *

Keep this method as the direct invocation only. The receivers are real codec owners from the - * current graph; this callsite must not contain type resolution, state checks, or synthetic - * receivers. + *

Keep this method generated-only and as the direct invocation. Root startup codecs would + * dominate the type profile before the generated receivers are published. The receivers here are + * real codec owners from the current graph; this callsite must not contain type resolution, state + * checks, or synthetic receivers. */ public static void writeUtf8(Utf8WriterCodec codec, Utf8JsonWriter writer, Object value) { codec.writeUtf8(writer, value); From c04043bc4fbe6f4fbe136cc266f028ed2a3c9f9e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 24 Jul 2026 10:41:18 +0800 Subject: [PATCH 20/20] docs(java): document JSON C2 ownership boundaries --- .agents/languages/java.md | 24 ++++++++ .../java/org/apache/fory/json/ForyJson.java | 8 +++ .../fory/json/codec/JsonTrampolineInvoke.java | 33 +++++++++-- .../apache/fory/json/codegen/JsonCodegen.java | 19 +++++- .../fory/json/codegen/JsonWriterCodegen.java | 15 ++++- .../codegen/Utf8CollectionWriterCodegen.java | 8 ++- .../fory/json/codegen/Utf8WriterCodegen.java | 3 + .../fory/json/reader/Utf8JsonReader.java | 7 +++ .../fory/json/writer/Utf8JsonWriter.java | 59 ++++++++++++++++--- 9 files changed, 157 insertions(+), 19 deletions(-) diff --git a/.agents/languages/java.md b/.agents/languages/java.md index 856db7dc00..da0cdd81b1 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -104,6 +104,30 @@ Load this file when changing anything under `java/` or when Java drives a cross- - In `MemoryBuffer` and `MemoryOps` hot paths, duplicate small straight-line copy/read/write logic when that keeps control flow direct. Do not add private helper indirection to hot paths just to reduce local code duplication; keep helpers for slow, cold, or error paths. +- In JDK 25 Fory JSON C2-sensitive code, preserve measured, naturally large hot-method boundaries. + A method that exceeds HotSpot's 325-byte hot-inline limit through real representation, scalar, + array, collection, or generated-schema work is an independent subtree owner. Generated group + planning counts only its invocation bytecodes, not the callee's transitive body. Do not shrink + such a method into a wrapper, add its body back into the caller budget, or manufacture a boundary + with padding, `@DontInline`, `CompileCommand`, fake receivers, or JVM flags. Keep escape, + malformed-input, Unicode, arbitrary-length, and other cold fallback work in separate methods. +- `JsonTrampolineInvoke` is the generated UTF-8 writer body's single shared + `invokeinterface`-bytecode owner. Only fully constructed generated object bodies and member groups + held in final interface-typed fields may call it. Root facades, startup `ObjectCodec`s, handwritten + codecs, arrays, collections, maps, readers, and writers must not call it. A generated collection + loop calls the element codec's ordinary entry; a qualifying generated element entry reaches its + own object-body trampoline. The static helper remains tiny and may inline because the shared + `invokeinterface` profile, not helper size, creates the boundary. Do not create one trampoline per + generated class, pass concrete receivers, add dispatch/state/resolution work, or introduce + synthetic receivers; each of those breaks the shared real-receiver profile or its clean owner + model. +- Intentional hot-path source duplication is required when helper extraction loses local + buffer/cursor state or makes C2 layout depend on compilation order. In particular, Long read/write + paths may repeat Int parsing or formatting logic, and unrolled array lanes may repeat complete + signed/range dispatch. Do not deduplicate these blocks without generated-source, `javap`, + PrintInlining, LogCompilation, nmethod, allocation, intrinsic, and aggregate evidence that the + independent boundary and performance remain stable. Preserve the nearby source comments that + record the owner and failure mode. - In `MemoryBuffer` small-varint read/write hot paths, once Android has exited through the single `MemoryOps` call, keep JVM bulk loads/stores local with raw Unsafe operations instead of routing through branchful `_unsafeGet*` or `_unsafePut*` helpers. Add or preserve source comments that 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..9317d7148c 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 @@ -174,6 +174,8 @@ public byte[] toJsonBytes(Object value) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); + // Root startup receivers must bypass the generated-only shared trampoline. Calling it + // here lets ObjectCodec dominate that BCI before generated body/group publication. typeInfo.utf8Writer().writeUtf8(writer, value); } } finally { @@ -223,6 +225,8 @@ public void writeJsonTo(Object value, OutputStream output) { writer.writeNull(); } else { JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); + // Keep root dispatch direct; startup ObjectCodec receivers must not enter the + // generated body/group profile owned by JsonTrampolineInvoke. typeInfo.utf8Writer().writeUtf8(writer, value); } } finally { @@ -288,6 +292,8 @@ private byte[] toJsonBytesDeclared(Object value, Type type, Class fallback) { try { state.typeResolver.lockJIT(); try { + // Declared root dispatch is still startup/root work. It must remain outside the + // generated-only JsonTrampolineInvoke receiver profile. state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value); } finally { state.typeResolver.unlockJIT(); @@ -310,6 +316,8 @@ private void writeJsonDeclared(Object value, Type type, Class fallback, Outpu try { state.typeResolver.lockJIT(); try { + // Declared root dispatch is direct for the same reason as toJsonBytesDeclared: only + // completed generated bodies and groups may contribute to the shared trampoline BCI. 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/JsonTrampolineInvoke.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java index 4f4b2b335d..8bcf1f8e51 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 @@ -22,7 +22,29 @@ import org.apache.fory.annotation.Internal; import org.apache.fory.json.writer.Utf8JsonWriter; -/** Shared invocation bytecode for independently compiled UTF-8 writer owners. */ +/** + * Owns the single interface-invocation bytecode used by large generated UTF-8 writer receivers. + * + *

This helper is deliberately not a general codec dispatcher. Generated object bodies and member + * groups store their completed receiver in a final interface-typed field and call this class. + * Consequently, every qualifying generated receiver contributes to the type profile of the same + * {@code invokeinterface} bytecode index instead of creating one profile per generated class. That + * real polymorphic profile prevents an outer generated collection or object from absorbing a + * receiver's complete transitive field closure before the receiver has formed its own level-4 + * nmethod. + * + *

The static helper itself is intentionally tiny and may inline into generated callers. The + * boundary comes from the shared interface-call profile that remains after helper inlining, not + * from making this method large. Generated receivers own the real work and any naturally large + * method; this class owns only their common interface-call bytecode index. + * + *

Do not duplicate this helper per generated type, replace the interface field with a concrete + * receiver, or move root/handwritten codec calls through it. Root startup codecs execute before + * generated capabilities are published and would dominate the shared profile; concrete receiver + * types would let C2 recover and inline the exact implementation. Do not add type resolution, + * lifecycle state, allocation, callbacks, or fallback logic here. Those concerns belong to the + * resolver or the generated receiver, while this class owns only the shared invocation bytecode. + */ @Internal public final class JsonTrampolineInvoke { private JsonTrampolineInvoke() {} @@ -30,10 +52,11 @@ private JsonTrampolineInvoke() {} /** * Invokes generated UTF-8 object bodies and field groups through one interface-call BCI. * - *

Keep this method generated-only and as the direct invocation. Root startup codecs would - * dominate the type profile before the generated receivers are published. The receivers here are - * real codec owners from the current graph; this callsite must not contain type resolution, state - * checks, or synthetic receivers. + *

The interface invocation below must remain the only operation and the only {@code + * invokeinterface} bytecode in this method. The static helper call may itself be inlined, but + * HotSpot still consumes the one method-data profile owned by this bytecode index. The receivers + * must be real, fully constructed generated capabilities published only after construction; never + * add synthetic receivers merely to force polymorphism. */ public static void writeUtf8(Utf8WriterCodec codec, Utf8JsonWriter writer, Object value) { codec.writeUtf8(writer, 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 01ea495b61..7482edda68 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 @@ -67,8 +67,13 @@ * generated source and constructor boundary; handwritten runtime capability APIs remain generic. */ public final class JsonCodegen { - // HotSpot's ordinary hot-callsite ceiling. Generated methods cross it with real schema work; - // padding or compiler directives would make the boundary dependent on deployment flags. + // HotSpot JDK 25's measured hot-callsite bytecode ceiling. This is a local generated-method + // limit, not a transitive subtree estimate: once a concrete String/scalar/container callee owns + // a natural method larger than this limit, a generated group pays only the call bytecodes. Large + // generated groups cross the limit with real schema work and are then routed through the shared + // generated-only interface profile. Never use padding, annotations, or compiler directives to + // manufacture the boundary, and never add the already-independent callee body back to this + // planner's budget. private static final int HOT_INLINE_LIMIT = 325; private static final Map> ID_GENERATOR = new ConcurrentHashMap<>(); @@ -402,6 +407,11 @@ private Class compileWriterClass( return compileCodecClass(generatedPackage, className, source.apply(null)); } if (trampolineGroups) { + // Measure the exact generated compilation unit. Stable leaf owners such as writeString and + // writeLongArray appear here only as calls; adding their transitive implementation cost would + // make schema grouping depend on C2's compilation order instead of generated bytecode. + // Small complete schemas remain direct. A large schema is split into real body/group + // receivers, and those receivers all reach the one JsonTrampolineInvoke interface BCI. JaninoUtils.CodeStats directStats = codeStats(generatedPackage, className, source.apply(null)); if (methodSize(directStats, writeMethod) <= HOT_INLINE_LIMIT) { @@ -451,6 +461,11 @@ private int[] writerTrampolineGroupEnds( int firstGroupMember, String writeMethod, Function source) { + // Start with declaration-order field ranges, compile the actual nested receiver classes, and + // merge a range forward while its own emitted method is still small enough to inline. The + // decision is made from local bytecode only: an independently compiled leaf contributes its + // call instruction, never a guessed recursive cost. Do not replace this with field-count, + // benchmark-shape, runtime-value, or transitive-subtree heuristics. List ends = new ArrayList<>(propertyCount - firstGroupMember); for (int end = firstGroupMember + 1; end <= propertyCount; end++) { ends.add(end); 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 383f6e63e8..10342ce835 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 @@ -968,6 +968,13 @@ private String addTrampolineBody(CodegenContext ctx, Expression body, Class b ctx.addImports(JsonTrampolineInvoke.class, bodyType); String bodyTypeName = ctx.type(bodyType); ctx.addField(true, bodyTypeName, fieldName, null); + // The generated receiver below owns the real object body, not a forwarding call to another + // generated method. It is constructed once, stored through a final interface-typed field, and + // invoked through JsonTrampolineInvoke's one shared invokeinterface BCI. Keeping all three + // properties is essential: a concrete field reveals the exact type, a per-class interface call + // creates separate profiles, and a forwarding receiver lets compilation order decide whether + // the real body is absorbed before it reaches level 4. The enclosing generated codec + // constructor finishes all receiver assignment before the resolver publishes that codec. ctx.addInitCode( "class " + bodyClass @@ -1185,8 +1192,12 @@ private Expression trampolineGroupInvoke( ctx.addImports(JsonTrampolineInvoke.class, groupType); String groupTypeName = ctx.type(groupType); ctx.addField(true, groupTypeName, fieldName, null); - // The receiver directly owns the field body. A forwarding method creates two valid C2 - // products depending on whether the body finishes L4 compilation before the receiver. + // This receiver directly owns the schema field body and is stored in a final interface-typed + // field. Do not extract the body into a generated forwarding method: the forwarding receiver + // can then compile either before or after the target body reaches level 4, producing two valid + // but materially different C2 layouts. Do not emit invokeinterface here either; every body and + // group must contribute its real receiver to JsonTrampolineInvoke's one shared profile. The + // enclosing generated codec is published only after every final receiver field is assigned. ctx.addInitCode( "class " + groupClass diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java index 152dc0e865..89da739dc9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java @@ -58,9 +58,11 @@ String genCode(String generatedPackage, String className, boolean stringElements } private static String writeBody(boolean stringElements) { - // Object-level compilation boundaries belong to the final element codec. The collection keeps - // its one ordinary element call so member-group receiver profiles cannot be skewed by element - // cardinality. + // The collection owns iteration and calls the final element codec's ordinary writeUtf8 entry. + // That entry owns null handling and, for a qualifying generated object, reaches the shared + // object-body trampoline itself. Do not call JsonTrampolineInvoke from this loop: collection + // cardinality would then dominate the body/group receiver profile and the element entry would + // no longer be the owner of its complete value semantics. String elementWrite = stringElements ? "String element = (String) list.get(index);\n" 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 dbd2240ac0..89b2cc600b 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 @@ -82,6 +82,9 @@ String memberGroupMethod() { @Override Class memberGroupType() { + // Returning the interface type opts only generated UTF-8 object bodies and member groups into + // JsonTrampolineInvoke's shared callsite. Collection loops keep their ordinary element-codec + // call, and other writer representations keep their own measured method layout. return Utf8WriterCodec.class; } 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 fba9bcf0e3..ee796a9280 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 @@ -852,6 +852,13 @@ public float readFloatTokenValue() { return readFloatToken(); } + // Long parsing deliberately repeats the initial digit checks, zero handling, block scan, and + // short tail used by Int parsing instead of sharing one generic token loop. The widths have + // different safe digit counts, overflow rules, and runtime profiles; a small shared helper lets + // one profile determine both callers' inline layout and loses the width-specific locals. Keep + // malformed input and overflow in their cold tails. Do not deduplicate this common path without + // matched intrinsic and aggregate C2 evidence, and never add padding or benchmark-specific + // digit-count branches to create an inline boundary. private long readLongToken() { byte[] bytes = input; int offset = position; 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 838ba4849e..b5ab0df76d 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 @@ -261,6 +261,25 @@ public void writeChar(char value) { writeByteRaw((byte) '"'); } + /** + * Writes one String through a deliberately independent UTF-8 representation boundary. + * + *

The compact-Latin1 common entry is intentionally large enough, through real representation + * dispatch, scanning, validation, and copying work, to exceed HotSpot JDK 25's 325-byte + * hot-inline limit. Generated object and collection code must call this method directly and + * account only for that invocation in its own group budget. If this method is reduced to a small + * wrapper around deeper helpers, C2 can absorb the wrapper and choose different transitive String + * closures according to compilation order. + * + *

Fixed-length Latin1 paths belong to this representation owner. When a measured layout keeps + * those paths directly in this method, repeated loads, predicates, and stores are intentional: + * extracting them merely to reduce source duplication can make C2's changing inline budget decide + * which String lengths pay another call. The local form also keeps {@code buffer}, {@code + * position}, and the cursor visible to C2. Keep escape, Unicode, malformed-input, and + * arbitrary-length work in their existing cold or long-tail methods; do not enlarge this method + * with cold behavior merely to preserve its size, and never use bytecode padding or compiler + * directives. + */ @Override public void writeString(String value) { if (STRING_BYTES_BACKED) { @@ -796,14 +815,30 @@ public void writeStringArray(String[] values) { writeArrayEnd(); } + /** + * Writes a long array as one naturally independent generated-caller boundary. + * + *

The first element, even-length element, and two pair-loop lanes intentionally contain the + * same signed-Long to Int/full-width dispatch. The repeated real work keeps this method above + * HotSpot JDK 25's 325-byte hot-inline limit, so a generated object group retains only the array + * call and the array loop forms its own level-4 nmethod. A small shared element helper is not an + * equivalent cleanup: it can be inlined first and then pull the complete array closure into the + * generated caller according to compilation timing. + * + *

The duplication also preserves the pair-unrolled control flow, one capacity check per + * unrolled chunk, and direct {@code buffer}/{@code position} state. It is valid for Long encoding + * to repeat the Int-range fast path when that is faster than a generic numeric helper. Do not + * deduplicate these lanes unless generated-source, {@code javap}, PrintInlining, LogCompilation, + * nmethod, allocation, and intrinsic/aggregate benchmarks prove the same stable boundary and no + * regression. This is data-shape-independent code; never replace it with fixed array-length + * assumptions. + */ public void writeLongArray(long[] values) { enterDepth(); ensure(2); buffer[position++] = '['; int length = values.length; if (length != 0) { - // Keep the signed-Long dispatch in each pair-loop lane. Extracting this real work into a - // small helper lets compilation order move the array closure into generated object methods. ensure(22); { long value = values[0]; @@ -1857,6 +1892,11 @@ private void writeIntNoEnsure(int value) { writePositiveIntNoEnsure(value); } + // Keep general Long and generated-field Long entries separate even though their common code is + // intentionally repeated. They have different range profiles and callers; merging them into a + // generic scalar helper lets one profile dictate the other's inline shape. Reusing the + // independently compiled positive Int and full-width Long leaves is safe because those leaves + // own complete formatting work and pass buffer/cursor state explicitly. private void writeLongNoEnsure(long value) { if (value == Long.MIN_VALUE) { writeRawNoEnsure(MIN_LONG_BYTES); @@ -1873,8 +1913,9 @@ private void writeLongNoEnsure(long value) { position = writePositiveLong(buffer, position, value); } - // Generated object fields and general scalar Long calls have independent range profiles. Keep - // their entries separate; array element dispatch is owned directly by writeLongArray. + // This duplicate entry is owned by generated object fields. Do not merge it with + // writeLongNoEnsure merely to remove source repetition: field and scalar callers have independent + // range/type profiles, while array element dispatch is deliberately owned by writeLongArray. private void writeLongFieldNoEnsure(long value) { if (value == Long.MIN_VALUE) { writeRawNoEnsure(MIN_LONG_BYTES); @@ -1895,9 +1936,13 @@ private void writePositiveIntNoEnsure(int value) { position = writePositiveInt(buffer, position, value); } - // Keep the full-width formatter behind one natural C2 boundary. The explicit byte/cursor - // carrier lets its callers publish the returned cursor once without giving the large callee - // ownership of mutable writer state. + // The full-width formatter is one real-work owner shared by the deliberately separate scalar, + // field, and array entries above. Passing the byte array and cursor and returning the new cursor + // lets each caller keep buffer/position state live across the call and publish position once. + // Do not replace this with a writer callback, carrier object, or mutable-writer lookup. Whether + // this leaf inlines is measured independently; the guaranteed greater-than-325-BCI boundary for + // long[] is writeLongArray, so do not copy this formatter into generated callers merely to make + // another method large. private static int writePositiveLong(byte[] bytes, int pos, long value) { long high = value / EIGHT_DIGITS; int low = (int) (value - high * EIGHT_DIGITS);