From 840000b513ddb5b64bb7366ca85e3bdda2efeea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Thu, 25 Jun 2026 12:09:15 +0800 Subject: [PATCH 1/6] perf(java): improve compatible mode performance --- .../fory/builder/BaseObjectCodecBuilder.java | 179 +++++++++++++++--- .../builder/LayerMarkerClassGenerator.java | 2 +- .../fory/builder/ObjectCodecBuilder.java | 5 +- .../apache/fory/context/MetaWriteContext.java | 92 ++++++++- .../org/apache/fory/context/WriteContext.java | 2 +- .../org/apache/fory/memory/MemoryBuffer.java | 4 +- .../apache/fory/resolver/TypeResolver.java | 103 +++++++++- .../CompatibleLayerSerializerBase.java | 6 +- .../fory/serializer/EnumSerializer.java | 16 +- .../serializer/UnknownClassSerializers.java | 6 +- .../collection/ChildContainerSerializers.java | 4 +- .../collection/CollectionLikeSerializer.java | 130 +++++++++++++ .../org/apache/fory/memory/MemoryBuffer.java | 4 +- 13 files changed, 491 insertions(+), 62 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index c431f7ec7c..e76176dc33 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -701,6 +701,18 @@ private Expression serializeForNotNullObjectForField( Expression inputObject, Expression buffer, Descriptor descriptor, Expression serializer) { TypeRef typeRef = descriptor.getTypeRef(); Class clz = getRawType(typeRef); + if (isEnumType(clz)) { + Expression enumSerializer = + cast( + serializer == null ? getSerializerForField(clz) : serializer, + TypeRef.of(EnumSerializer.class)); + return new Invoke( + enumSerializer, + "writeValue", + writeContextRef(), + buffer, + cast(inputObject, TypeRef.of(Enum.class))); + } if (serializer != null) { return new Invoke(serializer, writeMethodName, writeContextRef(), inputObject); } @@ -717,6 +729,10 @@ private Expression getSerializerForField(Class cls) { return getOrCreateSerializer(cls, true); } + private static boolean isEnumType(Class cls) { + return cls != Enum.class && Enum.class.isAssignableFrom(cls); + } + protected Expression serializeForNullable( Expression inputObject, Expression buffer, TypeRef typeRef, boolean nullable) { return serializeForNullable(inputObject, buffer, typeRef, null, false, nullable); @@ -885,6 +901,7 @@ protected Expression writeForNotNullNonFinalObject( Class clz = getRawType(typeRef); Expression clsExpr = new Invoke(inputObject, "getClass", "cls", CLASS_TYPE); ListExpression writeClassAndObject = new ListExpression(); + Expression exactClassWrite = exactClassWrite(inputObject, clz); Tuple2 classInfoRef = addTypeInfoField(clz); Expression classInfo = classInfoRef.f0; if (classInfoRef.f1) { @@ -904,12 +921,24 @@ protected Expression writeForNotNullNonFinalObject( PRIMITIVE_VOID_TYPE, writeContextRef(), inputObject)); + Expression write = + exactClassWrite == null + ? writeClassAndObject + : new If(eq(clsExpr, getClassExpr(clz)), exactClassWrite, writeClassAndObject, false); return invokeGenerated( - ctx, - writeCutPoints(buffer, inputObject), - writeClassAndObject, - "writeClassAndObject", - false); + ctx, writeCutPoints(buffer, inputObject), write, "writeClassAndObject", false); + } + + private Expression exactClassWrite(Expression inputObject, Class clz) { + if (clz.isInterface() || Modifier.isAbstract(clz.getModifiers())) { + return null; + } + Reference typeInfo = addExactTypeInfoField(clz); + Expression serializer = getOrCreateSerializer(clz); + return new ListExpression( + typeResolver(r -> r.writeClassExpr(typeResolverRef, writeContextRef(), typeInfo)), + new Invoke( + serializer, writeMethodName, PRIMITIVE_VOID_TYPE, writeContextRef(), inputObject)); } protected Expression writeTypeInfo( @@ -1128,6 +1157,21 @@ protected Tuple2 addTypeInfoField(Class cls) { return classInfoRef; } + protected Reference addExactTypeInfoField(Class cls) { + String key = "exactClassInfo:" + cls; + Reference reference = (Reference) sharedFieldMap.get(key); + if (reference != null) { + return reference; + } + Expression classInfoExpr = + inlineInvoke(typeResolverRef, "getTypeInfo", classInfoTypeRef, getClassExpr(cls)); + String name = ctx.newName(ctx.newName(cls) + "ExactTypeInfo"); + ctx.addField(true, ctx.type(TypeInfo.class), name, classInfoExpr); + reference = fieldRef(name, classInfoTypeRef); + sharedFieldMap.put(key, reference); + return reference; + } + protected Reference addTypeInfoHolderField(Class cls) { // Final type need to write classinfo when meta share enabled. String key; @@ -1325,7 +1369,7 @@ protected Expression writeCollectionData( Class elemClass = TypeUtils.getRawType(elementType); boolean trackingRef = needWriteRef(elementType); Tuple2 writeElementsHeader = - writeElementsHeader(elemClass, trackingRef, serializer, buffer, collection); + writeElementsHeader(elemClass, trackingRef, serializer, buffer, collection, size); Expression flags = writeElementsHeader.f0; builder.add(flags); boolean finalType = isMonomorphic(elemClass); @@ -1334,11 +1378,18 @@ protected Expression writeCollectionData( builder.add( writeContainerElements(elementType, true, null, null, buffer, collection, size)); } else { + Expression declSameNoNull = + eq(flags, ofInt(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL), "declSameNoNull"); Literal hasNullFlag = ofInt(CollectionFlags.HAS_NULL); Expression hasNull = eq(new BitAnd(flags, hasNullFlag), hasNullFlag, "hasNull"); builder.add( + declSameNoNull, hasNull, - writeContainerElements(elementType, false, null, hasNull, buffer, collection, size)); + new If( + declSameNoNull, + writeContainerElements(elementType, false, null, null, buffer, collection, size), + writeContainerElements(elementType, false, null, hasNull, buffer, collection, size), + false)); } } else { Literal flag = ofInt(CollectionFlags.IS_SAME_TYPE); @@ -1398,9 +1449,28 @@ protected Expression writeCollectionData( differentTypeWrite); } else { // if declared elem type don't track ref, all elements must not write ref. + Expression declSameNoNull = + eq(flags, ofInt(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL), "declSameNoNull"); Literal hasNullFlag = ofInt(CollectionFlags.HAS_NULL); Expression hasNull = eq(new BitAnd(flags, hasNullFlag), hasNullFlag, "hasNull"); - builder.add(hasNull); + builder.add(declSameNoNull, hasNull); + Expression declaredNoNullWrite = null; + if (maybeDecl) { + declaredNoNullWrite = + invokeGenerated( + ctx, + writeCutPoints(buffer, collection, size), + writeContainerElements( + elementType, + false, + cast(getOrCreateSerializer(elemClass), serializerType), + null, + buffer, + collection, + size), + "declSameNoNullWrite", + false); + } ListExpression writeBuilder = new ListExpression(elemSerializer); writeBuilder.add( writeContainerElements( @@ -1415,6 +1485,9 @@ protected Expression writeCollectionData( invokeGenerated(ctx, cutPoint, writeBuilder, "sameElementClassWrite", false), writeContainerElements( elementType, false, null, hasNull, buffer, collection, size)); + if (declaredNoNullWrite != null) { + action = new If(declSameNoNull, declaredNoNullWrite, action, false); + } } builder.add(action); } @@ -1436,7 +1509,9 @@ private Tuple2 writeElementsHeader( boolean trackingRef, Expression collectionSerializer, Expression buffer, - Expression value) { + Expression value, + Expression size) { + boolean isList = List.class.isAssignableFrom(getRawType(value.type())); if (isMonomorphic(elementType)) { Expression bitmap; if (trackingRef) { @@ -1445,9 +1520,24 @@ private Tuple2 writeElementsHeader( new Invoke(buffer, "writeByte", ofInt(CollectionFlags.DECL_SAME_TYPE_TRACKING_REF)), ofInt(CollectionFlags.DECL_SAME_TYPE_TRACKING_REF)); } else { - bitmap = - new Invoke( - collectionSerializer, "writeNullabilityHeader", PRIMITIVE_INT_TYPE, buffer, value); + if (isList) { + bitmap = + new Invoke( + collectionSerializer, + "writeNullabilityHeader", + PRIMITIVE_INT_TYPE, + buffer, + value, + size); + } else { + bitmap = + new Invoke( + collectionSerializer, + "writeNullabilityHeader", + PRIMITIVE_INT_TYPE, + buffer, + value); + } } return Tuple2.of(bitmap, null); } else { @@ -1476,15 +1566,28 @@ private Tuple2 writeElementsHeader( classInfoHolder); } } else { - bitmap = - new Invoke( - collectionSerializer, - "writeTypeNullabilityHeader", - PRIMITIVE_INT_TYPE, - writeContextRef(), - value, - elementTypeExpr, - classInfoHolder); + if (isList) { + bitmap = + new Invoke( + collectionSerializer, + "writeTypeNullabilityHeader", + PRIMITIVE_INT_TYPE, + writeContextRef(), + value, + size, + elementTypeExpr, + classInfoHolder); + } else { + bitmap = + new Invoke( + collectionSerializer, + "writeTypeNullabilityHeader", + PRIMITIVE_INT_TYPE, + writeContextRef(), + value, + elementTypeExpr, + classInfoHolder); + } } Invoke serializer = new Invoke(classInfoHolder, "getSerializer", SERIALIZER_TYPE); return Tuple2.of(bitmap, serializer); @@ -2344,6 +2447,14 @@ private Expression deserializeForNotNullForField( return StringSerializer.readStringExpr( getOrCreateStringSerializer(), buffer, config.compressString()); } + if (isEnumType(cls)) { + Expression enumSerializer = + cast( + serializer == null ? getSerializerForField(cls) : serializer, + TypeRef.of(EnumSerializer.class)); + return new Invoke( + enumSerializer, "readValue", TypeRef.of(Enum.class), readContextRef(), buffer); + } Expression obj; if (usesPrimitiveListArrayProtocol(descriptor)) { serializer = getPrimitiveListArraySerializer(cls); @@ -2588,14 +2699,20 @@ protected Expression readForNotNullNonFinal( || typeInfo.getTypeId() == Types.NAMED_COMPATIBLE_STRUCT)) { String name = ctx.newName(StringUtils.uncapitalize(rawType.getSimpleName()) + "Class"); Expression clsExpr = staticClassFieldExpr(rawType, name); + Reference classInfoHolderRef = addTypeInfoHolderField(rawType); classInfo = inlineInvoke( - typeResolverRef, "readTypeInfo", classInfoTypeRef, readContextRef, clsExpr); + typeResolverRef, + "readTypeInfo", + classInfoTypeRef, + readContextRef, + clsExpr, + classInfoHolderRef); } else { classInfo = readTypeInfo(getRawType(typeRef), buffer); } } else { - classInfo = readTypeInfo(getRawType(typeRef), buffer); + classInfo = readTypeInfo(rawType, buffer); } serializer = inlineInvoke(classInfo, "getSerializer", SERIALIZER_TYPE); } @@ -2638,16 +2755,18 @@ protected Expression deserializeForCollection( Expression collection = new Invoke(serializer, "newCollection", COLLECTION_TYPE, readContextRef); Expression size = new Invoke(serializer, "getAndClearNumElements", "size", PRIMITIVE_INT_TYPE); - // if add branch by `ArrayList`, generated code will be > 325 bytes. - // and List#add is more likely be inlined if there is only one subclass. Expression hookRead = readCollectionCodegen(buffer, collection, size, elementType); hookRead = new Invoke(serializer, "onCollectionRead", OBJECT_TYPE, hookRead); - Expression action = - new If( - supportHook, - new ListExpression(collection, hookRead), - read(serializer, buffer, OBJECT_TYPE), + Expression fallbackAction = read(serializer, buffer, OBJECT_TYPE); + Expression fallbackRead = + invokeGenerated( + ctx, + readCutPoints(buffer, serializer), + new ListExpression(fallbackAction, new Return(fallbackAction)), + "readCollectionFallback", false); + Expression action = + new If(supportHook, new ListExpression(collection, hookRead), fallbackRead, false); if (invokeHint != null && invokeHint.genNewMethod) { invokeHint.add(buffer); invokeHint.add(readContextRef()); diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java b/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java index 3e761b55a2..c9e2e8c78d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java @@ -26,7 +26,7 @@ /** * Creates unique marker classes for each layer in a class hierarchy. These marker classes serve as - * unique keys in {@code metaContext.classMap} to distinguish different layers during serialization. + * unique keys in {@code MetaWriteContext} to distinguish different layers during serialization. * *

For a class hierarchy {@code C extends B extends A}, this generator creates unique marker * classes for: diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java index e22cd2b38c..b467361ac0 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java @@ -799,9 +799,10 @@ public Expression buildDecodeExpression() { Expression bean; if (!isRecord) { bean = newBean(); - Expression referenceObject = invokeReadContext("reference", bean); expressions.add(bean); - expressions.add(referenceObject); + if (typeResolver.trackingRef()) { + expressions.add(invokeReadContext("reference", bean)); + } } else { if (recordCtrAccessible) { bean = new FieldsCollector(); diff --git a/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java index a39d2ef0b2..84d164c2e0 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java @@ -19,8 +19,6 @@ package org.apache.fory.context; -import org.apache.fory.collection.IdentityObjectIntMap; - /** * Write-side state for meta-share serialization. * @@ -28,9 +26,93 @@ * already announced classes are not sent repeatedly. */ public class MetaWriteContext { + private static final int INITIAL_CAPACITY = 8; + private static final int MISSING_ID = -1; + + private Object[] keys = new Object[INITIAL_CAPACITY]; + private int[] idsBySlot = new int[INITIAL_CAPACITY]; + private int[] usedSlots = new int[INITIAL_CAPACITY]; + private int mask = INITIAL_CAPACITY - 1; + private int threshold = INITIAL_CAPACITY >> 1; + private int size; + + /** Returns the next protocol id that will be assigned to a new metadata key. */ + public int size() { + return size; + } + /** - * Classes whose definitions have already been announced to the peer, mapped to the protocol id - * used by the current or shared meta-share session. + * Returns the existing protocol id for {@code key}, or records it and returns {@code -1}. + * + *

Meta-share keys use identity semantics. Class objects and layer marker classes are already + * identity keys, and unknown-struct TypeDef ids historically used the same identity map path. */ - public final IdentityObjectIntMap> classMap = new IdentityObjectIntMap<>(1, 0.5f); + public int putOrGetMetaId(Object key) { + if (key == null) { + throw new NullPointerException("Meta key must not be null"); + } + int slot = locate(key, keys, mask); + if (slot >= 0) { + return idsBySlot[slot]; + } + if (size >= threshold) { + resize(keys.length << 1); + slot = locate(key, keys, mask); + } + slot = -(slot + 1); + int id = size; + keys[slot] = key; + idsBySlot[slot] = id; + usedSlots[id] = slot; + size = id + 1; + return MISSING_ID; + } + + /** Clears operation-local metadata ids while retaining reusable table storage. */ + public void reset() { + int size = this.size; + if (size == 0) { + return; + } + Object[] keys = this.keys; + int[] usedSlots = this.usedSlots; + for (int i = 0; i < size; i++) { + keys[usedSlots[i]] = null; + } + this.size = 0; + } + + private static int locate(Object key, Object[] keys, int mask) { + for (int slot = System.identityHashCode(key) & mask; ; slot = (slot + 1) & mask) { + Object other = keys[slot]; + if (other == null) { + return -(slot + 1); + } + if (other == key) { + return slot; + } + } + } + + private void resize(int newCapacity) { + Object[] oldKeys = keys; + int[] oldUsedSlots = usedSlots; + int oldSize = size; + Object[] newKeys = new Object[newCapacity]; + int[] newIdsBySlot = new int[newCapacity]; + int[] newUsedSlots = new int[newCapacity]; + int newMask = newCapacity - 1; + for (int id = 0; id < oldSize; id++) { + Object key = oldKeys[oldUsedSlots[id]]; + int slot = -(locate(key, newKeys, newMask) + 1); + newKeys[slot] = key; + newIdsBySlot[slot] = id; + newUsedSlots[id] = slot; + } + keys = newKeys; + idsBySlot = newIdsBySlot; + usedSlots = newUsedSlots; + mask = newMask; + threshold = newCapacity >> 1; + } } diff --git a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java index ebab5330b0..b2be9b7eb5 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java @@ -295,7 +295,7 @@ public void reset() { contextObjects.clear(); } if (scopedMetaShareEnabled) { - metaWriteContext.classMap.clear(); + metaWriteContext.reset(); } else { metaWriteContext = null; } diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java index 94dd961889..c0f002ca98 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java @@ -962,7 +962,9 @@ public void writeByte(byte value) { } else { final int writerIdx = writerIndex; final int newIdx = writerIdx + 1; - ensure(newIdx); + if (newIdx > size) { + globalAllocator.grow(this, newIdx); + } final long pos = address + writerIdx; UNSAFE.putByte(heapMemory, pos, value); writerIndex = newIdx; diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index b104985527..a480f45325 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -55,7 +55,6 @@ import org.apache.fory.collection.BiMap; import org.apache.fory.collection.ConcurrentIdentityMap; import org.apache.fory.collection.IdentityMap; -import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.Tuple2; import org.apache.fory.config.Config; @@ -587,9 +586,8 @@ protected final void writeSharedClassMeta(WriteContext writeContext, TypeInfo ty MemoryBuffer buffer = writeContext.getBuffer(); MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); assert metaWriteContext != null : SET_META_WRITE_CONTEXT_MSG; - IdentityObjectIntMap> classMap = metaWriteContext.classMap; - int newId = classMap.size; - int id = classMap.putOrGet(typeInfo.type, newId); + int newId = metaWriteContext.size(); + int id = metaWriteContext.putOrGetMetaId(typeInfo.type); if (id >= 0) { // Reference to previously written type: (index << 1) | 1, LSB=1 buffer.writeVarUInt32((id << 1) | 1); @@ -626,7 +624,7 @@ public final TypeInfo readTypeInfo(ReadContext readContext) { case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32())); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), typeInfoCache); break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: @@ -671,7 +669,7 @@ public final TypeInfo readTypeInfo(ReadContext readContext, Class targetClass case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32())); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), typeInfoCache); break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: @@ -703,6 +701,60 @@ public final TypeInfo readTypeInfo(ReadContext readContext, Class targetClass return typeInfo; } + /** + * Read class info from buffer using a target class and generated field-local cache. + * + *

The holder caches only checked metadata owned by this resolver. It never caches payload + * data. + */ + @CodegenInvoke + public final TypeInfo readTypeInfo( + ReadContext readContext, Class targetClass, TypeInfoHolder classInfoHolder) { + MemoryBuffer buffer = readContext.getBuffer(); + int typeId = buffer.readUInt8(); + TypeInfo typeInfo; + boolean updateCache = false; + switch (typeId) { + case Types.ENUM: + case Types.STRUCT: + case Types.EXT: + case Types.TYPED_UNION: + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), classInfoHolder.typeInfo); + updateCache = typeInfo != classInfoHolder.typeInfo; + break; + case Types.COMPATIBLE_STRUCT: + case Types.NAMED_COMPATIBLE_STRUCT: + typeInfo = readSharedClassMeta(readContext, targetClass, classInfoHolder); + break; + case Types.NAMED_ENUM: + case Types.NAMED_STRUCT: + case Types.NAMED_EXT: + case Types.NAMED_UNION: + if (!metaContextShareEnabled) { + typeInfo = readTypeInfoFromBytes(readContext, classInfoHolder.typeInfo, typeId); + updateCache = true; + } else { + typeInfo = readSharedClassMeta(readContext, targetClass, classInfoHolder); + } + break; + case Types.LIST: + typeInfo = readListTypeInfo(readContext); + break; + case Types.TIMESTAMP: + typeInfo = readTimestampTypeInfo(readContext); + break; + default: + typeInfo = Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId)); + } + if (typeInfo.serializer == null) { + typeInfo = ensureSerializerForTypeInfo(typeInfo); + } + if (updateCache) { + classInfoHolder.typeInfo = typeInfo; + } + return typeInfo; + } + /** * Read class info from buffer with TypeInfo cache. This version is faster than {@link * #readTypeInfo(ReadContext)} because it uses the provided classInfoCache to reduce map lookups @@ -721,7 +773,7 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfo typeInfoCac case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32())); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), typeInfoCache); break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: @@ -770,7 +822,8 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32())); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), classInfoHolder.typeInfo); + updateCache = typeInfo != classInfoHolder.typeInfo; break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: @@ -805,6 +858,14 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class return typeInfo; } + private TypeInfo readRegisteredTypeInfo(int typeId, int userTypeId, TypeInfo cachedTypeInfo) { + TypeInfo typeInfo = cachedTypeInfo; + if (typeInfo == null || typeInfo.typeId != typeId || typeInfo.userTypeId != userTypeId) { + typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(userTypeId)); + } + return typeInfo; + } + /** * Read class info using the provided cache. Returns cached TypeInfo if the namespace and type * name bytes match. @@ -850,6 +911,18 @@ protected final TypeInfo readTypeInfoFromBytes( public final TypeInfo readSharedClassMeta(ReadContext readContext, Class targetClass) { TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass); + return adaptSharedClassTarget(typeInfo, targetClass); + } + + private TypeInfo readSharedClassMeta( + ReadContext readContext, Class targetClass, TypeInfoHolder classInfoHolder) { + TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass, classInfoHolder.typeInfo); + typeInfo = adaptSharedClassTarget(typeInfo, targetClass); + classInfoHolder.typeInfo = typeInfo; + return typeInfo; + } + + private TypeInfo adaptSharedClassTarget(TypeInfo typeInfo, Class targetClass) { Class readClass = typeInfo.getType(); if (targetClass != readClass) { return getTargetTypeInfo(typeInfo, targetClass); @@ -858,6 +931,11 @@ public final TypeInfo readSharedClassMeta(ReadContext readContext, Class targ } private TypeInfo readSharedClassTypeInfo(ReadContext readContext, Class targetClass) { + return readSharedClassTypeInfo(readContext, targetClass, null); + } + + private TypeInfo readSharedClassTypeInfo( + ReadContext readContext, Class targetClass, TypeInfo cachedTypeInfo) { MemoryBuffer buffer = readContext.getBuffer(); MetaReadContext metaReadContext = readContext.getMetaReadContext(); assert metaReadContext != null : SET_META_READ_CONTEXT_MSG; @@ -875,7 +953,14 @@ private TypeInfo readSharedClassTypeInfo(ReadContext readContext, Class targe // body/hash/schema-limit/exact-local checks here; the header-miss path owns them before // cache publish. long id = buffer.readInt64(); - typeInfo = extRegistry.typeInfoByTypeDefId.get(id); + TypeDef cachedTypeDef = cachedTypeInfo == null ? null : cachedTypeInfo.getTypeDef(); + // A field-local cache hit is valid only when the cached TypeInfo carries the exact checked + // TypeDef id that was parsed and accepted earlier by this resolver. + if (cachedTypeDef != null && cachedTypeDef.getId() == id) { + typeInfo = cachedTypeInfo; + } else { + typeInfo = extRegistry.typeInfoByTypeDefId.get(id); + } if (typeInfo != null) { TypeDef.skipTypeDef(buffer, id); } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java index e7040f16f4..e7ce30603e 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java @@ -19,7 +19,6 @@ package org.apache.fory.serializer; -import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.ObjectIntMap; import org.apache.fory.context.CopyContext; import org.apache.fory.context.MetaWriteContext; @@ -87,9 +86,8 @@ public void writeLayerClassMeta(WriteContext writeContext) { if (metaWriteContext == null) { return; } - IdentityObjectIntMap> classMap = metaWriteContext.classMap; - int newId = classMap.size; - int id = classMap.putOrGet(layerMarkerClass, newId); + int newId = metaWriteContext.size(); + int id = metaWriteContext.putOrGetMetaId(layerMarkerClass); if (id >= 0) { buffer.writeVarUInt32((id << 1) | 1); } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java index f07098d6c1..7e7e1d27bc 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java @@ -26,11 +26,13 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import org.apache.fory.annotation.CodegenInvoke; import org.apache.fory.annotation.ForyEnumId; import org.apache.fory.collection.LongMap; import org.apache.fory.config.Config; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; +import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.util.Preconditions; @SuppressWarnings("rawtypes") @@ -65,15 +67,25 @@ public EnumSerializer(Config config, Class cls) { @Override public void write(WriteContext writeContext, Enum value) { + writeValue(writeContext, writeContext.getBuffer(), value); + } + + @CodegenInvoke + public final void writeValue(WriteContext writeContext, MemoryBuffer buffer, Enum value) { if (!config.isXlang() && config.serializeEnumByName()) { writeContext.writeString(value.name()); } else { - writeContext.getBuffer().writeVarUInt32Small7(tagByOrdinal[value.ordinal()]); + buffer.writeVarUInt32Small7(tagByOrdinal[value.ordinal()]); } } @Override public Enum read(ReadContext readContext) { + return readValue(readContext, readContext.getBuffer()); + } + + @CodegenInvoke + public final Enum readValue(ReadContext readContext, MemoryBuffer buffer) { if (!config.isXlang() && config.serializeEnumByName()) { String name = readContext.readString(); Enum e = stringToEnum.get(name); @@ -82,7 +94,7 @@ public Enum read(ReadContext readContext) { } return handleUnknownEnumValue(name); } else { - int tag = readContext.getBuffer().readVarUInt32Small7(); + int tag = buffer.readVarUInt32Small7(); Enum value = null; if (enumConstantByTagArray != null && tag < enumConstantByTagArray.length) { value = enumConstantByTagArray[tag]; diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java index ef03486e11..5fdae10109 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.List; -import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.MapEntry; import org.apache.fory.config.Config; @@ -92,10 +91,9 @@ public UnknownStructSerializer(TypeResolver typeResolver, TypeDef typeDef) { private void writeTypeDef(WriteContext writeContext, UnknownClass.UnknownStruct value) { MemoryBuffer buffer = writeContext.getBuffer(); MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); - IdentityObjectIntMap classMap = metaWriteContext.classMap; - int newId = classMap.size; + int newId = metaWriteContext.size(); // class not exist, use class def id for identity. - int id = classMap.putOrGet(value.typeDef.getId(), newId); + int id = metaWriteContext.putOrGetMetaId(value.typeDef.getId()); if (id >= 0) { // Reference to previously written type: (index << 1) | 1, LSB=1 buffer.writeVarUInt32((id << 1) | 1); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java index f7840349ef..acb1700311 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java @@ -710,8 +710,8 @@ private static void readAndSkipLayerClassMeta(ReadContext readContext) { // New type - need to read and skip the TypeDef bytes long id = buffer.readInt64(); TypeDef.skipTypeDef(buffer, id); - // Add a placeholder to keep readTypeInfos indices in sync with the write side's classMap. - // The write side (writeLayerClassMeta) adds layer marker classes to classMap which shares + // Add a placeholder to keep readTypeInfos indices in sync with the write side's meta ids. + // The write side (writeLayerClassMeta) adds layer marker classes to the metadata table, sharing // the same index space as writeSharedClassMeta. Without this placeholder, subsequent // readSharedClassMeta reference lookups would use wrong indices. metaReadContext.readTypeInfos.add(null); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java index 3915b5d888..221b104508 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java @@ -22,6 +22,7 @@ import java.lang.invoke.MethodHandle; import java.lang.reflect.Constructor; import java.util.Collection; +import java.util.List; import org.apache.fory.Fory; import org.apache.fory.annotation.CodegenInvoke; import org.apache.fory.config.Config; @@ -178,6 +179,44 @@ public int writeNullabilityHeader(MemoryBuffer buffer, Collection value) { return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; } + /** Element type is final, write whether any elements is null. */ + @CodegenInvoke + public int writeNullabilityHeader(MemoryBuffer buffer, List value, int size) { + switch (size) { + case 1: + if (value.get(0) == null) { + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; + } + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; + case 2: + if (value.get(0) == null || value.get(1) == null) { + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; + } + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; + case 3: + if (value.get(0) == null || value.get(1) == null || value.get(2) == null) { + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; + } + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; + default: + break; + } + for (int i = 0; i < size; i++) { + if (value.get(i) == null) { + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; + } + } + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; + } + /** * Need to track elements ref, declared element type is not morphic, can't check elements * nullability. @@ -320,6 +359,97 @@ public int writeTypeNullabilityHeader( return bitmap; } + /** + * Element type is not final by {@link ClassResolver#isMonomorphic}, need to write element type. + * Elements ref tracking is disabled, write whether any elements is null. + */ + @CodegenInvoke + public int writeTypeNullabilityHeader( + WriteContext writeContext, + List value, + int size, + Class declareElementType, + TypeInfoHolder cache) { + MemoryBuffer buffer = writeContext.getBuffer(); + if (size > 0 && size <= 3) { + Object elem0 = value.get(0); + Object elem1 = size > 1 ? value.get(1) : null; + Object elem2 = size > 2 ? value.get(2) : null; + boolean containsNull = + elem0 == null || (size > 1 && elem1 == null) || (size > 2 && elem2 == null); + Class elemClass = + elem0 != null ? elem0.getClass() : elem1 != null ? elem1.getClass() : null; + if (elemClass == null && elem2 != null) { + elemClass = elem2.getClass(); + } + boolean hasDifferentClass = + elemClass != null + && ((elem0 != null && elem0.getClass() != elemClass) + || (elem1 != null && elem1.getClass() != elemClass) + || (elem2 != null && elem2.getClass() != elemClass)); + int bitmap = containsNull ? CollectionFlags.HAS_NULL : 0; + if (hasDifferentClass) { + buffer.writeByte(bitmap); + } else { + if (elemClass == null) { + elemClass = Object.class; + } + bitmap |= CollectionFlags.IS_SAME_TYPE; + // Write class in case peer doesn't have this class. + if (!config.isMetaShareEnabled() && elemClass == declareElementType) { + bitmap |= CollectionFlags.IS_DECL_ELEMENT_TYPE; + buffer.writeByte(bitmap); + } else { + buffer.writeByte(bitmap); + TypeResolver typeResolver = this.typeResolver; + TypeInfo typeInfo = typeResolver.getTypeInfo(elemClass, cache); + typeResolver.writeTypeInfo(writeContext, typeInfo); + } + } + return bitmap; + } + int bitmap = 0; + boolean containsNull = false; + boolean hasDifferentClass = false; + Class elemClass = null; + for (int i = 0; i < size; i++) { + Object elem = value.get(i); + if (elem == null) { + containsNull = true; + } else if (elemClass == null) { + elemClass = elem.getClass(); + } else { + if (!hasDifferentClass && elem.getClass() != elemClass) { + hasDifferentClass = true; + } + } + } + if (containsNull) { + bitmap |= CollectionFlags.HAS_NULL; + } + if (hasDifferentClass) { + buffer.writeByte(bitmap); + } else { + // When serialize a collection with all elements null directly, the declare type + // will be equal to element type: null + if (elemClass == null) { + elemClass = Object.class; + } + bitmap |= CollectionFlags.IS_SAME_TYPE; + // Write class in case peer doesn't have this class. + if (!config.isMetaShareEnabled() && elemClass == declareElementType) { + bitmap |= CollectionFlags.IS_DECL_ELEMENT_TYPE; + buffer.writeByte(bitmap); + } else { + buffer.writeByte(bitmap); + TypeResolver typeResolver = this.typeResolver; + TypeInfo typeInfo = typeResolver.getTypeInfo(elemClass, cache); + typeResolver.writeTypeInfo(writeContext, typeInfo); + } + } + return bitmap; + } + @Override public void write(WriteContext writeContext, T value) { Collection collection = onCollectionWrite(writeContext, value); diff --git a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java index 7fbf021ff5..52c4d0ce50 100644 --- a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java @@ -1108,7 +1108,9 @@ public void writeUInt8(int value) { public void writeByte(byte value) { final int writerIdx = writerIndex; final int newIdx = writerIdx + 1; - ensure(newIdx); + if (newIdx > size) { + globalAllocator.grow(this, newIdx); + } final long pos = address + writerIdx; storeByte(pos, value); writerIndex = newIdx; From 157aba07dcbce0fed58fd034e2645543aad97d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Thu, 25 Jun 2026 14:57:22 +0800 Subject: [PATCH 2/6] perf(java): improve compatible serialization paths --- .../src/main/java/org/apache/fory/Fory.java | 4 +- .../fory/builder/BaseObjectCodecBuilder.java | 19 ++++++-- .../org/apache/fory/context/ReadContext.java | 18 ++++++++ .../org/apache/fory/context/WriteContext.java | 28 ++++++++++++ .../apache/fory/resolver/TypeResolver.java | 6 ++- .../fory/serializer/StringSerializer.java | 43 ++++++++++++++++++- .../fory/serializer/StringSerializerTest.java | 41 ++++++++++++++++++ 7 files changed, 150 insertions(+), 9 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/Fory.java b/java/fory-core/src/main/java/org/apache/fory/Fory.java index 5ad083d69e..a7916400ae 100644 --- a/java/fory-core/src/main/java/org/apache/fory/Fory.java +++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java @@ -352,7 +352,7 @@ public MemoryBuffer serialize(MemoryBuffer buffer, Object obj, BufferCallback ca if (writeContext.getDepth() > 0) { throwDepthSerializationException(); } - writeContext.writeRef(obj); + writeContext.writeRootRef(obj); return buffer; } catch (Throwable t) { throw processSerializationError(t); @@ -512,7 +512,7 @@ public Object deserialize(MemoryBuffer buffer, Iterable outOfBandB if (readContext.getDepth() > 0) { throwDepthDeserializationException(); } - return readContext.readRef(); + return readContext.readRootRef(); } finally { jitContext.unlock(); } diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index e76176dc33..8546b06e84 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -1219,6 +1219,17 @@ protected Expression readTypeInfo(Class cls, Expression ignored, boolean inli } } + protected Expression readTypeInfoWithTarget(Class cls) { + Reference classInfoHolderRef = addTypeInfoHolderField(cls); + return inlineInvoke( + typeResolverRef, + "readTypeInfo", + classInfoTypeRef, + readContextRef, + getClassExpr(cls), + classInfoHolderRef); + } + protected TypeRef getSerializerType(TypeRef objType) { return getSerializerType(objType.getRawType()); } @@ -2783,8 +2794,10 @@ protected Expression deserializeForCollection( protected Expression readCollectionCodegen( Expression buffer, Expression collection, Expression size, TypeRef elementType) { ListExpression builder = new ListExpression(); - Invoke flags = new Invoke(buffer, "readByte", "flags", PRIMITIVE_INT_TYPE, false); - builder.add(flags); + Expression readerIndex = new Invoke(buffer, "readerIndex", "readerIndex", PRIMITIVE_INT_TYPE); + Expression flags = + cast(new Invoke(buffer, "_unsafeGetByte", PRIMITIVE_BYTE_TYPE, readerIndex), PRIMITIVE_INT_TYPE); + builder.add(readerIndex, flags, new Invoke(buffer, "_increaseReaderIndexUnsafe", ofInt(1))); Class elemClass = TypeUtils.getRawType(elementType); walkPath.add(elementType.toString()); boolean finalType = isMonomorphic(elemClass); @@ -2816,7 +2829,7 @@ protected Expression readCollectionCodegen( Literal isDeclTypeFlag = ofInt(CollectionFlags.IS_DECL_ELEMENT_TYPE); Expression isDeclType = eq(new BitAnd(flags, isDeclTypeFlag), isDeclTypeFlag); Invoke serializer = - inlineInvoke(readTypeInfo(elemClass, buffer), "getSerializer", SERIALIZER_TYPE); + inlineInvoke(readTypeInfoWithTarget(elemClass), "getSerializer", SERIALIZER_TYPE); TypeRef serializerType = getSerializerType(elementType); Expression elemSerializer; // make it in scope of `if(sameElementClass)` boolean maybeDecl = typeResolver(r -> r.isSerializable(elemClass)); diff --git a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java index 6dca2e503a..dcc7553b59 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java @@ -55,9 +55,11 @@ public final class ReadContext { private final Generics generics; private final TypeResolver typeResolver; private final RefReader refReader; + private final TypeInfoHolder rootTypeInfoHolder; private final MetaStringReader metaStringReader; private final StringSerializer stringSerializer; private final boolean crossLanguage; + private final boolean trackingRef; private final boolean compressInt; private final Int64Encoding longEncoding; private final int maxDepth; @@ -86,9 +88,11 @@ public ReadContext( this.generics = generics; this.typeResolver = typeResolver; this.refReader = refReader; + rootTypeInfoHolder = typeResolver.nilTypeInfoHolder(); this.metaStringReader = metaStringReader; stringSerializer = (StringSerializer) typeResolver.getSerializer(String.class); crossLanguage = config.isXlang(); + trackingRef = config.trackingRef(); compressInt = config.compressInt(); longEncoding = config.longEncoding(); maxDepth = config.maxDepth(); @@ -537,6 +541,20 @@ public Object readRef() { return refReader.getReadRef(); } + /** Reads the root object for one deserialization operation. */ + public Object readRootRef() { + if (trackingRef) { + return readRef(rootTypeInfoHolder); + } + MemoryBuffer buffer = this.buffer; + int headFlag = buffer.readByte(); + if (headFlag >= Fory.NOT_NULL_VALUE_FLAG) { + TypeInfo typeInfo = typeResolver.readTypeInfo(this, rootTypeInfoHolder); + return readNonRef(typeInfo); + } + return null; + } + /** Variant of {@link #readRef()} that uses already resolved {@link TypeInfo}. */ public Object readRef(TypeInfo typeInfo) { int nextReadRefId = refReader.tryPreserveRefId(buffer); diff --git a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java index b2be9b7eb5..745a992f77 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java @@ -56,9 +56,11 @@ public final class WriteContext { private final Generics generics; private final TypeResolver typeResolver; private final RefWriter refWriter; + private final TypeInfoHolder rootTypeInfoHolder; private final MetaStringWriter metaStringWriter; private final StringSerializer stringSerializer; private final boolean crossLanguage; + private final boolean trackingRef; private final boolean compressInt; private final Int64Encoding longEncoding; private final boolean forVirtualThread; @@ -85,9 +87,11 @@ public WriteContext( this.generics = generics; this.typeResolver = typeResolver; this.refWriter = refWriter; + rootTypeInfoHolder = typeResolver.nilTypeInfoHolder(); this.metaStringWriter = metaStringWriter; stringSerializer = (StringSerializer) typeResolver.getSerializer(String.class); crossLanguage = config.isXlang(); + trackingRef = config.trackingRef(); compressInt = config.compressInt(); longEncoding = config.longEncoding(); forVirtualThread = config.forVirtualThread(); @@ -460,6 +464,30 @@ public void writeRef(Object obj) { } } + /** Writes the root object for one serialization operation. */ + public void writeRootRef(Object obj) { + if (trackingRef) { + writeRef(obj, rootTypeInfoHolder); + return; + } + MemoryBuffer buffer = this.buffer; + if (obj == null) { + buffer.writeByte(Fory.NULL_FLAG); + return; + } + buffer.writeByte(Fory.NOT_NULL_VALUE_FLAG); + TypeResolver resolver = typeResolver; + TypeInfo typeInfo = resolver.getTypeInfo(obj.getClass(), rootTypeInfoHolder); + if (crossLanguage && typeInfo.getType() == UnknownStruct.class) { + depth++; + typeInfo.getSerializer().write(this, obj); + depth--; + return; + } + resolver.writeTypeInfo(this, typeInfo); + writeData(typeInfo, obj); + } + /** Variant of {@link #writeRef(Object)} that reuses a cached type-info holder. */ public void writeRef(Object obj, TypeInfoHolder classInfoHolder) { MemoryBuffer buffer = this.buffer; diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index a480f45325..e2e845c1dc 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -827,7 +827,8 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: - typeInfo = readSharedClassTypeInfo(readContext, null); + typeInfo = readSharedClassTypeInfo(readContext, null, classInfoHolder.typeInfo); + updateCache = typeInfo != classInfoHolder.typeInfo; break; case Types.NAMED_ENUM: case Types.NAMED_STRUCT: @@ -837,7 +838,8 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class typeInfo = readTypeInfoFromBytes(readContext, classInfoHolder.typeInfo, typeId); updateCache = true; } else { - typeInfo = readSharedClassTypeInfo(readContext, null); + typeInfo = readSharedClassTypeInfo(readContext, null, classInfoHolder.typeInfo); + updateCache = typeInfo != classInfoHolder.typeInfo; } break; case Types.LIST: diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java index bb3d93f22a..a331f85fdb 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java @@ -125,7 +125,7 @@ public static Expression writeStringExpr( if (compressString) { return new Invoke(strSerializer, "writeCompressedBytesString", buffer, str); } else { - return new StaticInvoke(StringSerializer.class, "writeBytesString", buffer, str); + return new StaticInvoke(StringSerializer.class, "writeBytesStringCodegen", buffer, str); } } else { if (!STRING_VALUE_FIELD_IS_CHARS) { @@ -529,6 +529,45 @@ public static void writeBytesString(MemoryBuffer buffer, String value) { writeBytesString(buffer, coder, bytes); } + @Internal + @CodegenInvoke + public static void writeBytesStringCodegen(MemoryBuffer buffer, String value) { + byte[] bytes = (byte[]) getStringValue(value); + byte coder = getStringCoder(value); + writeBytesStringCodegen(buffer, coder, bytes); + } + + private static void writeBytesStringCodegen(MemoryBuffer buffer, byte coder, byte[] bytes) { + if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { + writeBytesStringUTF16BE(buffer, bytes); + return; + } + int bytesLen = bytes.length; + long header = ((long) bytesLen << 2) | coder; + int writerIndex = buffer.writerIndex(); + buffer.ensure(writerIndex + 9 + bytesLen); + final byte[] targetArray = buffer.getHeapMemory(); + if (targetArray != null) { + final int targetIndex = buffer._unsafeHeapWriterIndex(); + int arrIndex = targetIndex; + if (header >>> 7 == 0) { + targetArray[arrIndex++] = (byte) header; + } else if (header >>> 14 == 0) { + targetArray[arrIndex++] = (byte) ((header & 0x7F) | 0x80); + targetArray[arrIndex++] = (byte) (header >>> 7); + } else { + arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); + } + writerIndex += arrIndex - targetIndex; + System.arraycopy(bytes, 0, targetArray, arrIndex, bytesLen); + } else { + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + PlatformStringUtils.putBytes(buffer, writerIndex, bytes, bytesLen); + } + writerIndex += bytesLen; + buffer._unsafeWriterIndex(writerIndex); + } + public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] bytes) { if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { writeBytesStringUTF16BE(buffer, bytes); @@ -644,7 +683,7 @@ public byte[] readBytesUnCompressedUTF16(MemoryBuffer buffer, int numBytes) { byte[] heapMemory = buffer.getHeapMemory(); if (heapMemory != null) { final int arrIndex = buffer._unsafeHeapReaderIndex(); - buffer.increaseReaderIndex(numBytes); + buffer._increaseReaderIndexUnsafe(numBytes); bytes = new byte[numBytes]; System.arraycopy(heapMemory, arrIndex, bytes, 0, numBytes); } else { diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java index 756c00e554..c7b10e1ad8 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java @@ -60,6 +60,47 @@ public void testRejectOddUtf16ByteSize() { Assert.assertThrows(IllegalArgumentException.class, () -> serializer.readString(buffer)); } + @Test + public void testCodegenByteStringWire() { + if (!stringValueIsBytes()) { + throw new SkipException("Skip when string value is not byte[]"); + } + String[] values = { + "", + "a", + StringUtils.random(31), + StringUtils.random(40), + StringUtils.random(5000), + "你好", + "abc你好" + }; + for (String value : values) { + int capacity = Math.max(64, value.length() * 8 + 64); + MemoryBuffer control = MemoryBuffer.newHeapBuffer(capacity); + MemoryBuffer codegen = MemoryBuffer.newHeapBuffer(capacity); + StringSerializer.writeBytesString(control, value); + StringSerializer.writeBytesStringCodegen(codegen, value); + Assert.assertEquals( + codegen.getBytes(0, codegen.writerIndex()), + control.getBytes(0, control.writerIndex()), + value); + Assert.assertEquals( + readJDK11String(MemoryBuffer.fromByteArray(codegen.getBytes(0, codegen.writerIndex()))), + value); + } + } + + private static boolean stringValueIsBytes() { + try { + Field valueIsBytesField = + StringSerializer.class.getDeclaredField("STRING_VALUE_FIELD_IS_BYTES"); + valueIsBytesField.setAccessible(true); + return (boolean) valueIsBytesField.get(null); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + @Test public void testJavaStringZeroCopy() { if (JdkVersion.MAJOR_VERSION >= 17) { From 232c6cef8a41a7d8227da3f9ad0b675f9cb57835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Thu, 25 Jun 2026 16:36:53 +0800 Subject: [PATCH 3/6] perf(java): clean compatible mode optimizations --- .../fory/builder/BaseObjectCodecBuilder.java | 62 +++------ .../org/apache/fory/builder/CodecBuilder.java | 3 + .../builder/LayerMarkerClassGenerator.java | 2 +- .../apache/fory/context/MetaWriteContext.java | 92 +------------ .../org/apache/fory/context/WriteContext.java | 2 +- .../apache/fory/resolver/TypeResolver.java | 6 +- .../CompatibleLayerSerializerBase.java | 6 +- .../serializer/UnknownClassSerializers.java | 6 +- .../collection/ChildContainerSerializers.java | 4 +- .../collection/CollectionLikeSerializer.java | 130 ------------------ 10 files changed, 41 insertions(+), 272 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 8546b06e84..70f0ff757d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -1380,7 +1380,7 @@ protected Expression writeCollectionData( Class elemClass = TypeUtils.getRawType(elementType); boolean trackingRef = needWriteRef(elementType); Tuple2 writeElementsHeader = - writeElementsHeader(elemClass, trackingRef, serializer, buffer, collection, size); + writeElementsHeader(elemClass, trackingRef, serializer, buffer, collection); Expression flags = writeElementsHeader.f0; builder.add(flags); boolean finalType = isMonomorphic(elemClass); @@ -1520,9 +1520,7 @@ private Tuple2 writeElementsHeader( boolean trackingRef, Expression collectionSerializer, Expression buffer, - Expression value, - Expression size) { - boolean isList = List.class.isAssignableFrom(getRawType(value.type())); + Expression value) { if (isMonomorphic(elementType)) { Expression bitmap; if (trackingRef) { @@ -1531,24 +1529,9 @@ private Tuple2 writeElementsHeader( new Invoke(buffer, "writeByte", ofInt(CollectionFlags.DECL_SAME_TYPE_TRACKING_REF)), ofInt(CollectionFlags.DECL_SAME_TYPE_TRACKING_REF)); } else { - if (isList) { - bitmap = - new Invoke( - collectionSerializer, - "writeNullabilityHeader", - PRIMITIVE_INT_TYPE, - buffer, - value, - size); - } else { - bitmap = - new Invoke( - collectionSerializer, - "writeNullabilityHeader", - PRIMITIVE_INT_TYPE, - buffer, - value); - } + bitmap = + new Invoke( + collectionSerializer, "writeNullabilityHeader", PRIMITIVE_INT_TYPE, buffer, value); } return Tuple2.of(bitmap, null); } else { @@ -1577,28 +1560,15 @@ private Tuple2 writeElementsHeader( classInfoHolder); } } else { - if (isList) { - bitmap = - new Invoke( - collectionSerializer, - "writeTypeNullabilityHeader", - PRIMITIVE_INT_TYPE, - writeContextRef(), - value, - size, - elementTypeExpr, - classInfoHolder); - } else { - bitmap = - new Invoke( - collectionSerializer, - "writeTypeNullabilityHeader", - PRIMITIVE_INT_TYPE, - writeContextRef(), - value, - elementTypeExpr, - classInfoHolder); - } + bitmap = + new Invoke( + collectionSerializer, + "writeTypeNullabilityHeader", + PRIMITIVE_INT_TYPE, + writeContextRef(), + value, + elementTypeExpr, + classInfoHolder); } Invoke serializer = new Invoke(classInfoHolder, "getSerializer", SERIALIZER_TYPE); return Tuple2.of(bitmap, serializer); @@ -2796,7 +2766,9 @@ protected Expression readCollectionCodegen( ListExpression builder = new ListExpression(); Expression readerIndex = new Invoke(buffer, "readerIndex", "readerIndex", PRIMITIVE_INT_TYPE); Expression flags = - cast(new Invoke(buffer, "_unsafeGetByte", PRIMITIVE_BYTE_TYPE, readerIndex), PRIMITIVE_INT_TYPE); + cast( + new Invoke(buffer, "_unsafeGetByte", PRIMITIVE_BYTE_TYPE, readerIndex), + PRIMITIVE_INT_TYPE); builder.add(readerIndex, flags, new Invoke(buffer, "_increaseReaderIndexUnsafe", ofInt(1))); Class elemClass = TypeUtils.getRawType(elementType); walkPath.add(elementType.toString()); diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java index 7bfd4898c9..8e5083b6ca 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java @@ -518,6 +518,8 @@ private Expression varHandleSetField(Expression bean, Descriptor descriptor, Exp if (descriptor.getTypeRef().isPrimitive()) { Preconditions.checkArgument(getRawType(value.type()) == getRawType(fieldType)); } + // Janino cannot compile VarHandle's signature-polymorphic access modes directly. Keep generated + // serializers on typed helper methods even though the VarHandle itself is a static final field. return new StaticInvoke( varHandleSupportClass(), varHandleSetMethod(fieldType), @@ -609,6 +611,7 @@ protected Reference getOrCreateField( private Expression varHandleGetField(Expression inputObject, Descriptor descriptor) { TypeRef returnType = descriptor.getTypeRef().isPrimitive() ? descriptor.getTypeRef() : OBJECT_TYPE; + // See varHandleSetField: direct VarHandle access-mode calls are not valid Janino output. Expression getValue = new StaticInvoke( varHandleSupportClass(), diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java b/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java index c9e2e8c78d..3e761b55a2 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java @@ -26,7 +26,7 @@ /** * Creates unique marker classes for each layer in a class hierarchy. These marker classes serve as - * unique keys in {@code MetaWriteContext} to distinguish different layers during serialization. + * unique keys in {@code metaContext.classMap} to distinguish different layers during serialization. * *

For a class hierarchy {@code C extends B extends A}, this generator creates unique marker * classes for: diff --git a/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java index 84d164c2e0..a39d2ef0b2 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java @@ -19,6 +19,8 @@ package org.apache.fory.context; +import org.apache.fory.collection.IdentityObjectIntMap; + /** * Write-side state for meta-share serialization. * @@ -26,93 +28,9 @@ * already announced classes are not sent repeatedly. */ public class MetaWriteContext { - private static final int INITIAL_CAPACITY = 8; - private static final int MISSING_ID = -1; - - private Object[] keys = new Object[INITIAL_CAPACITY]; - private int[] idsBySlot = new int[INITIAL_CAPACITY]; - private int[] usedSlots = new int[INITIAL_CAPACITY]; - private int mask = INITIAL_CAPACITY - 1; - private int threshold = INITIAL_CAPACITY >> 1; - private int size; - - /** Returns the next protocol id that will be assigned to a new metadata key. */ - public int size() { - return size; - } - /** - * Returns the existing protocol id for {@code key}, or records it and returns {@code -1}. - * - *

Meta-share keys use identity semantics. Class objects and layer marker classes are already - * identity keys, and unknown-struct TypeDef ids historically used the same identity map path. + * Classes whose definitions have already been announced to the peer, mapped to the protocol id + * used by the current or shared meta-share session. */ - public int putOrGetMetaId(Object key) { - if (key == null) { - throw new NullPointerException("Meta key must not be null"); - } - int slot = locate(key, keys, mask); - if (slot >= 0) { - return idsBySlot[slot]; - } - if (size >= threshold) { - resize(keys.length << 1); - slot = locate(key, keys, mask); - } - slot = -(slot + 1); - int id = size; - keys[slot] = key; - idsBySlot[slot] = id; - usedSlots[id] = slot; - size = id + 1; - return MISSING_ID; - } - - /** Clears operation-local metadata ids while retaining reusable table storage. */ - public void reset() { - int size = this.size; - if (size == 0) { - return; - } - Object[] keys = this.keys; - int[] usedSlots = this.usedSlots; - for (int i = 0; i < size; i++) { - keys[usedSlots[i]] = null; - } - this.size = 0; - } - - private static int locate(Object key, Object[] keys, int mask) { - for (int slot = System.identityHashCode(key) & mask; ; slot = (slot + 1) & mask) { - Object other = keys[slot]; - if (other == null) { - return -(slot + 1); - } - if (other == key) { - return slot; - } - } - } - - private void resize(int newCapacity) { - Object[] oldKeys = keys; - int[] oldUsedSlots = usedSlots; - int oldSize = size; - Object[] newKeys = new Object[newCapacity]; - int[] newIdsBySlot = new int[newCapacity]; - int[] newUsedSlots = new int[newCapacity]; - int newMask = newCapacity - 1; - for (int id = 0; id < oldSize; id++) { - Object key = oldKeys[oldUsedSlots[id]]; - int slot = -(locate(key, newKeys, newMask) + 1); - newKeys[slot] = key; - newIdsBySlot[slot] = id; - newUsedSlots[id] = slot; - } - keys = newKeys; - idsBySlot = newIdsBySlot; - usedSlots = newUsedSlots; - mask = newMask; - threshold = newCapacity >> 1; - } + public final IdentityObjectIntMap> classMap = new IdentityObjectIntMap<>(1, 0.5f); } diff --git a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java index 745a992f77..257afb991a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java @@ -299,7 +299,7 @@ public void reset() { contextObjects.clear(); } if (scopedMetaShareEnabled) { - metaWriteContext.reset(); + metaWriteContext.classMap.clear(); } else { metaWriteContext = null; } diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index e2e845c1dc..e22bed10c9 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -55,6 +55,7 @@ import org.apache.fory.collection.BiMap; import org.apache.fory.collection.ConcurrentIdentityMap; import org.apache.fory.collection.IdentityMap; +import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.Tuple2; import org.apache.fory.config.Config; @@ -586,8 +587,9 @@ protected final void writeSharedClassMeta(WriteContext writeContext, TypeInfo ty MemoryBuffer buffer = writeContext.getBuffer(); MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); assert metaWriteContext != null : SET_META_WRITE_CONTEXT_MSG; - int newId = metaWriteContext.size(); - int id = metaWriteContext.putOrGetMetaId(typeInfo.type); + IdentityObjectIntMap> classMap = metaWriteContext.classMap; + int newId = classMap.size; + int id = classMap.putOrGet(typeInfo.type, newId); if (id >= 0) { // Reference to previously written type: (index << 1) | 1, LSB=1 buffer.writeVarUInt32((id << 1) | 1); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java index e7ce30603e..e7040f16f4 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java @@ -19,6 +19,7 @@ package org.apache.fory.serializer; +import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.ObjectIntMap; import org.apache.fory.context.CopyContext; import org.apache.fory.context.MetaWriteContext; @@ -86,8 +87,9 @@ public void writeLayerClassMeta(WriteContext writeContext) { if (metaWriteContext == null) { return; } - int newId = metaWriteContext.size(); - int id = metaWriteContext.putOrGetMetaId(layerMarkerClass); + IdentityObjectIntMap> classMap = metaWriteContext.classMap; + int newId = classMap.size; + int id = classMap.putOrGet(layerMarkerClass, newId); if (id >= 0) { buffer.writeVarUInt32((id << 1) | 1); } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java index 5fdae10109..ef03486e11 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.MapEntry; import org.apache.fory.config.Config; @@ -91,9 +92,10 @@ public UnknownStructSerializer(TypeResolver typeResolver, TypeDef typeDef) { private void writeTypeDef(WriteContext writeContext, UnknownClass.UnknownStruct value) { MemoryBuffer buffer = writeContext.getBuffer(); MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); - int newId = metaWriteContext.size(); + IdentityObjectIntMap classMap = metaWriteContext.classMap; + int newId = classMap.size; // class not exist, use class def id for identity. - int id = metaWriteContext.putOrGetMetaId(value.typeDef.getId()); + int id = classMap.putOrGet(value.typeDef.getId(), newId); if (id >= 0) { // Reference to previously written type: (index << 1) | 1, LSB=1 buffer.writeVarUInt32((id << 1) | 1); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java index acb1700311..f7840349ef 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java @@ -710,8 +710,8 @@ private static void readAndSkipLayerClassMeta(ReadContext readContext) { // New type - need to read and skip the TypeDef bytes long id = buffer.readInt64(); TypeDef.skipTypeDef(buffer, id); - // Add a placeholder to keep readTypeInfos indices in sync with the write side's meta ids. - // The write side (writeLayerClassMeta) adds layer marker classes to the metadata table, sharing + // Add a placeholder to keep readTypeInfos indices in sync with the write side's classMap. + // The write side (writeLayerClassMeta) adds layer marker classes to classMap which shares // the same index space as writeSharedClassMeta. Without this placeholder, subsequent // readSharedClassMeta reference lookups would use wrong indices. metaReadContext.readTypeInfos.add(null); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java index 221b104508..3915b5d888 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java @@ -22,7 +22,6 @@ import java.lang.invoke.MethodHandle; import java.lang.reflect.Constructor; import java.util.Collection; -import java.util.List; import org.apache.fory.Fory; import org.apache.fory.annotation.CodegenInvoke; import org.apache.fory.config.Config; @@ -179,44 +178,6 @@ public int writeNullabilityHeader(MemoryBuffer buffer, Collection value) { return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; } - /** Element type is final, write whether any elements is null. */ - @CodegenInvoke - public int writeNullabilityHeader(MemoryBuffer buffer, List value, int size) { - switch (size) { - case 1: - if (value.get(0) == null) { - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; - } - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; - case 2: - if (value.get(0) == null || value.get(1) == null) { - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; - } - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; - case 3: - if (value.get(0) == null || value.get(1) == null || value.get(2) == null) { - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; - } - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; - default: - break; - } - for (int i = 0; i < size; i++) { - if (value.get(i) == null) { - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; - } - } - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; - } - /** * Need to track elements ref, declared element type is not morphic, can't check elements * nullability. @@ -359,97 +320,6 @@ public int writeTypeNullabilityHeader( return bitmap; } - /** - * Element type is not final by {@link ClassResolver#isMonomorphic}, need to write element type. - * Elements ref tracking is disabled, write whether any elements is null. - */ - @CodegenInvoke - public int writeTypeNullabilityHeader( - WriteContext writeContext, - List value, - int size, - Class declareElementType, - TypeInfoHolder cache) { - MemoryBuffer buffer = writeContext.getBuffer(); - if (size > 0 && size <= 3) { - Object elem0 = value.get(0); - Object elem1 = size > 1 ? value.get(1) : null; - Object elem2 = size > 2 ? value.get(2) : null; - boolean containsNull = - elem0 == null || (size > 1 && elem1 == null) || (size > 2 && elem2 == null); - Class elemClass = - elem0 != null ? elem0.getClass() : elem1 != null ? elem1.getClass() : null; - if (elemClass == null && elem2 != null) { - elemClass = elem2.getClass(); - } - boolean hasDifferentClass = - elemClass != null - && ((elem0 != null && elem0.getClass() != elemClass) - || (elem1 != null && elem1.getClass() != elemClass) - || (elem2 != null && elem2.getClass() != elemClass)); - int bitmap = containsNull ? CollectionFlags.HAS_NULL : 0; - if (hasDifferentClass) { - buffer.writeByte(bitmap); - } else { - if (elemClass == null) { - elemClass = Object.class; - } - bitmap |= CollectionFlags.IS_SAME_TYPE; - // Write class in case peer doesn't have this class. - if (!config.isMetaShareEnabled() && elemClass == declareElementType) { - bitmap |= CollectionFlags.IS_DECL_ELEMENT_TYPE; - buffer.writeByte(bitmap); - } else { - buffer.writeByte(bitmap); - TypeResolver typeResolver = this.typeResolver; - TypeInfo typeInfo = typeResolver.getTypeInfo(elemClass, cache); - typeResolver.writeTypeInfo(writeContext, typeInfo); - } - } - return bitmap; - } - int bitmap = 0; - boolean containsNull = false; - boolean hasDifferentClass = false; - Class elemClass = null; - for (int i = 0; i < size; i++) { - Object elem = value.get(i); - if (elem == null) { - containsNull = true; - } else if (elemClass == null) { - elemClass = elem.getClass(); - } else { - if (!hasDifferentClass && elem.getClass() != elemClass) { - hasDifferentClass = true; - } - } - } - if (containsNull) { - bitmap |= CollectionFlags.HAS_NULL; - } - if (hasDifferentClass) { - buffer.writeByte(bitmap); - } else { - // When serialize a collection with all elements null directly, the declare type - // will be equal to element type: null - if (elemClass == null) { - elemClass = Object.class; - } - bitmap |= CollectionFlags.IS_SAME_TYPE; - // Write class in case peer doesn't have this class. - if (!config.isMetaShareEnabled() && elemClass == declareElementType) { - bitmap |= CollectionFlags.IS_DECL_ELEMENT_TYPE; - buffer.writeByte(bitmap); - } else { - buffer.writeByte(bitmap); - TypeResolver typeResolver = this.typeResolver; - TypeInfo typeInfo = typeResolver.getTypeInfo(elemClass, cache); - typeResolver.writeTypeInfo(writeContext, typeInfo); - } - } - return bitmap; - } - @Override public void write(WriteContext writeContext, T value) { Collection collection = onCollectionWrite(writeContext, value); From 31e048cb870d71f45b000ac56dc107545efb1f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Thu, 25 Jun 2026 19:05:37 +0800 Subject: [PATCH 4/6] perf(java): speed compatible type info writes --- .../fory/builder/BaseObjectCodecBuilder.java | 7 ++-- .../apache/fory/resolver/TypeResolver.java | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 70f0ff757d..930385a79e 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -901,7 +901,7 @@ protected Expression writeForNotNullNonFinalObject( Class clz = getRawType(typeRef); Expression clsExpr = new Invoke(inputObject, "getClass", "cls", CLASS_TYPE); ListExpression writeClassAndObject = new ListExpression(); - Expression exactClassWrite = exactClassWrite(inputObject, clz); + Expression exactClassWrite = exactClassWrite(inputObject, buffer, clz); Tuple2 classInfoRef = addTypeInfoField(clz); Expression classInfo = classInfoRef.f0; if (classInfoRef.f1) { @@ -929,14 +929,15 @@ protected Expression writeForNotNullNonFinalObject( ctx, writeCutPoints(buffer, inputObject), write, "writeClassAndObject", false); } - private Expression exactClassWrite(Expression inputObject, Class clz) { + private Expression exactClassWrite(Expression inputObject, Expression buffer, Class clz) { if (clz.isInterface() || Modifier.isAbstract(clz.getModifiers())) { return null; } Reference typeInfo = addExactTypeInfoField(clz); Expression serializer = getOrCreateSerializer(clz); return new ListExpression( - typeResolver(r -> r.writeClassExpr(typeResolverRef, writeContextRef(), typeInfo)), + typeResolver( + r -> r.writeExactClassExpr(typeResolverRef, writeContextRef(), buffer, typeInfo, clz)), new Invoke( serializer, writeMethodName, PRIMITIVE_VOID_TYPE, writeContextRef(), inputObject)); } diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index e22bed10c9..7b1a635969 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -557,6 +557,27 @@ public final void writeTypeInfo(WriteContext writeContext, TypeInfo typeInfo) { } } + // Generated exact-class branches call this only after code generation resolves the TypeInfo to a + // compatible struct id. The wire body stays in writeSharedClassMeta; this helper only skips the + // generic type-id switch. + @Internal + public final void writeCompatibleTypeInfo( + WriteContext writeContext, MemoryBuffer buffer, TypeInfo typeInfo) { + assert isCompatibleStructTypeId(typeInfo.typeId); + buffer.writeUInt8(typeInfo.typeId); + writeSharedClassMeta(writeContext, buffer, typeInfo); + } + + private static boolean isCompatibleStructTypeId(int typeId) { + switch (typeId) { + case Types.COMPATIBLE_STRUCT: + case Types.NAMED_COMPATIBLE_STRUCT: + return true; + default: + return false; + } + } + static int toUserTypeId(long userTypeId) { Preconditions.checkArgument( userTypeId >= 0 && userTypeId <= MAX_USER_TYPE_ID, @@ -576,6 +597,21 @@ public Expression writeClassExpr( return new Invoke(classResolverRef, "writeTypeInfo", buffer, classInfo); } + // Note: Thread safe for jit thread to call. + public Expression writeExactClassExpr( + Expression classResolverRef, + Expression writeContext, + Expression buffer, + Expression classInfo, + Class cls) { + TypeInfo typeInfo = getTypeInfo(cls); + if (isCompatibleStructTypeId(typeInfo.typeId)) { + return new Invoke( + classResolverRef, "writeCompatibleTypeInfo", writeContext, buffer, classInfo); + } + return writeClassExpr(classResolverRef, writeContext, classInfo); + } + /** * Writes shared class metadata using the meta-share protocol. Protocol: If class already written, * writes {@code (index << 1) | 1} (reference). If new class, writes {@code (index << 1)} followed @@ -585,6 +621,11 @@ public Expression writeClassExpr( */ protected final void writeSharedClassMeta(WriteContext writeContext, TypeInfo typeInfo) { MemoryBuffer buffer = writeContext.getBuffer(); + writeSharedClassMeta(writeContext, buffer, typeInfo); + } + + private void writeSharedClassMeta( + WriteContext writeContext, MemoryBuffer buffer, TypeInfo typeInfo) { MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); assert metaWriteContext != null : SET_META_WRITE_CONTEXT_MSG; IdentityObjectIntMap> classMap = metaWriteContext.classMap; From 496da59e76c5a42ac20337231aa1e3500b1b83b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Fri, 26 Jun 2026 15:30:20 +0800 Subject: [PATCH 5/6] perf(java): unify compatible string writes --- .../fory/builder/BaseObjectCodecBuilder.java | 21 +------ .../apache/fory/resolver/TypeResolver.java | 62 ------------------- .../fory/serializer/StringSerializer.java | 40 +----------- .../fory/serializer/StringSerializerTest.java | 14 ++--- 4 files changed, 8 insertions(+), 129 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 930385a79e..64bdba0394 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -1220,17 +1220,6 @@ protected Expression readTypeInfo(Class cls, Expression ignored, boolean inli } } - protected Expression readTypeInfoWithTarget(Class cls) { - Reference classInfoHolderRef = addTypeInfoHolderField(cls); - return inlineInvoke( - typeResolverRef, - "readTypeInfo", - classInfoTypeRef, - readContextRef, - getClassExpr(cls), - classInfoHolderRef); - } - protected TypeRef getSerializerType(TypeRef objType) { return getSerializerType(objType.getRawType()); } @@ -2681,15 +2670,9 @@ protected Expression readForNotNullNonFinal( || typeInfo.getTypeId() == Types.NAMED_COMPATIBLE_STRUCT)) { String name = ctx.newName(StringUtils.uncapitalize(rawType.getSimpleName()) + "Class"); Expression clsExpr = staticClassFieldExpr(rawType, name); - Reference classInfoHolderRef = addTypeInfoHolderField(rawType); classInfo = inlineInvoke( - typeResolverRef, - "readTypeInfo", - classInfoTypeRef, - readContextRef, - clsExpr, - classInfoHolderRef); + typeResolverRef, "readTypeInfo", classInfoTypeRef, readContextRef, clsExpr); } else { classInfo = readTypeInfo(getRawType(typeRef), buffer); } @@ -2802,7 +2785,7 @@ protected Expression readCollectionCodegen( Literal isDeclTypeFlag = ofInt(CollectionFlags.IS_DECL_ELEMENT_TYPE); Expression isDeclType = eq(new BitAnd(flags, isDeclTypeFlag), isDeclTypeFlag); Invoke serializer = - inlineInvoke(readTypeInfoWithTarget(elemClass), "getSerializer", SERIALIZER_TYPE); + inlineInvoke(readTypeInfo(elemClass, buffer), "getSerializer", SERIALIZER_TYPE); TypeRef serializerType = getSerializerType(elementType); Expression elemSerializer; // make it in scope of `if(sameElementClass)` boolean maybeDecl = typeResolver(r -> r.isSerializable(elemClass)); diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index 7b1a635969..35417977f0 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -744,60 +744,6 @@ public final TypeInfo readTypeInfo(ReadContext readContext, Class targetClass return typeInfo; } - /** - * Read class info from buffer using a target class and generated field-local cache. - * - *

The holder caches only checked metadata owned by this resolver. It never caches payload - * data. - */ - @CodegenInvoke - public final TypeInfo readTypeInfo( - ReadContext readContext, Class targetClass, TypeInfoHolder classInfoHolder) { - MemoryBuffer buffer = readContext.getBuffer(); - int typeId = buffer.readUInt8(); - TypeInfo typeInfo; - boolean updateCache = false; - switch (typeId) { - case Types.ENUM: - case Types.STRUCT: - case Types.EXT: - case Types.TYPED_UNION: - typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), classInfoHolder.typeInfo); - updateCache = typeInfo != classInfoHolder.typeInfo; - break; - case Types.COMPATIBLE_STRUCT: - case Types.NAMED_COMPATIBLE_STRUCT: - typeInfo = readSharedClassMeta(readContext, targetClass, classInfoHolder); - break; - case Types.NAMED_ENUM: - case Types.NAMED_STRUCT: - case Types.NAMED_EXT: - case Types.NAMED_UNION: - if (!metaContextShareEnabled) { - typeInfo = readTypeInfoFromBytes(readContext, classInfoHolder.typeInfo, typeId); - updateCache = true; - } else { - typeInfo = readSharedClassMeta(readContext, targetClass, classInfoHolder); - } - break; - case Types.LIST: - typeInfo = readListTypeInfo(readContext); - break; - case Types.TIMESTAMP: - typeInfo = readTimestampTypeInfo(readContext); - break; - default: - typeInfo = Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId)); - } - if (typeInfo.serializer == null) { - typeInfo = ensureSerializerForTypeInfo(typeInfo); - } - if (updateCache) { - classInfoHolder.typeInfo = typeInfo; - } - return typeInfo; - } - /** * Read class info from buffer with TypeInfo cache. This version is faster than {@link * #readTypeInfo(ReadContext)} because it uses the provided classInfoCache to reduce map lookups @@ -959,14 +905,6 @@ public final TypeInfo readSharedClassMeta(ReadContext readContext, Class targ return adaptSharedClassTarget(typeInfo, targetClass); } - private TypeInfo readSharedClassMeta( - ReadContext readContext, Class targetClass, TypeInfoHolder classInfoHolder) { - TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass, classInfoHolder.typeInfo); - typeInfo = adaptSharedClassTarget(typeInfo, targetClass); - classInfoHolder.typeInfo = typeInfo; - return typeInfo; - } - private TypeInfo adaptSharedClassTarget(TypeInfo typeInfo, Class targetClass) { Class readClass = typeInfo.getType(); if (targetClass != readClass) { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java index a331f85fdb..2ad935101f 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java @@ -125,7 +125,7 @@ public static Expression writeStringExpr( if (compressString) { return new Invoke(strSerializer, "writeCompressedBytesString", buffer, str); } else { - return new StaticInvoke(StringSerializer.class, "writeBytesStringCodegen", buffer, str); + return new StaticInvoke(StringSerializer.class, "writeBytesString", buffer, str); } } else { if (!STRING_VALUE_FIELD_IS_CHARS) { @@ -529,15 +529,7 @@ public static void writeBytesString(MemoryBuffer buffer, String value) { writeBytesString(buffer, coder, bytes); } - @Internal - @CodegenInvoke - public static void writeBytesStringCodegen(MemoryBuffer buffer, String value) { - byte[] bytes = (byte[]) getStringValue(value); - byte coder = getStringCoder(value); - writeBytesStringCodegen(buffer, coder, bytes); - } - - private static void writeBytesStringCodegen(MemoryBuffer buffer, byte coder, byte[] bytes) { + public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] bytes) { if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { writeBytesStringUTF16BE(buffer, bytes); return; @@ -568,34 +560,6 @@ private static void writeBytesStringCodegen(MemoryBuffer buffer, byte coder, byt buffer._unsafeWriterIndex(writerIndex); } - public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] bytes) { - if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { - writeBytesStringUTF16BE(buffer, bytes); - return; - } - int bytesLen = bytes.length; - long header = ((long) bytesLen << 2) | coder; - int writerIndex = buffer.writerIndex(); - // The `ensure` ensure next operations are safe without bound checks, - // and inner heap buffer doesn't change. - buffer.ensure(writerIndex + 9 + bytesLen); // 1 byte coder + varint max 8 bytes - final byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - // Some JDK11 Unsafe.copyMemory will `copyMemoryChecks`, and - // jvm doesn't eliminate well in some jdk. - final int targetIndex = buffer._unsafeHeapWriterIndex(); - int arrIndex = targetIndex; - arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - writerIndex += arrIndex - targetIndex; - System.arraycopy(bytes, 0, targetArray, arrIndex, bytesLen); - } else { - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - PlatformStringUtils.putBytes(buffer, writerIndex, bytes, bytesLen); - } - writerIndex += bytesLen; - buffer._unsafeWriterIndex(writerIndex); - } - @CodegenInvoke public void writeCharsString(MemoryBuffer buffer, String value) { final char[] chars = (char[]) getStringValue(value); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java index c7b10e1ad8..c910414959 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java @@ -61,7 +61,7 @@ public void testRejectOddUtf16ByteSize() { } @Test - public void testCodegenByteStringWire() { + public void testBytesStringWire() { if (!stringValueIsBytes()) { throw new SkipException("Skip when string value is not byte[]"); } @@ -76,16 +76,10 @@ public void testCodegenByteStringWire() { }; for (String value : values) { int capacity = Math.max(64, value.length() * 8 + 64); - MemoryBuffer control = MemoryBuffer.newHeapBuffer(capacity); - MemoryBuffer codegen = MemoryBuffer.newHeapBuffer(capacity); - StringSerializer.writeBytesString(control, value); - StringSerializer.writeBytesStringCodegen(codegen, value); + MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(capacity); + StringSerializer.writeBytesString(buffer, value); Assert.assertEquals( - codegen.getBytes(0, codegen.writerIndex()), - control.getBytes(0, control.writerIndex()), - value); - Assert.assertEquals( - readJDK11String(MemoryBuffer.fromByteArray(codegen.getBytes(0, codegen.writerIndex()))), + readJDK11String(MemoryBuffer.fromByteArray(buffer.getBytes(0, buffer.writerIndex()))), value); } } From 573cd3814ee269089665c3441f2e277b725e095f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Fri, 26 Jun 2026 16:42:53 +0800 Subject: [PATCH 6/6] fix(java): handle unknown enum codegen --- .../fory/builder/BaseObjectCodecBuilder.java | 38 +++++++++------ .../org/apache/fory/context/ReadContext.java | 28 +++++------ .../org/apache/fory/context/WriteContext.java | 48 +++++++++---------- .../serializer/UnknownClassSerializers.java | 16 ++++++- 4 files changed, 76 insertions(+), 54 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 64bdba0394..c1f281ed3b 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -138,6 +138,7 @@ import org.apache.fory.serializer.ReplaceResolveSerializer; import org.apache.fory.serializer.Serializer; import org.apache.fory.serializer.StringSerializer; +import org.apache.fory.serializer.UnknownClassSerializers; import org.apache.fory.serializer.collection.CollectionFlags; import org.apache.fory.serializer.collection.CollectionLikeSerializer; import org.apache.fory.serializer.collection.CollectionSerializer; @@ -702,16 +703,16 @@ private Expression serializeForNotNullObjectForField( TypeRef typeRef = descriptor.getTypeRef(); Class clz = getRawType(typeRef); if (isEnumType(clz)) { - Expression enumSerializer = - cast( - serializer == null ? getSerializerForField(clz) : serializer, - TypeRef.of(EnumSerializer.class)); - return new Invoke( - enumSerializer, - "writeValue", - writeContextRef(), - buffer, - cast(inputObject, TypeRef.of(Enum.class))); + Expression enumSerializer = serializer == null ? getSerializerForField(clz) : serializer; + if (hasEnumValueMethods(enumSerializer)) { + Class serializerClass = getRawType(enumSerializer.type()); + Expression value = + EnumSerializer.class.isAssignableFrom(serializerClass) + ? cast(inputObject, TypeRef.of(Enum.class)) + : inputObject; + return new Invoke(enumSerializer, "writeValue", writeContextRef(), buffer, value); + } + return new Invoke(enumSerializer, writeMethodName, writeContextRef(), inputObject); } if (serializer != null) { return new Invoke(serializer, writeMethodName, writeContextRef(), inputObject); @@ -733,6 +734,13 @@ private static boolean isEnumType(Class cls) { return cls != Enum.class && Enum.class.isAssignableFrom(cls); } + private static boolean hasEnumValueMethods(Expression serializer) { + Class serializerClass = getRawType(serializer.type()); + // UnknownEnumSerializer is enum-shaped but does not extend EnumSerializer. + return EnumSerializer.class.isAssignableFrom(serializerClass) + || UnknownClassSerializers.UnknownEnumSerializer.class.isAssignableFrom(serializerClass); + } + protected Expression serializeForNullable( Expression inputObject, Expression buffer, TypeRef typeRef, boolean nullable) { return serializeForNullable(inputObject, buffer, typeRef, null, false, nullable); @@ -2419,10 +2427,12 @@ private Expression deserializeForNotNullForField( getOrCreateStringSerializer(), buffer, config.compressString()); } if (isEnumType(cls)) { - Expression enumSerializer = - cast( - serializer == null ? getSerializerForField(cls) : serializer, - TypeRef.of(EnumSerializer.class)); + Expression enumSerializer = serializer == null ? getSerializerForField(cls) : serializer; + if (!hasEnumValueMethods(enumSerializer)) { + return cast( + new Invoke(enumSerializer, readMethodName, OBJECT_TYPE, readContextRef()), + TypeRef.of(Enum.class)); + } return new Invoke( enumSerializer, "readValue", TypeRef.of(Enum.class), readContextRef(), buffer); } diff --git a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java index dcc7553b59..b1000161b3 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java @@ -541,20 +541,6 @@ public Object readRef() { return refReader.getReadRef(); } - /** Reads the root object for one deserialization operation. */ - public Object readRootRef() { - if (trackingRef) { - return readRef(rootTypeInfoHolder); - } - MemoryBuffer buffer = this.buffer; - int headFlag = buffer.readByte(); - if (headFlag >= Fory.NOT_NULL_VALUE_FLAG) { - TypeInfo typeInfo = typeResolver.readTypeInfo(this, rootTypeInfoHolder); - return readNonRef(typeInfo); - } - return null; - } - /** Variant of {@link #readRef()} that uses already resolved {@link TypeInfo}. */ public Object readRef(TypeInfo typeInfo) { int nextReadRefId = refReader.tryPreserveRefId(buffer); @@ -596,6 +582,20 @@ public T readRef(Serializer serializer) { return (T) readNonRef(serializer); } + /** Reads the root object for one deserialization operation. */ + public Object readRootRef() { + if (trackingRef) { + return readRef(rootTypeInfoHolder); + } + MemoryBuffer buffer = this.buffer; + int headFlag = buffer.readByte(); + if (headFlag >= Fory.NOT_NULL_VALUE_FLAG) { + TypeInfo typeInfo = typeResolver.readTypeInfo(this, rootTypeInfoHolder); + return readNonRef(typeInfo); + } + return null; + } + /** Reads a non-null, first-seen object together with its type metadata. */ public Object readNonRef() { TypeInfo typeInfo = typeResolver.readTypeInfo(this); diff --git a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java index 257afb991a..bf74c0a67a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java @@ -464,30 +464,6 @@ public void writeRef(Object obj) { } } - /** Writes the root object for one serialization operation. */ - public void writeRootRef(Object obj) { - if (trackingRef) { - writeRef(obj, rootTypeInfoHolder); - return; - } - MemoryBuffer buffer = this.buffer; - if (obj == null) { - buffer.writeByte(Fory.NULL_FLAG); - return; - } - buffer.writeByte(Fory.NOT_NULL_VALUE_FLAG); - TypeResolver resolver = typeResolver; - TypeInfo typeInfo = resolver.getTypeInfo(obj.getClass(), rootTypeInfoHolder); - if (crossLanguage && typeInfo.getType() == UnknownStruct.class) { - depth++; - typeInfo.getSerializer().write(this, obj); - depth--; - return; - } - resolver.writeTypeInfo(this, typeInfo); - writeData(typeInfo, obj); - } - /** Variant of {@link #writeRef(Object)} that reuses a cached type-info holder. */ public void writeRef(Object obj, TypeInfoHolder classInfoHolder) { MemoryBuffer buffer = this.buffer; @@ -555,6 +531,30 @@ public void writeRef(T obj, Serializer serializer) { } } + /** Writes the root object for one serialization operation. */ + public void writeRootRef(Object obj) { + if (trackingRef) { + writeRef(obj, rootTypeInfoHolder); + return; + } + MemoryBuffer buffer = this.buffer; + if (obj == null) { + buffer.writeByte(Fory.NULL_FLAG); + return; + } + buffer.writeByte(Fory.NOT_NULL_VALUE_FLAG); + TypeResolver resolver = typeResolver; + TypeInfo typeInfo = resolver.getTypeInfo(obj.getClass(), rootTypeInfoHolder); + if (crossLanguage && typeInfo.getType() == UnknownStruct.class) { + depth++; + typeInfo.getSerializer().write(this, obj); + depth--; + return; + } + resolver.writeTypeInfo(this, typeInfo); + writeData(typeInfo, obj); + } + /** * Writes a non-null, first-seen object together with its type metadata. * diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java index ef03486e11..1356d80a35 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import org.apache.fory.annotation.CodegenInvoke; import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.MapEntry; @@ -294,21 +295,32 @@ public UnknownEnumSerializer(TypeResolver typeResolver) { @Override public void write(WriteContext writeContext, UnknownEnum value) { + writeValue(writeContext, writeContext.getBuffer(), value); + } + + @CodegenInvoke + public final void writeValue( + WriteContext writeContext, MemoryBuffer buffer, UnknownEnum value) { if (!config.isXlang() && config.serializeEnumByName()) { writeContext.writeString(value.name()); } else { - writeContext.getBuffer().writeVarUInt32Small7(value.ordinal()); + buffer.writeVarUInt32Small7(value.ordinal()); } } @Override public UnknownEnum read(ReadContext readContext) { + return readValue(readContext, readContext.getBuffer()); + } + + @CodegenInvoke + public final UnknownEnum readValue(ReadContext readContext, MemoryBuffer buffer) { if (!config.isXlang() && config.serializeEnumByName()) { readContext.readString(); return UnknownEnum.UNKNOWN; } - int ordinal = readContext.getBuffer().readVarUInt32Small7(); + int ordinal = buffer.readVarUInt32Small7(); if (ordinal >= enumConstants.length) { return UnknownEnum.UNKNOWN; }