From 0658c3a6396c08f4928f8e14fe78bad83d557c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Wed, 1 Jul 2026 14:54:23 +0800 Subject: [PATCH 001/120] feat(java): support JSON bean properties --- .../java/org/apache/fory/json/ForyJson.java | 10 +- .../org/apache/fory/json/ForyJsonBuilder.java | 10 +- .../fory/json/codec/BaseObjectCodec.java | 241 +++++++++++++++--- .../apache/fory/json/codegen/JsonCodegen.java | 17 +- .../codegen/JsonGeneratedCodecBuilder.java | 18 ++ .../fory/json/meta/JsonFieldAccessor.java | 79 +++++- .../apache/fory/json/meta/JsonFieldInfo.java | 43 +++- .../json/resolver/JsonSharedRegistry.java | 11 +- .../fory/json/resolver/JsonTypeResolver.java | 2 +- .../org/apache/fory/json/JsonObjectTest.java | 4 +- .../apache/fory/json/JsonPropertyTest.java | 131 ++++++++++ .../apache/fory/json/data/BeanProperties.java | 186 ++++++++++++++ 12 files changed, 697 insertions(+), 55 deletions(-) create mode 100644 java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java create mode 100644 java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index 8fc4662da3..890e25b2a7 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -55,10 +55,16 @@ public final class ForyJson { private final AtomicReferenceArray slots; ForyJson( - boolean writeNullFields, boolean codegenEnabled, int maxDepth, CodecRegistry codecRegistry) { + boolean writeNullFields, + boolean codegenEnabled, + boolean propertyDiscoveryEnabled, + int maxDepth, + CodecRegistry codecRegistry) { this.writeNullFields = writeNullFields; this.maxDepth = maxDepth; - sharedRegistry = new JsonSharedRegistry(codegenEnabled, writeNullFields, codecRegistry); + sharedRegistry = + new JsonSharedRegistry( + codegenEnabled, writeNullFields, propertyDiscoveryEnabled, codecRegistry); poolSize = DEFAULT_POOL_SIZE; primarySlot = new AtomicReference<>( diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java index 17a5cb6c53..b0ed1f58b5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java @@ -26,6 +26,7 @@ public final class ForyJsonBuilder { private boolean writeNullFields; private boolean codegenEnabled = true; + private boolean propertyDiscoveryEnabled = true; private int maxDepth = ForyJson.DEFAULT_MAX_DEPTH; private final CodecRegistry codecRegistry = new CodecRegistry(); @@ -43,6 +44,12 @@ public ForyJsonBuilder withCodegen(boolean codegenEnabled) { return this; } + /** Enables JavaBean getter/setter JSON property discovery. */ + public ForyJsonBuilder withPropertyDiscovery(boolean propertyDiscoveryEnabled) { + this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; + return this; + } + /** Sets the maximum nested JSON object/array depth allowed while parsing. */ public ForyJsonBuilder maxDepth(int maxDepth) { if (maxDepth < 1) { @@ -59,6 +66,7 @@ public ForyJsonBuilder registerCodec(Class type, JsonCodec codec) { } public ForyJson build() { - return new ForyJson(writeNullFields, codegenEnabled, maxDepth, codecRegistry); + return new ForyJson( + writeNullFields, codegenEnabled, propertyDiscoveryEnabled, maxDepth, codecRegistry); } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java index 3566caa8bc..2c7cad25f2 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java @@ -20,7 +20,9 @@ package org.apache.fory.json.codec; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -81,7 +83,7 @@ record = RecordUtils.isRecord(type); } } - public static ObjectCodec build(Class type) { + public static ObjectCodec build(Class type, boolean propertyDiscoveryEnabled) { if (type.isInterface() || Modifier.isAbstract(type.getModifiers()) || type.isPrimitive() @@ -93,39 +95,21 @@ boolean record = RecordUtils.isRecord(type); boolean writeExpose = hasWriteExpose(type); boolean readExpose = hasReadExpose(type, record); TreeMap builders = new TreeMap<>(); - for (Class current = type; - current != null && current != Object.class; - current = current.getSuperclass()) { - for (Field field : current.getDeclaredFields()) { - int modifiers = field.getModifiers(); - if (!isEligibleField(field)) { - continue; - } - boolean write = includeWrite(field, writeExpose); - boolean read = (record || !Modifier.isFinal(modifiers)) && includeRead(field, readExpose); - if (!write && !read) { - continue; - } - FieldBuilder builder = new FieldBuilder(field.getName()); - if (write) { - builder.setWriteField(field); - } - if (read) { - builder.setReadField(field); - } - if (builders.put(field.getName(), builder) != null) { - throw new ForyJsonException("Duplicate JSON field " + field.getName()); - } - } + addFields(type, record, writeExpose, readExpose, propertyDiscoveryEnabled, builders); + if (propertyDiscoveryEnabled && !record) { + addAccessors(type, writeExpose, readExpose, builders); } List writes = new ArrayList<>(); List reads = new ArrayList<>(); for (FieldBuilder builder : builders.values()) { + if (!builder.hasWriteSource() && !builder.hasReadSink()) { + continue; + } JsonFieldInfo field = builder.build(record); - if (builder.writeAccessor != null) { + if (builder.hasWriteSource()) { writes.add(field); } - if (builder.readField != null) { + if (builder.hasReadSink()) { reads.add(field); } } @@ -138,6 +122,71 @@ boolean record = RecordUtils.isRecord(type); type, writeArray, readArray, ObjectInstantiators.createObjectInstantiator(type)); } + private static void addFields( + Class type, + boolean record, + boolean writeExpose, + boolean readExpose, + boolean propertyDiscoveryEnabled, + TreeMap builders) { + for (Class current = type; + current != null && current != Object.class; + current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (!isEligibleField(field)) { + continue; + } + boolean write = includeWrite(field, writeExpose); + boolean readAllowed = includeRead(field, readExpose); + boolean read = (record || !Modifier.isFinal(modifiers)) && readAllowed; + if (!propertyDiscoveryEnabled && !write && !read) { + continue; + } + FieldBuilder builder = + builders.computeIfAbsent(field.getName(), name -> new FieldBuilder(name)); + builder.setField(field, write, read, write, readAllowed); + } + } + } + + private static void addAccessors( + Class type, + boolean writeExpose, + boolean readExpose, + TreeMap builders) { + for (Method method : type.getMethods()) { + if (!isEligibleAccessor(method)) { + continue; + } + String propertyName = getterPropertyName(method); + if (propertyName != null) { + FieldBuilder builder = builders.get(propertyName); + if (builder == null) { + if (writeExpose) { + continue; + } + builder = new FieldBuilder(propertyName); + builders.put(propertyName, builder); + } + builder.setWriteGetter(method, writeExpose); + continue; + } + propertyName = setterPropertyName(method); + if (propertyName != null) { + FieldBuilder builder = builders.get(propertyName); + if (builder == null) { + if (readExpose) { + continue; + } + builder = new FieldBuilder(propertyName); + builders.put(propertyName, builder); + } + builder.setReadSetter(method, readExpose); + } + } + } + public final Class type() { return type; } @@ -384,6 +433,53 @@ private static boolean isEligibleField(Field field) { && !field.isSynthetic(); } + private static boolean isEligibleAccessor(Method method) { + int modifiers = method.getModifiers(); + return Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && !method.isSynthetic() + && !method.isBridge(); + } + + private static String getterPropertyName(Method method) { + if (method.getParameterCount() != 0 || method.getReturnType() == void.class) { + return null; + } + String name = method.getName(); + if (name.equals("getClass")) { + return null; + } + if (name.length() > 3 && name.startsWith("get")) { + return decapitalize(name.substring(3)); + } + if (name.length() > 2 + && name.startsWith("is") + && (method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class)) { + return decapitalize(name.substring(2)); + } + return null; + } + + private static String setterPropertyName(Method method) { + if (method.getParameterCount() != 1 || method.getReturnType() != void.class) { + return null; + } + String name = method.getName(); + if (name.length() > 3 && name.startsWith("set")) { + return decapitalize(name.substring(3)); + } + return null; + } + + private static String decapitalize(String name) { + if (name.length() > 1 + && Character.isUpperCase(name.charAt(0)) + && Character.isUpperCase(name.charAt(1))) { + return name; + } + return Character.toLowerCase(name.charAt(0)) + name.substring(1); + } + private static boolean includeWrite(Field field, boolean exposeMode) { return include(field, exposeMode, true); } @@ -419,8 +515,13 @@ private static Object[] recordFieldDefaults( private static final class FieldBuilder { private final String name; + private Field field; + private boolean fieldWriteAllowed; + private boolean fieldReadAllowed; private Field writeField; private Field readField; + private Method writeGetter; + private Method readSetter; private JsonFieldAccessor writeAccessor; private JsonFieldAccessor readAccessor; @@ -428,24 +529,88 @@ private FieldBuilder(String name) { this.name = name; } - private void setWriteField(Field field) { - if (writeField != null) { - throw new ForyJsonException("Duplicate public JSON field " + name); + private void setField( + Field field, + boolean writeSource, + boolean readSink, + boolean writeAllowed, + boolean readAllowed) { + if (this.field != null) { + throw new ForyJsonException("Duplicate JSON field " + name); + } + this.field = field; + fieldWriteAllowed = writeAllowed; + fieldReadAllowed = readAllowed; + if (writeSource) { + writeField = field; + } + if (readSink) { + readField = field; } - writeField = field; } - private void setReadField(Field field) { - if (readField != null) { - throw new ForyJsonException("Duplicate public JSON field " + name); + private void setWriteGetter(Method getter, boolean exposeMode) { + if (!methodAllowed(exposeMode, fieldWriteAllowed)) { + return; + } + if (writeGetter != null) { + throw new ForyJsonException("Duplicate JSON getter for property " + name); } - readField = field; + writeGetter = getter; + writeField = null; + } + + private void setReadSetter(Method setter, boolean exposeMode) { + if (!methodAllowed(exposeMode, fieldReadAllowed)) { + return; + } + if (readSetter != null) { + throw new ForyJsonException("Duplicate JSON setter for property " + name); + } + readSetter = setter; + readField = null; + } + + private boolean hasWriteSource() { + return writeGetter != null || writeField != null; + } + + private boolean hasReadSink() { + return readSetter != null || readField != null; } private JsonFieldInfo build(boolean record) { - writeAccessor = writeField == null ? null : JsonFieldAccessor.forField(writeField); - readAccessor = readField == null || record ? null : JsonFieldAccessor.forField(readField); - return new JsonFieldInfo(name, writeField, readField, writeAccessor, readAccessor); + validateTypes(); + writeAccessor = + writeGetter != null + ? JsonFieldAccessor.forGetter(writeGetter) + : (writeField == null ? null : JsonFieldAccessor.forField(writeField)); + readAccessor = + readSetter != null + ? JsonFieldAccessor.forSetter(readSetter) + : (readField == null || record ? null : JsonFieldAccessor.forField(readField)); + return new JsonFieldInfo( + name, writeField, writeGetter, readField, readSetter, writeAccessor, readAccessor); + } + + private boolean methodAllowed(boolean exposeMode, boolean fieldAllowed) { + // A same-named field owns @Expose/@JsonIgnore direction decisions for the JSON property. + return field == null ? !exposeMode : fieldAllowed; + } + + private void validateTypes() { + Type writeType = + writeGetter == null ? fieldType(writeField) : writeGetter.getGenericReturnType(); + Type readType = + readSetter == null ? fieldType(readField) : readSetter.getGenericParameterTypes()[0]; + if (writeType != null && readType != null && !writeType.equals(readType)) { + throw new ForyJsonException( + "Conflicting JSON property types for " + name + ": " + writeType + " and " + readType); + } + } + + private static Type fieldType(Field field) { + return field == null ? null : field.getGenericType(); } } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index df786e028e..b2be63e1e3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -21,6 +21,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.Map; @@ -203,10 +204,10 @@ static Class readNestedType(JsonFieldInfo property) { private boolean canCompileWrite(JsonFieldInfo property) { Field field = property.writeField(); - if (field == null) { + if (field == null && property.writeGetter() == null) { return false; } - if (!isRecordField(property) && property.writeField() == null) { + if (property.writeGetter() != null && !canCall(property.writeGetter())) { return false; } Class rawType = property.writeRawType(); @@ -221,7 +222,12 @@ private boolean canCompileRead(JsonFieldInfo property, boolean record) { if (!record && property.readAccessor() == null) { return false; } - if (!record && property.readAccessor().coreAccessor() == null) { + if (!record && property.readSetter() != null && !canCall(property.readSetter())) { + return false; + } + if (!record + && property.readSetter() == null + && property.readAccessor().coreAccessor() == null) { return false; } Class rawType = property.readRawType(); @@ -236,6 +242,11 @@ private boolean canCompile(Class type) { return CodeGenerator.sourcePublicAccessible(type) && isVisible(type); } + private boolean canCall(Method method) { + return Modifier.isPublic(method.getModifiers()) + && CodeGenerator.sourcePublicAccessible(method.getDeclaringClass()); + } + private boolean isVisible(Class type) { if (type.isPrimitive()) { return true; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java index e5e0877993..55b0f463a3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java @@ -119,6 +119,15 @@ private Method recordReadMethod(Field field) { } Expression fieldValue(JsonFieldInfo property, Expression object) { + Method getter = property.writeGetter(); + if (getter != null) { + return new Expression.Invoke( + object, + getter.getName(), + property.name(), + TypeRef.of(getter.getGenericReturnType()), + !getter.getReturnType().isPrimitive()); + } return getFieldValue(object, writeDescriptor(property)); } @@ -127,6 +136,15 @@ Expression newObject() { } Expression setField(JsonFieldInfo property, Expression object, Expression value) { + Method setter = property.readSetter(); + if (setter != null) { + Class rawType = setter.getParameterTypes()[0]; + TypeRef typeRef = TypeRef.of(setter.getGenericParameterTypes()[0]); + if (!rawType.isAssignableFrom(value.type().getRawType())) { + value = tryInlineCast(value, typeRef); + } + return new Expression.Invoke(object, setter.getName(), value); + } return setFieldValue( object, readDescriptor(property), diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java index 5bed687f76..76503a7759 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java @@ -20,6 +20,9 @@ package org.apache.fory.json.meta; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.apache.fory.json.ForyJsonException; import org.apache.fory.reflect.FieldAccessor; public abstract class JsonFieldAccessor { @@ -27,9 +30,21 @@ public Object getObject(Object target) { throw new UnsupportedOperationException(); } - public abstract Field field(); + public Field field() { + return null; + } - public abstract FieldAccessor coreAccessor(); + public Method getter() { + return null; + } + + public Method setter() { + return null; + } + + public FieldAccessor coreAccessor() { + return null; + } public boolean getBoolean(Object target) { return (Boolean) getObject(target); @@ -103,6 +118,14 @@ public static JsonFieldAccessor forField(Field field) { return new FieldJsonAccessor(FieldAccessor.createAccessor(field)); } + public static JsonFieldAccessor forGetter(Method getter) { + return new GetterJsonAccessor(getter); + } + + public static JsonFieldAccessor forSetter(Method setter) { + return new SetterJsonAccessor(setter); + } + private static final class FieldJsonAccessor extends JsonFieldAccessor { private final FieldAccessor accessor; @@ -210,4 +233,56 @@ public void putChar(Object target, char value) { accessor.putChar(target, value); } } + + private static final class GetterJsonAccessor extends JsonFieldAccessor { + private final Method getter; + + private GetterJsonAccessor(Method getter) { + this.getter = getter; + getter.setAccessible(true); + } + + @Override + public Method getter() { + return getter; + } + + @Override + public Object getObject(Object target) { + try { + return getter.invoke(target); + } catch (IllegalAccessException | InvocationTargetException e) { + throw accessException(getter, e); + } + } + } + + private static final class SetterJsonAccessor extends JsonFieldAccessor { + private final Method setter; + + private SetterJsonAccessor(Method setter) { + this.setter = setter; + setter.setAccessible(true); + } + + @Override + public Method setter() { + return setter; + } + + @Override + public void putObject(Object target, Object value) { + try { + setter.invoke(target, value); + } catch (IllegalAccessException | InvocationTargetException e) { + throw accessException(setter, e); + } + } + } + + private static ForyJsonException accessException(Method method, ReflectiveOperationException e) { + Throwable cause = + e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e; + return new ForyJsonException("Cannot access JSON property method " + method, cause); + } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index 01a31a374a..b1a4a148f8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -20,6 +20,7 @@ package org.apache.fory.json.meta; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.Collection; @@ -54,6 +55,9 @@ public final class JsonFieldInfo { private final String name; private final Field writeField; + private final Method writeGetter; + private final Field readField; + private final Method readSetter; private final Type writeType; private final Class writeRawType; private final Type readType; @@ -98,16 +102,21 @@ public final class JsonFieldInfo { public JsonFieldInfo( String name, Field writeField, + Method writeGetter, Field readField, + Method readSetter, JsonFieldAccessor writeAccessor, JsonFieldAccessor readAccessor) { this.name = name; nameHash = JsonFieldNameHash.hash(name); this.writeField = writeField; - this.writeType = fieldType(writeField); - this.writeRawType = fieldRawType(writeField); - this.readType = fieldType(readField); - this.readRawType = fieldRawType(readField); + this.writeGetter = writeGetter; + this.readField = readField; + this.readSetter = readSetter; + this.writeType = writeType(writeField, writeGetter); + this.writeRawType = writeRawType(writeField, writeGetter); + this.readType = readType(readField, readSetter); + this.readRawType = readRawType(readField, readSetter); this.writeAccessor = writeAccessor; this.readAccessor = readAccessor; writeKind = writeRawType == null ? null : kind(writeRawType); @@ -181,6 +190,10 @@ public Field writeField() { return writeField; } + public Method writeGetter() { + return writeGetter; + } + public Type writeType() { return writeType; } @@ -226,7 +239,11 @@ public Type readType() { } public Field readField() { - return readAccessor == null ? null : readAccessor.field(); + return readField; + } + + public Method readSetter() { + return readSetter; } public Class readRawType() { @@ -249,6 +266,22 @@ private static Class fieldRawType(Field field) { return field == null ? null : field.getType(); } + private static Type writeType(Field field, Method getter) { + return getter == null ? fieldType(field) : getter.getGenericReturnType(); + } + + private static Class writeRawType(Field field, Method getter) { + return getter == null ? fieldRawType(field) : getter.getReturnType(); + } + + private static Type readType(Field field, Method setter) { + return setter == null ? fieldType(field) : setter.getGenericParameterTypes()[0]; + } + + private static Class readRawType(Field field, Method setter) { + return setter == null ? fieldRawType(field) : setter.getParameterTypes()[0]; + } + public void resolveTypes(JsonTypeResolver typeResolver) { if (writeRawType != null) { writeTypeInfo = typeResolver.getTypeInfo(writeType, writeRawType); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index 4fe557959e..a31d05ceb1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -79,10 +79,15 @@ public final class JsonSharedRegistry { private final CodecRegistry customCodecs; private final IdentityHashMap, JsonCodec> exactCodecs; private final JsonCodegen codegen; + private final boolean propertyDiscoveryEnabled; public JsonSharedRegistry( - boolean codegenEnabled, boolean writeNullFields, CodecRegistry customCodecs) { + boolean codegenEnabled, + boolean writeNullFields, + boolean propertyDiscoveryEnabled, + CodecRegistry customCodecs) { this.customCodecs = customCodecs.copy(); + this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; exactCodecs = new IdentityHashMap<>(); codegen = codegenEnabled ? new JsonCodegen(writeNullFields) : null; registerExactCodecs(); @@ -190,6 +195,10 @@ public ObjectCodecs compileObject(BaseObjectCodec codec, JsonTypeResolver localR return codegen == null ? null : codegen.compile(codec, localResolver); } + boolean propertyDiscoveryEnabled() { + return propertyDiscoveryEnabled; + } + private void registerExactCodecs() { exactCodecs.put(Object.class, ScalarCodecs.NaturalCodec.INSTANCE); exactCodecs.put(void.class, ScalarCodecs.VoidCodec.INSTANCE); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 0b23bbb38b..8b0e43e744 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -81,7 +81,7 @@ private BaseObjectCodec buildObjectCodec(Class type) { if (cached != null) { return cached; } - ObjectCodec codec = BaseObjectCodec.build(type); + ObjectCodec codec = BaseObjectCodec.build(type, sharedRegistry.propertyDiscoveryEnabled()); // Codegen may ask for nested object metadata that points back to this type. // Publishing before compiling keeps recursive ownership in this resolver cache. objectCodecs.put(type, codec); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java index 1cd303cb3d..3164b9d5af 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java @@ -130,8 +130,8 @@ public void writeNullFields() { } @Test - public void ignoreMethods() { - ForyJson json = ForyJson.builder().build(); + public void fieldOnlyModeIgnoresMethods() { + ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); assertEquals( json.toJson(new MethodsIgnored()), "{\"hidden\":\"hidden\",\"setterCalls\":0,\"value\":\"field\"}"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java new file mode 100644 index 0000000000..904f0bb4e3 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.json; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertThrows; + +import org.apache.fory.json.data.BeanProperties.BooleanBean; +import org.apache.fory.json.data.BeanProperties.ConflictingTypesBean; +import org.apache.fory.json.data.BeanProperties.DuplicateGetterBean; +import org.apache.fory.json.data.BeanProperties.FinalFieldBean; +import org.apache.fory.json.data.BeanProperties.GetterBean; +import org.apache.fory.json.data.BeanProperties.GetterOnlyBean; +import org.apache.fory.json.data.BeanProperties.InheritedChild; +import org.apache.fory.json.data.BeanProperties.InheritedParent; +import org.apache.fory.json.data.BeanProperties.InvalidAccessorBean; +import org.apache.fory.json.data.BeanProperties.MixedBean; +import org.apache.fory.json.data.BeanProperties.OverloadedSetterBean; +import org.apache.fory.json.data.BeanProperties.SetterBean; +import org.apache.fory.json.data.BeanProperties.SetterOnlyBean; +import org.testng.annotations.Test; + +public class JsonPropertyTest extends ForyJsonTestModels { + @Test + public void writePrivateGetters() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new GetterBean()), "{\"id\":17,\"name\":\"getter-field\"}"); + assertGeneratedWhenSupported(json, GetterBean.class); + } + + @Test + public void readPrivateSetters() { + ForyJson json = ForyJson.builder().build(); + SetterBean value = json.fromJson("{\"id\":4,\"name\":\"alpha\"}", SetterBean.class); + assertEquals(SetterBean.id(value), 5); + assertEquals(SetterBean.name(value), "set-alpha"); + assertEquals(SetterBean.setterCalls(value), 2); + assertGeneratedWhenSupported(json, SetterBean.class); + } + + @Test + public void roundTripMixedProperties() { + ForyJson json = ForyJson.builder().build(); + String text = "{\"count\":3,\"name\":\"mixed\",\"score\":5}"; + assertEquals(json.toJson(new MixedBean()), text); + assertEquals(json.toJson(json.fromJson(text, MixedBean.class)), text); + assertGeneratedWhenSupported(json, MixedBean.class); + } + + @Test + public void writeBooleanIsGetter() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new BooleanBean()), "{\"ready\":true}"); + } + + @Test + public void getterOnlyWrites() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new GetterOnlyBean()), "{\"computed\":6}"); + GetterOnlyBean value = json.fromJson("{\"computed\":99}", GetterOnlyBean.class); + assertEquals(json.toJson(value), "{\"computed\":6}"); + } + + @Test + public void setterOnlyReads() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new SetterOnlyBean()), "{}"); + SetterOnlyBean value = json.fromJson("{\"secret\":\"alpha\"}", SetterOnlyBean.class); + assertEquals(SetterOnlyBean.received(value), "set-alpha"); + } + + @Test + public void fieldOnlyMode() { + ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); + assertEquals(json.toJson(new GetterBean()), "{\"id\":7,\"name\":\"field\"}"); + assertEquals(json.toJson(new GetterOnlyBean()), "{}"); + SetterOnlyBean value = json.fromJson("{\"secret\":\"alpha\"}", SetterOnlyBean.class); + assertEquals(SetterOnlyBean.received(value), null); + } + + @Test + public void rejectPropertyConflicts() { + ForyJson json = ForyJson.builder().build(); + assertThrows(ForyJsonException.class, () -> json.toJson(new DuplicateGetterBean())); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("{\"value\":\"alpha\"}", OverloadedSetterBean.class)); + assertThrows(ForyJsonException.class, () -> json.toJson(new ConflictingTypesBean())); + } + + @Test + public void inheritedProperties() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new InheritedChild()), "{\"id\":4,\"name\":\"child\"}"); + InheritedChild value = json.fromJson("{\"id\":7,\"name\":\"json\"}", InheritedChild.class); + assertEquals(InheritedParent.id(value), 8); + assertEquals(value.name, "json"); + } + + @Test + public void ignoreInvalidAccessors() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new InvalidAccessorBean()), "{\"value\":\"field\"}"); + } + + @Test + public void finalFieldsStayReadOnly() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new FinalFieldBean()), "{\"id\":1,\"name\":\"field\"}"); + FinalFieldBean value = json.fromJson("{\"id\":9,\"name\":\"json\"}", FinalFieldBean.class); + assertEquals(value.id, 1); + assertEquals(value.name, "json"); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java b/java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java new file mode 100644 index 0000000000..b54169e461 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.json.data; + +public final class BeanProperties { + private BeanProperties() {} + + public static final class GetterBean { + private int id = 7; + private String name = "field"; + + public int getId() { + return id + 10; + } + + public String getName() { + return "getter-" + name; + } + } + + public static final class SetterBean { + private int id; + private String name; + private int setterCalls; + + public void setId(int id) { + this.id = id + 1; + setterCalls++; + } + + public void setName(String name) { + this.name = "set-" + name; + setterCalls++; + } + + public static int id(SetterBean value) { + return value.id; + } + + public static String name(SetterBean value) { + return value.name; + } + + public static int setterCalls(SetterBean value) { + return value.setterCalls; + } + } + + public static final class MixedBean { + public int count = 3; + private String name = "mixed"; + private int score = 5; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + } + + public static final class BooleanBean { + private boolean ready = true; + + public boolean isReady() { + return ready; + } + } + + public static final class GetterOnlyBean { + private transient int seed = 3; + + public int getComputed() { + return seed * 2; + } + } + + public static final class SetterOnlyBean { + private transient String received; + + public void setSecret(String secret) { + received = "set-" + secret; + } + + public static String received(SetterOnlyBean value) { + return value.received; + } + } + + public static final class DuplicateGetterBean { + public boolean getActive() { + return true; + } + + public boolean isActive() { + return true; + } + } + + public static final class OverloadedSetterBean { + public void setValue(int value) {} + + public void setValue(String value) {} + } + + public static final class ConflictingTypesBean { + private String value = "text"; + + public String getValue() { + return value; + } + + public void setValue(int value) {} + } + + public static class InheritedParent { + private int id = 4; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id + 1; + } + + public static int id(InheritedParent value) { + return value.id; + } + } + + public static final class InheritedChild extends InheritedParent { + public String name = "child"; + } + + public static final class InvalidAccessorBean { + public String value = "field"; + + public static String getStaticValue() { + return "static"; + } + + public String getWithArg(String ignored) { + return ignored; + } + + public void getVoidValue() {} + + public String setWrongReturn(String value) { + return value; + } + + public void setNoValue() {} + } + + public static final class FinalFieldBean { + public final int id = 1; + public String name = "field"; + } +} From 4e2e154fb765e677cec54e958b808f432ce0cb6c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 16:23:41 +0800 Subject: [PATCH 002/120] perf(java): optimize Fory JSON UTF8 reads --- .../fory/json/codec/BaseObjectCodec.java | 13 +- .../apache/fory/json/codec/ScalarCodecs.java | 157 ++++++++++++++- .../apache/fory/json/codegen/JsonCodegen.java | 4 +- .../fory/json/codegen/JsonReaderCodegen.java | 22 +-- .../fory/json/reader/Utf8JsonReader.java | 180 ++++++++++++++++++ .../org/apache/fory/json/JsonObjectTest.java | 4 +- .../org/apache/fory/json/JsonScalarTest.java | 111 +++++++++++ 7 files changed, 463 insertions(+), 28 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java index 2c7cad25f2..631141b2d1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java @@ -25,9 +25,9 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; import org.apache.fory.annotation.Expose; import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; @@ -94,7 +94,7 @@ public static ObjectCodec build(Class type, boolean propertyDiscoveryEnabled) boolean record = RecordUtils.isRecord(type); boolean writeExpose = hasWriteExpose(type); boolean readExpose = hasReadExpose(type, record); - TreeMap builders = new TreeMap<>(); + LinkedHashMap builders = new LinkedHashMap<>(); addFields(type, record, writeExpose, readExpose, propertyDiscoveryEnabled, builders); if (propertyDiscoveryEnabled && !record) { addAccessors(type, writeExpose, readExpose, builders); @@ -128,10 +128,15 @@ private static void addFields( boolean writeExpose, boolean readExpose, boolean propertyDiscoveryEnabled, - TreeMap builders) { + LinkedHashMap builders) { + List> hierarchy = new ArrayList<>(); for (Class current = type; current != null && current != Object.class; current = current.getSuperclass()) { + hierarchy.add(current); + } + for (int i = hierarchy.size() - 1; i >= 0; i--) { + Class current = hierarchy.get(i); for (Field field : current.getDeclaredFields()) { int modifiers = field.getModifiers(); if (!isEligibleField(field)) { @@ -154,7 +159,7 @@ private static void addAccessors( Class type, boolean writeExpose, boolean readExpose, - TreeMap builders) { + LinkedHashMap builders) { for (Method method : type.getMethods()) { if (!isEligibleAccessor(method)) { continue; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 2b58fc72c2..0d928448dc 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -515,7 +515,10 @@ final void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolve @Override final Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - String value = reader.readString(); + return readStringValue(reader.readString(), typeInfo); + } + + final Object readStringValue(String value, JsonTypeInfo typeInfo) { try { return fromJsonString(value); } catch (ForyJsonException e) { @@ -897,10 +900,20 @@ String toJsonString(Object value) { @Override Object fromJsonString(String value) { - if (value.length() > 10 && value.charAt(10) == 'T') { - return LocalDate.parse(value.substring(0, 10)); + return parseIsoLocalDate(value); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoLocalDate(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); } - return LocalDate.parse(value); } } @@ -1082,10 +1095,146 @@ String toJsonString(Object value) { @Override Object fromJsonString(String value) { + return parseIsoOffsetDateTime(value); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoOffsetDateTime(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); + } + } + } + + private static LocalDate parseIsoLocalDate(String value) { + int length = value.length(); + if (length >= 10 + && (length == 10 || value.charAt(10) == 'T') + && value.charAt(4) == '-' + && value.charAt(7) == '-') { + try { + return LocalDate.of(parse4(value, 0), parse2(value, 5), parse2(value, 8)); + } catch (RuntimeException e) { + if (length > 10 && value.charAt(10) == 'T') { + return LocalDate.parse(value.substring(0, 10)); + } + return LocalDate.parse(value); + } + } + return LocalDate.parse(value); + } + + private static OffsetDateTime parseIsoOffsetDateTime(String value) { + try { + // java.time emits these ISO forms, and parsing them directly avoids DateTimeFormatter's + // parsed-field maps on JSON scalar hot paths. Uncommon forms still use the JDK parser. + return parseIsoOffsetDateTimeFast(value); + } catch (RuntimeException e) { return OffsetDateTime.parse(value); } } + private static OffsetDateTime parseIsoOffsetDateTimeFast(String value) { + int length = value.length(); + if (length < 17 + || value.charAt(4) != '-' + || value.charAt(7) != '-' + || value.charAt(10) != 'T' + || value.charAt(13) != ':') { + throw new IllegalArgumentException(); + } + int year = parse4(value, 0); + int month = parse2(value, 5); + int day = parse2(value, 8); + int hour = parse2(value, 11); + int minute = parse2(value, 14); + int second = 0; + int nano = 0; + int index = 16; + if (index < length && value.charAt(index) == ':') { + second = parse2(value, index + 1); + index += 3; + if (index < length && value.charAt(index) == '.') { + int fractionStart = index + 1; + int fractionEnd = fractionStart; + while (fractionEnd < length && isDigit(value.charAt(fractionEnd))) { + fractionEnd++; + } + if (fractionEnd == fractionStart || fractionEnd - fractionStart > 9) { + throw new IllegalArgumentException(); + } + nano = parseNano(value, fractionStart, fractionEnd); + index = fractionEnd; + } + } + int offsetSeconds = parseOffsetSeconds(value, index); + return OffsetDateTime.of( + year, month, day, hour, minute, second, nano, ZoneOffset.ofTotalSeconds(offsetSeconds)); + } + + private static int parseOffsetSeconds(String value, int index) { + int length = value.length(); + char offset = value.charAt(index); + if (offset == 'Z') { + if (index + 1 != length) { + throw new IllegalArgumentException(); + } + return 0; + } + if (offset != '+' && offset != '-') { + throw new IllegalArgumentException(); + } + if (index + 6 > length || value.charAt(index + 3) != ':') { + throw new IllegalArgumentException(); + } + int hour = parse2(value, index + 1); + int minute = parse2(value, index + 4); + int second = 0; + int end = index + 6; + if (end < length) { + if (end + 3 != length || value.charAt(end) != ':') { + throw new IllegalArgumentException(); + } + second = parse2(value, end + 1); + } + int total = hour * 3600 + minute * 60 + second; + return offset == '-' ? -total : total; + } + + private static int parseNano(String value, int start, int end) { + int nano = 0; + for (int i = start; i < end; i++) { + nano = nano * 10 + value.charAt(i) - '0'; + } + for (int i = end - start; i < 9; i++) { + nano *= 10; + } + return nano; + } + + private static int parse4(String value, int index) { + return parse2(value, index) * 100 + parse2(value, index + 2); + } + + private static int parse2(String value, int index) { + int high = value.charAt(index) - '0'; + int low = value.charAt(index + 1) - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + throw new IllegalArgumentException(); + } + return high * 10 + low; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + public static final class AtomicBooleanCodec extends AbstractJsonCodec { public static final AtomicBooleanCodec INSTANCE = new AtomicBooleanCodec(); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index b2be63e1e3..ed79b7ce60 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -305,6 +305,8 @@ static boolean usesReadCodec(JsonFieldInfo property) { case COLLECTION: case MAP: return true; + case OBJECT: + return !usesReadObjectCodec(property); default: return false; } @@ -317,7 +319,7 @@ static boolean usesReadTypeField(JsonFieldInfo property) { case MAP: return true; case OBJECT: - return usesReadObjectCodec(property); + return true; default: return false; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 1736168059..da7f5e87a0 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -1017,6 +1017,8 @@ private static boolean usesReadCodec(JsonFieldInfo property) { case COLLECTION: case MAP: return true; + case OBJECT: + return !usesReadObjectCodec(property); default: return false; } @@ -1029,7 +1031,7 @@ private static boolean usesReadTypeField(JsonFieldInfo property) { case MAP: return true; case OBJECT: - return usesReadObjectCodec(property); + return true; default: return false; } @@ -1175,16 +1177,7 @@ private static Expression readRecordObject( Class type, JsonFieldInfo property, int id, int readerMode, Expression object) { if (property.readRawType() == Object.class || !(property.readTypeInfo().codec() instanceof BaseObjectCodec)) { - return assignRecord( - object, - id, - new Expression.Invoke( - fieldRef("p" + id, JsonFieldInfo.class), - "readValue", - TypeRef.of(Object.class), - true, - readerRef(readerMode), - typeResolverRef())); + return assignRecord(object, id, readResolvedValue(property, id, readerMode)); } return new Expression.If( tryReadNullExpr(readerMode), @@ -1331,12 +1324,7 @@ private static Expression readObject( Expression object) { if (property.readRawType() == Object.class || !(property.readTypeInfo().codec() instanceof BaseObjectCodec)) { - return new Expression.Invoke( - fieldRef("p" + id, JsonFieldInfo.class), - "read", - readerRef(readerMode), - object, - typeResolverRef()); + return readResolvedField(builder, property, id, readerMode, object); } return new Expression.If( tryReadNullExpr(readerMode), diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index df345945ea..855c58f411 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -19,6 +19,9 @@ package org.apache.fory.json.reader; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; @@ -623,6 +626,28 @@ public String readNullableStringToken() { return readStringToken(); } + public LocalDate readIsoLocalDate() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoLocalDateToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + + public OffsetDateTime readIsoOffsetDateTime() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoOffsetDateTimeToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + private String readStringToken() { byte[] bytes = input; int inputLength = bytes.length; @@ -680,6 +705,161 @@ private String readStringToken() { throw error("Unterminated string"); } + private LocalDate readIsoLocalDateToken() { + byte[] bytes = input; + int offset = position; + int length = bytes.length; + if (offset + 12 > length || bytes[offset++] != '"') { + throw error("Expected string"); + } + int dateStart = offset; + if (bytes[dateStart + 4] != '-' || bytes[dateStart + 7] != '-') { + throw new IllegalArgumentException(); + } + int year = parse4(bytes, dateStart); + int month = parse2(bytes, dateStart + 5); + int day = parse2(bytes, dateStart + 8); + int end = dateStart + 10; + int ch = bytes[end]; + if (ch == '"') { + position = end + 1; + return LocalDate.of(year, month, day); + } + if (ch == 'T') { + position = scanSimpleStringTail(bytes, end + 1); + return LocalDate.of(year, month, day); + } + throw new IllegalArgumentException(); + } + + private OffsetDateTime readIsoOffsetDateTimeToken() { + byte[] bytes = input; + int offset = position; + int length = bytes.length; + if (offset + 19 > length || bytes[offset++] != '"') { + throw error("Expected string"); + } + int start = offset; + if (bytes[start + 4] != '-' + || bytes[start + 7] != '-' + || bytes[start + 10] != 'T' + || bytes[start + 13] != ':') { + throw new IllegalArgumentException(); + } + int year = parse4(bytes, start); + int month = parse2(bytes, start + 5); + int day = parse2(bytes, start + 8); + int hour = parse2(bytes, start + 11); + int minute = parse2(bytes, start + 14); + int second = 0; + int nano = 0; + int index = start + 16; + if (index < length && bytes[index] == ':') { + second = parse2(bytes, index + 1); + index += 3; + if (index < length && bytes[index] == '.') { + int fractionStart = index + 1; + int fractionEnd = fractionStart; + while (fractionEnd < length && isDigit(bytes[fractionEnd])) { + fractionEnd++; + } + if (fractionEnd == fractionStart || fractionEnd - fractionStart > 9) { + throw new IllegalArgumentException(); + } + nano = parseNano(bytes, fractionStart, fractionEnd); + index = fractionEnd; + } + } + long offsetAndEnd = parseOffsetAndEnd(bytes, index, length); + position = (int) offsetAndEnd; + return OffsetDateTime.of( + year, + month, + day, + hour, + minute, + second, + nano, + ZoneOffset.ofTotalSeconds((int) (offsetAndEnd >> 32))); + } + + private int scanSimpleStringTail(byte[] bytes, int offset) { + int length = bytes.length; + while (offset < length) { + int b = bytes[offset++]; + if (b == '"') { + return offset; + } + if (b == '\\' || b < 0x20 || b < 0) { + throw new IllegalArgumentException(); + } + } + throw error("Unterminated string"); + } + + private static long parseOffsetAndEnd(byte[] bytes, int index, int length) { + int offset = bytes[index]; + if (offset == 'Z') { + if (index + 1 >= length || bytes[index + 1] != '"') { + throw new IllegalArgumentException(); + } + return ((long) (index + 2)) & 0xFFFF_FFFFL; + } + if (offset != '+' && offset != '-') { + throw new IllegalArgumentException(); + } + if (index + 6 >= length || bytes[index + 3] != ':') { + throw new IllegalArgumentException(); + } + int hour = parse2(bytes, index + 1); + int minute = parse2(bytes, index + 4); + int second = 0; + int end = index + 6; + if (bytes[end] == ':') { + if (end + 3 >= length) { + throw new IllegalArgumentException(); + } + second = parse2(bytes, end + 1); + end += 3; + } + if (bytes[end] != '"') { + throw new IllegalArgumentException(); + } + int total = hour * 3600 + minute * 60 + second; + if (offset == '-') { + total = -total; + } + return ((long) total << 32) | ((long) (end + 1) & 0xFFFF_FFFFL); + } + + private static int parseNano(byte[] bytes, int start, int end) { + int nano = 0; + for (int i = start; i < end; i++) { + nano = nano * 10 + bytes[i] - '0'; + } + for (int i = end - start; i < 9; i++) { + nano *= 10; + } + return nano; + } + + private static int parse4(byte[] bytes, int index) { + return parse2(bytes, index) * 100 + parse2(bytes, index + 2); + } + + private static int parse2(byte[] bytes, int index) { + int high = bytes[index] - '0'; + int low = bytes[index + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + throw new IllegalArgumentException(); + } + return high * 10 + low; + } + + private static boolean isDigit(byte b) { + return b >= '0' && b <= '9'; + } + private String readStringStop(int start, int stop, int b) { position = stop + 1; if (b >= 0 && b < 0x20) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java index 3164b9d5af..070b45354b 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java @@ -126,7 +126,7 @@ public void writeNullFields() { ForyJson json = ForyJson.builder().writeNullFields(true).build(); assertEquals( json.toJson(new PublicFields()), - "{\"active\":true,\"id\":7,\"missing\":null,\"name\":\"fory\"}"); + "{\"active\":true,\"id\":7,\"name\":\"fory\",\"missing\":null}"); } @Test @@ -134,7 +134,7 @@ public void fieldOnlyModeIgnoresMethods() { ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); assertEquals( json.toJson(new MethodsIgnored()), - "{\"hidden\":\"hidden\",\"setterCalls\":0,\"value\":\"field\"}"); + "{\"setterCalls\":0,\"value\":\"field\",\"hidden\":\"hidden\"}"); MethodsIgnored value = json.fromJson("{\"hidden\":\"json\",\"value\":\"json\"}", MethodsIgnored.class); assertEquals(hiddenValue(value), "json"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 84c1f8afbf..242aae6bf2 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -29,13 +29,25 @@ import java.nio.file.Paths; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.Optional; +import org.apache.fory.json.codec.JsonCodec; import org.apache.fory.json.data.BoxedScalars; import org.apache.fory.json.data.CoreScalarFields; import org.apache.fory.json.data.NaturalObjectValue; import org.apache.fory.json.data.NaturalValues; import org.apache.fory.json.data.NumericBoundaries; import org.apache.fory.json.data.PublicFields; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; import org.testng.annotations.Test; public class JsonScalarTest extends ForyJsonTestModels { @@ -225,11 +237,50 @@ public void readLocalDateFromDateTime() { ForyJson json = ForyJson.builder().build(); LocalDate expected = LocalDate.of(2023, 7, 2); assertEquals(json.fromJson("\"2023-07-02T16:00:00.000Z\"", LocalDate.class), expected); + assertEquals( + json.fromJson( + "\"2023-07-02T16:00:00.000Z\"".getBytes(StandardCharsets.UTF_8), LocalDate.class), + expected); LocalDateFields fields = json.fromJson("{\"value\":\"2023-07-02T16:00:00.000Z\"}", LocalDateFields.class); assertEquals(fields.value, expected); } + @Test + public void readOffsetDateTime() { + ForyJson json = ForyJson.builder().build(); + OffsetDateTime utc = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 0, ZoneOffset.UTC); + assertEquals(json.fromJson("\"2024-02-03T04:05:06Z\"", OffsetDateTime.class), utc); + assertEquals( + json.fromJson( + "\"2024-02-03T04:05:06\\u005A\"".getBytes(StandardCharsets.UTF_8), + OffsetDateTime.class), + utc); + + OffsetDateTime nanos = + OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.ofHoursMinutes(8, 30)); + assertEquals( + json.fromJson( + "\"2024-02-03T04:05:06.123456789+08:30\"".getBytes(StandardCharsets.UTF_8), + OffsetDateTime.class), + nanos); + + OffsetDateTime minutePrecision = + OffsetDateTime.of(2024, 2, 3, 4, 5, 0, 0, ZoneOffset.ofHoursMinutes(-5, -30)); + OffsetDateTimeFields fields = + json.fromJson("{\"value\":\"2024-02-03T04:05-05:30\"}", OffsetDateTimeFields.class); + assertEquals(fields.value, minutePrecision); + } + + @Test + public void objectFieldUsesUtf8Codec() { + ForyJson json = + ForyJson.builder().registerCodec(ModeAwareValue.class, new ModeAwareCodec()).build(); + ModeAwareHolder holder = + json.fromJson("{\"value\":{}}".getBytes(StandardCharsets.UTF_8), ModeAwareHolder.class); + assertEquals(holder.value.mode, "utf8"); + } + @Test public void wrapStringScalarParseErrors() { ForyJson json = ForyJson.builder().build(); @@ -263,4 +314,64 @@ public static final class FilePathFields { public static final class LocalDateFields { public LocalDate value; } + + public static final class OffsetDateTimeFields { + public OffsetDateTime value; + } + + public static final class ModeAwareHolder { + public ModeAwareValue value; + } + + public static final class ModeAwareValue { + public final String mode; + + ModeAwareValue(String mode) { + this.mode = mode; + } + } + + private static final class ModeAwareCodec implements JsonCodec { + @Override + public void write(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + public void writeString(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + public void writeUtf8(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("generic"); + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("latin1"); + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("utf16"); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("utf8"); + } + } } From ca8dbeae4b04e6596e9989abd130ef6e44c3bd12 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 16:31:22 +0800 Subject: [PATCH 003/120] perf(java): specialize JSON string arrays --- .../apache/fory/json/codec/ArrayCodec.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 9724abbd92..4aad04c1f9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -58,6 +58,8 @@ public static ArrayCodec create(Class componentType, JsonTypeResolver resolve return FloatArrayCodec.INSTANCE; } else if (componentType == double.class) { return DoubleArrayCodec.INSTANCE; + } else if (componentType == String.class) { + return StringArrayCodec.INSTANCE; } return new ObjectArrayCodec(componentType, resolver.getTypeInfo(componentType, componentType)); } @@ -634,6 +636,145 @@ Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver re } } + public static final class StringArrayCodec extends ArrayCodec { + private static final StringArrayCodec INSTANCE = new StringArrayCodec(); + + private StringArrayCodec() { + super(String.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + String[] array = (String[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + if (array[i] == null) { + writer.writeNull(); + } else { + writer.writeString(array[i]); + } + } + writer.writeArrayEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + String[] array = (String[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeStringElement(i, array[i]); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + String[] array = (String[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeStringElement(i, array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNullableString(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNextNullableString(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNextNullableString(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNextNullableString(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + public static final class ObjectArrayCodec extends ArrayCodec { private final JsonTypeInfo elementTypeInfo; private final JsonCodec elementCodec; From 06a2b669f88f0f32740d1559d24443651c969954 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 17:19:04 +0800 Subject: [PATCH 004/120] perf(java): speed up Fory JSON long writes --- .../fory/json/writer/Utf8JsonWriter.java | 68 +++++++++---------- .../org/apache/fory/json/JsonScalarTest.java | 18 +++++ 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 5ba1ec5e08..b55563ae0e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -31,6 +31,7 @@ public final class Utf8JsonWriter extends JsonWriter { "-2147483648".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); private static final byte[] MIN_LONG_BYTES = "-9223372036854775808".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); + private static final long EIGHT_DIGITS = 100_000_000L; private static final long HIGH_BITS = 0x8080808080808080L; private static final int INT_HIGH_BITS = 0x80808080; private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; @@ -115,16 +116,7 @@ public void writeLong(long value) { buffer[position++] = (byte) '-'; value = -value; } - if (value <= Integer.MAX_VALUE) { - writePositiveIntNoEnsure((int) value); - return; - } - int start = position; - do { - buffer[position++] = (byte) ('0' + value % 10); - value /= 10; - } while (value != 0); - reverse(start, position - 1); + writePositiveLongNoEnsure(value); } @Override @@ -672,14 +664,6 @@ private void writeByteRaw(byte value) { buffer[position++] = value; } - private void reverse(int start, int end) { - while (start < end) { - byte tmp = buffer[start]; - buffer[start++] = buffer[end]; - buffer[end--] = tmp; - } - } - private void ensure(int additional) { int minCapacity = position + additional; if (minCapacity > buffer.length) { @@ -750,40 +734,56 @@ private void writeLongNoEnsure(long value) { buffer[position++] = (byte) '-'; value = -value; } + writePositiveLongNoEnsure(value); + } + + private void writePositiveIntNoEnsure(int value) { + position = writePositiveInt(buffer, position, value); + } + + private void writePositiveLongNoEnsure(long value) { if (value <= Integer.MAX_VALUE) { writePositiveIntNoEnsure((int) value); return; } - int start = position; - do { - buffer[position++] = (byte) ('0' + value % 10); - value /= 10; - } while (value != 0); - reverse(start, position - 1); - } - - private void writePositiveIntNoEnsure(int value) { byte[] bytes = buffer; int pos = position; + long high = value / EIGHT_DIGITS; + int low = (int) (value - high * EIGHT_DIGITS); + if (high <= Integer.MAX_VALUE) { + pos = writePositiveInt(bytes, pos, (int) high); + } else { + long top = high / EIGHT_DIGITS; + int middle = (int) (high - top * EIGHT_DIGITS); + pos = writePositiveInt(bytes, pos, (int) top); + pos = writePadded8Digits(bytes, pos, middle); + } + position = writePadded8Digits(bytes, pos, low); + } + + private static int writePositiveInt(byte[] bytes, int pos, int value) { if (value < 10000) { - position = writeIntUpTo4(bytes, pos, value); - return; + return writeIntUpTo4(bytes, pos, value); } int high = divide10000(value); int low = value - high * 10000; if (high < 10000) { if (high >= 1000) { - position = writePadded8(bytes, pos, high, low); - return; + return writePadded8(bytes, pos, high, low); } pos = writeIntUpTo4(bytes, pos, high); - position = writePadded4(bytes, pos, low); - return; + return writePadded4(bytes, pos, low); } int top = divide10000(high); int middle = high - top * 10000; pos = writeIntUpTo4(bytes, pos, top); - position = writePadded8(bytes, pos, middle, low); + return writePadded8(bytes, pos, middle, low); + } + + private static int writePadded8Digits(byte[] bytes, int pos, int value) { + int high = divide10000(value); + int low = value - high * 10000; + return writePadded8(bytes, pos, high, low); } private static int divide10000(int value) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 242aae6bf2..ac5ee0a583 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -127,6 +127,24 @@ public void readNumericBoundaries() { "{\"intMax\":2147483648,\"text\":\"" + ZH_TEXT + "\"}", NumericBoundaries.class)); } + @Test + public void writeNumericBoundaries() { + ForyJson json = ForyJson.builder().build(); + NumericBoundaries value = new NumericBoundaries(); + value.intMax = Integer.MAX_VALUE; + value.intMin = Integer.MIN_VALUE; + value.longMax = Long.MAX_VALUE; + value.longMin = Long.MIN_VALUE; + value.small = -7; + value.text = "ok"; + String expected = + "{\"intMax\":2147483647,\"intMin\":-2147483648," + + "\"longMax\":9223372036854775807,\"longMin\":-9223372036854775808," + + "\"small\":-7,\"text\":\"ok\"}"; + assertEquals(json.toJson(value), expected); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + } + @Test public void writeReadCoreScalarFields() { ForyJson json = ForyJson.builder().build(); From 4416e3eb9a267ebc3b9f5ba5e6619e6b277d8a66 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 17:29:00 +0800 Subject: [PATCH 005/120] perf(java): write JSON scalar dates directly --- .../apache/fory/json/codec/ScalarCodecs.java | 17 ++- .../fory/json/writer/Utf8JsonWriter.java | 128 ++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 46 +++++++ 3 files changed, 190 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 0d928448dc..fa0caf8745 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -509,7 +509,7 @@ final void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolv } @Override - final void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { writer.writeString(toJsonString(value)); } @@ -745,6 +745,11 @@ String toJsonString(Object value) { return value.toString(); } + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeUuid((UUID) value); + } + @Override Object fromJsonString(String value) { return UUID.fromString(value); @@ -898,6 +903,11 @@ String toJsonString(Object value) { return value.toString(); } + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLocalDate((LocalDate) value); + } + @Override Object fromJsonString(String value) { return parseIsoLocalDate(value); @@ -1093,6 +1103,11 @@ String toJsonString(Object value) { return value.toString(); } + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeOffsetDateTime((OffsetDateTime) value); + } + @Override Object fromJsonString(String value) { return parseIsoOffsetDateTime(value); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index b55563ae0e..a430cfcedd 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -19,7 +19,10 @@ package org.apache.fory.json.writer; +import java.time.LocalDate; +import java.time.OffsetDateTime; import java.util.Arrays; +import java.util.UUID; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.memory.LittleEndian; @@ -38,6 +41,8 @@ public final class Utf8JsonWriter extends JsonWriter { private static final int INT_ASCII_CONTROL_OFFSET = 0x60606060; private static final long ONE_BYTES = 0x0101010101010101L; private static final int INT_ONE_BYTES = 0x01010101; + private static final byte[] HEX_DIGITS = + "0123456789abcdef".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); private static final long QUOTE_BYTES_COMPLEMENT = ~0x2222222222222222L; private static final int INT_QUOTE_BYTES_COMPLEMENT = ~0x22222222; private static final long BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C5C5C5C5CL; @@ -168,6 +173,61 @@ public void writeString(String value) { writeStringChars(value); } + public void writeUuid(UUID value) { + ensure(38); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + long high = value.getMostSignificantBits(); + pos = writeHex(bytes, pos, high, 60, 8); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, high, 28, 4); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, high, 12, 4); + long low = value.getLeastSignificantBits(); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, low, 60, 4); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, low, 44, 12); + bytes[pos++] = (byte) '"'; + position = pos; + } + + public void writeLocalDate(LocalDate value) { + int year = value.getYear(); + if (year < 0 || year > 9999) { + writeString(value.toString()); + return; + } + ensure(12); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + pos = writeLocalDate(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + bytes[pos++] = (byte) '"'; + position = pos; + } + + public void writeOffsetDateTime(OffsetDateTime value) { + int year = value.getYear(); + if (year < 0 || year > 9999 || value.getOffset().getTotalSeconds() != 0) { + writeString(value.toString()); + return; + } + ensure(32); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + pos = writeLocalDate(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + bytes[pos++] = (byte) 'T'; + pos = + writeTime( + bytes, pos, value.getHour(), value.getMinute(), value.getSecond(), value.getNano()); + bytes[pos++] = (byte) 'Z'; + bytes[pos++] = (byte) '"'; + position = pos; + } + private void writeStringChars(String value) { int length = value.length(); ensure(length + 2); @@ -786,6 +846,74 @@ private static int writePadded8Digits(byte[] bytes, int pos, int value) { return writePadded8(bytes, pos, high, low); } + private static int writeHex(byte[] bytes, int pos, long value, int shift, int count) { + for (int i = 0; i < count; i++) { + bytes[pos++] = HEX_DIGITS[(int) ((value >>> shift) & 0xF)]; + shift -= 4; + } + return pos; + } + + private static int writeLocalDate(byte[] bytes, int pos, int year, int month, int day) { + pos = writePadded4(bytes, pos, year); + bytes[pos++] = (byte) '-'; + pos = writeTwoDigits(bytes, pos, month); + bytes[pos++] = (byte) '-'; + return writeTwoDigits(bytes, pos, day); + } + + private static int writeTime(byte[] bytes, int pos, int hour, int minute, int second, int nano) { + pos = writeTwoDigits(bytes, pos, hour); + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, minute); + if (second != 0 || nano != 0) { + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, second); + if (nano != 0) { + bytes[pos++] = (byte) '.'; + pos = writeNano(bytes, pos, nano); + } + } + return pos; + } + + private static int writeNano(byte[] bytes, int pos, int nano) { + if (nano % 1_000_000 == 0) { + return writePadded3(bytes, pos, nano / 1_000_000); + } + if (nano % 1000 == 0) { + int micros = nano / 1000; + int high = micros / 1000; + int low = micros - high * 1000; + pos = writePadded3(bytes, pos, high); + return writePadded3(bytes, pos, low); + } + int first = nano / 100000000; + int rem = nano - first * 100000000; + int middle = rem / 10000; + int low = rem - middle * 10000; + bytes[pos++] = (byte) ('0' + first); + pos = writePadded4(bytes, pos, middle); + return writePadded4(bytes, pos, low); + } + + private static int writePadded3(byte[] bytes, int pos, int value) { + int high = value / 100; + int rem = value - high * 100; + int middle = rem / 10; + bytes[pos++] = (byte) ('0' + high); + bytes[pos++] = (byte) ('0' + middle); + bytes[pos++] = (byte) ('0' + (rem - middle * 10)); + return pos; + } + + private static int writeTwoDigits(byte[] bytes, int pos, int value) { + int high = value / 10; + bytes[pos++] = (byte) ('0' + high); + bytes[pos++] = (byte) ('0' + (value - high * 10)); + return pos; + } + private static int divide10000(int value) { return (int) (((long) value * 1759218605L) >> 44); } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index ac5ee0a583..a611405673 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -32,6 +32,7 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Optional; +import java.util.UUID; import org.apache.fory.json.codec.JsonCodec; import org.apache.fory.json.data.BoxedScalars; import org.apache.fory.json.data.CoreScalarFields; @@ -180,6 +181,51 @@ public void writeReadCoreScalarFields() { assertEquals(read.uuid, value.uuid); } + @Test + public void writeUtf8ScalarFormats() { + ForyJson json = ForyJson.builder().build(); + UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + assertEquals( + new String(json.toJsonBytes(uuid), StandardCharsets.UTF_8), + "\"123e4567-e89b-12d3-a456-426614174000\""); + assertEquals( + new String(json.toJsonBytes(LocalDate.of(2024, 2, 3)), StandardCharsets.UTF_8), + "\"2024-02-03\""); + + OffsetDateTimeFields fields = new OffsetDateTimeFields(); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 0, 0, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 0, 1_000_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:00.001Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 120_000_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.120Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123_400_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.123400Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 1_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.000001Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.123456789Z\"}"); + + OffsetDateTime offset = + OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.ofHoursMinutes(8, 30)); + fields.value = offset; + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"" + offset + "\"}"); + } + @Test public void readScalarRoots() { ForyJson json = ForyJson.builder().build(); From b59753d1ecfab83adb7c5aa34e7ed4412bedd0f1 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 18:03:04 +0800 Subject: [PATCH 006/120] feat(java): add Fory JSON OutputStream writes --- .../java/org/apache/fory/json/ForyJson.java | 23 +++++++++++++ .../fory/json/writer/Utf8JsonWriter.java | 10 ++++++ .../org/apache/fory/json/JsonObjectTest.java | 32 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index 890e25b2a7..a5a0b43bad 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -19,7 +19,9 @@ package org.apache.fory.json; +import java.io.OutputStream; import java.lang.reflect.Type; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; import org.apache.fory.json.codec.GeneratedObjectCodec; @@ -117,6 +119,27 @@ public byte[] toJsonBytes(Object value) { } } + /** Serializes {@code value} as UTF-8 JSON to {@code output}. */ + public void writeJsonTo(Object value, OutputStream output) { + Objects.requireNonNull(output, "output"); + PooledState entry = acquire(); + JsonState state = entry.state; + Utf8JsonWriter writer = state.utf8Writer; + try { + if (value == null) { + writer.writeNull(); + } else { + JsonTypeResolver resolver = state.typeResolver; + JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); + typeInfo.codec().writeUtf8(writer, value, resolver); + } + writer.writeTo(output); + } finally { + writer.reset(); + release(entry); + } + } + public T fromJson(String json, Class type) { PooledState entry = acquire(); JsonState state = entry.state; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index a430cfcedd..7d3e5510ee 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -19,6 +19,8 @@ package org.apache.fory.json.writer; +import java.io.IOException; +import java.io.OutputStream; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.Arrays; @@ -94,6 +96,14 @@ public byte[] toJsonBytes() { return Arrays.copyOf(buffer, position); } + public void writeTo(OutputStream output) { + try { + output.write(buffer, 0, position); + } catch (IOException e) { + throw new ForyJsonException("Cannot write JSON output", e); + } + } + @Override public void writeNull() { writeAscii("null"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java index 070b45354b..1fc6ba3bca 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java @@ -20,7 +20,11 @@ package org.apache.fory.json; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertThrows; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -48,6 +52,34 @@ public void writePublicFields() { "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); } + @Test + public void writeJsonToOutputStream() { + ForyJson json = ForyJson.builder().build(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + json.writeJsonTo(new PublicFields(), output); + assertEquals( + new String(output.toByteArray(), StandardCharsets.UTF_8), + "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); + + output.reset(); + json.writeJsonTo(null, output); + assertEquals(new String(output.toByteArray(), StandardCharsets.UTF_8), "null"); + + OutputStream failing = + new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException("closed"); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + throw new IOException("closed"); + } + }; + assertThrows(ForyJsonException.class, () -> json.writeJsonTo(new PublicFields(), failing)); + } + @Test public void writeFirstIntGenerated() { ForyJson json = ForyJson.builder().build(); From 7a0c0b2752fb3fc79bcf52190641e01f6da0351c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 18:13:04 +0800 Subject: [PATCH 007/120] perf(java): specialize Fory JSON generated scalars --- .../fory/json/codegen/JsonWriterCodegen.java | 23 +++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 22 ++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index 689dfa2951..dd555048d6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -19,6 +19,9 @@ package org.apache.fory.json.codegen; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.UUID; import org.apache.fory.codegen.Code; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.Expression; @@ -454,6 +457,9 @@ private Expression writeValue( return writeStringCollection(value, utf8); } return writeCodec(id, value, utf8); + case OBJECT: + Expression scalar = writeExactUtf8Scalar(property.writeRawType(), value, utf8); + return scalar == null ? writeCodec(id, value, utf8) : scalar; default: return writeCodec(id, value, utf8); } @@ -501,6 +507,23 @@ private static Expression writeCodec(int id, Expression value, boolean utf8) { typeResolverRef()); } + private static Expression writeExactUtf8Scalar(Class rawType, Expression value, boolean utf8) { + if (!utf8) { + return null; + } + String writerMethod; + if (rawType == UUID.class) { + writerMethod = "writeUuid"; + } else if (rawType == LocalDate.class) { + writerMethod = "writeLocalDate"; + } else if (rawType == OffsetDateTime.class) { + writerMethod = "writeOffsetDateTime"; + } else { + return null; + } + return new Expression.Invoke(writerRef(true), writerMethod, value); + } + private static Expression writeStringCollection(Expression value, boolean utf8) { return new Expression.ListExpression( new Expression.Invoke(writerRef(utf8), "writeArrayStart"), diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index a611405673..c3c8a7d2e9 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -226,6 +226,22 @@ public void writeUtf8ScalarFormats() { "{\"value\":\"" + offset + "\"}"); } + @Test + public void writeGeneratedUtf8Scalars() { + ForyJson json = ForyJson.builder().build(); + Utf8ScalarFields fields = new Utf8ScalarFields(); + fields.uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + fields.date = LocalDate.of(2024, 2, 3); + fields.timestamp = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.UTC); + String expected = + "{\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"," + + "\"date\":\"2024-02-03\"," + + "\"timestamp\":\"2024-02-03T04:05:06.123456789Z\"}"; + assertEquals(new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), expected); + assertEquals(json.toJson(fields), expected); + assertGeneratedWhenSupported(json, Utf8ScalarFields.class); + } + @Test public void readScalarRoots() { ForyJson json = ForyJson.builder().build(); @@ -383,6 +399,12 @@ public static final class OffsetDateTimeFields { public OffsetDateTime value; } + public static final class Utf8ScalarFields { + public UUID uuid; + public LocalDate date; + public OffsetDateTime timestamp; + } + public static final class ModeAwareHolder { public ModeAwareValue value; } From 515c7ff4933537fd592ff6228c5be236581173c5 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 18:35:01 +0800 Subject: [PATCH 008/120] perf(java): optimize Fory JSON string collections --- .../fory/json/codegen/JsonWriterCodegen.java | 3 +++ .../fory/json/writer/Utf8JsonWriter.java | 18 ++++++++++++++++++ .../org/apache/fory/json/JsonRecordTest.java | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index dd555048d6..c5af91c933 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -525,6 +525,9 @@ private static Expression writeExactUtf8Scalar(Class rawType, Expression valu } private static Expression writeStringCollection(Expression value, boolean utf8) { + if (utf8) { + return new Expression.Invoke(writerRef(true), "writeStringCollection", value); + } return new Expression.ListExpression( new Expression.Invoke(writerRef(utf8), "writeArrayStart"), new Expression.ForEach( diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 7d3e5510ee..420a5a78a7 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -23,7 +23,9 @@ import java.io.OutputStream; import java.time.LocalDate; import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.UUID; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.meta.JsonFieldInfo; @@ -386,6 +388,22 @@ private void writeStringFieldChars(byte[] prefix, String value) { writeStringNoEnsure(value); } + public void writeStringCollection(Collection values) { + writeArrayStart(); + if (values.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) values; + for (int i = 0, size = list.size(); i < size; i++) { + writeStringElement(i, list.get(i)); + } + } else { + int index = 0; + for (String value : values) { + writeStringElement(index++, value); + } + } + writeArrayEnd(); + } + public void writeStringElement(int index, String value) { int comma = index == 0 ? 0 : 1; if (value == null) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java index b65328b5bc..d2b23f264f 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java @@ -50,7 +50,7 @@ public void writeReadRecordClass() throws Exception { .newInstance(7, ZH_TEXT, Arrays.asList("a", "b"), child); ForyJson json = ForyJson.builder().build(); String expected = - "{\"child\":{\"label\":\"kid\"},\"id\":7,\"name\":\"你好,Fory\"," + "\"tags\":[\"a\",\"b\"]}"; + "{\"id\":7,\"name\":\"你好,Fory\",\"tags\":[\"a\",\"b\"]," + "\"child\":{\"label\":\"kid\"}}"; assertEquals(json.toJson(value), expected); assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); assertGeneratedWhenSupported(json, type); From 792b35a8450b2edd05f1a94fa1a331f3bbe6acc7 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 18:49:06 +0800 Subject: [PATCH 009/120] perf(java): specialize Fory JSON BigDecimal writes --- .../org/apache/fory/json/codegen/JsonWriterCodegen.java | 6 ++++++ .../src/test/java/org/apache/fory/json/JsonScalarTest.java | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index c5af91c933..f9dd5e6b5f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -19,6 +19,7 @@ package org.apache.fory.json.codegen; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.UUID; @@ -518,6 +519,11 @@ private static Expression writeExactUtf8Scalar(Class rawType, Expression valu writerMethod = "writeLocalDate"; } else if (rawType == OffsetDateTime.class) { writerMethod = "writeOffsetDateTime"; + } else if (rawType == BigDecimal.class) { + return new Expression.Invoke( + writerRef(true), + "writeNumber", + new Expression.Invoke(value, "toString", TypeRef.of(String.class)).inline()); } else { return null; } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index c3c8a7d2e9..3f1dcdefe1 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -23,6 +23,7 @@ import static org.testng.Assert.assertThrows; import java.io.File; +import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -231,10 +232,12 @@ public void writeGeneratedUtf8Scalars() { ForyJson json = ForyJson.builder().build(); Utf8ScalarFields fields = new Utf8ScalarFields(); fields.uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + fields.decimal = new BigDecimal("12345.6789"); fields.date = LocalDate.of(2024, 2, 3); fields.timestamp = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.UTC); String expected = "{\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"," + + "\"decimal\":12345.6789," + "\"date\":\"2024-02-03\"," + "\"timestamp\":\"2024-02-03T04:05:06.123456789Z\"}"; assertEquals(new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), expected); @@ -401,6 +404,7 @@ public static final class OffsetDateTimeFields { public static final class Utf8ScalarFields { public UUID uuid; + public BigDecimal decimal; public LocalDate date; public OffsetDateTime timestamp; } From 19f6470163a5e71bc148a4ec5668c2b097f84fab Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 19:29:09 +0800 Subject: [PATCH 010/120] perf(java): speed up Fory JSON BigDecimal reads --- .../apache/fory/json/codec/ScalarCodecs.java | 9 ++ .../fory/json/reader/Utf8JsonReader.java | 83 +++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 20 +++++ 3 files changed, 112 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index fa0caf8745..83ee1ed779 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -581,6 +581,15 @@ String toJsonNumber(Object value) { Object fromJsonNumber(String value) { return new BigDecimal(value); } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return reader.readBigDecimal(); + } } public static final class Float16Codec extends AbstractJsonCodec { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 855c58f411..7c74b74b61 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -19,6 +19,7 @@ package org.apache.fory.json.reader; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; @@ -354,6 +355,11 @@ public long readLongTokenValue() { return readLongToken(); } + public BigDecimal readBigDecimal() { + skipWhitespaceFast(); + return readBigDecimalToken(); + } + private long readLongToken() { byte[] bytes = input; int offset = position; @@ -456,6 +462,83 @@ private long readNegativeLongToken(int start) { return result; } + private BigDecimal readBigDecimalToken() { + byte[] bytes = input; + int offset = position; + int start = offset; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + boolean negative = false; + int ch = bytes[offset]; + if (ch == '-') { + negative = true; + offset++; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + ch = bytes[offset]; + } + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + position = offset; + rejectLeadingDigitFast(); + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readBigDecimalFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { + return readBigDecimalFallback(start); + } + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readBigDecimalFallback(start); + } + } + position = offset; + return BigDecimal.valueOf(negative ? -unscaled : unscaled, scale); + } + + private BigDecimal readBigDecimalFallback(int start) { + // Keep overflow and exponent forms on the existing string constructor path so the fast path + // only owns decimals that fit exactly as long + scale. + position = start; + return new BigDecimal(readNumber()); + } + @Override public int readFieldNameInt() { skipWhitespaceFast(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 3f1dcdefe1..c0697e3cd6 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -254,6 +254,26 @@ public void readScalarRoots() { assertEquals( json.fromJson("\"\uD83D\uDE00\u1234\"".getBytes(StandardCharsets.UTF_8), String.class), "\uD83D\uDE00\u1234"); + assertEquals( + json.fromJson("0.100".getBytes(StandardCharsets.UTF_8), BigDecimal.class), + new BigDecimal("0.100")); + assertEquals( + json.fromJson( + "12345678901234567890.123".getBytes(StandardCharsets.UTF_8), BigDecimal.class), + new BigDecimal("12345678901234567890.123")); + } + + @Test + public void readGeneratedUtf8BigDecimal() { + ForyJson json = ForyJson.builder().build(); + byte[] input = + ("{\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"," + + "\"decimal\":0.12345678901234567," + + "\"date\":\"2024-02-03\"," + + "\"timestamp\":\"2024-02-03T04:05:06Z\"}") + .getBytes(StandardCharsets.UTF_8); + Utf8ScalarFields fields = json.fromJson(input, Utf8ScalarFields.class); + assertEquals(fields.decimal, new BigDecimal("0.12345678901234567")); } @Test From 821883aeb25519b7d417c7ce52c538991ec29c3a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 19:47:48 +0800 Subject: [PATCH 011/120] perf(java): fast-path UTC JSON timestamps --- .../java/org/apache/fory/json/reader/Utf8JsonReader.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 7c74b74b61..57372d529c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -853,6 +853,13 @@ private OffsetDateTime readIsoOffsetDateTimeToken() { index = fractionEnd; } } + if (index < length && bytes[index] == 'Z') { + if (index + 1 >= length || bytes[index + 1] != '"') { + throw new IllegalArgumentException(); + } + position = index + 2; + return OffsetDateTime.of(year, month, day, hour, minute, second, nano, ZoneOffset.UTC); + } long offsetAndEnd = parseOffsetAndEnd(bytes, index, length); position = (int) offsetAndEnd; return OffsetDateTime.of( From 83baa2883f16121382e9a7ddcd05bd8a48d31f40 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 20:42:27 +0800 Subject: [PATCH 012/120] perf(java): split Fory JSON generated writers --- .../codegen/JsonGeneratedCodecBuilder.java | 5 +- .../fory/json/codegen/JsonWriterCodegen.java | 214 ++++++++++++------ 2 files changed, 147 insertions(+), 72 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java index 55b0f463a3..33ee0e568a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java @@ -121,12 +121,15 @@ private Method recordReadMethod(Field field) { Expression fieldValue(JsonFieldInfo property, Expression object) { Method getter = property.writeGetter(); if (getter != null) { + // JSON writers check the returned member value directly. Requesting expression-level null + // state here only emits an unused boolean for each nullable getter and bloats generated + // object writers enough to hurt C2 inlining. return new Expression.Invoke( object, getter.getName(), property.name(), TypeRef.of(getter.getGenericReturnType()), - !getter.getReturnType().isPrimitive()); + false); } return getFieldValue(object, writeDescriptor(property)); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index f9dd5e6b5f..a5cca49ba9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -22,11 +22,15 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; import java.util.UUID; import org.apache.fory.codegen.Code; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.Expression; import org.apache.fory.codegen.Expression.Reference; +import org.apache.fory.codegen.ExpressionOptimizer; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.JsonCodec; import org.apache.fory.json.meta.JsonFieldInfo; @@ -40,6 +44,11 @@ import org.apache.fory.reflect.TypeRef; final class JsonWriterCodegen { + private static final int MIN_SPLIT_MEMBERS = 12; + // Keep generated member helpers below C2's big-method range without fragmenting them into + // tiny calls. This mirrors Fory core's object-codec split strategy for large generated codecs. + private static final int MEMBER_GROUP_SIZE = 10; + private final JsonCodegen codegen; private final boolean writeNullFields; @@ -213,31 +222,81 @@ private Expression writeExpression( "object", new Expression.Cast( new Reference("value", TypeRef.of(Object.class)), TypeRef.of(type))); + Reference writer = writerRef(utf8); + Reference typeResolver = typeResolverRef(); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(object); Expression index = null; if (!objectStartFused) { - expressions.add(new Expression.Invoke(writerRef(utf8), "writeObjectStart")); + expressions.add(new Expression.Invoke(writer, "writeObjectStart")); index = new Expression.Variable("index", Expression.Literal.ofInt(0)); expressions.add(index); } boolean commaKnown = objectStartFused; + boolean splitMembers = properties.length >= MIN_SPLIT_MEMBERS; + List memberGroup = splitMembers ? new ArrayList<>(MEMBER_GROUP_SIZE) : null; for (int i = 0; i < properties.length; i++) { + Expression member; if (objectStartFused && i == 0) { - expressions.add( + member = writeObjectStartPrimitive( - properties[i], builder.fieldValue(properties[i], object), utf8)); + properties[i], builder.fieldValue(properties[i], object), utf8, writer); } else { - expressions.add(writeProp(builder, properties[i], i, utf8, commaKnown, index, object)); + member = + writeProp( + builder, properties[i], i, utf8, commaKnown, index, object, writer, typeResolver); + } + if (splitMembers && commaKnown) { + memberGroup.add(member); + if (memberGroup.size() == MEMBER_GROUP_SIZE) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + } + } else { + if (splitMembers) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + } + expressions.add(member); } if (writeNullFields || properties[i].writeRawType().isPrimitive()) { commaKnown = true; } } - expressions.add(new Expression.Invoke(writerRef(utf8), "writeObjectEnd")); + if (splitMembers) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + } + expressions.add(new Expression.Invoke(writer, "writeObjectEnd")); return expressions; } + private static void addMemberGroup( + JsonGeneratedCodecBuilder builder, + Expression.ListExpression expressions, + List memberGroup, + Expression object, + Reference writer, + Reference typeResolver) { + if (memberGroup.isEmpty()) { + return; + } + if (memberGroup.size() == 1) { + expressions.add(memberGroup.get(0)); + memberGroup.clear(); + return; + } + LinkedHashSet cutPoints = new LinkedHashSet<>(); + cutPoints.add(object); + cutPoints.add(writer); + cutPoints.add(typeResolver); + expressions.add( + ExpressionOptimizer.invokeGenerated( + builder.context(), + cutPoints, + new Expression.ListExpression(new ArrayList<>(memberGroup)), + "writeMembers", + false)); + memberGroup.clear(); + } + private static boolean canFuseObjectStart(JsonFieldInfo[] properties) { if (properties.length == 0 || !properties[0].writeRawType().isPrimitive()) { return false; @@ -254,16 +313,16 @@ private static boolean canFuseObjectStart(JsonFieldInfo[] properties) { } private static Expression writeObjectStartPrimitive( - JsonFieldInfo property, Expression value, boolean utf8) { + JsonFieldInfo property, Expression value, boolean utf8, Expression writer) { switch (property.writeKind()) { case BYTE: case SHORT: case INT: return new Expression.Invoke( - writerRef(utf8), "writeObjectIntField", prefixRef(utf8, false, 0), value); + writer, "writeObjectIntField", prefixRef(utf8, false, 0), value); case LONG: return new Expression.Invoke( - writerRef(utf8), "writeObjectLongField", prefixRef(utf8, false, 0), value); + writer, "writeObjectLongField", prefixRef(utf8, false, 0), value); default: throw new ForyJsonException( "Unsupported generated object-start kind " + property.writeKind()); @@ -277,11 +336,13 @@ private Expression writeProp( boolean utf8, boolean commaKnown, Expression index, - Expression object) { + Expression object, + Expression writer, + Expression typeResolver) { Class rawType = property.writeRawType(); if (rawType.isPrimitive()) { return writePrimitive( - property, id, builder.fieldValue(property, object), utf8, commaKnown, index); + property, id, builder.fieldValue(property, object), utf8, commaKnown, index, writer); } Expression value = new Expression.Variable( @@ -295,24 +356,24 @@ private Expression writeProp( new Expression.If( eq(value, nullValue), new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index), - new Expression.Invoke(writerRef(utf8), "writeNull")), - writeValue(property, id, value, utf8, commaKnown, index))); + writeFieldName(id, utf8, commaKnown, index, writer), + new Expression.Invoke(writer, "writeNull")), + writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver))); } return new Expression.ListExpression( value, - writeFieldName(id, utf8, commaKnown, index), + writeFieldName(id, utf8, commaKnown, index, writer), new Expression.If( eq(value, nullValue), - new Expression.Invoke(writerRef(utf8), "writeNull"), - writeValue(property, id, value, utf8, true, index))); + new Expression.Invoke(writer, "writeNull"), + writeValue(property, id, value, utf8, true, index, writer, typeResolver))); } Expression write = isPrefixValue(property.writeKind()) - ? writeValue(property, id, value, utf8, commaKnown, index) + ? writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver) : new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index), - writeValue(property, id, value, utf8, true, index)); + writeFieldName(id, utf8, commaKnown, index, writer), + writeValue(property, id, value, utf8, true, index, writer, typeResolver)); return new Expression.ListExpression(value, new Expression.If(ne(value, nullValue), write)); } @@ -322,21 +383,22 @@ private Expression writePrimitive( Expression value, boolean utf8, boolean commaKnown, - Expression index) { + Expression index, + Expression writer) { switch (property.writeKind()) { case BOOLEAN: return writeRawFieldValue( - utf8, commaKnown, index, booleanFieldValue(id, value, utf8, commaKnown, index)); + commaKnown, index, booleanFieldValue(id, value, utf8, commaKnown, index), writer); case BYTE: case SHORT: case INT: - return writeNumberField(id, value, false, utf8, commaKnown, index); + return writeNumberField(id, value, false, utf8, commaKnown, index, writer); case LONG: - return writeNumberField(id, value, true, utf8, commaKnown, index); + return writeNumberField(id, value, true, utf8, commaKnown, index, writer); default: return new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index), - writePrimitiveScalar(property.writeKind(), value, utf8)); + writeFieldName(id, utf8, commaKnown, index, writer), + writePrimitiveScalar(property.writeKind(), value, writer)); } } @@ -346,15 +408,16 @@ private static Expression writeNumberField( boolean longValue, boolean utf8, boolean commaKnown, - Expression index) { + Expression index, + Expression writer) { String writerMethod = longValue ? "writeLongField" : "writeIntField"; if (commaKnown) { - return new Expression.Invoke(writerRef(utf8), writerMethod, prefixRef(utf8, true, id), value); + return new Expression.Invoke(writer, writerMethod, prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = new Expression.ListExpression( new Expression.Invoke( - writerRef(utf8), + writer, writerMethod, prefixRef(utf8, false, id), prefixRef(utf8, true, id), @@ -365,15 +428,19 @@ private static Expression writeNumberField( } private static Expression writeStringField( - int id, Expression value, boolean utf8, boolean commaKnown, Expression index) { + int id, + Expression value, + boolean utf8, + boolean commaKnown, + Expression index, + Expression writer) { if (commaKnown) { - return new Expression.Invoke( - writerRef(utf8), "writeStringField", prefixRef(utf8, true, id), value); + return new Expression.Invoke(writer, "writeStringField", prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = new Expression.ListExpression( new Expression.Invoke( - writerRef(utf8), + writer, "writeStringField", prefixRef(utf8, false, id), prefixRef(utf8, true, id), @@ -384,7 +451,7 @@ private static Expression writeStringField( } private static Expression writeFieldName( - int id, boolean utf8, boolean commaKnown, Expression index) { + int id, boolean utf8, boolean commaKnown, Expression index, Expression writer) { Expression prefix = commaKnown ? prefixRef(utf8, true, id) @@ -395,8 +462,7 @@ private static Expression writeFieldName( true, TypeRef.of(byte[].class)); Expression.ListExpression expressions = - new Expression.ListExpression( - new Expression.Invoke(writerRef(utf8), "writeRawValue", prefix)); + new Expression.ListExpression(new Expression.Invoke(writer, "writeRawValue", prefix)); if (!commaKnown) { expressions.add(increment(index)); } @@ -409,12 +475,13 @@ private Expression writeValue( Expression value, boolean utf8, boolean commaKnown, - Expression index) { + Expression index, + Expression writer, + Expression typeResolver) { JsonFieldKind kind = property.writeKind(); switch (kind) { case BOOLEAN: return writeRawFieldValue( - utf8, commaKnown, index, booleanFieldValue( @@ -422,7 +489,8 @@ private Expression writeValue( new Expression.Invoke(value, "booleanValue", TypeRef.of(boolean.class)).inline(), utf8, commaKnown, - index)); + index), + writer); case BYTE: case SHORT: case INT: @@ -432,7 +500,8 @@ private Expression writeValue( false, utf8, commaKnown, - index); + index, + writer); case LONG: return writeNumberField( id, @@ -440,37 +509,37 @@ private Expression writeValue( true, utf8, commaKnown, - index); + index, + writer); case STRING: - return writeStringField(id, value, utf8, commaKnown, index); + return writeStringField(id, value, utf8, commaKnown, index, writer); case ENUM: return writeRawFieldValue( - utf8, commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index)); + commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index), writer); case FLOAT: case DOUBLE: case CHAR: - return writeScalar(kind, value, utf8); + return writeScalar(kind, value, writer); case ARRAY: case MAP: - return writeCodec(id, value, utf8); + return writeCodec(id, value, utf8, writer, typeResolver); case COLLECTION: if (property.writeElementRawType() == String.class) { - return writeStringCollection(value, utf8); + return writeStringCollection(value, utf8, writer); } - return writeCodec(id, value, utf8); + return writeCodec(id, value, utf8, writer, typeResolver); case OBJECT: - Expression scalar = writeExactUtf8Scalar(property.writeRawType(), value, utf8); - return scalar == null ? writeCodec(id, value, utf8) : scalar; + Expression scalar = writeExactUtf8Scalar(property.writeRawType(), value, utf8, writer); + return scalar == null ? writeCodec(id, value, utf8, writer, typeResolver) : scalar; default: - return writeCodec(id, value, utf8); + return writeCodec(id, value, utf8, writer, typeResolver); } } private static Expression writeRawFieldValue( - boolean utf8, boolean commaKnown, Expression index, Expression value) { + boolean commaKnown, Expression index, Expression value, Expression writer) { Expression.ListExpression expressions = - new Expression.ListExpression( - new Expression.Invoke(writerRef(utf8), "writeRawValue", value)); + new Expression.ListExpression(new Expression.Invoke(writer, "writeRawValue", value)); if (!commaKnown) { expressions.add(increment(index)); } @@ -499,16 +568,18 @@ private static Expression enumFieldValue( .inline(); } - private static Expression writeCodec(int id, Expression value, boolean utf8) { + private static Expression writeCodec( + int id, Expression value, boolean utf8, Expression writer, Expression typeResolver) { return new Expression.Invoke( fieldRef("c" + id, JsonCodec.class), utf8 ? "writeUtf8" : "writeString", - writerRef(utf8), + writer, value, - typeResolverRef()); + typeResolver); } - private static Expression writeExactUtf8Scalar(Class rawType, Expression value, boolean utf8) { + private static Expression writeExactUtf8Scalar( + Class rawType, Expression value, boolean utf8, Expression writer) { if (!utf8) { return null; } @@ -521,45 +592,46 @@ private static Expression writeExactUtf8Scalar(Class rawType, Expression valu writerMethod = "writeOffsetDateTime"; } else if (rawType == BigDecimal.class) { return new Expression.Invoke( - writerRef(true), + writer, "writeNumber", new Expression.Invoke(value, "toString", TypeRef.of(String.class)).inline()); } else { return null; } - return new Expression.Invoke(writerRef(true), writerMethod, value); + return new Expression.Invoke(writer, writerMethod, value); } - private static Expression writeStringCollection(Expression value, boolean utf8) { + private static Expression writeStringCollection( + Expression value, boolean utf8, Expression writer) { if (utf8) { - return new Expression.Invoke(writerRef(true), "writeStringCollection", value); + return new Expression.Invoke(writer, "writeStringCollection", value); } return new Expression.ListExpression( - new Expression.Invoke(writerRef(utf8), "writeArrayStart"), + new Expression.Invoke(writer, "writeArrayStart"), new Expression.ForEach( value, TypeRef.of(String.class), true, (index, element) -> - new Expression.Invoke(writerRef(utf8), "writeStringElement", index, element)), - new Expression.Invoke(writerRef(utf8), "writeArrayEnd")); + new Expression.Invoke(writer, "writeStringElement", index, element)), + new Expression.Invoke(writer, "writeArrayEnd")); } - private Expression writeScalar(JsonFieldKind kind, Expression value, boolean utf8) { + private Expression writeScalar(JsonFieldKind kind, Expression value, Expression writer) { switch (kind) { case FLOAT: return new Expression.Invoke( - writerRef(utf8), + writer, "writeFloat", new Expression.Invoke(value, "floatValue", TypeRef.of(float.class)).inline()); case DOUBLE: return new Expression.Invoke( - writerRef(utf8), + writer, "writeDouble", new Expression.Invoke(value, "doubleValue", TypeRef.of(double.class)).inline()); case CHAR: return new Expression.Invoke( - writerRef(utf8), + writer, "writeChar", new Expression.Invoke(value, "charValue", TypeRef.of(char.class)).inline()); default: @@ -567,14 +639,14 @@ private Expression writeScalar(JsonFieldKind kind, Expression value, boolean utf } } - private Expression writePrimitiveScalar(JsonFieldKind kind, Expression value, boolean utf8) { + private Expression writePrimitiveScalar(JsonFieldKind kind, Expression value, Expression writer) { switch (kind) { case FLOAT: - return new Expression.Invoke(writerRef(utf8), "writeFloat", value); + return new Expression.Invoke(writer, "writeFloat", value); case DOUBLE: - return new Expression.Invoke(writerRef(utf8), "writeDouble", value); + return new Expression.Invoke(writer, "writeDouble", value); case CHAR: - return new Expression.Invoke(writerRef(utf8), "writeChar", value); + return new Expression.Invoke(writer, "writeChar", value); default: throw new ForyJsonException("Unsupported generated primitive kind " + kind); } From 5982976953c900c5c710fee4fcbcf4ffec641637 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 20:55:14 +0800 Subject: [PATCH 013/120] perf(java): add Fory JSON common array codecs --- .../apache/fory/json/codec/ArrayCodec.java | 382 +++++++++++++++++- .../apache/fory/json/codec/ScalarCodecs.java | 157 +++++++ .../json/resolver/JsonSharedRegistry.java | 9 + .../apache/fory/json/JsonContainerTest.java | 100 +++++ .../org/apache/fory/json/JsonScalarTest.java | 24 ++ 5 files changed, 671 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 4aad04c1f9..de3ca843b0 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -58,6 +58,22 @@ public static ArrayCodec create(Class componentType, JsonTypeResolver resolve return FloatArrayCodec.INSTANCE; } else if (componentType == double.class) { return DoubleArrayCodec.INSTANCE; + } else if (componentType == Integer.class) { + return BoxedIntArrayCodec.INSTANCE; + } else if (componentType == Long.class) { + return BoxedLongArrayCodec.INSTANCE; + } else if (componentType == Boolean.class) { + return BoxedBooleanArrayCodec.INSTANCE; + } else if (componentType == Short.class) { + return BoxedShortArrayCodec.INSTANCE; + } else if (componentType == Byte.class) { + return BoxedByteArrayCodec.INSTANCE; + } else if (componentType == Character.class) { + return BoxedCharArrayCodec.INSTANCE; + } else if (componentType == Float.class) { + return BoxedFloatArrayCodec.INSTANCE; + } else if (componentType == Double.class) { + return BoxedDoubleArrayCodec.INSTANCE; } else if (componentType == String.class) { return StringArrayCodec.INSTANCE; } @@ -842,6 +858,367 @@ private Object toArray(List values) { } } + public static final class BoxedIntArrayCodec extends ArrayCodec { + private static final BoxedIntArrayCodec INSTANCE = new BoxedIntArrayCodec(); + + private BoxedIntArrayCodec() { + super(Integer.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Integer[] array = (Integer[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Integer element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeInt(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Integer[0]; + } + Integer[] values = new Integer[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : reader.readInt(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedLongArrayCodec extends ArrayCodec { + private static final BoxedLongArrayCodec INSTANCE = new BoxedLongArrayCodec(); + + private BoxedLongArrayCodec() { + super(Long.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Long[] array = (Long[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Long element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeLong(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Long[0]; + } + Long[] values = new Long[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : reader.readLong(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedBooleanArrayCodec extends ArrayCodec { + private static final BoxedBooleanArrayCodec INSTANCE = new BoxedBooleanArrayCodec(); + + private BoxedBooleanArrayCodec() { + super(Boolean.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Boolean[] array = (Boolean[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Boolean element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeBoolean(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Boolean[0]; + } + Boolean[] values = new Boolean[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : reader.readBoolean(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedShortArrayCodec extends ArrayCodec { + private static final BoxedShortArrayCodec INSTANCE = new BoxedShortArrayCodec(); + + private BoxedShortArrayCodec() { + super(Short.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Short[] array = (Short[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Short element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeInt(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Short[0]; + } + Short[] values = new Short[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : readShort(reader.readInt()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedByteArrayCodec extends ArrayCodec { + private static final BoxedByteArrayCodec INSTANCE = new BoxedByteArrayCodec(); + + private BoxedByteArrayCodec() { + super(Byte.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Byte[] array = (Byte[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Byte element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeInt(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Byte[0]; + } + Byte[] values = new Byte[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : readByte(reader.readInt()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedCharArrayCodec extends ArrayCodec { + private static final BoxedCharArrayCodec INSTANCE = new BoxedCharArrayCodec(); + + private BoxedCharArrayCodec() { + super(Character.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Character[] array = (Character[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Character element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeChar(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Character[0]; + } + Character[] values = new Character[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + String value = reader.readNullableString(); + values[size++] = value == null ? null : readChar(value); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedFloatArrayCodec extends ArrayCodec { + private static final BoxedFloatArrayCodec INSTANCE = new BoxedFloatArrayCodec(); + + private BoxedFloatArrayCodec() { + super(Float.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Float[] array = (Float[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Float element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeFloat(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Float[0]; + } + Float[] values = new Float[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : Float.parseFloat(reader.readNumber()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedDoubleArrayCodec extends ArrayCodec { + private static final BoxedDoubleArrayCodec INSTANCE = new BoxedDoubleArrayCodec(); + + private BoxedDoubleArrayCodec() { + super(Double.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Double[] array = (Double[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Double element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeDouble(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Double[0]; + } + Double[] values = new Double[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : Double.parseDouble(reader.readNumber()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + private static void rejectNull(JsonReader reader) { if (reader.tryReadNull()) { throw new ForyJsonException("Cannot read null into primitive array element"); @@ -881,7 +1258,10 @@ private static byte readByte(int value) { } private static char readChar(JsonReader reader) { - String value = reader.readString(); + return readChar(reader.readString()); + } + + private static char readChar(String value) { if (value.length() != 1) { throw new ForyJsonException("Expected one-character JSON string for char"); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 83ee1ed779..fbb00260b2 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -56,8 +56,11 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Pattern; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.meta.JsonAsciiToken; @@ -1351,6 +1354,160 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso } } + public static final class AtomicIntegerArrayCodec extends AbstractJsonCodec { + public static final AtomicIntegerArrayCodec INSTANCE = new AtomicIntegerArrayCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicIntegerArray array = (AtomicIntegerArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeInt(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicIntegerArray array = (AtomicIntegerArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeInt(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new AtomicIntegerArray(0); + } + int[] values = new int[8]; + int size = 0; + do { + if (reader.tryReadNull()) { + throw new ForyJsonException("Cannot read null into AtomicIntegerArray element"); + } + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readInt(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return new AtomicIntegerArray(Arrays.copyOf(values, size)); + } + } + + public static final class AtomicLongArrayCodec extends AbstractJsonCodec { + public static final AtomicLongArrayCodec INSTANCE = new AtomicLongArrayCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicLongArray array = (AtomicLongArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeLong(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicLongArray array = (AtomicLongArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeLong(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new AtomicLongArray(0); + } + long[] values = new long[8]; + int size = 0; + do { + if (reader.tryReadNull()) { + throw new ForyJsonException("Cannot read null into AtomicLongArray element"); + } + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readLong(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return new AtomicLongArray(Arrays.copyOf(values, size)); + } + } + + public static final class AtomicReferenceArrayCodec extends AbstractJsonCodec { + private final JsonTypeInfo valueTypeInfo; + private final JsonCodec valueCodec; + + public AtomicReferenceArrayCodec(java.lang.reflect.Type valueType, JsonTypeResolver resolver) { + Class valueRawType = CodecUtils.rawType(valueType, Object.class); + valueTypeInfo = resolver.getTypeInfo(valueType, valueRawType); + valueCodec = valueTypeInfo.codec(); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicReferenceArray array = (AtomicReferenceArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + valueCodec.write(writer, array.get(i), resolver); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicReferenceArray array = (AtomicReferenceArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + valueCodec.writeUtf8(writer, array.get(i), resolver); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new AtomicReferenceArray<>(0); + } + Object[] values = new Object[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = valueCodec.read(reader, valueTypeInfo, resolver); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return new AtomicReferenceArray<>(Arrays.copyOf(values, size)); + } + } + public static final class OptionalCodec extends AbstractJsonCodec { private final JsonTypeInfo valueTypeInfo; private final JsonCodec valueCodec; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index a31d05ceb1..8bdb3c3e26 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -56,8 +56,11 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Pattern; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.ArrayCodec; @@ -121,6 +124,10 @@ public JsonCodec createCodec( return new ScalarCodecs.AtomicReferenceCodec( CodecUtils.elementType(typeRef.getType()), localResolver); } + if (rawType == AtomicReferenceArray.class) { + return new ScalarCodecs.AtomicReferenceArrayCodec( + CodecUtils.elementType(typeRef.getType()), localResolver); + } if (Calendar.class.isAssignableFrom(rawType)) { return ScalarCodecs.CalendarCodec.INSTANCE; } @@ -228,7 +235,9 @@ private void registerExactCodecs() { exactCodecs.put(StringBuffer.class, ScalarCodecs.StringBufferCodec.INSTANCE); exactCodecs.put(AtomicBoolean.class, ScalarCodecs.AtomicBooleanCodec.INSTANCE); exactCodecs.put(AtomicInteger.class, ScalarCodecs.AtomicIntegerCodec.INSTANCE); + exactCodecs.put(AtomicIntegerArray.class, ScalarCodecs.AtomicIntegerArrayCodec.INSTANCE); exactCodecs.put(AtomicLong.class, ScalarCodecs.AtomicLongCodec.INSTANCE); + exactCodecs.put(AtomicLongArray.class, ScalarCodecs.AtomicLongArrayCodec.INSTANCE); exactCodecs.put(Currency.class, ScalarCodecs.CurrencyCodec.INSTANCE); exactCodecs.put(File.class, ScalarCodecs.FileCodec.INSTANCE); exactCodecs.put(URI.class, ScalarCodecs.UriCodec.INSTANCE); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 43bec41c67..b9c5007e90 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -31,6 +31,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.concurrent.atomic.AtomicLongArray; +import java.util.concurrent.atomic.AtomicReferenceArray; import org.apache.fory.json.data.FastContainers; import org.apache.fory.json.data.MapKeyFields; import org.apache.fory.json.data.Nested; @@ -261,6 +264,76 @@ public void readPrimitiveArrayRoots() { assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", int[].class)); } + @Test + public void writeReadBoxedPrimitiveArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new Integer[] {1, null, -2}), "[1,null,-2]"); + assertEquals( + json.fromJson("[1,null,-2]".getBytes(StandardCharsets.UTF_8), Integer[].class), + new Integer[] {1, null, -2}); + assertEquals( + json.fromJson("[9223372036854775807,null,-9]", Long[].class), + new Long[] {Long.MAX_VALUE, null, -9L}); + assertEquals( + json.fromJson("[true,null,false]".getBytes(StandardCharsets.UTF_8), Boolean[].class), + new Boolean[] {Boolean.TRUE, null, Boolean.FALSE}); + assertEquals( + json.fromJson("[32767,null,-32768]", Short[].class), + new Short[] {Short.MAX_VALUE, null, Short.MIN_VALUE}); + assertEquals( + json.fromJson("[127,null,-128]", Byte[].class), + new Byte[] {Byte.MAX_VALUE, null, Byte.MIN_VALUE}); + assertEquals( + json.fromJson("[\"a\",null,\"你\"]", Character[].class), new Character[] {'a', null, '你'}); + assertEquals( + json.fromJson("[1.5,null,-2.25]", Float[].class), new Float[] {1.5f, null, -2.25f}); + assertEquals( + json.fromJson("[1.5,null,-2.25]".getBytes(StandardCharsets.UTF_8), Double[].class), + new Double[] {1.5d, null, -2.25d}); + assertEquals( + new String(json.toJsonBytes(new Character[] {'x', null, '文'}), StandardCharsets.UTF_8), + "[\"x\",null,\"文\"]"); + + assertThrows(ForyJsonException.class, () -> json.fromJson("[128]", Byte[].class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("[\"ab\"]", Character[].class)); + } + + @Test + public void writeReadAtomicArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new AtomicIntegerArray(new int[] {1, -2, 3})), "[1,-2,3]"); + assertAtomicInts(json.fromJson("[1,-2,3]", AtomicIntegerArray.class), 1, -2, 3); + assertEquals( + new String( + json.toJsonBytes(new AtomicLongArray(new long[] {4L, -5L})), StandardCharsets.UTF_8), + "[4,-5]"); + assertAtomicLongs( + json.fromJson("[4,-5]".getBytes(StandardCharsets.UTF_8), AtomicLongArray.class), 4L, -5L); + + AtomicReferenceArray refs = + new AtomicReferenceArray<>(new String[] {"a", null, ZH_TEXT}); + assertEquals(json.toJson(refs), "[\"a\",null,\"你好,Fory\"]"); + assertAtomicRefs( + json.fromJson( + "[\"a\",null,\"你好,Fory\"]".getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}), + "a", + null, + ZH_TEXT); + + String fieldsJson = "{\"ints\":[7,8],\"longs\":[9,-10],\"refs\":[\"b\",null]}"; + AtomicArrayFields fields = + json.fromJson(fieldsJson.getBytes(StandardCharsets.UTF_8), AtomicArrayFields.class); + assertAtomicInts(fields.ints, 7, 8); + assertAtomicLongs(fields.longs, 9L, -10L); + assertAtomicRefs(fields.refs, "b", null); + assertEquals(json.toJson(fields), fieldsJson); + + assertThrows( + ForyJsonException.class, () -> json.fromJson("[1,null]", AtomicIntegerArray.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", AtomicLongArray.class)); + } + public static final class Shelf { public NoteList notes; } @@ -274,4 +347,31 @@ public static final class Note { public static final class PaletteGroups extends HashMap {} public static final class PaletteCodes extends HashMap {} + + public static final class AtomicArrayFields { + public AtomicIntegerArray ints = new AtomicIntegerArray(new int[] {7, 8}); + public AtomicLongArray longs = new AtomicLongArray(new long[] {9L, -10L}); + public AtomicReferenceArray refs = new AtomicReferenceArray<>(new String[] {"b", null}); + } + + private static void assertAtomicInts(AtomicIntegerArray array, int... expected) { + assertEquals(array.length(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(array.get(i), expected[i]); + } + } + + private static void assertAtomicLongs(AtomicLongArray array, long... expected) { + assertEquals(array.length(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(array.get(i), expected[i]); + } + } + + private static void assertAtomicRefs(AtomicReferenceArray array, String... expected) { + assertEquals(array.length(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(array.get(i), expected[i]); + } + } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index c0697e3cd6..02fa6e7c10 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -34,6 +34,10 @@ import java.time.ZoneOffset; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import org.apache.fory.json.codec.JsonCodec; import org.apache.fory.json.data.BoxedScalars; import org.apache.fory.json.data.CoreScalarFields; @@ -50,6 +54,7 @@ import org.apache.fory.json.writer.JsonWriter; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.reflect.TypeRef; import org.testng.annotations.Test; public class JsonScalarTest extends ForyJsonTestModels { @@ -182,6 +187,25 @@ public void writeReadCoreScalarFields() { assertEquals(read.uuid, value.uuid); } + @Test + public void writeReadAtomicScalars() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new AtomicBoolean(true)), "true"); + assertEquals(json.fromJson("false", AtomicBoolean.class).get(), false); + assertEquals(json.toJson(new AtomicInteger(12)), "12"); + assertEquals(json.fromJson("13", AtomicInteger.class).get(), 13); + assertEquals(json.toJson(new AtomicLong(14L)), "14"); + assertEquals(json.fromJson("15", AtomicLong.class).get(), 15L); + assertEquals(json.toJson(new AtomicReference<>("value")), "\"value\""); + + AtomicReference value = + json.fromJson("\"typed\"", new TypeRef>() {}); + assertEquals(value.get(), "typed"); + AtomicReference nullValue = + json.fromJson("null", new TypeRef>() {}); + assertEquals(nullValue.get(), null); + } + @Test public void writeUtf8ScalarFormats() { ForyJson json = ForyJson.builder().build(); From 4643cf475cd0da1369803352587b5408791ed855 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 22:48:24 +0800 Subject: [PATCH 014/120] perf(java): split Fory JSON generated readers --- .../fory/json/codegen/JsonReaderCodegen.java | 406 ++++++++++++++++-- .../fory/json/JsonGeneratedCodecTest.java | 54 +++ 2 files changed, 435 insertions(+), 25 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index da7f5e87a0..d44722105b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -46,6 +46,8 @@ final class JsonReaderCodegen { private static final int LATIN1_READER = JsonCodegen.LATIN1_READER; private static final int UTF16_READER = JsonCodegen.UTF16_READER; private static final int UTF8_READER = JsonCodegen.UTF8_READER; + private static final int MIN_SPLIT_READ_FIELDS = 12; + private static final int READ_FIELD_GROUP_SIZE = 2; private final JsonCodegen codegen; @@ -125,7 +127,8 @@ String genReaderCode( ctx, "public", "readLatin1", - fastReadExpression(builder, "readLatin1Slow", type, properties, LATIN1_READER, record), + fastReadExpression( + builder, "readLatin1", "readLatin1Slow", type, properties, LATIN1_READER, record), Object.class, Latin1JsonReader.class, "reader", @@ -133,6 +136,16 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addFastReadGroupMethods( + ctx, + builder, + "readLatin1", + "readLatin1Slow", + Latin1JsonReader.class, + type, + properties, + LATIN1_READER, + record); addSlowReadMethods( ctx, builder, @@ -146,7 +159,8 @@ String genReaderCode( ctx, "public", "readUtf16", - fastReadExpression(builder, "readUtf16Slow", type, properties, UTF16_READER, record), + fastReadExpression( + builder, "readUtf16", "readUtf16Slow", type, properties, UTF16_READER, record), Object.class, Utf16JsonReader.class, "reader", @@ -154,6 +168,16 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addFastReadGroupMethods( + ctx, + builder, + "readUtf16", + "readUtf16Slow", + Utf16JsonReader.class, + type, + properties, + UTF16_READER, + record); addSlowReadMethods( ctx, builder, @@ -167,7 +191,8 @@ String genReaderCode( ctx, "public", "readUtf8", - fastReadExpression(builder, "readUtf8Slow", type, properties, UTF8_READER, record), + fastReadExpression( + builder, "readUtf8", "readUtf8Slow", type, properties, UTF8_READER, record), Object.class, Utf8JsonReader.class, "reader", @@ -175,11 +200,58 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addFastReadGroupMethods( + ctx, + builder, + "readUtf8", + "readUtf8Slow", + Utf8JsonReader.class, + type, + properties, + UTF8_READER, + record); addSlowReadMethods( ctx, builder, "readUtf8Slow", Utf8JsonReader.class, type, properties, UTF8_READER, record); return ctx.genCode(); } + private void addFastReadGroupMethods( + CodegenContext ctx, + JsonGeneratedCodecBuilder builder, + String readMethod, + String slowMethod, + Class readerType, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + if (!shouldSplitFastRead(properties)) { + return; + } + Class objectType = record ? Object[].class : type; + for (int start = 0; start < properties.length; ) { + int end = readGroupEnd(properties, start); + addGeneratedMethod( + ctx, + "final", + readGroupMethod(readMethod, start), + fastReadGroupExpression( + builder, slowMethod, type, properties, start, end, readerMode, record), + boolean.class, + readerType, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver", + objectType, + "object", + long[].class, + "fieldHashes"); + start = end; + } + } + private void addSlowReadMethods( CodegenContext ctx, JsonGeneratedCodecBuilder builder, @@ -338,11 +410,16 @@ private Expression readExpression( private Expression fastReadExpression( JsonGeneratedCodecBuilder builder, + String readMethod, String slowMethod, Class type, JsonFieldInfo[] properties, int readerMode, boolean record) { + if (shouldSplitFastRead(properties)) { + return splitFastReadExpression( + builder, readMethod, slowMethod, type, properties, readerMode, record); + } Expression object = objectExpression(builder, record); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(object); @@ -371,6 +448,86 @@ private Expression fastReadExpression( return expressions; } + private Expression splitFastReadExpression( + JsonGeneratedCodecBuilder builder, + String readMethod, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + Expression object = objectExpression(builder, record); + Expression.ListExpression expressions = new Expression.ListExpression(); + expressions.add(object); + expressions.add(expectExpr(readerMode, '{')); + expressions.add(new Expression.If(consumeExpr(readerMode, '}'), returnObject(object, record))); + Expression hashes = + new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); + expressions.add(hashes); + for (int start = 0; start < properties.length; ) { + int end = readGroupEnd(properties, start); + Expression groupCall = + new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + readGroupMethod(readMethod, start), + "", + TypeRef.of(boolean.class), + false, + false, + readerRef(readerMode), + ownerRef(), + typeResolverRef(), + object, + hashes); + expressions.add(new Expression.If(not(groupCall), returnObject(object, record))); + start = end; + } + expressions.add(returnObject(object, record)); + return expressions; + } + + private Expression fastReadGroupExpression( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int start, + int end, + int readerMode, + boolean record) { + Expression object = objectParam(type, record); + Expression hashes = new Reference("fieldHashes", TypeRef.of(long[].class)); + Expression[] skips = new Expression[properties.length]; + Expression.ListExpression expressions = new Expression.ListExpression(); + for (int i = start + 1; i < end; i++) { + skips[i] = new Expression.Variable("skip" + i, Expression.Literal.False); + expressions.add(skips[i]); + } + for (int i = start; i < end; i++) { + Expression read = + fastReadField( + builder, + slowMethod, + type, + properties, + i, + end, + true, + readerMode, + object, + hashes, + skips, + record); + expressions.add(i == start ? read : new Expression.If(not(skips[i]), read)); + } + if (end < properties.length) { + expressions.add(returnTrue()); + } else { + expressions.add(returnFalse()); + } + return expressions; + } + private Expression fastReadField( JsonGeneratedCodecBuilder builder, String slowMethod, @@ -382,8 +539,36 @@ private Expression fastReadField( Expression hashes, Expression[] skips, boolean record) { + return fastReadField( + builder, + slowMethod, + type, + properties, + index, + properties.length, + false, + readerMode, + object, + hashes, + skips, + record); + } + + private Expression fastReadField( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int index, + int groupEnd, + boolean groupHelper, + int readerMode, + Expression object, + Expression hashes, + Expression[] skips, + boolean record) { if (isPackedName(properties[index].name())) { - return new Expression.If( + return statementIf( tryReadNextFieldNameColon(readerMode, properties[index]), new Expression.ListExpression( readField( @@ -395,21 +580,43 @@ private Expression fastReadField( object, record, usesTokenValueRead(readerMode)), - fieldEnd(slowMethod, properties.length, index, readerMode, object, record)), + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + index, + readerMode, + object, + record)), nextDirectFallback( builder, slowMethod, type, properties, index, + groupEnd, + groupHelper, readerMode, object, hashes, skips, - record)); + record), + groupHelper); } return nextDirectFallback( - builder, slowMethod, type, properties, index, readerMode, object, hashes, skips, record); + builder, + slowMethod, + type, + properties, + index, + groupEnd, + groupHelper, + readerMode, + object, + hashes, + skips, + record); } private Expression nextDirectFallback( @@ -418,14 +625,16 @@ private Expression nextDirectFallback( Class type, JsonFieldInfo[] properties, int index, + int groupEnd, + boolean groupHelper, int readerMode, Expression object, Expression hashes, Expression[] skips, boolean record) { int nextIndex = index + 1; - if (nextIndex < properties.length && isPackedName(properties[nextIndex].name())) { - return new Expression.If( + if (nextIndex < groupEnd && isPackedName(properties[nextIndex].name())) { + return statementIf( tryReadNextFieldNameColon(readerMode, properties[nextIndex]), new Expression.ListExpression( readField( @@ -437,22 +646,44 @@ private Expression nextDirectFallback( object, record, usesTokenValueRead(readerMode)), - fieldEnd(slowMethod, properties.length, nextIndex, readerMode, object, record), - new Expression.Assign(skips[nextIndex], Expression.Literal.True)), + new Expression.Assign(skips[nextIndex], Expression.Literal.True), + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + nextIndex, + readerMode, + object, + record)), hashFallback( builder, slowMethod, type, properties, index, + groupEnd, + groupHelper, readerMode, object, hashes, skips, - record)); + record), + groupHelper); } return hashFallback( - builder, slowMethod, type, properties, index, readerMode, object, hashes, skips, record); + builder, + slowMethod, + type, + properties, + index, + groupEnd, + groupHelper, + readerMode, + object, + hashes, + skips, + record); } private Expression hashFallback( @@ -461,6 +692,8 @@ private Expression hashFallback( Class type, JsonFieldInfo[] properties, int index, + int groupEnd, + boolean groupHelper, int readerMode, Expression object, Expression hashes, @@ -475,6 +708,8 @@ private Expression hashFallback( type, properties, index, + groupEnd, + groupHelper, readerMode, object, hashes, @@ -489,6 +724,8 @@ private Expression fastReadFieldFromHash( Class type, JsonFieldInfo[] properties, int index, + int groupEnd, + boolean groupHelper, int readerMode, Expression object, Expression hashes, @@ -496,9 +733,9 @@ private Expression fastReadFieldFromHash( Expression fieldHash, boolean record) { Expression fallback; - if (index + 1 < properties.length) { + if (index + 1 < groupEnd) { fallback = - new Expression.If( + statementIf( eq(fieldHash, arrayValue(hashes, index + 1)), new Expression.ListExpression( expectExpr(readerMode, ':'), @@ -511,19 +748,40 @@ private Expression fastReadFieldFromHash( object, record, false), - fieldEnd(slowMethod, properties.length, index + 1, readerMode, object, record), - new Expression.Assign(skips[index + 1], Expression.Literal.True)), - slowConsumedReturn(slowMethod, index, fieldIndexInvoke(fieldHash), object, record)); + new Expression.Assign(skips[index + 1], Expression.Literal.True), + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + index + 1, + readerMode, + object, + record)), + slowConsumedReturn( + slowMethod, index, fieldIndexInvoke(fieldHash), object, record, groupHelper), + groupHelper); } else { - fallback = slowConsumedReturn(slowMethod, index, fieldIndexInvoke(fieldHash), object, record); + fallback = + slowConsumedReturn( + slowMethod, index, fieldIndexInvoke(fieldHash), object, record, groupHelper); } - return new Expression.If( + return statementIf( ne(fieldHash, arrayValue(hashes, index)), fallback, new Expression.ListExpression( expectExpr(readerMode, ':'), readField(builder, type, properties[index], index, readerMode, object, record, false), - fieldEnd(slowMethod, properties.length, index, readerMode, object, record))); + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + index, + readerMode, + object, + record)), + groupHelper); } private static boolean isPackedName(String name) { @@ -544,6 +802,44 @@ private static long packedNameMask(int length) { return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L; } + private static boolean shouldSplitFastRead(JsonFieldInfo[] properties) { + return properties.length >= MIN_SPLIT_READ_FIELDS; + } + + private static String readGroupMethod(String readMethod, int start) { + return readMethod + "Group" + start; + } + + private static int readGroupEnd(JsonFieldInfo[] properties, int start) { + int weight = 0; + int index = start; + while (index < properties.length) { + int fieldWeight = readFieldWeight(properties[index]); + if (index > start && weight + fieldWeight > READ_FIELD_GROUP_SIZE) { + break; + } + weight += fieldWeight; + index++; + if (fieldWeight >= READ_FIELD_GROUP_SIZE) { + break; + } + } + return index; + } + + private static int readFieldWeight(JsonFieldInfo property) { + switch (property.readKind()) { + case ARRAY: + case COLLECTION: + case MAP: + case OBJECT: + case ENUM: + return READ_FIELD_GROUP_SIZE; + default: + return 1; + } + } + private Expression objectExpression(JsonGeneratedCodecBuilder builder, boolean record) { if (record) { return new Expression.Variable( @@ -562,6 +858,22 @@ private Expression returnObject(Expression object, boolean record) { return new Expression.Return(object); } + private static Expression returnTrue() { + return new Expression.Return(Expression.Literal.True); + } + + private static Expression returnFalse() { + return new Expression.Return(Expression.Literal.False); + } + + private static Expression statementIf( + Expression predicate, Expression trueExpr, Expression falseExpr, boolean statementOnly) { + if (statementOnly) { + return new Expression.If(predicate, trueExpr, falseExpr, false, TypeRef.of(void.class)); + } + return new Expression.If(predicate, trueExpr, falseExpr); + } + private Expression slowConsumedReturn( String slowMethod, int index, Expression firstFieldIndex, Expression object, boolean record) { return new Expression.ListExpression( @@ -569,6 +881,36 @@ private Expression slowConsumedReturn( returnObject(object, record)); } + private Expression slowConsumedReturn( + String slowMethod, + int index, + Expression firstFieldIndex, + Expression object, + boolean record, + boolean groupHelper) { + if (!groupHelper) { + return slowConsumedReturn(slowMethod, index, firstFieldIndex, object, record); + } + return new Expression.ListExpression( + slowCall(slowMethod, object, Expression.Literal.ofInt(index), firstFieldIndex), + returnFalse()); + } + + private Expression fieldEnd( + String slowMethod, + int propertyCount, + int groupEnd, + boolean groupHelper, + int index, + int readerMode, + Expression object, + boolean record) { + if (!groupHelper) { + return fieldEnd(slowMethod, propertyCount, index, readerMode, object, record); + } + return fastReadGroupEnd(slowMethod, propertyCount, index, readerMode, object); + } + private Expression fieldEnd( String slowMethod, int propertyCount, @@ -585,6 +927,17 @@ private Expression fieldEnd( slowCall(slowMethod, object, Expression.Literal.ofInt(propertyCount))); } + private Expression fastReadGroupEnd( + String slowMethod, int propertyCount, int index, int readerMode, Expression object) { + if (index + 1 < propertyCount) { + return new Expression.If(not(consumeCommaOrEndObjectExpr(readerMode)), returnFalse()); + } + return new Expression.ListExpression( + new Expression.If( + consumeCommaOrEndObjectExpr(readerMode), + slowCall(slowMethod, object, Expression.Literal.ofInt(propertyCount)))); + } + private Expression slowReadExpression( JsonGeneratedCodecBuilder builder, Class type, @@ -1354,19 +1707,22 @@ private static Expression readEnumValue( readEnumMethod(readerMode, tokenValueRead, hashFallback), "", TypeRef.of(Object.class), - true, + false, false, readerRef(readerMode)), TypeRef.of(enumType)); } private static Expression readResolvedValue(JsonFieldInfo property, int id, int readerMode) { + // Generated readers either branch on JSON null before calling a non-null codec path, or assign + // nullable reference results directly. Requesting expression null-state here only emits dead + // boolean locals around codec calls and bloats hot generated reader methods. return new Expression.Cast( new Expression.Invoke( fieldRef("r" + id, JsonCodec.class), readObjectMethod(readerMode), TypeRef.of(Object.class), - true, + false, readerRef(readerMode), fieldRef("t" + id, JsonTypeInfo.class), typeResolverRef()), @@ -1379,7 +1735,7 @@ private static Expression readCollectionValue(JsonFieldInfo property, int id, in fieldRef("r" + id, CollectionCodec.class), readObjectNonNullMethod(readerMode), TypeRef.of(Object.class), - true, + false, readerRef(readerMode), fieldRef("t" + id, JsonTypeInfo.class), typeResolverRef()), @@ -1395,7 +1751,7 @@ private static Expression readObjectValue( codec, readObjectNonNullMethod(readerMode), TypeRef.of(Object.class), - true, + false, readerRef(readerMode), fieldRef("t" + id, JsonTypeInfo.class), typeResolverRef()), diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index cf6a10d7d1..5c58e516ed 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -119,4 +119,58 @@ public void readGeneratedCollectionFields() { json.fromJson(input.getBytes(StandardCharsets.UTF_8), GeneratedCollectionFields.class)); assertGeneratedWhenSupported(json, GeneratedCollectionFields.class); } + + @Test + public void readSplitGeneratedFields() { + ForyJson json = ForyJson.builder().build(); + String ordered = + "{\"f0\":0,\"f1\":\"one\",\"f2\":2,\"f3\":\"three\",\"f4\":4,\"f5\":\"five\"," + + "\"f6\":6,\"f7\":\"seven\",\"f8\":8,\"f9\":\"nine\",\"f10\":10," + + "\"f11\":\"eleven\",\"f12\":12,\"f13\":\"thirteen\"}"; + assertWideFields(json.fromJson(ordered, WideFields.class)); + assertWideFields(json.fromJson(ordered.getBytes(StandardCharsets.UTF_8), WideFields.class)); + + String boundaryFallback = + "{\"f0\":0,\"f2\":2,\"f1\":\"one\",\"f3\":\"three\",\"f4\":4,\"f5\":\"five\"," + + "\"f6\":6,\"f7\":\"seven\",\"f8\":8,\"f9\":\"nine\",\"f10\":10," + + "\"f11\":\"eleven\",\"f12\":12,\"f13\":\"thirteen\"}"; + assertWideFields(json.fromJson(boundaryFallback, WideFields.class)); + assertWideFields( + json.fromJson(boundaryFallback.getBytes(StandardCharsets.UTF_8), WideFields.class)); + assertGeneratedWhenSupported(json, WideFields.class); + } + + private static void assertWideFields(WideFields value) { + assertEquals(value.f0, 0); + assertEquals(value.f1, "one"); + assertEquals(value.f2, 2); + assertEquals(value.f3, "three"); + assertEquals(value.f4, 4); + assertEquals(value.f5, "five"); + assertEquals(value.f6, 6); + assertEquals(value.f7, "seven"); + assertEquals(value.f8, 8); + assertEquals(value.f9, "nine"); + assertEquals(value.f10, 10); + assertEquals(value.f11, "eleven"); + assertEquals(value.f12, 12); + assertEquals(value.f13, "thirteen"); + } + + public static class WideFields { + public int f0; + public String f1; + public int f2; + public String f3; + public int f4; + public String f5; + public int f6; + public String f7; + public int f8; + public String f9; + public int f10; + public String f11; + public int f12; + public String f13; + } } From 8d7492c21c215f1a1de11290e4401e0b95ef19e0 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 23:44:09 +0800 Subject: [PATCH 015/120] perf(java): pack Fory JSON generated field prefixes --- .../fory/json/codegen/JsonWriterCodegen.java | 68 ++++++++++++++-- .../fory/json/writer/Utf8JsonWriter.java | 77 +++++++++++++++++++ 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index a5cca49ba9..af6333d7c1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -318,9 +318,17 @@ private static Expression writeObjectStartPrimitive( case BYTE: case SHORT: case INT: + if (utf8 && canPackPrefix(property, false)) { + return new Expression.Invoke( + writer, "writeObjectIntField", packedPrefixArgs(property, false, value)); + } return new Expression.Invoke( writer, "writeObjectIntField", prefixRef(utf8, false, 0), value); case LONG: + if (utf8 && canPackPrefix(property, false)) { + return new Expression.Invoke( + writer, "writeObjectLongField", packedPrefixArgs(property, false, value)); + } return new Expression.Invoke( writer, "writeObjectLongField", prefixRef(utf8, false, 0), value); default: @@ -356,13 +364,13 @@ private Expression writeProp( new Expression.If( eq(value, nullValue), new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index, writer), + writeFieldName(property, id, utf8, commaKnown, index, writer), new Expression.Invoke(writer, "writeNull")), writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver))); } return new Expression.ListExpression( value, - writeFieldName(id, utf8, commaKnown, index, writer), + writeFieldName(property, id, utf8, commaKnown, index, writer), new Expression.If( eq(value, nullValue), new Expression.Invoke(writer, "writeNull"), @@ -372,7 +380,7 @@ private Expression writeProp( isPrefixValue(property.writeKind()) ? writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver) : new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index, writer), + writeFieldName(property, id, utf8, commaKnown, index, writer), writeValue(property, id, value, utf8, true, index, writer, typeResolver)); return new Expression.ListExpression(value, new Expression.If(ne(value, nullValue), write)); } @@ -392,17 +400,18 @@ private Expression writePrimitive( case BYTE: case SHORT: case INT: - return writeNumberField(id, value, false, utf8, commaKnown, index, writer); + return writeNumberField(property, id, value, false, utf8, commaKnown, index, writer); case LONG: - return writeNumberField(id, value, true, utf8, commaKnown, index, writer); + return writeNumberField(property, id, value, true, utf8, commaKnown, index, writer); default: return new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index, writer), + writeFieldName(property, id, utf8, commaKnown, index, writer), writePrimitiveScalar(property.writeKind(), value, writer)); } } private static Expression writeNumberField( + JsonFieldInfo property, int id, Expression value, boolean longValue, @@ -412,6 +421,9 @@ private static Expression writeNumberField( Expression writer) { String writerMethod = longValue ? "writeLongField" : "writeIntField"; if (commaKnown) { + if (utf8 && canPackPrefix(property, true)) { + return new Expression.Invoke(writer, writerMethod, packedPrefixArgs(property, true, value)); + } return new Expression.Invoke(writer, writerMethod, prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = @@ -428,6 +440,7 @@ private static Expression writeNumberField( } private static Expression writeStringField( + JsonFieldInfo property, int id, Expression value, boolean utf8, @@ -435,6 +448,10 @@ private static Expression writeStringField( Expression index, Expression writer) { if (commaKnown) { + if (utf8 && canPackPrefix(property, true)) { + return new Expression.Invoke( + writer, "writeStringField", packedPrefixArgs(property, true, value)); + } return new Expression.Invoke(writer, "writeStringField", prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = @@ -451,7 +468,15 @@ private static Expression writeStringField( } private static Expression writeFieldName( - int id, boolean utf8, boolean commaKnown, Expression index, Expression writer) { + JsonFieldInfo property, + int id, + boolean utf8, + boolean commaKnown, + Expression index, + Expression writer) { + if (commaKnown && utf8 && canPackPrefix(property, true)) { + return new Expression.Invoke(writer, "writeRawValue", packedPrefixArgs(property, true)); + } Expression prefix = commaKnown ? prefixRef(utf8, true, id) @@ -495,6 +520,7 @@ private Expression writeValue( case SHORT: case INT: return writeNumberField( + property, id, new Expression.Invoke(value, "intValue", TypeRef.of(int.class)).inline(), false, @@ -504,6 +530,7 @@ private Expression writeValue( writer); case LONG: return writeNumberField( + property, id, new Expression.Invoke(value, "longValue", TypeRef.of(long.class)).inline(), true, @@ -512,7 +539,7 @@ private Expression writeValue( index, writer); case STRING: - return writeStringField(id, value, utf8, commaKnown, index, writer); + return writeStringField(property, id, value, utf8, commaKnown, index, writer); case ENUM: return writeRawFieldValue( commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index), writer); @@ -546,6 +573,31 @@ private static Expression writeRawFieldValue( return expressions; } + private static Expression[] packedPrefixArgs( + JsonFieldInfo property, boolean comma, Expression... extraArgs) { + byte[] prefix = comma ? property.utf8CommaNamePrefix() : property.utf8NamePrefix(); + Expression[] args = new Expression[3 + extraArgs.length]; + args[0] = Expression.Literal.ofLong(packedPrefixWord(prefix, 0)); + args[1] = Expression.Literal.ofLong(packedPrefixWord(prefix, Long.BYTES)); + args[2] = Expression.Literal.ofInt(prefix.length); + System.arraycopy(extraArgs, 0, args, 3, extraArgs.length); + return args; + } + + private static boolean canPackPrefix(JsonFieldInfo property, boolean comma) { + int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length; + return length <= Long.BYTES * 2; + } + + private static long packedPrefixWord(byte[] prefix, int offset) { + long word = 0; + int end = Math.min(prefix.length, offset + Long.BYTES); + for (int i = offset; i < end; i++) { + word |= (prefix[i] & 0xffL) << ((i - offset) << 3); + } + return word; + } + private static Expression booleanFieldValue( int id, Expression value, boolean utf8, boolean commaKnown, Expression index) { return new Expression.Invoke( diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 420a5a78a7..7d431f61a8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -328,6 +328,12 @@ public void writeIntField(byte[] prefix, int value) { writeIntNoEnsure(value); } + public void writeIntField(long prefix0, long prefix1, int prefixLength, int value) { + ensurePackedPrefix(prefixLength, 11); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeIntNoEnsure(value); + } + public void writeObjectIntField(byte[] namePrefix, int value) { ensure(namePrefix.length + 12); buffer[position++] = (byte) '{'; @@ -335,6 +341,13 @@ public void writeObjectIntField(byte[] namePrefix, int value) { writeIntNoEnsure(value); } + public void writeObjectIntField(long prefix0, long prefix1, int prefixLength, int value) { + ensurePackedPrefix(prefixLength, 12); + buffer[position++] = (byte) '{'; + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeIntNoEnsure(value); + } + public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, long value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeLongField(prefix, value); @@ -346,6 +359,12 @@ public void writeLongField(byte[] prefix, long value) { writeLongNoEnsure(value); } + public void writeLongField(long prefix0, long prefix1, int prefixLength, long value) { + ensurePackedPrefix(prefixLength, 20); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeLongNoEnsure(value); + } + public void writeObjectLongField(byte[] namePrefix, long value) { ensure(namePrefix.length + 21); buffer[position++] = (byte) '{'; @@ -353,6 +372,13 @@ public void writeObjectLongField(byte[] namePrefix, long value) { writeLongNoEnsure(value); } + public void writeObjectLongField(long prefix0, long prefix1, int prefixLength, long value) { + ensurePackedPrefix(prefixLength, 21); + buffer[position++] = (byte) '{'; + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeLongNoEnsure(value); + } + public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeStringField(prefix, value); @@ -382,12 +408,42 @@ public void writeStringField(byte[] prefix, String value) { writeStringFieldChars(prefix, value); } + public void writeStringField(long prefix0, long prefix1, int prefixLength, String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + int start = position; + if (StringSerializer.isLatin1Coder(stringCoder)) { + ensurePackedPrefix(prefixLength, bytes.length + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + position = start; + } else if (StringSerializer.isUtf16Coder(stringCoder)) { + ensurePackedPrefix(prefixLength, (bytes.length >> 1) * 3 + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + position = start; + } + } + writeStringFieldChars(prefix0, prefix1, prefixLength, value); + } + private void writeStringFieldChars(byte[] prefix, String value) { ensure(prefix.length + value.length() * 3 + 2); writeRawNoEnsure(prefix); writeStringNoEnsure(value); } + private void writeStringFieldChars(long prefix0, long prefix1, int prefixLength, String value) { + ensurePackedPrefix(prefixLength, value.length() * 3 + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeStringNoEnsure(value); + } + public void writeStringCollection(Collection values) { writeArrayStart(); if (values.getClass() == ArrayList.class) { @@ -457,6 +513,11 @@ public void writeRawValue(byte[] value) { writeRaw(value); } + public void writeRawValue(long prefix0, long prefix1, int prefixLength) { + ensure(packedPrefixSize(prefixLength)); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + } + @Override public void writeObjectStart() { writeByteRaw((byte) '{'); @@ -747,6 +808,22 @@ private void writeRawNoEnsure(byte[] bytes) { position += bytes.length; } + private void writePackedRawNoEnsure(long prefix0, long prefix1, int prefixLength) { + LittleEndian.putInt64(buffer, position, prefix0); + if (prefixLength > Long.BYTES) { + LittleEndian.putInt64(buffer, position + Long.BYTES, prefix1); + } + position += prefixLength; + } + + private void ensurePackedPrefix(int prefixLength, int additionalAfterPrefix) { + ensure(Math.max(packedPrefixSize(prefixLength), prefixLength + additionalAfterPrefix)); + } + + private static int packedPrefixSize(int prefixLength) { + return prefixLength <= Long.BYTES ? Long.BYTES : Long.BYTES * 2; + } + private void writeByteRaw(byte value) { ensure(1); buffer[position++] = value; From 15500ab4dd3a6d656cbd82de2c3d84593ce6374c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 00:01:10 +0800 Subject: [PATCH 016/120] perf(java): specialize Fory JSON ArrayList object writes --- .../fory/json/codec/CollectionCodec.java | 81 ++++++++++++++----- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index dfd70c8346..66c4d78ac9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -447,13 +447,26 @@ private ObjectCollectionCodec( @Override void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { writer.writeArrayStart(); - int index = 0; - for (Object element : (Collection) value) { - writer.writeComma(index++); - if (element == null) { - writer.writeNull(); - } else { - elementCodec.writeNonNull(writer, element, resolver); + if (value.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) value; + for (int index = 0, size = list.size(); index < size; index++) { + Object element = list.get(index); + writer.writeComma(index); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeNonNull(writer, element, resolver); + } + } + } else { + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeNonNull(writer, element, resolver); + } } } writer.writeArrayEnd(); @@ -462,13 +475,26 @@ void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { @Override void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { writer.writeArrayStart(); - int index = 0; - for (Object element : (Collection) value) { - writer.writeComma(index++); - if (element == null) { - writer.writeNull(); - } else { - elementCodec.writeStringNonNull(writer, element, resolver); + if (value.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) value; + for (int index = 0, size = list.size(); index < size; index++) { + Object element = list.get(index); + writer.writeComma(index); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeStringNonNull(writer, element, resolver); + } + } + } else { + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeStringNonNull(writer, element, resolver); + } } } writer.writeArrayEnd(); @@ -477,13 +503,26 @@ void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver @Override void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { writer.writeArrayStart(); - int index = 0; - for (Object element : (Collection) value) { - writer.writeComma(index++); - if (element == null) { - writer.writeNull(); - } else { - elementCodec.writeUtf8NonNull(writer, element, resolver); + if (value.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) value; + for (int index = 0, size = list.size(); index < size; index++) { + Object element = list.get(index); + writer.writeComma(index); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeUtf8NonNull(writer, element, resolver); + } + } + } else { + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeUtf8NonNull(writer, element, resolver); + } } } writer.writeArrayEnd(); From 091cc9dcfb57589d79ef1dabcea38e1c55586ca5 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 00:14:17 +0800 Subject: [PATCH 017/120] perf(java): copy Fory JSON numeric bytes directly --- .../apache/fory/json/writer/Utf8JsonWriter.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 7d431f61a8..9ae87903ea 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -141,7 +141,7 @@ public void writeFloat(float value) { if (!Float.isFinite(value)) { throw new ForyJsonException("JSON does not support non-finite float " + value); } - writeAscii(Float.toString(value)); + writeAsciiNumber(Float.toString(value)); } @Override @@ -149,12 +149,12 @@ public void writeDouble(double value) { if (!Double.isFinite(value)) { throw new ForyJsonException("JSON does not support non-finite double " + value); } - writeAscii(Double.toString(value)); + writeAsciiNumber(Double.toString(value)); } @Override public void writeNumber(String value) { - writeAscii(value); + writeAsciiNumber(value); } @Override @@ -798,6 +798,15 @@ private void writeAsciiNoEnsure(String value) { } } + private void writeAsciiNumber(String value) { + if (STRING_BYTES_BACKED + && StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(value))) { + writeRaw(StringSerializer.getStringBytes(value)); + return; + } + writeAscii(value); + } + private void writeRaw(byte[] bytes) { ensure(bytes.length); writeRawNoEnsure(bytes); From 50f563d2354dba5ce409ebe6d791a2d190ec589e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 00:54:57 +0800 Subject: [PATCH 018/120] perf(java): copy Fory JSON short Latin1 tails --- .../fory/json/writer/Utf8JsonWriter.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 9ae87903ea..54d766d361 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -41,16 +41,21 @@ public final class Utf8JsonWriter extends JsonWriter { private static final long EIGHT_DIGITS = 100_000_000L; private static final long HIGH_BITS = 0x8080808080808080L; private static final int INT_HIGH_BITS = 0x80808080; + private static final int SHORT_HIGH_BITS = 0x8080; private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; private static final int INT_ASCII_CONTROL_OFFSET = 0x60606060; + private static final int SHORT_ASCII_CONTROL_OFFSET = 0x6060; private static final long ONE_BYTES = 0x0101010101010101L; private static final int INT_ONE_BYTES = 0x01010101; + private static final int SHORT_ONE_BYTES = 0x0101; private static final byte[] HEX_DIGITS = "0123456789abcdef".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); private static final long QUOTE_BYTES_COMPLEMENT = ~0x2222222222222222L; private static final int INT_QUOTE_BYTES_COMPLEMENT = ~0x22222222; + private static final int SHORT_QUOTE_BYTES_COMPLEMENT = ~0x2222; private static final long BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C5C5C5C5CL; private static final int INT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C; + private static final int SHORT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C; private static final long UTF16_ASCII_MASK = 0xFF80FF80FF80FF80L; private static final int[] DIGIT_TRIPLES = new int[1000]; private static final int[] DIGIT_QUADS = new int[10000]; @@ -586,6 +591,15 @@ private boolean writeLatin1StringNoEnsure(byte[] value) { i += 4; } } + if (i + 2 <= length) { + int word = (value[i] & 0xFF) | ((value[i + 1] & 0xFF) << 8); + if (isJsonAsciiShort(word)) { + bytes[pos] = (byte) word; + bytes[pos + 1] = (byte) (word >>> 8); + pos += 2; + i += 2; + } + } for (; i < length; i++) { byte ch = value[i]; if (isJsonAsciiByte(ch)) { @@ -879,6 +893,14 @@ private static boolean isJsonAsciiInt(int word) { == INT_HIGH_BITS; } + private static boolean isJsonAsciiShort(int word) { + return (((word + SHORT_ASCII_CONTROL_OFFSET) & ~word) & SHORT_HIGH_BITS) == SHORT_HIGH_BITS + && (((word ^ SHORT_QUOTE_BYTES_COMPLEMENT) + SHORT_ONE_BYTES) & SHORT_HIGH_BITS) + == SHORT_HIGH_BITS + && (((word ^ SHORT_BACKSLASH_BYTES_COMPLEMENT) + SHORT_ONE_BYTES) & SHORT_HIGH_BITS) + == SHORT_HIGH_BITS; + } + private static int packUtf16Ascii(long word) { return (int) ((word & 0xFFL) From 78a0c19c14537d37563a955f118cde82a1580bb5 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 01:54:37 +0800 Subject: [PATCH 019/120] perf(java): retain medium Fory JSON writer buffers --- .../java/org/apache/fory/json/writer/StringJsonWriter.java | 3 ++- .../main/java/org/apache/fory/json/writer/Utf8JsonWriter.java | 3 ++- .../src/test/java/org/apache/fory/json/JsonStringTest.java | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index ebe10dcffc..c273195f9d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -30,7 +30,8 @@ public final class StringJsonWriter extends JsonWriter { private static final byte LATIN1 = 0; private static final byte UTF16 = 1; - private static final int RETAINED_CAPACITY = 8192; + // Pooled writers should retain medium buffers to avoid reallocating common JSON outputs. + private static final int RETAINED_CAPACITY = 64 * 1024; private static final byte[] MIN_INT_BYTES = "-2147483648".getBytes(StandardCharsets.ISO_8859_1); private static final byte[] MIN_LONG_BYTES = "-9223372036854775808".getBytes(StandardCharsets.ISO_8859_1); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 54d766d361..d62912637c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -33,7 +33,8 @@ import org.apache.fory.serializer.StringSerializer; public final class Utf8JsonWriter extends JsonWriter { - private static final int RETAINED_CAPACITY = 8192; + // Pooled writers should retain medium buffers to avoid reallocating common JSON outputs. + private static final int RETAINED_CAPACITY = 64 * 1024; private static final byte[] MIN_INT_BYTES = "-2147483648".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); private static final byte[] MIN_LONG_BYTES = diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index 823873b160..c6beef60dc 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -119,7 +119,7 @@ public void stringWriterShrinksOnReset() throws Exception { assertTrue(writerBufferLength(writer) > 8192); writer.toJson(); writer.reset(); - assertEquals(writerBufferLength(writer), 8192); + assertEquals(writerBufferLength(writer), 65536); writer.writeString("café"); assertEquals(writer.toJson(), "\"café\""); } From aab2406edf30df6681bdb9b2df710ce63e233bcd Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 02:00:21 +0800 Subject: [PATCH 020/120] perf(java): emit exact Fory JSON array writes --- .../fory/json/codegen/JsonWriterCodegen.java | 16 ++++++++++++++ .../fory/json/writer/Utf8JsonWriter.java | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index af6333d7c1..a0ce024439 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -548,6 +548,8 @@ private Expression writeValue( case CHAR: return writeScalar(kind, value, writer); case ARRAY: + Expression array = writeExactUtf8Array(property.writeRawType(), value, utf8, writer); + return array == null ? writeCodec(id, value, utf8, writer, typeResolver) : array; case MAP: return writeCodec(id, value, utf8, writer, typeResolver); case COLLECTION: @@ -653,6 +655,20 @@ private static Expression writeExactUtf8Scalar( return new Expression.Invoke(writer, writerMethod, value); } + private static Expression writeExactUtf8Array( + Class rawType, Expression value, boolean utf8, Expression writer) { + if (!utf8) { + return null; + } + if (rawType == String[].class) { + return new Expression.Invoke(writer, "writeStringArray", value); + } + if (rawType == long[].class) { + return new Expression.Invoke(writer, "writeLongArray", value); + } + return null; + } + private static Expression writeStringCollection( Expression value, boolean utf8, Expression writer) { if (utf8) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index d62912637c..84d6509d8e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -466,6 +466,27 @@ public void writeStringCollection(Collection values) { writeArrayEnd(); } + public void writeStringArray(String[] values) { + writeArrayStart(); + for (int i = 0; i < values.length; i++) { + writeStringElement(i, values[i]); + } + writeArrayEnd(); + } + + public void writeLongArray(long[] values) { + ensure(2); + buffer[position++] = '['; + for (int i = 0; i < values.length; i++) { + ensure(22); + if (i != 0) { + buffer[position++] = ','; + } + writeLongNoEnsure(values[i]); + } + buffer[position++] = ']'; + } + public void writeStringElement(int index, String value) { int comma = index == 0 ? 0 : 1; if (value == null) { From c7a12fcf223b9726ac4cec054ef7e524d601af70 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 04:29:29 +0800 Subject: [PATCH 021/120] perf(json): add short latin1 string writer path --- .../fory/json/writer/Utf8JsonWriter.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 84d6509d8e..47710c4999 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -580,6 +580,9 @@ private boolean writeLatin1String(byte[] value) { private boolean writeLatin1StringNoEnsure(byte[] value) { int length = value.length; + if (length <= 32) { + return writeShortLatin1StringNoEnsure(value, length); + } byte[] bytes = buffer; int start = position; int pos = start; @@ -636,6 +639,68 @@ private boolean writeLatin1StringNoEnsure(byte[] value) { return true; } + private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { + byte[] bytes = buffer; + int start = position; + int pos = start; + bytes[pos++] = (byte) '"'; + int i = 0; + if (length > 15) { + long word0 = LittleEndian.getInt64(value, 0); + long word1 = LittleEndian.getInt64(value, 8); + if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + pos += 16; + i = 16; + } + if (i + 8 <= length) { + long word = LittleEndian.getInt64(value, i); + if (!isJsonAsciiWord(word)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos, word); + pos += 8; + i += 8; + } + if (i + 4 <= length) { + int word = LittleEndian.getInt32(value, i); + if (!isJsonAsciiInt(word)) { + position = start; + return false; + } + LittleEndian.putInt32(bytes, pos, word); + pos += 4; + i += 4; + } + if (i + 2 <= length) { + int word = (value[i] & 0xFF) | ((value[i + 1] & 0xFF) << 8); + if (!isJsonAsciiShort(word)) { + position = start; + return false; + } + bytes[pos] = (byte) word; + bytes[pos + 1] = (byte) (word >>> 8); + pos += 2; + i += 2; + } + if (i < length) { + byte ch = value[i]; + if (!isJsonAsciiByte(ch)) { + position = start; + return false; + } + bytes[pos++] = ch; + } + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + private boolean writeUtf16String(byte[] value) { int length = value.length; ensure((length >> 1) * 3 + 2); From 6908a27ebcb0323c56cb51f99e4ea9f10e231648 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 04:44:00 +0800 Subject: [PATCH 022/120] perf(json): split latin1 string writer dispatch --- .../org/apache/fory/json/writer/Utf8JsonWriter.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 47710c4999..eb67d1e70a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -575,14 +575,22 @@ public void writeComma(int index) { private boolean writeLatin1String(byte[] value) { int length = value.length; ensure(length + 2); - return writeLatin1StringNoEnsure(value); + return writeLatin1StringNoEnsure(value, length); } private boolean writeLatin1StringNoEnsure(byte[] value) { int length = value.length; + return writeLatin1StringNoEnsure(value, length); + } + + private boolean writeLatin1StringNoEnsure(byte[] value, int length) { if (length <= 32) { return writeShortLatin1StringNoEnsure(value, length); } + return writeLongLatin1StringNoEnsure(value, length); + } + + private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { byte[] bytes = buffer; int start = position; int pos = start; From ece77e74ea2727a63fd1935f7b1ad8f8dd285bee Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 05:39:39 +0800 Subject: [PATCH 023/120] perf(json): match long ascii field tokens --- .../fory/json/codegen/JsonReaderCodegen.java | 27 ++++++- .../apache/fory/json/meta/JsonAsciiToken.java | 28 ++++++++ .../fory/json/reader/Latin1JsonReader.java | 31 ++++++++ .../fory/json/reader/Utf8JsonReader.java | 31 ++++++++ .../fory/json/JsonGeneratedCodecTest.java | 71 +++++++++++++++++++ 5 files changed, 186 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index d44722105b..a99ae52ea8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -567,7 +567,7 @@ private Expression fastReadField( Expression hashes, Expression[] skips, boolean record) { - if (isPackedName(properties[index].name())) { + if (isDirectName(readerMode, properties[index].name())) { return statementIf( tryReadNextFieldNameColon(readerMode, properties[index]), new Expression.ListExpression( @@ -633,7 +633,7 @@ private Expression nextDirectFallback( Expression[] skips, boolean record) { int nextIndex = index + 1; - if (nextIndex < groupEnd && isPackedName(properties[nextIndex].name())) { + if (nextIndex < groupEnd && isDirectName(readerMode, properties[nextIndex].name())) { return statementIf( tryReadNextFieldNameColon(readerMode, properties[nextIndex]), new Expression.ListExpression( @@ -789,6 +789,18 @@ private static boolean isPackedName(String name) { if (length == 0 || length > Long.BYTES) { return false; } + return isAsciiName(name); + } + + private static boolean isDirectName(int readerMode, String name) { + if (readerMode == LATIN1_READER || readerMode == UTF8_READER) { + return JsonAsciiToken.isLongPackable(fieldNameToken(name)); + } + return isPackedName(name); + } + + private static boolean isAsciiName(String name) { + int length = name.length(); for (int i = 0; i < length; i++) { char ch = name.charAt(i); if (ch == 0 || ch > 0xFF) { @@ -1234,6 +1246,17 @@ private static Expression tryReadNextFieldNameColon(int readerMode, JsonFieldInf Expression.Literal.ofInt(tokenLength)) .inline(); } + if (suffixLength > 3) { + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextFieldNameToken8", + TypeRef.of(boolean.class), + Expression.Literal.ofLong(JsonAsciiToken.prefix(token)), + Expression.Literal.ofLong(JsonAsciiToken.suffixLong(token)), + Expression.Literal.ofLong(JsonAsciiToken.suffixMask(tokenLength)), + Expression.Literal.ofInt(tokenLength)) + .inline(); + } return new Expression.Invoke( readerRef(readerMode), "tryReadNextFieldNameToken" + suffixLength, diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java index 44b504e865..16285ff41e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java @@ -21,6 +21,7 @@ public final class JsonAsciiToken { private static final int MAX_SUFFIX_LENGTH = 3; + private static final int MAX_LONG_SUFFIX_LENGTH = Long.BYTES; private JsonAsciiToken() {} @@ -29,6 +30,19 @@ public static boolean isPackable(String token) { if (length == 0 || suffixLength(length) > MAX_SUFFIX_LENGTH) { return false; } + return isLatin1Token(token); + } + + public static boolean isLongPackable(String token) { + int length = token.length(); + if (length == 0 || suffixLength(length) > MAX_LONG_SUFFIX_LENGTH) { + return false; + } + return isLatin1Token(token); + } + + private static boolean isLatin1Token(String token) { + int length = token.length(); for (int i = 0; i < length; i++) { char ch = token.charAt(i); if (ch == 0 || ch > 0xFF) { @@ -61,6 +75,20 @@ public static int suffix(String token) { return value; } + public static long suffixLong(String token) { + int suffixLength = suffixLength(token.length()); + long value = 0; + for (int i = 0; i < suffixLength; i++) { + value |= (long) (token.charAt(i + Long.BYTES) & 0xFF) << (i << 3); + } + return value; + } + + public static long suffixMask(int tokenLength) { + int suffixLength = suffixLength(tokenLength); + return suffixLength == Long.BYTES ? -1L : (1L << (suffixLength << 3)) - 1; + } + public static int suffixLength(int tokenLength) { return Math.max(0, tokenLength - Long.BYTES); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index 8a9ae15696..729d7159f1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -854,6 +854,37 @@ private boolean tryReadNextRawToken3(long prefix, long prefixMask, int suffix, i return false; } + public boolean tryReadNextFieldNameToken8( + long prefix, long suffix, long suffixMask, int tokenLength) { + return tryReadNextRawToken8(prefix, suffix, suffixMask, tokenLength); + } + + private boolean tryReadNextRawToken8(long prefix, long suffix, long suffixMask, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && LittleEndian.getInt64(bytes, mark) == prefix + && readTokenSuffix(bytes, suffixOffset, tokenLength, suffixMask) == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + private static long readTokenSuffix( + byte[] bytes, int suffixOffset, int tokenLength, long suffixMask) { + if (suffixOffset + Long.BYTES <= bytes.length) { + return LittleEndian.getInt64(bytes, suffixOffset) & suffixMask; + } + int suffixLength = tokenLength - Long.BYTES; + long suffix = 0; + for (int i = 0; i < suffixLength; i++) { + suffix |= (long) (bytes[suffixOffset + i] & 0xFF) << (i << 3); + } + return suffix; + } + private boolean tryReadFieldNameColonAt( int mark, long expectedHash, long expectedMask, int expectedLength) { byte[] bytes = input; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 57372d529c..dc28215a67 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -1130,6 +1130,37 @@ private boolean tryReadNextRawToken3(long prefix, long prefixMask, int suffix, i return false; } + public boolean tryReadNextFieldNameToken8( + long prefix, long suffix, long suffixMask, int tokenLength) { + return tryReadNextRawToken8(prefix, suffix, suffixMask, tokenLength); + } + + private boolean tryReadNextRawToken8(long prefix, long suffix, long suffixMask, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && LittleEndian.getInt64(bytes, mark) == prefix + && readTokenSuffix(bytes, suffixOffset, tokenLength, suffixMask) == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + private static long readTokenSuffix( + byte[] bytes, int suffixOffset, int tokenLength, long suffixMask) { + if (suffixOffset + Long.BYTES <= bytes.length) { + return LittleEndian.getInt64(bytes, suffixOffset) & suffixMask; + } + int suffixLength = tokenLength - Long.BYTES; + long suffix = 0; + for (int i = 0; i < suffixLength; i++) { + suffix |= (long) (bytes[suffixOffset + i] & 0xFF) << (i << 3); + } + return suffix; + } + private boolean tryReadFieldNameColonAt( int mark, long expectedHash, long expectedMask, int expectedLength) { byte[] bytes = input; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index 5c58e516ed..45c15e9c97 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -20,6 +20,8 @@ package org.apache.fory.json; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -28,6 +30,10 @@ import org.apache.fory.json.data.RecursiveParent; import org.apache.fory.json.data.TokenGroup; import org.apache.fory.json.data.TokenValues; +import org.apache.fory.json.meta.JsonAsciiToken; +import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; import org.testng.annotations.Test; public class JsonGeneratedCodecTest extends ForyJsonTestModels { @@ -120,6 +126,57 @@ public void readGeneratedCollectionFields() { assertGeneratedWhenSupported(json, GeneratedCollectionFields.class); } + @Test + public void readLongAsciiFieldToken() { + String token = "\"favoriteFruit\":"; + long prefix = JsonAsciiToken.prefix(token); + long suffix = JsonAsciiToken.suffixLong(token); + long suffixMask = JsonAsciiToken.suffixMask(token.length()); + Utf8JsonReader utf8 = + new Utf8JsonReader((token + "\"apple\"").getBytes(StandardCharsets.UTF_8)); + assertTrue(utf8.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(utf8.readNullableStringToken(), "apple"); + + Latin1JsonReader latin1 = new Latin1JsonReader(token + "\"pear\""); + assertTrue(latin1.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(latin1.readNullableStringToken(), "pear"); + + String tailToken = "\"registered\":"; + long tailPrefix = JsonAsciiToken.prefix(tailToken); + long tailSuffix = JsonAsciiToken.suffixLong(tailToken); + long tailSuffixMask = JsonAsciiToken.suffixMask(tailToken.length()); + Utf8JsonReader tailUtf8 = + new Utf8JsonReader((tailToken + "1").getBytes(StandardCharsets.UTF_8)); + assertTrue( + tailUtf8.tryReadNextFieldNameToken8( + tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); + assertEquals(tailUtf8.readIntTokenValue(), 1); + Latin1JsonReader tailLatin1 = new Latin1JsonReader(tailToken + "2"); + assertTrue( + tailLatin1.tryReadNextFieldNameToken8( + tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); + assertEquals(tailLatin1.readIntTokenValue(), 2); + + Utf8JsonReader mismatch = + new Utf8JsonReader("\"favoriteSeed\":\"pit\"".getBytes(StandardCharsets.UTF_8)); + assertFalse(mismatch.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(mismatch.readFieldNameHash(), JsonFieldNameHash.hash("favoriteSeed")); + mismatch.expectNextToken(':'); + assertEquals(mismatch.readNextNullableString(), "pit"); + } + + @Test + public void readGeneratedLongAsciiFields() { + ForyJson json = ForyJson.builder().build(); + String input = + "{\"registered\":\"today\",\"longitude\":12.5,\"favoriteFruit\":\"apple\"," + + "\"shortName\":\"core\"}"; + assertLongAsciiFields(json.fromJson(input, LongAsciiFields.class)); + assertLongAsciiFields( + json.fromJson(input.getBytes(StandardCharsets.UTF_8), LongAsciiFields.class)); + assertGeneratedWhenSupported(json, LongAsciiFields.class); + } + @Test public void readSplitGeneratedFields() { ForyJson json = ForyJson.builder().build(); @@ -157,6 +214,20 @@ private static void assertWideFields(WideFields value) { assertEquals(value.f13, "thirteen"); } + private static void assertLongAsciiFields(LongAsciiFields value) { + assertEquals(value.registered, "today"); + assertEquals(value.longitude, 12.5d); + assertEquals(value.favoriteFruit, "apple"); + assertEquals(value.shortName, "core"); + } + + public static class LongAsciiFields { + public String registered; + public double longitude; + public String favoriteFruit; + public String shortName; + } + public static class WideFields { public int f0; public String f1; From 139acbd87c90e698ea0abb89cc1131027f2aa207 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 06:16:51 +0800 Subject: [PATCH 024/120] perf(json): read utf8 doubles directly --- .../apache/fory/json/codec/ScalarCodecs.java | 6 +- .../fory/json/codegen/JsonReaderCodegen.java | 48 ++++++++ .../apache/fory/json/reader/JsonReader.java | 4 + .../fory/json/reader/Utf8JsonReader.java | 108 ++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 17 +++ 5 files changed, 180 insertions(+), 3 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index fbb00260b2..676904530d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -441,7 +441,7 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso @Override Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - return Double.parseDouble(reader.readNumber()); + return reader.readDouble(); } @Override @@ -454,9 +454,9 @@ public void readField( if (reader.peekNull()) { readFieldDefault(reader, object, accessor, typeInfo, resolver); } else if (typeInfo.primitive()) { - accessor.putDouble(object, Double.parseDouble(reader.readNumber())); + accessor.putDouble(object, reader.readDouble()); } else { - accessor.putObject(object, Double.parseDouble(reader.readNumber())); + accessor.putObject(object, reader.readDouble()); } } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index a99ae52ea8..46f6af6028 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -1166,6 +1166,14 @@ private static Expression readLongExpr(int readerMode, boolean tokenValueRead) { .inline(); } + private static Expression readDoubleExpr(int readerMode, boolean tokenValueRead) { + return new Expression.Invoke( + readerRef(readerMode), + readDoubleMethod(readerMode, tokenValueRead), + TypeRef.of(double.class)) + .inline(); + } + private static Expression readStringExpr(int readerMode) { return readStringExpr(readerMode, false); } @@ -1200,6 +1208,13 @@ private static String readLongMethod(int readerMode, boolean tokenValueRead) { return tokenValueRead ? "readLongTokenValue" : "readNextLongValue"; } + private static String readDoubleMethod(int readerMode, boolean tokenValueRead) { + if (readerMode == UTF8_READER) { + return tokenValueRead ? "readDoubleTokenValue" : "readNextDoubleValue"; + } + return "readDouble"; + } + private static String readStringMethod(int readerMode, boolean tokenValueRead) { if (readerMode == GENERIC_READER) { return "readNullableString"; @@ -1444,6 +1459,8 @@ private Expression readField( return readInt(builder, property, rawType, readerMode, object, tokenValueRead); case LONG: return readLong(builder, property, rawType, readerMode, object, tokenValueRead); + case DOUBLE: + return readDouble(builder, property, rawType, readerMode, object, tokenValueRead); case STRING: return builder.setField(property, object, readStringExpr(readerMode, tokenValueRead)); case ENUM: @@ -1480,6 +1497,8 @@ private Expression readRecordField( return readRecordInt(rawType, id, readerMode, object, tokenValueRead); case LONG: return readRecordLong(rawType, id, readerMode, object, tokenValueRead); + case DOUBLE: + return readRecordDouble(rawType, id, readerMode, object, tokenValueRead); case STRING: return assignRecord(object, id, readStringExpr(readerMode, tokenValueRead)); case ENUM: @@ -1541,6 +1560,18 @@ private static Expression readRecordLong( assignRecord(object, id, value)); } + private static Expression readRecordDouble( + Class rawType, int id, int readerMode, Expression object, boolean tokenValueRead) { + Expression value = box(Double.class, readDoubleExpr(readerMode, tokenValueRead)); + if (rawType.isPrimitive()) { + return assignRecord(object, id, value); + } + return new Expression.If( + tryReadNullExpr(readerMode), + assignRecord(object, id, new Expression.Null(TypeRef.of(Double.class), false)), + assignRecord(object, id, value)); + } + private static Expression readRecordEnum( int id, int readerMode, Expression object, boolean tokenValueRead) { return new Expression.If( @@ -1623,6 +1654,23 @@ private static Expression readLong( property, object, box(Long.class, readLongExpr(readerMode, tokenValueRead)))); } + private static Expression readDouble( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + Class rawType, + int readerMode, + Expression object, + boolean tokenValueRead) { + if (rawType.isPrimitive()) { + return builder.setField(property, object, readDoubleExpr(readerMode, tokenValueRead)); + } + return new Expression.If( + tryReadNullExpr(readerMode), + builder.setNull(property, object), + builder.setField( + property, object, box(Double.class, readDoubleExpr(readerMode, tokenValueRead)))); + } + private static Expression readEnum( JsonGeneratedCodecBuilder builder, JsonFieldInfo property, diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java index 1cb3552200..b76d6f8e2c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java @@ -281,6 +281,10 @@ public final long readLong() { return negative ? result : -result; } + public double readDouble() { + return Double.parseDouble(readNumber()); + } + public int readFieldNameInt() { try { return Integer.parseInt(readString()); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index dc28215a67..569f196208 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -360,6 +360,23 @@ public BigDecimal readBigDecimal() { return readBigDecimalToken(); } + @Override + public double readDouble() { + skipWhitespaceFast(); + return readDoubleToken(); + } + + public double readNextDoubleValue() { + if (position < input.length && !isWhitespace(input[position])) { + return readDoubleToken(); + } + return readDouble(); + } + + public double readDoubleTokenValue() { + return readDoubleToken(); + } + private long readLongToken() { byte[] bytes = input; int offset = position; @@ -539,6 +556,97 @@ private BigDecimal readBigDecimalFallback(int start) { return new BigDecimal(readNumber()); } + private double readDoubleToken() { + // Keep the fast path exact: compact plain decimals convert through BigDecimal's long+scale + // path, while exponents, overflow, and longer precision stay on Java's full parser. + byte[] bytes = input; + int offset = position; + int start = offset; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + boolean negative = false; + int ch = bytes[offset]; + if (ch == '-') { + negative = true; + offset++; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + ch = bytes[offset]; + } + long unscaled = 0; + int scale = 0; + int digits = 0; + if (ch == '0') { + offset++; + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readDoubleFallback(start); + } + } + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + digits++; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readDoubleFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + digits++; + offset++; + } + if (offset == fractionStart) { + return readDoubleFallback(start); + } + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readDoubleFallback(start); + } + } + if (digits > 18) { + return readDoubleFallback(start); + } + position = offset; + if (negative && unscaled == 0) { + return -0.0d; + } + long value = negative ? -unscaled : unscaled; + return BigDecimal.valueOf(value, scale).doubleValue(); + } + + private double readDoubleFallback(int start) { + position = start; + return Double.parseDouble(readNumber()); + } + @Override public int readFieldNameInt() { skipWhitespaceFast(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 02fa6e7c10..4dae4b52cf 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -134,6 +134,23 @@ public void readNumericBoundaries() { "{\"intMax\":2147483648,\"text\":\"" + ZH_TEXT + "\"}", NumericBoundaries.class)); } + @Test + public void readUtf8DoubleTokens() { + assertEquals( + new Utf8JsonReader("12.375".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue(), + 12.375d); + assertEquals( + Double.doubleToRawLongBits( + new Utf8JsonReader("-0.0".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue()), + Double.doubleToRawLongBits(-0.0d)); + assertEquals( + new Utf8JsonReader("1.25e2".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue(), + 125.0d); + assertThrows( + ForyJsonException.class, + () -> new Utf8JsonReader("01.5".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue()); + } + @Test public void writeNumericBoundaries() { ForyJson json = ForyJson.builder().build(); From 5f80129af9bb183ee7a14a816d2c3ed540086463 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 07:03:50 +0800 Subject: [PATCH 025/120] perf(json): read small utf8 string arrays directly --- .../apache/fory/json/codec/ArrayCodec.java | 60 ++++++++++++++++++- .../apache/fory/json/JsonContainerTest.java | 19 ++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index de3ca843b0..5d550fe59e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -777,8 +777,64 @@ public Object readUtf8( reader.exitDepth(); return new String[0]; } - String[] values = new String[8]; - int size = 0; + String v0 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0}; + } + String v1 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1}; + } + String v2 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2}; + } + String v3 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3}; + } + String v4 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4}; + } + String v5 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5}; + } + String v6 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6}; + } + String v7 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + String[] values = new String[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + int size = 8; do { if (size == values.length) { values = Arrays.copyOf(values, values.length << 1); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index b9c5007e90..7c010e7e23 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -264,6 +264,25 @@ public void readPrimitiveArrayRoots() { assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", int[].class)); } + @Test + public void readUtf8StringArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals( + json.fromJson("[\"a\"]".getBytes(StandardCharsets.UTF_8), String[].class), + new String[] {"a"}); + assertEquals( + json.fromJson( + "[\"a\",null,\"b\",\"c\",\"d\",\"e\",\"f\",\"g\"]".getBytes(StandardCharsets.UTF_8), + String[].class), + new String[] {"a", null, "b", "c", "d", "e", "f", "g"}); + assertEquals( + json.fromJson( + "[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\",\"h\",\"i\"]" + .getBytes(StandardCharsets.UTF_8), + String[].class), + new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i"}); + } + @Test public void writeReadBoxedPrimitiveArrays() { ForyJson json = ForyJson.builder().build(); From 397c7c7cf2eefc58a881016f2407b62af650cc01 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 07:33:37 +0800 Subject: [PATCH 026/120] perf(json): read utf8 uuids directly --- .../apache/fory/json/codec/ScalarCodecs.java | 15 ++++++ .../fory/json/reader/Utf8JsonReader.java | 54 +++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 6 +++ 3 files changed, 75 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 676904530d..7902bf7134 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -766,6 +766,21 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso Object fromJsonString(String value) { return UUID.fromString(value); } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readUuid(); + } catch (ForyJsonException e) { + throw e; + } catch (RuntimeException e) { + throw new ForyJsonException("Invalid " + typeInfo.rawType().getName() + " JSON string", e); + } + } } public static final class LocaleCodec extends StringValueCodec { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 569f196208..e402c5040e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -23,6 +23,7 @@ import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.UUID; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; @@ -360,6 +361,17 @@ public BigDecimal readBigDecimal() { return readBigDecimalToken(); } + public UUID readUuid() { + skipWhitespaceFast(); + int mark = position; + try { + return readUuidToken(); + } catch (RuntimeException e) { + position = mark; + return UUID.fromString(readStringToken()); + } + } + @Override public double readDouble() { skipWhitespaceFast(); @@ -556,6 +568,48 @@ private BigDecimal readBigDecimalFallback(int start) { return new BigDecimal(readNumber()); } + private UUID readUuidToken() { + byte[] bytes = input; + int offset = position; + int start = offset + 1; + if (offset + 38 > bytes.length || bytes[offset] != '"') { + throw new IllegalArgumentException(); + } + if (bytes[start + 8] != '-' + || bytes[start + 13] != '-' + || bytes[start + 18] != '-' + || bytes[start + 23] != '-' + || bytes[start + 36] != '"') { + throw new IllegalArgumentException(); + } + long msb = parseHex(bytes, start, 8); + msb = (msb << 16) | parseHex(bytes, start + 9, 4); + msb = (msb << 16) | parseHex(bytes, start + 14, 4); + long lsb = parseHex(bytes, start + 19, 4); + lsb = (lsb << 48) | parseHex(bytes, start + 24, 12); + position = start + 37; + return new UUID(msb, lsb); + } + + private static long parseHex(byte[] bytes, int offset, int length) { + long value = 0; + for (int i = 0; i < length; i++) { + value = (value << 4) | hexValue(bytes[offset + i]); + } + return value; + } + + private static int hexValue(int ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + int lower = ch | 0x20; + if (lower >= 'a' && lower <= 'f') { + return lower - 'a' + 10; + } + throw new IllegalArgumentException(); + } + private double readDoubleToken() { // Keep the fast path exact: compact plain decimals convert through BigDecimal's long+scale // path, while exponents, overflow, and longer precision stay on Java's full parser. diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 4dae4b52cf..99f69009ad 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -302,6 +302,11 @@ public void readScalarRoots() { json.fromJson( "12345678901234567890.123".getBytes(StandardCharsets.UTF_8), BigDecimal.class), new BigDecimal("12345678901234567890.123")); + assertEquals( + json.fromJson( + "\"123e4567-e89b-12d3-a456-426614174000\"".getBytes(StandardCharsets.UTF_8), + UUID.class), + UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); } @Test @@ -314,6 +319,7 @@ public void readGeneratedUtf8BigDecimal() { + "\"timestamp\":\"2024-02-03T04:05:06Z\"}") .getBytes(StandardCharsets.UTF_8); Utf8ScalarFields fields = json.fromJson(input, Utf8ScalarFields.class); + assertEquals(fields.uuid, UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); assertEquals(fields.decimal, new BigDecimal("0.12345678901234567")); } From ecdc09f8e3fe8f9ab309ca30faa851a8c07300d3 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 07:50:59 +0800 Subject: [PATCH 027/120] perf(json): read small utf8 long arrays directly --- .../apache/fory/json/codec/ArrayCodec.java | 68 ++++++++++++++++++- .../apache/fory/json/JsonContainerTest.java | 16 +++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 5d550fe59e..ca71456746 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -313,8 +313,72 @@ public Object readUtf8( reader.exitDepth(); return new long[0]; } - long[] values = new long[8]; - int size = 0; + rejectNull(reader); + long v0 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0}; + } + rejectNull(reader); + long v1 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1}; + } + rejectNull(reader); + long v2 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2}; + } + rejectNull(reader); + long v3 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3}; + } + rejectNull(reader); + long v4 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4}; + } + rejectNull(reader); + long v5 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5}; + } + rejectNull(reader); + long v6 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6}; + } + rejectNull(reader); + long v7 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + long[] values = new long[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + int size = 8; do { rejectNull(reader); if (size == values.length) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 7c010e7e23..9ca1d9f6ad 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -283,6 +283,22 @@ public void readUtf8StringArrays() { new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i"}); } + @Test + public void readUtf8LongArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals( + json.fromJson("[7]".getBytes(StandardCharsets.UTF_8), long[].class), new long[] {7L}); + assertEquals( + json.fromJson("[1,2,3,4,5,6,7,8]".getBytes(StandardCharsets.UTF_8), long[].class), + new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L}); + assertEquals( + json.fromJson("[1,2,3,4,5,6,7,8,9]".getBytes(StandardCharsets.UTF_8), long[].class), + new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L}); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("[1,null]".getBytes(StandardCharsets.UTF_8), long[].class)); + } + @Test public void writeReadBoxedPrimitiveArrays() { ForyJson json = ForyJson.builder().build(); From c35b43f1da07d2bdf6ee0f8725cacd687e6f8426 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 09:28:08 +0800 Subject: [PATCH 028/120] perf(json): read small utf8 arraylists exactly --- .../fory/json/codec/CollectionCodec.java | 135 +++++++++++++++++- .../apache/fory/json/JsonContainerTest.java | 8 ++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index 66c4d78ac9..e62aeed201 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -50,10 +50,12 @@ public abstract class CollectionCodec extends AbstractJsonCodec { private final TypeRef typeRef; private final CollectionFactory factory; + private final boolean createsArrayList; CollectionCodec(TypeRef typeRef, CollectionFactory factory) { this.typeRef = typeRef; this.factory = factory; + this.createsArrayList = factory.createsArrayList(); } public static CollectionCodec create( @@ -118,6 +120,10 @@ final Collection newCollection() { return factory.newCollection(); } + final boolean createsArrayList() { + return createsArrayList; + } + public abstract Object readLatin1NonNull( Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver); @@ -166,7 +172,7 @@ private static CollectionFactory collectionFactory(Class rawType, Class el if (Queue.class.isAssignableFrom(rawType)) { return ArrayDeque::new; } - return () -> new ArrayList<>(0); + return CollectionFactory.ARRAY_LIST; } return () -> { try { @@ -178,7 +184,24 @@ private static CollectionFactory collectionFactory(Class rawType, Class el } private interface CollectionFactory { + CollectionFactory ARRAY_LIST = + new CollectionFactory() { + @Override + public Collection newCollection() { + return new ArrayList<>(0); + } + + @Override + public boolean createsArrayList() { + return true; + } + }; + Collection newCollection(); + + default boolean createsArrayList() { + return false; + } } public abstract static class DirectCollectionCodec extends CollectionCodec { @@ -260,6 +283,9 @@ public final Object readUtf8( @Override public final Object readUtf8NonNull( Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readUtf8ArrayListNonNull(reader); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -272,6 +298,113 @@ public final Object readUtf8NonNull( return collection; } + private ArrayList readUtf8ArrayListNonNull(Utf8JsonReader reader) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + Object e4 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + Object e6 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableUtf8Element(reader)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + abstract Object readElement(JsonReader reader); Object readNullableElement(JsonReader reader) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 9ca1d9f6ad..942789ce88 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -122,6 +122,14 @@ public void parsedContainersStartSmall() { assertTrue(list instanceof ArrayList); assertEquals(arrayCapacity((ArrayList) list), 1); + List strings = + json.fromJson( + "[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\"]".getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}); + assertTrue(strings instanceof ArrayList); + assertEquals(strings, Arrays.asList("a", "b", "c", "d", "e", "f", "g")); + assertEquals(arrayCapacity((ArrayList) strings), 7); + JSONObject object = json.fromJson("{\"x\":1}", JSONObject.class); assertEquals(mapCapacity(object), 2); assertEquals(mapCapacity(json.fromJson("{}", JSONObject.class)), 0); From 8de2f1bcc3268edfaf0e7ad0a72a9d8287de65e7 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 09:47:29 +0800 Subject: [PATCH 029/120] perf(json): read nine utf8 strings exactly --- .../main/java/org/apache/fory/json/codec/ArrayCodec.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index ca71456746..5151ff52ea 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -889,6 +889,12 @@ public Object readUtf8( reader.exitDepth(); return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; } + String v8 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7, v8}; + } String[] values = new String[16]; values[0] = v0; values[1] = v1; @@ -898,7 +904,8 @@ public Object readUtf8( values[5] = v5; values[6] = v6; values[7] = v7; - int size = 8; + values[8] = v8; + int size = 9; do { if (size == values.length) { values = Arrays.copyOf(values, values.length << 1); From 83b7f6980d4a43f503b7b09c40f7bc1f8705ad3f Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 10:43:06 +0800 Subject: [PATCH 030/120] perf(json): split utf8 arraylist reads --- .../org/apache/fory/json/codec/CollectionCodec.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index e62aeed201..ed98da11dc 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -339,6 +339,11 @@ private ArrayList readUtf8ArrayListNonNull(Utf8JsonReader reader) { list.add(e3); return list; } + return readUtf8ArrayListTail(reader, e0, e1, e2, e3); + } + + private ArrayList readUtf8ArrayListTail( + Utf8JsonReader reader, Object e0, Object e1, Object e2, Object e3) { Object e4 = readNullableUtf8Element(reader); if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); @@ -362,6 +367,11 @@ private ArrayList readUtf8ArrayListNonNull(Utf8JsonReader reader) { list.add(e5); return list; } + return readUtf8ArrayListLongTail(reader, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readUtf8ArrayListLongTail( + Utf8JsonReader reader, Object e0, Object e1, Object e2, Object e3, Object e4, Object e5) { Object e6 = readNullableUtf8Element(reader); if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); From f5263b8298220a3f81882d37e987459bdf18c30d Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 13:12:34 +0800 Subject: [PATCH 031/120] perf(json): speed up negative long reads --- .../fory/json/reader/Utf8JsonReader.java | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index e402c5040e..b730fcb2a6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -454,15 +454,15 @@ private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, lon } private long readNegativeLongToken(int start) { - position = start + 1; - long result = 0; - long limit = Long.MIN_VALUE; - if (position >= input.length) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { throw error("Expected digit"); } - int ch = input[position]; + int ch = bytes[offset]; if (ch == '0') { - position++; + position = offset + 1; rejectLeadingDigitFast(); rejectFractionOrExponentFast(); return 0; @@ -470,23 +470,52 @@ private long readNegativeLongToken(int start) { if (ch < '1' || ch > '9') { throw error("Expected digit"); } - long multmin = limit / 10; - while (position < input.length) { - ch = input[position]; + long result = '0' - ch; + offset++; + int safeEnd = offset + 17; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + while (offset < safeEnd) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + result = result * 10 - (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readNegativeLongTail(bytes, offset, inputLength, result); + } + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readNegativeLongTail(byte[] bytes, int offset, int inputLength, long result) { + long multmin = Long.MIN_VALUE / 10; + while (offset < inputLength) { + int ch = bytes[offset]; if (ch < '0' || ch > '9') { break; } int digit = ch - '0'; if (result < multmin) { + position = offset; throw error("Long overflow"); } result *= 10; if (result < Long.MIN_VALUE + digit) { + position = offset; throw error("Long overflow"); } result -= digit; - position++; + offset++; } + position = offset; rejectFractionOrExponentFast(); return result; } From 4a69d7a031fb34ee633f0537393afa2a72e92482 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 13:20:19 +0800 Subject: [PATCH 032/120] perf(json): avoid long tail division --- .../apache/fory/json/reader/Utf8JsonReader.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index b730fcb2a6..81cadae41d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -42,6 +42,10 @@ public final class Utf8JsonReader extends JsonReader { private static final int INT_CONTROL_LIMIT_BYTES = 0x20202020; private static final long QUOTE_BYTES = 0x2222222222222222L; private static final int INT_QUOTE_BYTES = 0x22222222; + private static final long LONG_MAX_DIV_10 = Long.MAX_VALUE / 10; + private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); + private static final long LONG_MIN_DIV_10 = Long.MIN_VALUE / 10; + private static final int LONG_MIN_LAST_DIGIT = (int) -(Long.MIN_VALUE % 10); // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. // UTF-8 string decoding must keep unsigned byte conversion for non-ASCII content. @@ -441,7 +445,7 @@ private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, lon break; } int digit = ch - '0'; - if (result > (Long.MAX_VALUE - digit) / 10) { + if (result > LONG_MAX_DIV_10 || (result == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { position = offset; throw error("Long overflow"); } @@ -496,23 +500,17 @@ private long readNegativeLongToken(int start) { } private long readNegativeLongTail(byte[] bytes, int offset, int inputLength, long result) { - long multmin = Long.MIN_VALUE / 10; while (offset < inputLength) { int ch = bytes[offset]; if (ch < '0' || ch > '9') { break; } int digit = ch - '0'; - if (result < multmin) { - position = offset; - throw error("Long overflow"); - } - result *= 10; - if (result < Long.MIN_VALUE + digit) { + if (result < LONG_MIN_DIV_10 || (result == LONG_MIN_DIV_10 && digit > LONG_MIN_LAST_DIGIT)) { position = offset; throw error("Long overflow"); } - result -= digit; + result = result * 10 - digit; offset++; } position = offset; From cfa218b91d2485df56b1381072d3b09c6d88acd6 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 14:14:37 +0800 Subject: [PATCH 033/120] perf(json): cache object array reads --- .../apache/fory/json/codec/ArrayCodec.java | 62 +++++++++++++------ .../apache/fory/json/JsonContainerTest.java | 52 ++++++++++++++++ 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 5151ff52ea..54d6827329 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -20,9 +20,7 @@ package org.apache.fory.json.codec; import java.lang.reflect.Array; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.reader.JsonReader; import org.apache.fory.json.reader.Latin1JsonReader; @@ -919,8 +917,13 @@ public Object readUtf8( } public static final class ObjectArrayCodec extends ArrayCodec { + private static final int INITIAL_VALUES_SIZE = 8; + private static final int MAX_CACHED_VALUES_SIZE = 1024; + private final JsonTypeInfo elementTypeInfo; private final JsonCodec elementCodec; + // Null means an active read borrowed the scratch array; reentrant reads use temporary storage. + private Object[] valuesCache = new Object[INITIAL_VALUES_SIZE]; private ObjectArrayCodec(Class componentType, JsonTypeInfo elementTypeInfo) { super(componentType); @@ -963,25 +966,46 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso @Override Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - reader.enterDepth(); - List values = new ArrayList<>(0); - reader.expect('['); - if (!reader.consume(']')) { - do { - values.add(elementCodec.read(reader, elementTypeInfo, resolver)); - } while (reader.consume(',')); - reader.expect(']'); + Object[] values = valuesCache; + boolean restoreCache = values != null; + valuesCache = null; + if (values == null) { + values = new Object[INITIAL_VALUES_SIZE]; } - reader.exitDepth(); - return toArray(values); - } - - private Object toArray(List values) { - Object array = Array.newInstance(componentType, values.size()); - for (int i = 0; i < values.size(); i++) { - Array.set(array, i, values.get(i)); + int size = 0; + boolean entered = false; + try { + reader.enterDepth(); + entered = true; + reader.expect('['); + if (!reader.consume(']')) { + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = elementCodec.read(reader, elementTypeInfo, resolver); + } while (reader.consume(',')); + reader.expect(']'); + } + Object[] array = (Object[]) Array.newInstance(componentType, size); + System.arraycopy(values, 0, array, 0, size); + return array; + } finally { + try { + if (entered) { + reader.exitDepth(); + } + } finally { + if (restoreCache) { + if (values.length <= MAX_CACHED_VALUES_SIZE) { + Arrays.fill(values, 0, size, null); + valuesCache = values; + } else { + valuesCache = new Object[INITIAL_VALUES_SIZE]; + } + } + } } - return array; } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 942789ce88..9d6c6a9646 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -341,6 +341,53 @@ public void writeReadBoxedPrimitiveArrays() { assertThrows(ForyJsonException.class, () -> json.fromJson("[\"ab\"]", Character[].class)); } + @Test + public void readReferenceObjectArrays() { + ForyJson json = ForyJson.builder().build(); + Note[] notes = json.fromJson("[{\"title\":\"one\"},null,{\"title\":\"two\"}]", Note[].class); + assertEquals(notes.getClass(), Note[].class); + assertEquals(notes.length, 3); + assertEquals(notes[0].title, "one"); + assertEquals(notes[1], null); + assertEquals(notes[2].title, "two"); + + Note[] empty = json.fromJson("[]".getBytes(StandardCharsets.UTF_8), Note[].class); + assertEquals(empty.getClass(), Note[].class); + assertEquals(empty.length, 0); + + Note[][] grid = + json.fromJson( + "[[{\"title\":\"left\"}],null,[{\"title\":\"right\"},null]]" + .getBytes(StandardCharsets.UTF_8), + Note[][].class); + assertEquals(grid.getClass(), Note[][].class); + assertEquals(grid.length, 3); + assertEquals(grid[0].getClass(), Note[].class); + assertEquals(grid[0][0].title, "left"); + assertEquals(grid[1], null); + assertEquals(grid[2].getClass(), Note[].class); + assertEquals(grid[2][0].title, "right"); + assertEquals(grid[2][1], null); + } + + @Test + public void readReentrantObjectArray() { + ForyJson json = ForyJson.builder().build(); + ArrayNode[] roots = + json.fromJson( + "[{\"name\":\"root\",\"children\":[{\"name\":\"leaf\",\"children\":[]}]}]" + .getBytes(StandardCharsets.UTF_8), + ArrayNode[].class); + assertEquals(roots.getClass(), ArrayNode[].class); + assertEquals(roots.length, 1); + assertEquals(roots[0].name, "root"); + assertEquals(roots[0].children.getClass(), ArrayNode[].class); + assertEquals(roots[0].children.length, 1); + assertEquals(roots[0].children[0].name, "leaf"); + assertEquals(roots[0].children[0].children.getClass(), ArrayNode[].class); + assertEquals(roots[0].children[0].children.length, 0); + } + @Test public void writeReadAtomicArrays() { ForyJson json = ForyJson.builder().build(); @@ -387,6 +434,11 @@ public static final class Note { public String title; } + public static final class ArrayNode { + public String name; + public ArrayNode[] children; + } + public static final class PaletteGroups extends HashMap {} public static final class PaletteCodes extends HashMap {} From 55021c29ea661a417723b5850c9348c7d0fc8858 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 14:30:43 +0800 Subject: [PATCH 034/120] perf(json): stack object array scratch buffers --- .../apache/fory/json/codec/ArrayCodec.java | 46 +++++++++++-------- .../apache/fory/json/JsonContainerTest.java | 25 ++++++++++ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 54d6827329..968b15f86c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -917,13 +917,15 @@ public Object readUtf8( } public static final class ObjectArrayCodec extends ArrayCodec { + private static final int VALUES_CACHE_DEPTH = 8; private static final int INITIAL_VALUES_SIZE = 8; private static final int MAX_CACHED_VALUES_SIZE = 1024; private final JsonTypeInfo elementTypeInfo; private final JsonCodec elementCodec; - // Null means an active read borrowed the scratch array; reentrant reads use temporary storage. - private Object[] valuesCache = new Object[INITIAL_VALUES_SIZE]; + // Recursive object-array reads borrow one scratch slot per active depth. + private final Object[][] valuesCache = new Object[VALUES_CACHE_DEPTH][]; + private int valuesDepth; private ObjectArrayCodec(Class componentType, JsonTypeInfo elementTypeInfo) { super(componentType); @@ -966,17 +968,21 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso @Override Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - Object[] values = valuesCache; - boolean restoreCache = values != null; - valuesCache = null; + reader.enterDepth(); + int depth = valuesDepth; + boolean useCache = depth < VALUES_CACHE_DEPTH; + Object[] values = null; + if (useCache) { + values = valuesCache[depth]; + valuesCache[depth] = null; + } if (values == null) { values = new Object[INITIAL_VALUES_SIZE]; } int size = 0; - boolean entered = false; + boolean success = false; + valuesDepth = depth + 1; try { - reader.enterDepth(); - entered = true; reader.expect('['); if (!reader.consume(']')) { do { @@ -989,22 +995,22 @@ Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver re } Object[] array = (Object[]) Array.newInstance(componentType, size); System.arraycopy(values, 0, array, 0, size); + success = true; return array; } finally { - try { - if (entered) { - reader.exitDepth(); - } - } finally { - if (restoreCache) { - if (values.length <= MAX_CACHED_VALUES_SIZE) { - Arrays.fill(values, 0, size, null); - valuesCache = values; - } else { - valuesCache = new Object[INITIAL_VALUES_SIZE]; - } + reader.exitDepth(); + // Failed reads drop the scratch array, because it may contain partially parsed user values. + if (success && useCache) { + if (values.length <= MAX_CACHED_VALUES_SIZE) { + Arrays.fill(values, 0, size, null); + valuesCache[depth] = values; + } else { + // Keep the depth slot usable without retaining a grown array from one large value. + valuesCache[depth] = new Object[INITIAL_VALUES_SIZE]; } } + // Restore the codec recursion depth after the matching cache slot has been handled. + valuesDepth = depth; } } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 9d6c6a9646..00237c7e15 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -386,6 +386,22 @@ public void readReentrantObjectArray() { assertEquals(roots[0].children[0].name, "leaf"); assertEquals(roots[0].children[0].children.getClass(), ArrayNode[].class); assertEquals(roots[0].children[0].children.length, 0); + + ArrayNode[] deep = + json.fromJson(arrayNodeJson(9).getBytes(StandardCharsets.UTF_8), ArrayNode[].class); + assertEquals(deep.getClass(), ArrayNode[].class); + assertEquals(deep.length, 1); + ArrayNode node = deep[0]; + for (int i = 0; i < 9; i++) { + assertEquals(node.name, "node-" + i); + assertEquals(node.children.getClass(), ArrayNode[].class); + if (i == 8) { + assertEquals(node.children.length, 0); + } else { + assertEquals(node.children.length, 1); + node = node.children[0]; + } + } } @Test @@ -449,6 +465,15 @@ public static final class AtomicArrayFields { public AtomicReferenceArray refs = new AtomicReferenceArray<>(new String[] {"b", null}); } + private static String arrayNodeJson(int depth) { + return "[" + nodeJson(0, depth) + "]"; + } + + private static String nodeJson(int index, int depth) { + String children = index + 1 == depth ? "[]" : "[" + nodeJson(index + 1, depth) + "]"; + return "{\"name\":\"node-" + index + "\",\"children\":" + children + "}"; + } + private static void assertAtomicInts(AtomicIntegerArray array, int... expected) { assertEquals(array.length(), expected.length); for (int i = 0; i < expected.length; i++) { From 7673bd7a8c831e013c1e59a22f580a2d1874e3e4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 15:03:52 +0800 Subject: [PATCH 035/120] perf(json): read object arraylists exactly --- .../fory/json/codec/CollectionCodec.java | 139 ++++++++++++++++++ .../apache/fory/json/JsonContainerTest.java | 13 ++ 2 files changed, 152 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index ed98da11dc..a0af5b054a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -754,6 +754,9 @@ public Object readUtf8( @Override public Object readUtf8NonNull( Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readUtf8ArrayListNonNull(reader, resolver); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -768,6 +771,142 @@ public Object readUtf8NonNull( reader.exitDepth(); return collection; } + + private ArrayList readUtf8ArrayListNonNull( + Utf8JsonReader reader, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + return readUtf8ArrayListTail(reader, resolver, e0, e1, e2, e3); + } + + private ArrayList readUtf8ArrayListTail( + Utf8JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3) { + Object e4 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + return readUtf8ArrayListLongTail(reader, resolver, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readUtf8ArrayListLongTail( + Utf8JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3, + Object e4, + Object e5) { + Object e6 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableUtf8Element(reader, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + + private Object readNullableUtf8Element(Utf8JsonReader reader, JsonTypeResolver resolver) { + return reader.tryReadNextNullToken() + ? null + : elementCodec.readUtf8NonNull(reader, elementTypeInfo, resolver); + } } public static final class StringCollectionCodec extends DirectCollectionCodec { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 00237c7e15..d441b9de3a 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -130,6 +130,19 @@ public void parsedContainersStartSmall() { assertEquals(strings, Arrays.asList("a", "b", "c", "d", "e", "f", "g")); assertEquals(arrayCapacity((ArrayList) strings), 7); + List notes = + json.fromJson( + ("[{\"title\":\"a\"},{\"title\":\"b\"},null,{\"title\":\"d\"}," + + "{\"title\":\"e\"},{\"title\":\"f\"},{\"title\":\"g\"}]") + .getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}); + assertTrue(notes instanceof ArrayList); + assertEquals(notes.size(), 7); + assertEquals(notes.get(0).title, "a"); + assertEquals(notes.get(2), null); + assertEquals(notes.get(6).title, "g"); + assertEquals(arrayCapacity((ArrayList) notes), 7); + JSONObject object = json.fromJson("{\"x\":1}", JSONObject.class); assertEquals(mapCapacity(object), 2); assertEquals(mapCapacity(json.fromJson("{}", JSONObject.class)), 0); From 6ea110de86cd9704ff327c05014efdeae0d6b82e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 18:19:29 +0800 Subject: [PATCH 036/120] perf(java): compact Fory JSON reader hot paths --- .../apache/fory/json/codec/ArrayCodec.java | 67 ++++++- .../fory/json/codegen/JsonReaderCodegen.java | 163 +++++++++++++---- .../fory/json/reader/Utf8JsonReader.java | 170 ++++++++++++++++-- 3 files changed, 348 insertions(+), 52 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 968b15f86c..aff3817589 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -339,6 +339,10 @@ public Object readUtf8( reader.exitDepth(); return new long[] {v0, v1, v2, v3}; } + return readUtf8Tail(reader, v0, v1, v2, v3); + } + + private long[] readUtf8Tail(Utf8JsonReader reader, long v0, long v1, long v2, long v3) { rejectNull(reader); long v4 = reader.readLongValue(); if (!reader.consumeNextToken(',')) { @@ -367,6 +371,21 @@ public Object readUtf8( reader.exitDepth(); return new long[] {v0, v1, v2, v3, v4, v5, v6, v7}; } + return readUtf8LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7); + } + + // Keep dynamic growth out of the exact small-array path so C2 can inline the common cases + // without pulling the uncommon grow/copy loop into the caller's inline budget. + private long[] readUtf8LongTail( + Utf8JsonReader reader, + long v0, + long v1, + long v2, + long v3, + long v4, + long v5, + long v6, + long v7) { long[] values = new long[16]; values[0] = v0; values[1] = v1; @@ -863,29 +882,34 @@ public Object readUtf8( reader.exitDepth(); return new String[] {v0, v1, v2, v3}; } + return readUtf8Tail(reader, v0, v1, v2, v3); + } + + private String[] readUtf8Tail( + Utf8JsonReader reader, String v0, String v1, String v2, String v3) { String v4 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { reader.expectNextToken(']'); reader.exitDepth(); - return new String[] {v0, v1, v2, v3, v4}; + return stringArray5(v0, v1, v2, v3, v4); } String v5 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { reader.expectNextToken(']'); reader.exitDepth(); - return new String[] {v0, v1, v2, v3, v4, v5}; + return stringArray6(v0, v1, v2, v3, v4, v5); } String v6 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { reader.expectNextToken(']'); reader.exitDepth(); - return new String[] {v0, v1, v2, v3, v4, v5, v6}; + return stringArray7(v0, v1, v2, v3, v4, v5, v6); } String v7 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { reader.expectNextToken(']'); reader.exitDepth(); - return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; + return stringArray8(v0, v1, v2, v3, v4, v5, v6, v7); } String v8 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { @@ -893,6 +917,22 @@ public Object readUtf8( reader.exitDepth(); return new String[] {v0, v1, v2, v3, v4, v5, v6, v7, v8}; } + return readUtf8LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7, v8); + } + + // Keep dynamic growth out of the exact small-array path so C2 can inline the common cases + // without pulling the uncommon grow/copy loop into the caller's inline budget. + private String[] readUtf8LongTail( + Utf8JsonReader reader, + String v0, + String v1, + String v2, + String v3, + String v4, + String v5, + String v6, + String v7, + String v8) { String[] values = new String[16]; values[0] = v0; values[1] = v1; @@ -914,6 +954,25 @@ public Object readUtf8( reader.exitDepth(); return Arrays.copyOf(values, size); } + + private static String[] stringArray5(String v0, String v1, String v2, String v3, String v4) { + return new String[] {v0, v1, v2, v3, v4}; + } + + private static String[] stringArray6( + String v0, String v1, String v2, String v3, String v4, String v5) { + return new String[] {v0, v1, v2, v3, v4, v5}; + } + + private static String[] stringArray7( + String v0, String v1, String v2, String v3, String v4, String v5, String v6) { + return new String[] {v0, v1, v2, v3, v4, v5, v6}; + } + + private static String[] stringArray8( + String v0, String v1, String v2, String v3, String v4, String v5, String v6, String v7) { + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } } public static final class ObjectArrayCodec extends ArrayCodec { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 46f6af6028..d1cae5ad1d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -29,6 +29,7 @@ import org.apache.fory.json.meta.JsonAsciiToken; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.json.meta.JsonFieldTable; import org.apache.fory.json.reader.JsonReader; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Latin1ObjectReader; @@ -48,6 +49,7 @@ final class JsonReaderCodegen { private static final int UTF8_READER = JsonCodegen.UTF8_READER; private static final int MIN_SPLIT_READ_FIELDS = 12; private static final int READ_FIELD_GROUP_SIZE = 2; + private static final int READ_FIELD_SWITCH_SIZE = 8; private final JsonCodegen codegen; @@ -75,7 +77,8 @@ String genReaderCode( JsonReader.class, Latin1JsonReader.class, Utf16JsonReader.class, - Utf8JsonReader.class); + Utf8JsonReader.class, + JsonFieldTable.class); ctx.implementsInterfaces( ctx.type(ObjectReader.class), ctx.type(Latin1ObjectReader.class), @@ -103,14 +106,6 @@ String genReaderCode( "codecs", BaseObjectCodec[].class, "objectCodecs"); - addGeneratedMethod( - ctx, - "final", - "fieldIndex", - fieldIndexExpression(properties), - int.class, - long.class, - "fieldHash"); addGeneratedMethod( ctx, "public", @@ -123,6 +118,8 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addReadFieldMethods( + ctx, builder, "read", JsonReader.class, type, properties, GENERIC_READER, record); addGeneratedMethod( ctx, "public", @@ -146,6 +143,15 @@ String genReaderCode( properties, LATIN1_READER, record); + addReadFieldMethods( + ctx, + builder, + "readLatin1", + Latin1JsonReader.class, + type, + properties, + LATIN1_READER, + record); addSlowReadMethods( ctx, builder, @@ -178,6 +184,8 @@ String genReaderCode( properties, UTF16_READER, record); + addReadFieldMethods( + ctx, builder, "readUtf16", Utf16JsonReader.class, type, properties, UTF16_READER, record); addSlowReadMethods( ctx, builder, @@ -210,6 +218,8 @@ String genReaderCode( properties, UTF8_READER, record); + addReadFieldMethods( + ctx, builder, "readUtf8", Utf8JsonReader.class, type, properties, UTF8_READER, record); addSlowReadMethods( ctx, builder, "readUtf8Slow", Utf8JsonReader.class, type, properties, UTF8_READER, record); return ctx.genCode(); @@ -252,6 +262,49 @@ private void addFastReadGroupMethods( } } + private void addReadFieldMethods( + CodegenContext ctx, + JsonGeneratedCodecBuilder builder, + String readMethod, + Class readerType, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + if (!shouldSplitFieldSwitch(properties)) { + return; + } + Class objectType = record ? Object[].class : type; + for (int start = 0; start < properties.length; start += READ_FIELD_SWITCH_SIZE) { + int end = Math.min(start + READ_FIELD_SWITCH_SIZE, properties.length); + addGeneratedMethod( + ctx, + "final", + readFieldMethod(readMethod, start), + fieldSwitchRange( + builder, + type, + properties, + start, + end, + readerMode, + objectParam(type, record), + new Reference("fieldIndex", TypeRef.of(int.class)), + record), + void.class, + readerType, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver", + objectType, + "object", + int.class, + "fieldIndex"); + } + } + private void addSlowReadMethods( CodegenContext ctx, JsonGeneratedCodecBuilder builder, @@ -367,19 +420,6 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr return expressions; } - private Expression fieldIndexExpression(JsonFieldInfo[] properties) { - Expression.ListExpression expressions = new Expression.ListExpression(); - Reference fieldHash = new Reference("fieldHash", TypeRef.of(long.class)); - for (int i = 0; i < properties.length; i++) { - expressions.add( - new Expression.If( - eq(fieldHash, Expression.Literal.ofLong(properties[i].nameHash())), - new Expression.Return(Expression.Literal.ofInt(i)))); - } - expressions.add(new Expression.Return(Expression.Literal.ofInt(-1))); - return expressions; - } - private Expression readExpression( JsonGeneratedCodecBuilder builder, Class type, @@ -822,6 +862,10 @@ private static String readGroupMethod(String readMethod, int start) { return readMethod + "Group" + start; } + private static String readFieldMethod(String readMethod, int start) { + return readMethod + "Field" + start; + } + private static int readGroupEnd(JsonFieldInfo[] properties, int start) { int weight = 0; int index = start; @@ -852,6 +896,25 @@ private static int readFieldWeight(JsonFieldInfo property) { } } + private static String readMethod(int readerMode) { + switch (readerMode) { + case GENERIC_READER: + return "read"; + case LATIN1_READER: + return "readLatin1"; + case UTF16_READER: + return "readUtf16"; + case UTF8_READER: + return "readUtf8"; + default: + throw new IllegalArgumentException(String.valueOf(readerMode)); + } + } + + private static boolean shouldSplitFieldSwitch(JsonFieldInfo[] properties) { + return properties.length > READ_FIELD_SWITCH_SIZE; + } + private Expression objectExpression(JsonGeneratedCodecBuilder builder, boolean record) { if (record) { return new Expression.Variable( @@ -1029,9 +1092,51 @@ private Expression fieldSwitch( Expression object, Expression fieldIndex, boolean record) { - Expression.Switch.Case[] cases = new Expression.Switch.Case[properties.length]; - for (int i = 0; i < properties.length; i++) { - cases[i] = + if (shouldSplitFieldSwitch(properties)) { + int chunks = (properties.length + READ_FIELD_SWITCH_SIZE - 1) / READ_FIELD_SWITCH_SIZE; + Expression.Switch.Case[] cases = new Expression.Switch.Case[chunks]; + for (int i = 0, start = 0; i < chunks; i++, start += READ_FIELD_SWITCH_SIZE) { + cases[i] = + new Expression.Switch.Case( + i, + new Expression.ListExpression( + new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + readFieldMethod(readMethod(readerMode), start), + "", + TypeRef.of(void.class), + false, + false, + readerRef(readerMode), + ownerRef(), + typeResolverRef(), + object, + fieldIndex), + new Expression.Break())); + } + Expression chunkIndex = + new Expression.Arithmetic( + true, "/", fieldIndex, Expression.Literal.ofInt(READ_FIELD_SWITCH_SIZE)); + return new Expression.Switch( + chunkIndex, cases, new Expression.Invoke(readerRef(readerMode), "skipValue")); + } + return fieldSwitchRange( + builder, type, properties, 0, properties.length, readerMode, object, fieldIndex, record); + } + + private Expression fieldSwitchRange( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + int start, + int end, + int readerMode, + Expression object, + Expression fieldIndex, + boolean record) { + Expression.Switch.Case[] cases = new Expression.Switch.Case[end - start]; + for (int i = start; i < end; i++) { + cases[i - start] = new Expression.Switch.Case( i, new Expression.ListExpression( @@ -1233,12 +1338,10 @@ private static Expression readFieldNameHash(int readerMode, String namePrefix) { private static Expression fieldIndexInvoke(Expression fieldHash) { return new Expression.Invoke( - new Reference("this", TypeRef.of(Object.class)), - "fieldIndex", - "", + new Expression.Invoke(ownerRef(), "readTable", TypeRef.of(JsonFieldTable.class)), + "index", TypeRef.of(int.class), - false, - false, + true, fieldHash) .inline(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 81cadae41d..ed77b69883 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -526,16 +526,70 @@ private BigDecimal readBigDecimalToken() { if (offset >= inputLength) { return readBigDecimalFallback(start); } - boolean negative = false; int ch = bytes[offset]; if (ch == '-') { - negative = true; + return readSignedBigDecimalToken(start); + } + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + position = offset; + rejectLeadingDigitFast(); + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readBigDecimalFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { offset++; - if (offset >= inputLength) { + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { return readBigDecimalFallback(start); } + } + if (offset < inputLength) { ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readBigDecimalFallback(start); + } } + position = offset; + return BigDecimal.valueOf(unscaled, scale); + } + + private BigDecimal readSignedBigDecimalToken(int start) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + int ch = bytes[offset]; long unscaled = 0; int scale = 0; if (ch == '0') { @@ -585,7 +639,7 @@ private BigDecimal readBigDecimalToken() { } } position = offset; - return BigDecimal.valueOf(negative ? -unscaled : unscaled, scale); + return BigDecimal.valueOf(-unscaled, scale); } private BigDecimal readBigDecimalFallback(int start) { @@ -647,19 +701,69 @@ private double readDoubleToken() { if (offset >= inputLength) { return readDoubleFallback(start); } - boolean negative = false; int ch = bytes[offset]; if (ch == '-') { - negative = true; + return readSignedDoubleToken(start); + } + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readDoubleFallback(start); + } + } + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readDoubleFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { offset++; - if (offset >= inputLength) { + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { return readDoubleFallback(start); } - ch = bytes[offset]; } + return finishDoubleToken(bytes, offset, inputLength, start, unscaled, scale); + } + + private double readSignedDoubleToken(int start) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + int ch = bytes[offset]; long unscaled = 0; int scale = 0; - int digits = 0; if (ch == '0') { offset++; if (offset < inputLength) { @@ -675,7 +779,6 @@ private double readDoubleToken() { return readDoubleFallback(start); } unscaled = unscaled * 10 + digit; - digits++; offset++; if (offset >= inputLength) { break; @@ -699,28 +802,40 @@ private double readDoubleToken() { } unscaled = unscaled * 10 + digit; scale++; - digits++; offset++; } if (offset == fractionStart) { return readDoubleFallback(start); } } + return finishSignedDoubleToken(bytes, offset, inputLength, start, unscaled, scale); + } + + private double finishDoubleToken( + byte[] bytes, int offset, int inputLength, int start, long unscaled, int scale) { if (offset < inputLength) { - ch = bytes[offset]; + int ch = bytes[offset]; if (ch == 'e' || ch == 'E') { return readDoubleFallback(start); } } - if (digits > 18) { - return readDoubleFallback(start); + position = offset; + return BigDecimal.valueOf(unscaled, scale).doubleValue(); + } + + private double finishSignedDoubleToken( + byte[] bytes, int offset, int inputLength, int start, long unscaled, int scale) { + if (offset < inputLength) { + int ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readDoubleFallback(start); + } } position = offset; - if (negative && unscaled == 0) { + if (unscaled == 0) { return -0.0d; } - long value = negative ? -unscaled : unscaled; - return BigDecimal.valueOf(value, scale).doubleValue(); + return BigDecimal.valueOf(-unscaled, scale).doubleValue(); } private double readDoubleFallback(int start) { @@ -1023,9 +1138,13 @@ private OffsetDateTime readIsoOffsetDateTimeToken() { int day = parse2(bytes, start + 8); int hour = parse2(bytes, start + 11); int minute = parse2(bytes, start + 14); + return readIsoOffsetDateTimeTail(bytes, start + 16, length, year, month, day, hour, minute); + } + + private OffsetDateTime readIsoOffsetDateTimeTail( + byte[] bytes, int index, int length, int year, int month, int day, int hour, int minute) { int second = 0; int nano = 0; - int index = start + 16; if (index < length && bytes[index] == ':') { second = parse2(bytes, index + 1); index += 3; @@ -1049,6 +1168,21 @@ private OffsetDateTime readIsoOffsetDateTimeToken() { position = index + 2; return OffsetDateTime.of(year, month, day, hour, minute, second, nano, ZoneOffset.UTC); } + return readIsoOffsetDateTimeOffsetTail( + bytes, index, length, year, month, day, hour, minute, second, nano); + } + + private OffsetDateTime readIsoOffsetDateTimeOffsetTail( + byte[] bytes, + int index, + int length, + int year, + int month, + int day, + int hour, + int minute, + int second, + int nano) { long offsetAndEnd = parseOffsetAndEnd(bytes, index, length); position = (int) offsetAndEnd; return OffsetDateTime.of( From 3a62ded57a97b61a21882d98d19ac502637d5d58 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 18:40:03 +0800 Subject: [PATCH 037/120] perf(java): parse UTF-8 long digit blocks --- .../fory/json/reader/Utf8JsonReader.java | 40 +++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 25 ++++++++++++ 2 files changed, 65 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index ed77b69883..dc0c10c631 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -46,6 +46,10 @@ public final class Utf8JsonReader extends JsonReader { private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); private static final long LONG_MIN_DIV_10 = Long.MIN_VALUE / 10; private static final int LONG_MIN_LAST_DIGIT = (int) -(Long.MIN_VALUE % 10); + private static final long EIGHT_DIGITS = 100_000_000L; + private static final long ASCII_ZEROES = 0x3030_3030_3030_3030L; + private static final long ASCII_NINES = 0x3939_3939_3939_3939L; + private static final long ASCII_HIGH_BITS = 0x8080_8080_8080_8080L; // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. // UTF-8 string decoding must keep unsigned byte conversion for non-ASCII content. @@ -419,6 +423,16 @@ private long readLongToken() { if (safeEnd > inputLength) { safeEnd = inputLength; } + int block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS + block; + offset += 8; + block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS + block; + offset += 8; + } + } while (offset < safeEnd) { ch = bytes[offset]; if (ch < '0' || ch > '9') { @@ -480,6 +494,16 @@ private long readNegativeLongToken(int start) { if (safeEnd > inputLength) { safeEnd = inputLength; } + int block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS - block; + offset += 8; + block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS - block; + offset += 8; + } + } while (offset < safeEnd) { ch = bytes[offset]; if (ch < '0' || ch > '9') { @@ -518,6 +542,22 @@ private long readNegativeLongTail(byte[] bytes, int offset, int inputLength, lon return result; } + private static int parseEightDigits(byte[] bytes, int offset, int safeEnd) { + if (offset + 8 > safeEnd) { + return -1; + } + // Keep this as one unaligned little-endian load. Eight separate byte loads made the helper too + // large for C2 to place well under generated readers, while the byte-lane math stays compact. + long chunk = LittleEndian.getInt64(bytes, offset); + long digits = chunk - ASCII_ZEROES; + if (((digits | (ASCII_NINES - chunk)) & ASCII_HIGH_BITS) != 0) { + return -1; + } + long pairs = (digits * 10 + (digits >>> 8)) & 0x00FF_00FF_00FF_00FFL; + long quads = (pairs * 100 + (pairs >>> 16)) & 0x0000_FFFF_0000_FFFFL; + return (int) ((quads & 0xFFFF) * 10_000 + (quads >>> 32)); + } + private BigDecimal readBigDecimalToken() { byte[] bytes = input; int offset = position; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 99f69009ad..bcae107bb4 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -151,6 +151,31 @@ public void readUtf8DoubleTokens() { () -> new Utf8JsonReader("01.5".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue()); } + @Test + public void readUtf8LongBlocks() { + assertEquals( + new Utf8JsonReader("123456789012345678".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + 123456789012345678L); + assertEquals( + new Utf8JsonReader("-123456789012345678".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + -123456789012345678L); + assertEquals( + new Utf8JsonReader("9223372036854775807".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + Long.MAX_VALUE); + assertEquals( + new Utf8JsonReader("-9223372036854775808".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + Long.MIN_VALUE); + assertThrows( + ForyJsonException.class, + () -> + new Utf8JsonReader("9223372036854775808".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue()); + } + @Test public void writeNumericBoundaries() { ForyJson json = ForyJson.builder().build(); From 63d006f5c458a6ec7a883609ae493cf483272564 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 19:27:08 +0800 Subject: [PATCH 038/120] perf(java): compact Fory JSON UTF-8 string scan --- .../fory/json/reader/Utf8JsonReader.java | 22 +++++++------------ .../org/apache/fory/json/JsonStringTest.java | 2 +- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index dc0c10c631..b9328ee355 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -1337,29 +1337,23 @@ private StringBuilder newStringBuilder(int start, int stop) { } private static long stringStopMask(long word) { + // UTF-8 mode stops on every high-bit byte, so the quote/backslash equality mask can + // omit the usual `~match` term. Latin1JsonReader cannot use this shortcut because + // high-bit Latin-1 bytes are valid string payload. + long syntaxStop = ((word ^ QUOTE_BYTES) - BYTE_ONES) | ((word ^ BACKSLASH_BYTES) - BYTE_ONES); return (word & BYTE_HIGH_BITS) - | byteMatchMask(word, QUOTE_BYTES) - | byteMatchMask(word, BACKSLASH_BYTES) + | (syntaxStop & BYTE_HIGH_BITS) | ((word - CONTROL_LIMIT_BYTES) & ~word & BYTE_HIGH_BITS); } private static int stringStopMask(int word) { + int syntaxStop = + ((word ^ INT_QUOTE_BYTES) - INT_BYTE_ONES) | ((word ^ INT_BACKSLASH_BYTES) - INT_BYTE_ONES); return (word & INT_BYTE_HIGH_BITS) - | byteMatchMask(word, INT_QUOTE_BYTES) - | byteMatchMask(word, INT_BACKSLASH_BYTES) + | (syntaxStop & INT_BYTE_HIGH_BITS) | ((word - INT_CONTROL_LIMIT_BYTES) & ~word & INT_BYTE_HIGH_BITS); } - private static long byteMatchMask(long word, long repeatedByte) { - long match = word ^ repeatedByte; - return (match - BYTE_ONES) & ~match & BYTE_HIGH_BITS; - } - - private static int byteMatchMask(int word, int repeatedByte) { - int match = word ^ repeatedByte; - return (match - INT_BYTE_ONES) & ~match & INT_BYTE_HIGH_BITS; - } - @Override public JsonFieldInfo readField(JsonFieldTable table) { return table.get(readFieldNameHash()); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index c6beef60dc..d8e6143c3a 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -227,7 +227,7 @@ public void writeSurrogatePair() { @Test public void readStringScanBoundaries() { ForyJson json = ForyJson.builder().build(); - for (int length : new int[] {7, 8, 15, 16, 23, 24}) { + for (int length : new int[] {7, 8, 15, 16, 23, 24, 31, 32, 63, 64, 65}) { String value = repeat('a', length); String input = "\"" + value + "\""; assertEquals(json.fromJson(input, String.class), value); From 0edc61dbaf7b81d6e212ba75f594fd47739f86e4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 19:37:07 +0800 Subject: [PATCH 039/120] perf(java): fold Fory JSON UTF-8 string mask --- .../apache/fory/json/reader/Utf8JsonReader.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index b9328ee355..5df2e24528 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -1337,21 +1337,18 @@ private StringBuilder newStringBuilder(int start, int stop) { } private static long stringStopMask(long word) { - // UTF-8 mode stops on every high-bit byte, so the quote/backslash equality mask can - // omit the usual `~match` term. Latin1JsonReader cannot use this shortcut because - // high-bit Latin-1 bytes are valid string payload. + // UTF-8 mode stops on every high-bit byte, and readStringToken only uses the first stop bit. + // Subtraction borrow may only create later high bits after an earlier real stop, so the + // compact syntax/range expression preserves the first-stop position. Latin1JsonReader cannot + // use this shortcut because high-bit Latin-1 bytes are valid string payload. long syntaxStop = ((word ^ QUOTE_BYTES) - BYTE_ONES) | ((word ^ BACKSLASH_BYTES) - BYTE_ONES); - return (word & BYTE_HIGH_BITS) - | (syntaxStop & BYTE_HIGH_BITS) - | ((word - CONTROL_LIMIT_BYTES) & ~word & BYTE_HIGH_BITS); + return (syntaxStop | word | (word - CONTROL_LIMIT_BYTES)) & BYTE_HIGH_BITS; } private static int stringStopMask(int word) { int syntaxStop = ((word ^ INT_QUOTE_BYTES) - INT_BYTE_ONES) | ((word ^ INT_BACKSLASH_BYTES) - INT_BYTE_ONES); - return (word & INT_BYTE_HIGH_BITS) - | (syntaxStop & INT_BYTE_HIGH_BITS) - | ((word - INT_CONTROL_LIMIT_BYTES) & ~word & INT_BYTE_HIGH_BITS); + return (syntaxStop | word | (word - INT_CONTROL_LIMIT_BYTES)) & INT_BYTE_HIGH_BITS; } @Override From 8295a5cbf605436715a84cc5a5d9b090042d2fde Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 20:02:12 +0800 Subject: [PATCH 040/120] perf(java): split Fory JSON string tail scan --- .../java/org/apache/fory/json/reader/Utf8JsonReader.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 5df2e24528..7e9784324a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -1098,6 +1098,13 @@ private String readStringToken() { } return readStringStop(start, stop, b); } + return readStringTokenTail(start, offset, inputLength); + } + + private String readStringTokenTail(int start, int offset, int inputLength) { + byte[] bytes = input; + // Reached only when the whole input has fewer than eight bytes left. Keep this rare tail out + // of the hot word scanner so C2 has more budget for common string call sites. if (offset + Integer.BYTES <= inputLength) { int stopMask = stringStopMask(LittleEndian.getInt32(bytes, offset)); if (stopMask == 0) { From 5302de8d9ca1ad76cd4673699b1bc09058dc69ee Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 07:17:22 +0800 Subject: [PATCH 041/120] fix(java): handle Fory JSON Latin-1 string boundary --- .../apache/fory/json/writer/Utf8JsonWriter.java | 2 +- .../java/org/apache/fory/json/JsonStringTest.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index eb67d1e70a..077ccaafae 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -584,7 +584,7 @@ private boolean writeLatin1StringNoEnsure(byte[] value) { } private boolean writeLatin1StringNoEnsure(byte[] value, int length) { - if (length <= 32) { + if (length < 32) { return writeShortLatin1StringNoEnsure(value, length); } return writeLongLatin1StringNoEnsure(value, length); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index d8e6143c3a..f4bf6deb96 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -224,6 +224,21 @@ public void writeSurrogatePair() { assertEquals(json.fromJson(stringExpected, PublicFields.class).name, fields.name); } + @Test + public void writeStringScanBoundaries() { + ForyJson json = ForyJson.builder().build(); + for (int length : new int[] {7, 8, 15, 16, 17, 23, 24, 31, 32, 33, 63, 64, 65}) { + String value = repeat('a', length); + String expected = "\"" + value + "\""; + assertEquals(json.toJson(value), expected); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + } + String escaped = repeat('b', 24) + "\n"; + String expected = "\"" + repeat('b', 24) + "\\n\""; + assertEquals(json.toJson(escaped), expected); + assertEquals(new String(json.toJsonBytes(escaped), StandardCharsets.UTF_8), expected); + } + @Test public void readStringScanBoundaries() { ForyJson json = ForyJson.builder().build(); From ce5c01d9741783252e13c245de5427ec0c93762e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 07:36:36 +0800 Subject: [PATCH 042/120] perf(java): elide Fory JSON string coder reads --- .../fory/json/writer/Utf8JsonWriter.java | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 077ccaafae..8716e80d87 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -177,12 +177,11 @@ public void writeChar(char value) { public void writeString(String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - byte stringCoder = StringSerializer.getStringCoder(value); - if (StringSerializer.isLatin1Coder(stringCoder)) { + if (bytes.length == value.length()) { if (writeLatin1String(bytes)) { return; } - } else if (StringSerializer.isUtf16Coder(stringCoder)) { + } else { if (writeUtf16String(bytes)) { return; } @@ -393,16 +392,15 @@ public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int inde public void writeStringField(byte[] prefix, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - byte stringCoder = StringSerializer.getStringCoder(value); int start = position; - if (StringSerializer.isLatin1Coder(stringCoder)) { + if (bytes.length == value.length()) { ensure(prefix.length + bytes.length + 2); writeRawNoEnsure(prefix); if (writeLatin1StringNoEnsure(bytes)) { return; } position = start; - } else if (StringSerializer.isUtf16Coder(stringCoder)) { + } else { ensure(prefix.length + (bytes.length >> 1) * 3 + 2); writeRawNoEnsure(prefix); if (writeUtf16StringNoEnsure(bytes)) { @@ -417,16 +415,15 @@ public void writeStringField(byte[] prefix, String value) { public void writeStringField(long prefix0, long prefix1, int prefixLength, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - byte stringCoder = StringSerializer.getStringCoder(value); int start = position; - if (StringSerializer.isLatin1Coder(stringCoder)) { + if (bytes.length == value.length()) { ensurePackedPrefix(prefixLength, bytes.length + 2); writePackedRawNoEnsure(prefix0, prefix1, prefixLength); if (writeLatin1StringNoEnsure(bytes)) { return; } position = start; - } else if (StringSerializer.isUtf16Coder(stringCoder)) { + } else { ensurePackedPrefix(prefixLength, (bytes.length >> 1) * 3 + 2); writePackedRawNoEnsure(prefix0, prefix1, prefixLength); if (writeUtf16StringNoEnsure(bytes)) { @@ -495,9 +492,8 @@ public void writeStringElement(int index, String value) { } if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - byte stringCoder = StringSerializer.getStringCoder(value); int start = position; - if (StringSerializer.isLatin1Coder(stringCoder)) { + if (bytes.length == value.length()) { ensure(comma + bytes.length + 2); if (comma != 0) { buffer[position++] = ','; @@ -506,7 +502,7 @@ public void writeStringElement(int index, String value) { return; } position = start; - } else if (StringSerializer.isUtf16Coder(stringCoder)) { + } else { ensure(comma + (bytes.length >> 1) * 3 + 2); if (comma != 0) { buffer[position++] = ','; @@ -825,12 +821,11 @@ private void writeStringSlow(String value, int index, int length) { private void writeStringNoEnsure(String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - byte stringCoder = StringSerializer.getStringCoder(value); - if (StringSerializer.isLatin1Coder(stringCoder)) { + if (bytes.length == value.length()) { if (writeLatin1StringNoEnsure(bytes)) { return; } - } else if (StringSerializer.isUtf16Coder(stringCoder)) { + } else { if (writeUtf16StringNoEnsure(bytes)) { return; } @@ -908,10 +903,12 @@ private void writeAsciiNoEnsure(String value) { } private void writeAsciiNumber(String value) { - if (STRING_BYTES_BACKED - && StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(value))) { - writeRaw(StringSerializer.getStringBytes(value)); - return; + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + if (bytes.length == value.length()) { + writeRaw(bytes); + return; + } } writeAscii(value); } From 86142f89187a152100825148cb02d8c79160fb81 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 07:48:09 +0800 Subject: [PATCH 043/120] perf(java): tune Fory JSON writer split size --- .../java/org/apache/fory/json/codegen/JsonWriterCodegen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index a0ce024439..74185cf878 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -47,7 +47,7 @@ final class JsonWriterCodegen { private static final int MIN_SPLIT_MEMBERS = 12; // Keep generated member helpers below C2's big-method range without fragmenting them into // tiny calls. This mirrors Fory core's object-codec split strategy for large generated codecs. - private static final int MEMBER_GROUP_SIZE = 10; + private static final int MEMBER_GROUP_SIZE = 8; private final JsonCodegen codegen; private final boolean writeNullFields; From 114d9e552c06da091975a5ffe46ed6d950fe8993 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 08:50:55 +0800 Subject: [PATCH 044/120] perf(java): write Fory JSON doubles directly --- .../fory/json/writer/JdkDoubleFormatter.java | 74 +++++++++++++++++++ .../fory/json/writer/Utf8JsonWriter.java | 6 ++ .../org/apache/fory/json/JsonScalarTest.java | 26 +++++++ 3 files changed, 106 insertions(+) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/writer/JdkDoubleFormatter.java diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/JdkDoubleFormatter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/JdkDoubleFormatter.java new file mode 100644 index 0000000000..0491264b2d --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/JdkDoubleFormatter.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.json.writer; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.internal._JDKAccess; + +/** Exact JDK double formatter access for UTF-8 JSON number output. */ +final class JdkDoubleFormatter { + private static final MethodHandle PUT_DECIMAL = loadPutDecimal(); + + private JdkDoubleFormatter() {} + + static int write(byte[] buffer, int position, double value) { + MethodHandle putDecimal = PUT_DECIMAL; + if (putDecimal == null) { + return -1; + } + try { + return (int) putDecimal.invokeExact(buffer, position, value); + } catch (Throwable e) { + throw new ForyJsonException("Cannot write JSON double " + value, e); + } + } + + private static MethodHandle loadPutDecimal() { + if (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return null; + } + try { + // This is the same formatter used by Double.toString; using it directly avoids materializing + // the intermediate String while preserving JDK numeric spelling exactly. + Class formatterClass = Class.forName("jdk.internal.math.DoubleToDecimal"); + Lookup lookup = _JDKAccess._trustedLookup(formatterClass); + Object latin1 = lookup.findStaticGetter(formatterClass, "LATIN1", formatterClass).invoke(); + MethodHandle putDecimal = + lookup.findVirtual( + formatterClass, + "putDecimal", + MethodType.methodType(int.class, byte[].class, int.class, double.class)); + return putDecimal + .bindTo(latin1) + .asType(MethodType.methodType(int.class, byte[].class, int.class, double.class)); + } catch (ClassNotFoundException | NoSuchFieldException | NoSuchMethodException e) { + return null; + } catch (IllegalAccessException e) { + return null; + } catch (Throwable e) { + throw new ExceptionInInitializerError(e); + } + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 8716e80d87..de6b8aff24 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -155,6 +155,12 @@ public void writeDouble(double value) { if (!Double.isFinite(value)) { throw new ForyJsonException("JSON does not support non-finite double " + value); } + ensure(24); + int newPosition = JdkDoubleFormatter.write(buffer, position, value); + if (newPosition >= 0) { + position = newPosition; + return; + } writeAsciiNumber(Double.toString(value)); } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index bcae107bb4..a0e3e356f0 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -69,6 +69,32 @@ public void writeBoxedScalars() { new String(json.toJsonBytes(new BoxedScalars()), StandardCharsets.UTF_8), expected); } + @Test + public void writeUtf8Doubles() { + double[] values = { + 0.0d, + -0.0d, + 2.5d, + -3.75d, + Math.nextUp(1.0d), + 32.389082173209815d, + 69.85922221416756d, + 1.0e-4d, + 1.0e-3d, + 1.0e7d, + Double.MIN_VALUE, + Double.MAX_VALUE + }; + for (double value : values) { + Utf8JsonWriter writer = new Utf8JsonWriter(false, new byte[4]); + writer.writeDouble(value); + assertEquals( + new String(writer.toJsonBytes(), StandardCharsets.UTF_8), Double.toString(value)); + } + Utf8JsonWriter writer = new Utf8JsonWriter(false, new byte[4]); + assertThrows(ForyJsonException.class, () -> writer.writeDouble(Double.NaN)); + } + @Test public void writeNaturalObjectValues() { ForyJson json = ForyJson.builder().build(); From 2be8a2452f068353a655db8f4ed9b9804de8bd06 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 09:30:23 +0800 Subject: [PATCH 045/120] perf(java): combine Fory JSON Latin-1 ASCII checks --- .../apache/fory/json/writer/Utf8JsonWriter.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index de6b8aff24..f7a1897bf8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -602,7 +602,7 @@ private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { for (; i < upperBound; i += 16) { long word0 = LittleEndian.getInt64(value, i); long word1 = LittleEndian.getInt64(value, i + 8); - if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { + if (!isJsonAsciiWords(word0, word1)) { break; } LittleEndian.putInt64(bytes, pos, word0); @@ -658,7 +658,7 @@ private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { if (length > 15) { long word0 = LittleEndian.getInt64(value, 0); long word1 = LittleEndian.getInt64(value, 8); - if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { + if (!isJsonAsciiWords(word0, word1)) { position = start; return false; } @@ -984,6 +984,18 @@ private static boolean isJsonAsciiWord(long word) { && (((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS; } + private static boolean isJsonAsciiWords(long word0, long word1) { + long controlMask = + ((word0 + ASCII_CONTROL_OFFSET) & ~word0) & ((word1 + ASCII_CONTROL_OFFSET) & ~word1); + long quoteMask = + ((word0 ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES) + & ((word1 ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES); + long backslashMask = + ((word0 ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) + & ((word1 ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES); + return (controlMask & quoteMask & backslashMask & HIGH_BITS) == HIGH_BITS; + } + private static boolean isJsonAsciiInt(int word) { return (((word + INT_ASCII_CONTROL_OFFSET) & ~word) & INT_HIGH_BITS) == INT_HIGH_BITS && (((word ^ INT_QUOTE_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) == INT_HIGH_BITS From 28dcdaf6b58581fe3ea9b61169aab88a49ee524a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 09:46:33 +0800 Subject: [PATCH 046/120] Revert "perf(java): combine Fory JSON Latin-1 ASCII checks" This reverts commit 2be8a2452f068353a655db8f4ed9b9804de8bd06. --- .../apache/fory/json/writer/Utf8JsonWriter.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index f7a1897bf8..de6b8aff24 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -602,7 +602,7 @@ private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { for (; i < upperBound; i += 16) { long word0 = LittleEndian.getInt64(value, i); long word1 = LittleEndian.getInt64(value, i + 8); - if (!isJsonAsciiWords(word0, word1)) { + if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { break; } LittleEndian.putInt64(bytes, pos, word0); @@ -658,7 +658,7 @@ private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { if (length > 15) { long word0 = LittleEndian.getInt64(value, 0); long word1 = LittleEndian.getInt64(value, 8); - if (!isJsonAsciiWords(word0, word1)) { + if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { position = start; return false; } @@ -984,18 +984,6 @@ private static boolean isJsonAsciiWord(long word) { && (((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS; } - private static boolean isJsonAsciiWords(long word0, long word1) { - long controlMask = - ((word0 + ASCII_CONTROL_OFFSET) & ~word0) & ((word1 + ASCII_CONTROL_OFFSET) & ~word1); - long quoteMask = - ((word0 ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES) - & ((word1 ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES); - long backslashMask = - ((word0 ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) - & ((word1 ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES); - return (controlMask & quoteMask & backslashMask & HIGH_BITS) == HIGH_BITS; - } - private static boolean isJsonAsciiInt(int word) { return (((word + INT_ASCII_CONTROL_OFFSET) & ~word) & INT_HIGH_BITS) == INT_HIGH_BITS && (((word ^ INT_QUOTE_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) == INT_HIGH_BITS From abd2e2887570e9655d691b4417748107ce0a1fe4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 10:02:10 +0800 Subject: [PATCH 047/120] perf(java): pack Fory JSON dynamic field prefixes --- .../fory/json/codegen/JsonWriterCodegen.java | 33 +++++++++++++++++++ .../fory/json/writer/Utf8JsonWriter.java | 28 ++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index 74185cf878..29f33d6982 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -426,6 +426,12 @@ private static Expression writeNumberField( } return new Expression.Invoke(writer, writerMethod, prefixRef(utf8, true, id), value); } + if (utf8 && canPackSinglePrefix(property, false) && canPackSinglePrefix(property, true)) { + return new Expression.ListExpression( + new Expression.Invoke( + writer, writerMethod, packedDynamicPrefixArgs(property, index, value)), + increment(index)); + } Expression.ListExpression expressions = new Expression.ListExpression( new Expression.Invoke( @@ -454,6 +460,14 @@ private static Expression writeStringField( } return new Expression.Invoke(writer, "writeStringField", prefixRef(utf8, true, id), value); } + if (utf8 && canPackSinglePrefix(property, false) && canPackSinglePrefix(property, true)) { + Expression.ListExpression expressions = + new Expression.ListExpression( + new Expression.Invoke( + writer, "writeStringField", packedDynamicPrefixArgs(property, index, value))); + expressions.add(increment(index)); + return expressions; + } Expression.ListExpression expressions = new Expression.ListExpression( new Expression.Invoke( @@ -586,11 +600,30 @@ private static Expression[] packedPrefixArgs( return args; } + private static Expression[] packedDynamicPrefixArgs( + JsonFieldInfo property, Expression index, Expression... extraArgs) { + byte[] namePrefix = property.utf8NamePrefix(); + byte[] commaPrefix = property.utf8CommaNamePrefix(); + Expression[] args = new Expression[5 + extraArgs.length]; + args[0] = Expression.Literal.ofLong(packedPrefixWord(namePrefix, 0)); + args[1] = Expression.Literal.ofLong(packedPrefixWord(commaPrefix, 0)); + args[2] = Expression.Literal.ofInt(namePrefix.length); + args[3] = Expression.Literal.ofInt(commaPrefix.length); + args[4] = index; + System.arraycopy(extraArgs, 0, args, 5, extraArgs.length); + return args; + } + private static boolean canPackPrefix(JsonFieldInfo property, boolean comma) { int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length; return length <= Long.BYTES * 2; } + private static boolean canPackSinglePrefix(JsonFieldInfo property, boolean comma) { + int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length; + return length <= Long.BYTES; + } + private static long packedPrefixWord(byte[] prefix, int offset) { long word = 0; int end = Math.min(prefix.length, offset + Long.BYTES); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index de6b8aff24..279e14a68c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -333,6 +333,20 @@ public void writeIntField(byte[] namePrefix, byte[] commaNamePrefix, int index, writeIntField(prefix, value); } + public void writeIntField( + long namePrefix, + long commaPrefix, + int namePrefixLength, + int commaPrefixLength, + int index, + int value) { + if (index == 0) { + writeIntField(namePrefix, 0L, namePrefixLength, value); + } else { + writeIntField(commaPrefix, 0L, commaPrefixLength, value); + } + } + public void writeIntField(byte[] prefix, int value) { ensure(prefix.length + 11); writeRawNoEnsure(prefix); @@ -395,6 +409,20 @@ public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int inde writeStringField(prefix, value); } + public void writeStringField( + long namePrefix, + long commaPrefix, + int namePrefixLength, + int commaPrefixLength, + int index, + String value) { + if (index == 0) { + writeStringField(namePrefix, 0L, namePrefixLength, value); + } else { + writeStringField(commaPrefix, 0L, commaPrefixLength, value); + } + } + public void writeStringField(byte[] prefix, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); From ca44b51586b709db8e2aa8ac652a9ee7888da36e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 14:43:32 +0800 Subject: [PATCH 048/120] perf(json): generate codecs in user package --- .../java/org/apache/fory/json/ForyJson.java | 16 +-- .../org/apache/fory/json/ForyJsonBuilder.java | 3 +- .../java/org/apache/fory/json/JsonConfig.java | 105 ++++++++++++++++++ .../apache/fory/json/codegen/JsonCodegen.java | 93 ++++++++++++---- .../codegen/JsonGeneratedCodecBuilder.java | 3 +- .../fory/json/resolver/CodecRegistry.java | 17 +++ .../json/resolver/JsonSharedRegistry.java | 16 +-- .../fory/json/JsonGeneratedCodecTest.java | 67 +++++++++++ 8 files changed, 277 insertions(+), 43 deletions(-) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index a5a0b43bad..449cd15a18 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -28,7 +28,6 @@ import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf16JsonReader; import org.apache.fory.json.reader.Utf8JsonReader; -import org.apache.fory.json.resolver.CodecRegistry; import org.apache.fory.json.resolver.JsonSharedRegistry; import org.apache.fory.json.resolver.JsonTypeInfo; import org.apache.fory.json.resolver.JsonTypeResolver; @@ -56,17 +55,10 @@ public final class ForyJson { private final AtomicReference primarySlot; private final AtomicReferenceArray slots; - ForyJson( - boolean writeNullFields, - boolean codegenEnabled, - boolean propertyDiscoveryEnabled, - int maxDepth, - CodecRegistry codecRegistry) { - this.writeNullFields = writeNullFields; - this.maxDepth = maxDepth; - sharedRegistry = - new JsonSharedRegistry( - codegenEnabled, writeNullFields, propertyDiscoveryEnabled, codecRegistry); + ForyJson(JsonConfig config) { + this.writeNullFields = config.writeNullFields(); + this.maxDepth = config.maxDepth(); + sharedRegistry = new JsonSharedRegistry(config); poolSize = DEFAULT_POOL_SIZE; primarySlot = new AtomicReference<>( diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java index b0ed1f58b5..873ccb81b7 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java @@ -67,6 +67,7 @@ public ForyJsonBuilder registerCodec(Class type, JsonCodec codec) { public ForyJson build() { return new ForyJson( - writeNullFields, codegenEnabled, propertyDiscoveryEnabled, maxDepth, codecRegistry); + new JsonConfig( + writeNullFields, codegenEnabled, propertyDiscoveryEnabled, maxDepth, codecRegistry)); } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java new file mode 100644 index 0000000000..3cda718a47 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.json; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.fory.json.resolver.CodecRegistry; + +/** Immutable build-time configuration for one {@link ForyJson} instance. */ +public final class JsonConfig { + private final boolean writeNullFields; + private final boolean codegenEnabled; + private final boolean propertyDiscoveryEnabled; + private final int maxDepth; + private final CodecRegistry codecRegistry; + private final String codecRegistryKey; + private transient int configHash; + + JsonConfig( + boolean writeNullFields, + boolean codegenEnabled, + boolean propertyDiscoveryEnabled, + int maxDepth, + CodecRegistry codecRegistry) { + this.writeNullFields = writeNullFields; + this.codegenEnabled = codegenEnabled; + this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; + this.maxDepth = maxDepth; + this.codecRegistry = codecRegistry; + codecRegistryKey = codecRegistry.codegenKey(); + } + + public boolean writeNullFields() { + return writeNullFields; + } + + public boolean codegenEnabled() { + return codegenEnabled; + } + + public boolean propertyDiscoveryEnabled() { + return propertyDiscoveryEnabled; + } + + public int maxDepth() { + return maxDepth; + } + + public CodecRegistry codecRegistry() { + return codecRegistry; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + JsonConfig that = (JsonConfig) other; + return writeNullFields == that.writeNullFields + && codegenEnabled == that.codegenEnabled + && propertyDiscoveryEnabled == that.propertyDiscoveryEnabled + && maxDepth == that.maxDepth + && Objects.equals(codecRegistryKey, that.codecRegistryKey); + } + + @Override + public int hashCode() { + return Objects.hash( + writeNullFields, codegenEnabled, propertyDiscoveryEnabled, maxDepth, codecRegistryKey); + } + + private static final AtomicInteger COUNTER = new AtomicInteger(0); + + // Equal configs share one map entry, following core Config's generated-code naming model. + private static final ConcurrentMap CONFIG_ID_MAP = new ConcurrentHashMap<>(); + + public int getConfigHash() { + if (configHash == 0) { + configHash = CONFIG_ID_MAP.computeIfAbsent(this, key -> COUNTER.incrementAndGet()); + } + return configHash; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index ed79b7ce60..95fbb65755 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -25,7 +25,7 @@ import java.lang.reflect.Modifier; import java.util.Collection; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.ConcurrentHashMap; import org.apache.fory.codegen.CodeGenerator; import org.apache.fory.codegen.CompileUnit; import org.apache.fory.json.ForyJsonException; @@ -44,19 +44,21 @@ import org.apache.fory.util.record.RecordUtils; public final class JsonCodegen { - static final String PACKAGE = "org.apache.fory.json.codegen"; static final int GENERIC_READER = 0; static final int LATIN1_READER = 1; static final int UTF16_READER = 2; static final int UTF8_READER = 3; - private static final AtomicInteger ID = new AtomicInteger(); + + private static final Map> ID_GENERATOR = new ConcurrentHashMap<>(); final boolean writeNullFields; + private final int configHash; private final CodeGenerator codeGenerator; private final ClassLoader jsonLoader; - public JsonCodegen(boolean writeNullFields) { + public JsonCodegen(boolean writeNullFields, int configHash) { this.writeNullFields = writeNullFields; + this.configHash = configHash; jsonLoader = JsonCodegen.class.getClassLoader(); codeGenerator = new CodeGenerator(jsonLoader); } @@ -79,17 +81,29 @@ boolean record = objectCodec.isRecord(); return null; } } - String className = className(type); + String generatedPackage = CodeGenerator.getPackage(type); JsonCodec[] writeCodecs = writeCodecs(writeProperties); Utf8ObjectWriter utf8Writer = (Utf8ObjectWriter) - compileWriter(className + "_Utf8", type, writeProperties, writeCodecs, true); + compileWriter( + generatedPackage, + className(type, "Utf8"), + type, + writeProperties, + writeCodecs, + true); if (utf8Writer == null) { return null; } StringObjectWriter stringWriter = (StringObjectWriter) - compileWriter(className + "_String", type, writeProperties, writeCodecs, false); + compileWriter( + generatedPackage, + className(type, "String"), + type, + writeProperties, + writeCodecs, + false); if (stringWriter == null) { return null; } @@ -98,7 +112,13 @@ boolean record = objectCodec.isRecord(); ObjectReader reader = (ObjectReader) compileReader( - className + "_Reader", type, readProperties, readCodecs, readObjectCodecs, record); + generatedPackage, + className(type, "Reader"), + type, + readProperties, + readCodecs, + readObjectCodecs, + record); if (reader == null) { return null; } @@ -112,16 +132,18 @@ boolean record = objectCodec.isRecord(); } private Object compileWriter( + String generatedPackage, String className, Class type, JsonFieldInfo[] properties, JsonCodec[] nestedCodecs, boolean utf8) { String code = - new JsonGeneratedCodecBuilder(this, className, type, properties, utf8, true, false) + new JsonGeneratedCodecBuilder( + this, generatedPackage, className, type, properties, utf8, true, false) .genCode(); try { - Class writerClass = compileClass(className, code); + Class writerClass = compileClass(generatedPackage, className, code); Constructor constructor = writerClass.getDeclaredConstructor(JsonFieldInfo[].class, JsonCodec[].class); constructor.setAccessible(true); @@ -132,6 +154,7 @@ private Object compileWriter( } private Object compileReader( + String generatedPackage, String className, Class type, JsonFieldInfo[] properties, @@ -139,10 +162,11 @@ private Object compileReader( BaseObjectCodec[] nestedCodecs, boolean record) { String code = - new JsonGeneratedCodecBuilder(this, className, type, properties, false, false, record) + new JsonGeneratedCodecBuilder( + this, generatedPackage, className, type, properties, false, false, record) .genCode(); try { - Class readerClass = compileClass(className, code); + Class readerClass = compileClass(generatedPackage, className, code); Constructor constructor = readerClass.getDeclaredConstructor( JsonFieldInfo[].class, JsonCodec[].class, BaseObjectCodec[].class); @@ -153,10 +177,11 @@ private Object compileReader( } } - private Class compileClass(String className, String code) throws ClassNotFoundException { - CompileUnit unit = new CompileUnit(PACKAGE, className, code); + private Class compileClass(String generatedPackage, String className, String code) + throws ClassNotFoundException { + CompileUnit unit = new CompileUnit(generatedPackage, className, code); ClassLoader classLoader = codeGenerator.compile(unit); - return classLoader.loadClass(PACKAGE + "." + className); + return classLoader.loadClass(qualifiedClassName(generatedPackage, className)); } private static JsonCodec[] writeCodecs(JsonFieldInfo[] properties) { @@ -336,13 +361,39 @@ static boolean storesReadObjectCodec(Class type, JsonFieldInfo property) { return nestedType != null && nestedType != type; } - private static String className(Class type) { - String name = type.getName().replace('.', '_').replace('$', '_'); - String uniqueId = CodeGenerator.getClassUniqueId(type); - if (uniqueId.isEmpty()) { - uniqueId = String.valueOf(ID.incrementAndGet()); + private String className(Class type, String role) { + String name = simpleClassName(type) + role + "ForyJsonCodec"; + Map subGenerator = + ID_GENERATOR.computeIfAbsent(name, key -> new ConcurrentHashMap<>()); + String key = configHash + "_" + CodeGenerator.getClassUniqueId(type); + Integer id = subGenerator.get(key); + if (id == null) { + synchronized (subGenerator) { + id = subGenerator.computeIfAbsent(key, ignored -> subGenerator.size()); + } + } + return id == 0 ? name : name + id; + } + + private static String simpleClassName(Class type) { + String name = type.getName(); + Package declaringPackage = type.getPackage(); + if (declaringPackage != null) { + String prefix = declaringPackage.getName() + "."; + if (name.startsWith(prefix)) { + name = name.substring(prefix.length()); + } + } else { + int separator = name.lastIndexOf('.'); + if (separator >= 0) { + name = name.substring(separator + 1); + } } - return "JsonWriter_" + name + "_" + uniqueId; + return name.replace('.', '_').replace('$', '_'); + } + + private static String qualifiedClassName(String generatedPackage, String className) { + return generatedPackage.isEmpty() ? className : generatedPackage + "." + className; } private static boolean isPojo(Class type) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java index 33ee0e568a..37c9091769 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java @@ -42,6 +42,7 @@ final class JsonGeneratedCodecBuilder extends CodecBuilder { JsonGeneratedCodecBuilder( JsonCodegen codegen, + String generatedPackage, String generatedClassName, Class type, JsonFieldInfo[] properties, @@ -55,7 +56,7 @@ final class JsonGeneratedCodecBuilder extends CodecBuilder { this.utf8 = utf8; this.writer = writer; this.record = record; - ctx.setPackage(JsonCodegen.PACKAGE); + ctx.setPackage(generatedPackage); ctx.setClassName(generatedClassName); ctx.setClassModifiers("final"); ctx.addImports(JsonFieldInfo.class, JsonCodec.class, JsonTypeResolver.class); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java index 629eae9d3f..e4623c0baa 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java @@ -19,6 +19,9 @@ package org.apache.fory.json.resolver; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -54,4 +57,18 @@ public CodecRegistry copy() { } return new CodecRegistry(copied); } + + public String codegenKey() { + List, JsonCodec>> entries = new ArrayList<>(codecs.entrySet()); + entries.sort(Comparator.comparing(entry -> entry.getKey().getName())); + StringBuilder builder = new StringBuilder(entries.size() * 48); + for (Map.Entry, JsonCodec> entry : entries) { + builder + .append(entry.getKey().getName()) + .append('=') + .append(entry.getValue().getClass().getName()) + .append(';'); + } + return builder.toString(); + } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index 8bdb3c3e26..e8d2b95cf9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -63,6 +63,7 @@ import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Pattern; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.JsonConfig; import org.apache.fory.json.codec.ArrayCodec; import org.apache.fory.json.codec.BaseObjectCodec; import org.apache.fory.json.codec.CodecUtils; @@ -84,15 +85,14 @@ public final class JsonSharedRegistry { private final JsonCodegen codegen; private final boolean propertyDiscoveryEnabled; - public JsonSharedRegistry( - boolean codegenEnabled, - boolean writeNullFields, - boolean propertyDiscoveryEnabled, - CodecRegistry customCodecs) { - this.customCodecs = customCodecs.copy(); - this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; + public JsonSharedRegistry(JsonConfig config) { + this.customCodecs = config.codecRegistry().copy(); + this.propertyDiscoveryEnabled = config.propertyDiscoveryEnabled(); exactCodecs = new IdentityHashMap<>(); - codegen = codegenEnabled ? new JsonCodegen(writeNullFields) : null; + codegen = + config.codegenEnabled() + ? new JsonCodegen(config.writeNullFields(), config.getConfigHash()) + : null; registerExactCodecs(); } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index 45c15e9c97..cf434e72f4 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -21,11 +21,17 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertTrue; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.fory.json.codec.BaseObjectCodec; +import org.apache.fory.json.codec.GeneratedObjectCodec; import org.apache.fory.json.data.GeneratedCollectionFields; +import org.apache.fory.json.data.PublicFields; import org.apache.fory.json.data.RecursiveChild; import org.apache.fory.json.data.RecursiveParent; import org.apache.fory.json.data.TokenGroup; @@ -34,9 +40,12 @@ import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeResolver; import org.testng.annotations.Test; public class JsonGeneratedCodecTest extends ForyJsonTestModels { + private static final String GENERATED_SUFFIX = "ForyJsonCodec"; + @Test public void writeRecursiveGeneratedTypes() { ForyJson json = ForyJson.builder().build(); @@ -126,6 +135,29 @@ public void readGeneratedCollectionFields() { assertGeneratedWhenSupported(json, GeneratedCollectionFields.class); } + @Test + public void sameConfigUsesSameId() throws Exception { + ForyJson first = ForyJson.builder().build(); + ForyJson second = ForyJson.builder().build(); + ForyJson writeNullFields = ForyJson.builder().writeNullFields(true).build(); + first.toJsonBytes(new PublicFields()); + second.toJsonBytes(new PublicFields()); + writeNullFields.toJsonBytes(new PublicFields()); + + Class firstWriterClass = generatedUtf8WriterClass(first, PublicFields.class); + Class secondWriterClass = generatedUtf8WriterClass(second, PublicFields.class); + Class writeNullWriterClass = generatedUtf8WriterClass(writeNullFields, PublicFields.class); + assertEquals( + firstWriterClass.getPackage().getName(), PublicFields.class.getPackage().getName()); + assertEquals( + secondWriterClass.getPackage().getName(), PublicFields.class.getPackage().getName()); + assertGeneratedName(firstWriterClass, PublicFields.class, "Utf8"); + assertGeneratedName(secondWriterClass, PublicFields.class, "Utf8"); + assertGeneratedName(writeNullWriterClass, PublicFields.class, "Utf8"); + assertEquals(generatedId(secondWriterClass), generatedId(firstWriterClass)); + assertNotEquals(generatedId(writeNullWriterClass), generatedId(firstWriterClass)); + } + @Test public void readLongAsciiFieldToken() { String token = "\"favoriteFruit\":"; @@ -244,4 +276,39 @@ public static class WideFields { public int f12; public String f13; } + + private static Class generatedUtf8WriterClass(ForyJson json, Class type) throws Exception { + Field primarySlotField = ForyJson.class.getDeclaredField("primarySlot"); + primarySlotField.setAccessible(true); + AtomicReference primarySlot = (AtomicReference) primarySlotField.get(json); + Object pooledState = primarySlot.get(); + Field stateField = pooledState.getClass().getDeclaredField("state"); + stateField.setAccessible(true); + Object state = stateField.get(pooledState); + Field typeResolverField = state.getClass().getDeclaredField("typeResolver"); + typeResolverField.setAccessible(true); + JsonTypeResolver typeResolver = (JsonTypeResolver) typeResolverField.get(state); + BaseObjectCodec codec = typeResolver.getObjectCodec(type); + assertTrue(codec instanceof GeneratedObjectCodec); + Field utf8WriterField = GeneratedObjectCodec.class.getDeclaredField("utf8Writer"); + utf8WriterField.setAccessible(true); + return utf8WriterField.get(codec).getClass(); + } + + private static void assertGeneratedName( + Class generatedClass, Class valueType, String role) { + String simpleName = generatedClass.getSimpleName(); + assertTrue(simpleName.startsWith(valueType.getSimpleName()), generatedClass.getName()); + assertTrue(simpleName.contains(role + GENERATED_SUFFIX), generatedClass.getName()); + assertFalse(simpleName.contains(GENERATED_SUFFIX + "_"), generatedClass.getName()); + assertTrue(generatedId(generatedClass) >= 0, generatedClass.getName()); + } + + private static int generatedId(Class generatedClass) { + String simpleName = generatedClass.getSimpleName(); + int suffixStart = simpleName.lastIndexOf(GENERATED_SUFFIX); + assertTrue(suffixStart >= 0, generatedClass.getName()); + String id = simpleName.substring(suffixStart + GENERATED_SUFFIX.length()); + return id.isEmpty() ? 0 : Integer.parseInt(id); + } } From d1553882555eca8ffbe9b8d26c5afed5770d0a69 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 17:31:34 +0800 Subject: [PATCH 049/120] perf(java): remove unused json generated metadata --- .../org/apache/fory/codegen/Expression.java | 66 +++++++----------- .../codegen/JsonGeneratedCodecBuilder.java | 6 +- .../fory/json/codegen/JsonReaderCodegen.java | 67 ++++++++++++------- .../fory/json/codegen/JsonWriterCodegen.java | 53 ++++++++------- .../json/writer/GeneratedObjectWriter.java | 34 ---------- 5 files changed, 94 insertions(+), 132 deletions(-) delete mode 100644 java/fory-json/src/main/java/org/apache/fory/json/writer/GeneratedObjectWriter.java diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java index 185fb70c07..ce43201dd3 100644 --- a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java @@ -1597,7 +1597,7 @@ public String toString() { } } - class NewArray extends AbstractExpression { + class NewArray extends Inlineable { private TypeRef type; private Expression[] elements; @@ -1662,59 +1662,35 @@ public ExprCode doGenCode(CodegenContext ctx) { Class rawType = getRawType(type); String arrayType = getArrayType(rawType); String value = ctx.newName("arr"); + String arrayValue; if (dims != null) { // multi-dimension array ExprCode dimsExprCode = dims.genCode(ctx); if (StringUtils.isNotBlank(dimsExprCode.code())) { codeBuilder.append(dimsExprCode.code()).append('\n'); } - // "${arrType} ${value} = new ${elementType}[$?][$?]... - codeBuilder - .append(arrayType) - .append(' ') - .append(value) - .append(" = new ") - .append(ctx.type(elemType)); + // "new ${elementType}[$?][$?]..." + StringBuilder arrayValueBuilder = new StringBuilder("new ").append(ctx.type(elemType)); for (int i = 0; i < numDimensions; i++) { // dims is dimensions array, which store size of per dim. String idim = StringUtils.format("${dims}[${i}]", "dims", dimsExprCode.value(), "i", i); - codeBuilder.append('[').append(idim).append("]"); + arrayValueBuilder.append('[').append(idim).append("]"); } - codeBuilder.append(';'); + arrayValue = arrayValueBuilder.toString(); } else if (dim != null) { ExprCode dimExprCode = dim.genCode(ctx); if (StringUtils.isNotBlank(dimExprCode.code())) { codeBuilder.append(dimExprCode.code()).append('\n'); } + StringBuilder arrayValueBuilder = new StringBuilder("new ").append(ctx.type(elemType)); + arrayValueBuilder.append('[').append(dimExprCode.value()).append(']'); if (numDimensions > 1) { // multi-dimension array - // "${arrType} ${value} = new ${elementType}[$?][][][]... - codeBuilder - .append(arrayType) - .append(' ') - .append(value) - .append(" = new ") - .append(ctx.type(elemType)); - codeBuilder.append('[').append(dimExprCode.value()).append(']'); for (int i = 1; i < numDimensions; i++) { - codeBuilder.append('[').append("]"); + arrayValueBuilder.append('[').append("]"); } - codeBuilder.append(';'); - } else { - // one-dimension array - String code = - StringUtils.format( - "${type} ${value} = new ${elemType}[${dim}];", - "type", - arrayType, - "elemType", - ctx.type(elemType), - "value", - value, - "dim", - dimExprCode.value()); - codeBuilder.append(code); } + arrayValue = arrayValueBuilder.toString(); } else { // create array with init value int len = elements.length; @@ -1733,18 +1709,22 @@ public ExprCode doGenCode(CodegenContext ctx) { } } - String code = + arrayValue = StringUtils.format( - "${type} ${value} = new ${type} {${args}};", - "type", - arrayType, - "value", - value, - "args", - argsBuilder.toString()); - codeBuilder.append(code); + "new ${type} {${args}}", "type", arrayType, "args", argsBuilder.toString()); } + if (inlineCall) { + String code = StringUtils.isBlank(codeBuilder) ? null : codeBuilder.toString(); + return new ExprCode(code, null, Code.variable(rawType, arrayValue)); + } + codeBuilder + .append(arrayType) + .append(' ') + .append(value) + .append(" = ") + .append(arrayValue) + .append(';'); return new ExprCode(codeBuilder.toString(), null, Code.variable(rawType, value)); } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java index 37c9091769..88a66cc2c4 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java @@ -82,10 +82,8 @@ public String codecClassName(Class cls) { @Override public String genCode() { return writer - ? new JsonWriterCodegen(codegen) - .genWriterCode(this, generatedClassName, beanClass, properties, utf8) - : new JsonReaderCodegen(codegen) - .genReaderCode(this, generatedClassName, beanClass, properties, record); + ? new JsonWriterCodegen(codegen).genWriterCode(this, beanClass, properties, utf8) + : new JsonReaderCodegen(codegen).genReaderCode(this, beanClass, properties, record); } @Override diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index d1cae5ad1d..89fb6384f6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -19,6 +19,8 @@ package org.apache.fory.json.codegen; +import static org.apache.fory.codegen.ExpressionUtils.add; + import org.apache.fory.codegen.Code; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.Expression; @@ -67,7 +69,6 @@ private static Class readNestedType(JsonFieldInfo property) { String genReaderCode( JsonGeneratedCodecBuilder builder, - String className, Class type, JsonFieldInfo[] properties, boolean record) { @@ -86,7 +87,9 @@ String genReaderCode( ctx.type(Utf8ObjectReader.class)); ctx.addField(long[].class, "fieldHashes"); for (int i = 0; i < properties.length; i++) { - ctx.addField(JsonFieldInfo.class, "p" + i); + if (usesReadInfo(properties[i])) { + ctx.addField(JsonFieldInfo.class, "p" + i); + } if (usesReadCodec(properties[i])) { ctx.addField(codecFieldType(properties[i].readTypeInfo().codec()), "r" + i); } @@ -384,18 +387,23 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr new Expression.Assign( hashes, new Expression.NewArray( - long.class, - new Expression.FieldValue( - propertiesRef, "length", TypeRef.of(int.class), false, true)))); + long.class, + new Expression.FieldValue( + propertiesRef, "length", TypeRef.of(int.class), false, true)) + .inline())); for (int i = 0; i < properties.length; i++) { Expression id = Expression.Literal.ofInt(i); Expression property = new Expression.ArrayValue(propertiesRef, id); - expressions.add( - new Expression.Assign( - new Reference("this.p" + i, TypeRef.of(JsonFieldInfo.class)), property)); + if (usesReadInfo(properties[i])) { + expressions.add( + new Expression.Assign( + new Reference("this.p" + i, TypeRef.of(JsonFieldInfo.class)), property)); + } expressions.add( new Expression.AssignArrayElem( - hashes, new Expression.Invoke(property, "nameHash", TypeRef.of(long.class)), id)); + hashes, + new Expression.Invoke(property, "nameHash", TypeRef.of(long.class)).inline(), + id)); if (usesReadCodec(properties[i])) { Class codecType = codecFieldType(properties[i].readTypeInfo().codec()); expressions.add( @@ -408,7 +416,8 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr expressions.add( new Expression.Assign( new Reference("this.t" + i, TypeRef.of(JsonTypeInfo.class)), - new Expression.Invoke(property, "readTypeInfo", TypeRef.of(JsonTypeInfo.class)))); + new Expression.Invoke(property, "readTypeInfo", TypeRef.of(JsonTypeInfo.class)) + .inline())); } if (storesReadObjectCodec(type, properties[i])) { expressions.add( @@ -1150,8 +1159,7 @@ private Expression fieldSwitchRange( private Expression updateExpectedIndex(Expression expectedIndex, Expression fieldIndex) { return new Expression.If( ge(fieldIndex, Expression.Literal.ofInt(0)), - new Expression.Assign( - expectedIndex, new Expression.Add(fieldIndex, Expression.Literal.ofInt(1)))); + new Expression.Assign(expectedIndex, add(fieldIndex, Expression.Literal.ofInt(1)))); } private Expression fieldIndexValue( @@ -1491,19 +1499,6 @@ private static Expression not(Expression expression) { return new Expression.Not(expression); } - private static boolean usesWriteCodec(JsonFieldInfo property) { - switch (property.writeKind()) { - case ARRAY: - case MAP: - case OBJECT: - return true; - case COLLECTION: - return property.writeElementRawType() != String.class; - default: - return false; - } - } - private static boolean usesReadCodec(JsonFieldInfo property) { switch (property.readKind()) { case ENUM: @@ -1531,6 +1526,28 @@ private static boolean usesReadTypeField(JsonFieldInfo property) { } } + private static boolean usesReadInfo(JsonFieldInfo property) { + switch (property.readKind()) { + case BOOLEAN: + case INT: + case LONG: + case DOUBLE: + case STRING: + case ENUM: + case COLLECTION: + case ARRAY: + case MAP: + case OBJECT: + return false; + case BYTE: + case SHORT: + case FLOAT: + case CHAR: + default: + return true; + } + } + private static boolean usesReadObjectCodec(JsonFieldInfo property) { return property.readKind() == JsonFieldKind.OBJECT && property.readRawType() != Object.class diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index 29f33d6982..3797c7bd3f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -19,6 +19,10 @@ package org.apache.fory.json.codegen; +import static org.apache.fory.codegen.ExpressionUtils.add; +import static org.apache.fory.codegen.ExpressionUtils.cast; +import static org.apache.fory.codegen.ExpressionUtils.inline; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; @@ -36,7 +40,6 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; import org.apache.fory.json.resolver.JsonTypeResolver; -import org.apache.fory.json.writer.GeneratedObjectWriter; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.StringObjectWriter; import org.apache.fory.json.writer.Utf8JsonWriter; @@ -105,29 +108,20 @@ private static void addGeneratedMethod( } String genWriterCode( - JsonGeneratedCodecBuilder builder, - String className, - Class type, - JsonFieldInfo[] properties, - boolean utf8) { + JsonGeneratedCodecBuilder builder, Class type, JsonFieldInfo[] properties, boolean utf8) { CodegenContext ctx = builder.context(); ctx.addImports(StringJsonWriter.class, Utf8JsonWriter.class); - ctx.extendsClasses(ctx.type(GeneratedObjectWriter.class)); ctx.implementsInterfaces(ctx.type(utf8 ? Utf8ObjectWriter.class : StringObjectWriter.class)); boolean objectStartFused = canFuseObjectStart(properties); - boolean[] useInfo = new boolean[properties.length]; - boolean[] usePrefix = new boolean[properties.length]; for (int i = 0; i < properties.length; i++) { JsonFieldInfo property = properties[i]; - useInfo[i] = true; - usePrefix[i] = usesPrefix(property); - if (useInfo[i]) { + if (usesWriteInfo(property)) { ctx.addField(JsonFieldInfo.class, "p" + i); } if (usesWriteCodec(property)) { ctx.addField(codecFieldType(property.writeTypeInfo().codec()), "c" + i); } - if (usePrefix[i]) { + if (usesPrefix(property)) { if (utf8) { ctx.addField(byte[].class, "u" + i); ctx.addField(byte[].class, "uc" + i); @@ -165,17 +159,23 @@ private boolean usesPrefix(JsonFieldInfo property) { || writeNullFields && !property.writeRawType().isPrimitive(); } + private static boolean usesWriteInfo(JsonFieldInfo property) { + JsonFieldKind kind = property.writeKind(); + return kind == JsonFieldKind.BOOLEAN || kind == JsonFieldKind.ENUM; + } + private Expression writerConstructorExpression(JsonFieldInfo[] properties, boolean utf8) { Expression.ListExpression expressions = new Expression.ListExpression(); Reference propertiesRef = new Reference("properties", TypeRef.of(JsonFieldInfo[].class)); Reference codecsRef = new Reference("codecs", TypeRef.of(JsonCodec[].class)); - expressions.add(new Expression.SuperCall(propertiesRef, codecsRef)); for (int i = 0; i < properties.length; i++) { Expression id = Expression.Literal.ofInt(i); Expression property = new Expression.ArrayValue(propertiesRef, id); - expressions.add( - new Expression.Assign( - new Reference("this.p" + i, TypeRef.of(JsonFieldInfo.class)), property)); + if (usesWriteInfo(properties[i])) { + expressions.add( + new Expression.Assign( + new Reference("this.p" + i, TypeRef.of(JsonFieldInfo.class)), property)); + } if (usesWriteCodec(properties[i])) { Class codecType = codecFieldType(properties[i].writeTypeInfo().codec()); expressions.add( @@ -189,22 +189,24 @@ private Expression writerConstructorExpression(JsonFieldInfo[] properties, boole expressions.add( new Expression.Assign( new Reference("this.u" + i, TypeRef.of(byte[].class)), - new Expression.Invoke(property, "utf8NamePrefix", TypeRef.of(byte[].class)))); + new Expression.Invoke(property, "utf8NamePrefix", TypeRef.of(byte[].class)) + .inline())); expressions.add( new Expression.Assign( new Reference("this.uc" + i, TypeRef.of(byte[].class)), - new Expression.Invoke( - property, "utf8CommaNamePrefix", TypeRef.of(byte[].class)))); + new Expression.Invoke(property, "utf8CommaNamePrefix", TypeRef.of(byte[].class)) + .inline())); } else { expressions.add( new Expression.Assign( new Reference("this.s" + i, TypeRef.of(byte[].class)), - new Expression.Invoke(property, "stringNamePrefix", TypeRef.of(byte[].class)))); + new Expression.Invoke(property, "stringNamePrefix", TypeRef.of(byte[].class)) + .inline())); expressions.add( new Expression.Assign( new Reference("this.sc" + i, TypeRef.of(byte[].class)), - new Expression.Invoke( - property, "stringCommaNamePrefix", TypeRef.of(byte[].class)))); + new Expression.Invoke(property, "stringCommaNamePrefix", TypeRef.of(byte[].class)) + .inline())); } } } @@ -354,8 +356,7 @@ private Expression writeProp( } Expression value = new Expression.Variable( - "v" + id, - new Expression.Cast(builder.fieldValue(property, object), TypeRef.of(rawType))); + "v" + id, cast(inline(builder.fieldValue(property, object)), TypeRef.of(rawType))); Expression nullValue = new Expression.Null(TypeRef.of(rawType), false); if (writeNullFields) { if (isPrefixValue(property.writeKind())) { @@ -768,7 +769,7 @@ private static Expression commaFlag(boolean commaKnown, Expression index) { } private static Expression increment(Expression value) { - return new Expression.Assign(value, new Expression.Add(value, Expression.Literal.ofInt(1))); + return new Expression.Assign(value, add(value, Expression.Literal.ofInt(1))); } private static boolean isPrefixValue(JsonFieldKind kind) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/GeneratedObjectWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/GeneratedObjectWriter.java deleted file mode 100644 index 130ae08812..0000000000 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/GeneratedObjectWriter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.fory.json.writer; - -import org.apache.fory.json.codec.JsonCodec; -import org.apache.fory.json.meta.JsonFieldInfo; - -/** Immutable metadata carrier shared by generated JSON object writers. */ -public abstract class GeneratedObjectWriter { - protected final JsonFieldInfo[] fields; - protected final JsonCodec[] codecs; - - protected GeneratedObjectWriter(JsonFieldInfo[] fields, JsonCodec[] codecs) { - this.fields = fields; - this.codecs = codecs; - } -} From a26f95f2361bbafcf849dd2fc1eb5410869b1299 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 18:44:44 +0800 Subject: [PATCH 050/120] perf(java): bulk copy long JSON strings --- .../fory/json/writer/Utf8JsonWriter.java | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 279e14a68c..1c5e0e5db1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -623,57 +623,56 @@ private boolean writeLatin1StringNoEnsure(byte[] value, int length) { private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { byte[] bytes = buffer; int start = position; + if (!isJsonAsciiBytes(value, length)) { + return false; + } int pos = start; bytes[pos++] = (byte) '"'; + System.arraycopy(value, 0, bytes, pos, length); + pos += length; + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + + private static boolean isJsonAsciiBytes(byte[] value, int length) { int i = 0; int upperBound = length & ~15; for (; i < upperBound; i += 16) { long word0 = LittleEndian.getInt64(value, i); long word1 = LittleEndian.getInt64(value, i + 8); if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { - break; + return false; } - LittleEndian.putInt64(bytes, pos, word0); - LittleEndian.putInt64(bytes, pos + 8, word1); - pos += 16; } upperBound = length & ~7; for (; i < upperBound; i += 8) { long word = LittleEndian.getInt64(value, i); if (!isJsonAsciiWord(word)) { - break; + return false; } - LittleEndian.putInt64(bytes, pos, word); - pos += 8; } if (i + 4 <= length) { int word = LittleEndian.getInt32(value, i); if (isJsonAsciiInt(word)) { - LittleEndian.putInt32(bytes, pos, word); - pos += 4; i += 4; + } else { + return false; } } if (i + 2 <= length) { int word = (value[i] & 0xFF) | ((value[i + 1] & 0xFF) << 8); if (isJsonAsciiShort(word)) { - bytes[pos] = (byte) word; - bytes[pos + 1] = (byte) (word >>> 8); - pos += 2; i += 2; + } else { + return false; } } for (; i < length; i++) { - byte ch = value[i]; - if (isJsonAsciiByte(ch)) { - bytes[pos++] = ch; - } else { - position = start; + if (!isJsonAsciiByte(value[i])) { return false; } } - bytes[pos++] = (byte) '"'; - position = pos; return true; } From 5c0152d2a2f75dd7b9ddf9af25bdfe2cf6f6d41b Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 3 Jul 2026 22:50:41 +0800 Subject: [PATCH 051/120] perf(java): split JSON string element comma path --- .../fory/json/writer/Utf8JsonWriter.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 1c5e0e5db1..246a07376f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -485,13 +485,17 @@ public void writeStringCollection(Collection values) { writeArrayStart(); if (values.getClass() == ArrayList.class) { ArrayList list = (ArrayList) values; - for (int i = 0, size = list.size(); i < size; i++) { - writeStringElement(i, list.get(i)); + int size = list.size(); + if (size != 0) { + writeStringElementWithComma(0, list.get(0)); + for (int i = 1; i < size; i++) { + writeStringElementWithComma(1, list.get(i)); + } } } else { int index = 0; for (String value : values) { - writeStringElement(index++, value); + writeStringElementWithComma(index++ == 0 ? 0 : 1, value); } } writeArrayEnd(); @@ -499,8 +503,12 @@ public void writeStringCollection(Collection values) { public void writeStringArray(String[] values) { writeArrayStart(); - for (int i = 0; i < values.length; i++) { - writeStringElement(i, values[i]); + int length = values.length; + if (length != 0) { + writeStringElementWithComma(0, values[0]); + for (int i = 1; i < length; i++) { + writeStringElementWithComma(1, values[i]); + } } writeArrayEnd(); } @@ -519,7 +527,10 @@ public void writeLongArray(long[] values) { } public void writeStringElement(int index, String value) { - int comma = index == 0 ? 0 : 1; + writeStringElementWithComma(index == 0 ? 0 : 1, value); + } + + private void writeStringElementWithComma(int comma, String value) { if (value == null) { writeNullStringElement(comma); return; From ef949cb412f291dcf5ff7c24b38db9fd14008652 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 4 Jul 2026 01:54:56 +0800 Subject: [PATCH 052/120] perf(java): unroll JSON exact array writes --- .../fory/json/writer/Utf8JsonWriter.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 246a07376f..956648acd0 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -505,9 +505,15 @@ public void writeStringArray(String[] values) { writeArrayStart(); int length = values.length; if (length != 0) { + int i = 1; writeStringElementWithComma(0, values[0]); - for (int i = 1; i < length; i++) { + if ((length & 1) == 0) { writeStringElementWithComma(1, values[i]); + i++; + } + for (; i < length; i += 2) { + writeStringElementWithComma(1, values[i]); + writeStringElementWithComma(1, values[i + 1]); } } writeArrayEnd(); @@ -516,12 +522,24 @@ public void writeStringArray(String[] values) { public void writeLongArray(long[] values) { ensure(2); buffer[position++] = '['; - for (int i = 0; i < values.length; i++) { + int length = values.length; + if (length != 0) { ensure(22); - if (i != 0) { + writeLongNoEnsure(values[0]); + int i = 1; + if ((length & 1) == 0) { + ensure(22); + buffer[position++] = ','; + writeLongNoEnsure(values[i]); + i++; + } + for (; i < length; i += 2) { + ensure(44); + buffer[position++] = ','; + writeLongNoEnsure(values[i]); buffer[position++] = ','; + writeLongNoEnsure(values[i + 1]); } - writeLongNoEnsure(values[i]); } buffer[position++] = ']'; } From 73221a1d5dde6c3cf5f1bf39a4c1affd28bb0fc9 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 4 Jul 2026 17:02:49 +0800 Subject: [PATCH 053/120] perf(java): inline JSON reader group checks --- .../fory/json/codegen/JsonReaderCodegen.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 89fb6384f6..710439c28e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -20,6 +20,7 @@ package org.apache.fory.json.codegen; import static org.apache.fory.codegen.ExpressionUtils.add; +import static org.apache.fory.codegen.ExpressionUtils.inline; import org.apache.fory.codegen.Code; import org.apache.fory.codegen.CodegenContext; @@ -516,18 +517,19 @@ private Expression splitFastReadExpression( for (int start = 0; start < properties.length; ) { int end = readGroupEnd(properties, start); Expression groupCall = - new Expression.Invoke( - new Reference("this", TypeRef.of(Object.class)), - readGroupMethod(readMethod, start), - "", - TypeRef.of(boolean.class), - false, - false, - readerRef(readerMode), - ownerRef(), - typeResolverRef(), - object, - hashes); + inline( + new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + readGroupMethod(readMethod, start), + "", + TypeRef.of(boolean.class), + false, + false, + readerRef(readerMode), + ownerRef(), + typeResolverRef(), + object, + hashes)); expressions.add(new Expression.If(not(groupCall), returnObject(object, record))); start = end; } From 97a8a707ef69b7c60491646d0e207b598ba0ffe2 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 4 Jul 2026 17:24:23 +0800 Subject: [PATCH 054/120] perf(java): inline JSON reader codec values --- .../fory/json/codegen/JsonReaderCodegen.java | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 710439c28e..5721fee7a1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -1895,14 +1895,15 @@ private static Expression readEnumValue( private static Expression readEnumValue( Class enumType, int id, int readerMode, boolean tokenValueRead, boolean hashFallback) { return new Expression.Cast( - new Expression.Invoke( - fieldRef("r" + id, JsonCodec.class), - readEnumMethod(readerMode, tokenValueRead, hashFallback), - "", - TypeRef.of(Object.class), - false, - false, - readerRef(readerMode)), + inline( + new Expression.Invoke( + fieldRef("r" + id, JsonCodec.class), + readEnumMethod(readerMode, tokenValueRead, hashFallback), + "", + TypeRef.of(Object.class), + false, + false, + readerRef(readerMode))), TypeRef.of(enumType)); } @@ -1911,27 +1912,29 @@ private static Expression readResolvedValue(JsonFieldInfo property, int id, int // nullable reference results directly. Requesting expression null-state here only emits dead // boolean locals around codec calls and bloats hot generated reader methods. return new Expression.Cast( - new Expression.Invoke( - fieldRef("r" + id, JsonCodec.class), - readObjectMethod(readerMode), - TypeRef.of(Object.class), - false, - readerRef(readerMode), - fieldRef("t" + id, JsonTypeInfo.class), - typeResolverRef()), + inline( + new Expression.Invoke( + fieldRef("r" + id, JsonCodec.class), + readObjectMethod(readerMode), + TypeRef.of(Object.class), + false, + readerRef(readerMode), + fieldRef("t" + id, JsonTypeInfo.class), + typeResolverRef())), TypeRef.of(property.readRawType())); } private static Expression readCollectionValue(JsonFieldInfo property, int id, int readerMode) { return new Expression.Cast( - new Expression.Invoke( - fieldRef("r" + id, CollectionCodec.class), - readObjectNonNullMethod(readerMode), - TypeRef.of(Object.class), - false, - readerRef(readerMode), - fieldRef("t" + id, JsonTypeInfo.class), - typeResolverRef()), + inline( + new Expression.Invoke( + fieldRef("r" + id, CollectionCodec.class), + readObjectNonNullMethod(readerMode), + TypeRef.of(Object.class), + false, + readerRef(readerMode), + fieldRef("t" + id, JsonTypeInfo.class), + typeResolverRef())), TypeRef.of(property.readRawType())); } @@ -1940,14 +1943,15 @@ private static Expression readObjectValue( Expression codec = property.readRawType() == type ? ownerRef() : fieldRef("c" + id, BaseObjectCodec.class); return new Expression.Cast( - new Expression.Invoke( - codec, - readObjectNonNullMethod(readerMode), - TypeRef.of(Object.class), - false, - readerRef(readerMode), - fieldRef("t" + id, JsonTypeInfo.class), - typeResolverRef()), + inline( + new Expression.Invoke( + codec, + readObjectNonNullMethod(readerMode), + TypeRef.of(Object.class), + false, + readerRef(readerMode), + fieldRef("t" + id, JsonTypeInfo.class), + typeResolverRef())), TypeRef.of(property.readRawType())); } From 5bdd7c4d67e3406fd969bcad456c65d37751be3c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 4 Jul 2026 18:12:05 +0800 Subject: [PATCH 055/120] perf(java): trim Fory JSON UTF8 whitespace checks --- .../fory/json/reader/Utf8JsonReader.java | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 7e9784324a..dd6174f05b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -120,9 +120,6 @@ public boolean consumeNextCommaOrEndObject() { position++; return false; } - if (!isWhitespace(ch)) { - return consumeNextCommaOrEndObjectSlow(); - } } return consumeNextCommaOrEndObjectSlow(); } @@ -154,9 +151,6 @@ public boolean consumeNextCommaOrEndArray() { position++; return false; } - if (!isWhitespace(ch)) { - return consumeNextCommaOrEndArraySlow(); - } } return consumeNextCommaOrEndArraySlow(); } @@ -188,7 +182,7 @@ public boolean tryReadNextNullToken() { if (ch == 'n') { return tryReadNullLiteral(); } - if (!isWhitespace(ch)) { + if (ch > ' ' || !isWhitespace(ch)) { return false; } } @@ -209,8 +203,11 @@ public boolean readBooleanValue() { } public boolean readNextBooleanValue() { - if (position < input.length && !isWhitespace(input[position])) { - return readBooleanToken(); + if (position < input.length) { + int ch = input[position]; + if (ch > ' ' || !isWhitespace(ch)) { + return readBooleanToken(); + } } return readBooleanValue(); } @@ -236,8 +233,11 @@ public int readIntValue() { } public int readNextIntValue() { - if (position < input.length && !isWhitespace(input[position])) { - return readIntToken(); + if (position < input.length) { + int ch = input[position]; + if (ch > ' ' || !isWhitespace(ch)) { + return readIntToken(); + } } return readIntValue(); } @@ -354,8 +354,11 @@ public long readLongValue() { } public long readNextLongValue() { - if (position < input.length && !isWhitespace(input[position])) { - return readLongToken(); + if (position < input.length) { + int ch = input[position]; + if (ch > ' ' || !isWhitespace(ch)) { + return readLongToken(); + } } return readLongValue(); } @@ -387,8 +390,11 @@ public double readDouble() { } public double readNextDoubleValue() { - if (position < input.length && !isWhitespace(input[position])) { - return readDoubleToken(); + if (position < input.length) { + int ch = input[position]; + if (ch > ' ' || !isWhitespace(ch)) { + return readDoubleToken(); + } } return readDouble(); } @@ -1033,7 +1039,7 @@ public String readNextNullableString() { if (ch == 'n' && tryReadNullLiteral()) { return null; } - if (!isWhitespace(ch)) { + if (ch > ' ' || !isWhitespace(ch)) { return readStringToken(); } } @@ -1393,7 +1399,7 @@ public boolean tryReadNextFieldNameColon( if (ch == '"') { return tryReadFieldNameColonAt(mark, expectedHash, expectedMask, expectedLength); } - if (!isWhitespace(ch)) { + if (ch > ' ' || !isWhitespace(ch)) { return false; } } @@ -1585,8 +1591,11 @@ public long readPackedStringHash() { } public long readNextPackedStringHash() { - if (position < input.length && !isWhitespace(input[position])) { - return readPackedStringHashToken(); + if (position < input.length) { + int ch = input[position]; + if (ch > ' ' || !isWhitespace(ch)) { + return readPackedStringHashToken(); + } } return readPackedStringHash(); } From deb766df861cccc6b12d04cb2285202621f466e5 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 4 Jul 2026 20:16:30 +0800 Subject: [PATCH 056/120] perf(java): widen Fory JSON UTF8 string scan --- .../fory/json/reader/Utf8JsonReader.java | 26 +++++++++++++++++++ .../org/apache/fory/json/JsonStringTest.java | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index dd6174f05b..5a0324485f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -1089,6 +1089,32 @@ private String readStringToken() { } int start = position; int offset = start; + // The unroll only reduces scanner loop backedges; stop bytes still use the shared slow path. + int doubleWordEnd = inputLength - (Long.BYTES << 1); + while (offset <= doubleWordEnd) { + long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); + int b = bytes[stop]; + if (b == '"') { + position = stop + 1; + return newLatin1String(start, stop); + } + return readStringStop(start, stop, b); + } + int nextOffset = offset + Long.BYTES; + stopMask = stringStopMask(LittleEndian.getInt64(bytes, nextOffset)); + if (stopMask != 0) { + int stop = nextOffset + (Long.numberOfTrailingZeros(stopMask) >>> 3); + int b = bytes[stop]; + if (b == '"') { + position = stop + 1; + return newLatin1String(start, stop); + } + return readStringStop(start, stop, b); + } + offset = nextOffset + Long.BYTES; + } int wordEnd = inputLength - Long.BYTES; while (offset <= wordEnd) { long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index f4bf6deb96..a4b43df2cb 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -242,7 +242,7 @@ public void writeStringScanBoundaries() { @Test public void readStringScanBoundaries() { ForyJson json = ForyJson.builder().build(); - for (int length : new int[] {7, 8, 15, 16, 23, 24, 31, 32, 63, 64, 65}) { + for (int length : new int[] {0, 1, 7, 8, 15, 16, 17, 23, 24, 31, 32, 33, 63, 64, 65}) { String value = repeat('a', length); String input = "\"" + value + "\""; assertEquals(json.fromJson(input, String.class), value); From eb753a67449869772a04cffce2736fe1ed2ab07e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 4 Jul 2026 22:06:26 +0800 Subject: [PATCH 057/120] perf(java): optimize Fory JSON short string writes --- .../fory/json/writer/Utf8JsonWriter.java | 87 ++++++++++++++----- .../org/apache/fory/json/JsonStringTest.java | 5 +- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 956648acd0..4bb0392716 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -710,52 +710,93 @@ private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { int start = position; int pos = start; bytes[pos++] = (byte) '"'; - int i = 0; - if (length > 15) { + // Short compact strings dominate generated JSON writers. Copy fixed leading words plus an + // overlapping tail so every byte is still validated, while common 10/20/30-byte values avoid + // the branchy exact-tail sequence. + if (length > 24) { long word0 = LittleEndian.getInt64(value, 0); long word1 = LittleEndian.getInt64(value, 8); - if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { + long word2 = LittleEndian.getInt64(value, 16); + int tailOffset = length - Long.BYTES; + long tail = LittleEndian.getInt64(value, tailOffset); + if (!isJsonAsciiWord(word0) + || !isJsonAsciiWord(word1) + || !isJsonAsciiWord(word2) + || !isJsonAsciiWord(tail)) { position = start; return false; } LittleEndian.putInt64(bytes, pos, word0); LittleEndian.putInt64(bytes, pos + 8, word1); - pos += 16; - i = 16; - } - if (i + 8 <= length) { - long word = LittleEndian.getInt64(value, i); + LittleEndian.putInt64(bytes, pos + 16, word2); + LittleEndian.putInt64(bytes, pos + tailOffset, tail); + pos += length; + } else if (length > 16) { + long word0 = LittleEndian.getInt64(value, 0); + long word1 = LittleEndian.getInt64(value, 8); + int tailOffset = length - Long.BYTES; + long tail = LittleEndian.getInt64(value, tailOffset); + if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1) || !isJsonAsciiWord(tail)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + LittleEndian.putInt64(bytes, pos + tailOffset, tail); + pos += length; + } else if (length >= 8) { + long word = LittleEndian.getInt64(value, 0); if (!isJsonAsciiWord(word)) { position = start; return false; } LittleEndian.putInt64(bytes, pos, word); - pos += 8; - i += 8; - } - if (i + 4 <= length) { - int word = LittleEndian.getInt32(value, i); + if (length > 8) { + int tailOffset = length - Long.BYTES; + long tail = LittleEndian.getInt64(value, tailOffset); + if (!isJsonAsciiWord(tail)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos + tailOffset, tail); + } + pos += length; + } else if (length >= 4) { + int word = LittleEndian.getInt32(value, 0); if (!isJsonAsciiInt(word)) { position = start; return false; } LittleEndian.putInt32(bytes, pos, word); - pos += 4; - i += 4; - } - if (i + 2 <= length) { - int word = (value[i] & 0xFF) | ((value[i + 1] & 0xFF) << 8); + if (length > 4) { + int tailOffset = length - Integer.BYTES; + int tail = LittleEndian.getInt32(value, tailOffset); + if (!isJsonAsciiInt(tail)) { + position = start; + return false; + } + LittleEndian.putInt32(bytes, pos + tailOffset, tail); + } + pos += length; + } else if (length >= 2) { + int word = (value[0] & 0xFF) | ((value[1] & 0xFF) << 8); if (!isJsonAsciiShort(word)) { position = start; return false; } bytes[pos] = (byte) word; bytes[pos + 1] = (byte) (word >>> 8); - pos += 2; - i += 2; - } - if (i < length) { - byte ch = value[i]; + if (length == 3) { + byte ch = value[2]; + if (!isJsonAsciiByte(ch)) { + position = start; + return false; + } + bytes[pos + 2] = ch; + } + pos += length; + } else if (length == 1) { + byte ch = value[0]; if (!isJsonAsciiByte(ch)) { position = start; return false; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index a4b43df2cb..d5a6ebf9af 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -227,7 +227,10 @@ public void writeSurrogatePair() { @Test public void writeStringScanBoundaries() { ForyJson json = ForyJson.builder().build(); - for (int length : new int[] {7, 8, 15, 16, 17, 23, 24, 31, 32, 33, 63, 64, 65}) { + for (int length : + new int[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 20, 23, 24, 25, 30, 31, 32, 33, 63, 64, 65 + }) { String value = repeat('a', length); String expected = "\"" + value + "\""; assertEquals(json.toJson(value), expected); From 41933fc1b4b5ebf0483cef41622173e746ea7a1a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 4 Jul 2026 23:05:41 +0800 Subject: [PATCH 058/120] perf(java): split JSON double token parser --- .../org/apache/fory/json/reader/Utf8JsonReader.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 5a0324485f..9def5f890e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -742,15 +742,19 @@ private double readDoubleToken() { // path, while exponents, overflow, and longer precision stay on Java's full parser. byte[] bytes = input; int offset = position; - int start = offset; int inputLength = bytes.length; if (offset >= inputLength) { - return readDoubleFallback(start); + return readDoubleFallback(offset); } int ch = bytes[offset]; if (ch == '-') { - return readSignedDoubleToken(start); + return readSignedDoubleToken(offset); } + return readPositiveDoubleToken(bytes, offset, inputLength, ch); + } + + private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength, int ch) { + int start = offset; long unscaled = 0; int scale = 0; if (ch == '0') { From 389b760a0eee79b0208e2ed5cc4318dec9a6388d Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 01:31:57 +0800 Subject: [PATCH 059/120] perf(java): split JSON depth overflow path --- .../main/java/org/apache/fory/json/reader/JsonReader.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java index b76d6f8e2c..81a4d7981f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java @@ -48,11 +48,15 @@ public final void clearDepth() { public final void enterDepth() { int nextDepth = depth + 1; if (nextDepth > maxDepth) { - throw error("JSON max depth " + maxDepth + " exceeded"); + throwMaxDepthExceeded(); } depth = nextDepth; } + private void throwMaxDepthExceeded() { + throw error("JSON max depth " + maxDepth + " exceeded"); + } + public final void exitDepth() { depth--; } From 197c247c6e5d9b58bb781b9283d9bb7eaebf41d9 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 01:44:41 +0800 Subject: [PATCH 060/120] perf(java): split JSON comma fast path --- .../fory/json/reader/Utf8JsonReader.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 9def5f890e..136312a1fb 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -116,10 +116,15 @@ public boolean consumeNextCommaOrEndObject() { position++; return true; } - if (ch == '}') { - position++; - return false; - } + return consumeNextObjectEndOrSlow(ch); + } + return consumeNextCommaOrEndObjectSlow(); + } + + private boolean consumeNextObjectEndOrSlow(int ch) { + if (ch == '}') { + position++; + return false; } return consumeNextCommaOrEndObjectSlow(); } @@ -147,10 +152,15 @@ public boolean consumeNextCommaOrEndArray() { position++; return true; } - if (ch == ']') { - position++; - return false; - } + return consumeNextArrayEndOrSlow(ch); + } + return consumeNextCommaOrEndArraySlow(); + } + + private boolean consumeNextArrayEndOrSlow(int ch) { + if (ch == ']') { + position++; + return false; } return consumeNextCommaOrEndArraySlow(); } From 73bc7534ab08988c641ccacf94a1e06b963a606f Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 07:22:54 +0800 Subject: [PATCH 061/120] perf(json): split medium Latin1 string writes --- .../fory/json/writer/Utf8JsonWriter.java | 88 ++++++++++++------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 4bb0392716..4dd8331c98 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -706,6 +706,12 @@ private static boolean isJsonAsciiBytes(byte[] value, int length) { } private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { + if (length > 24) { + return writeLatin1String25To31(value, length); + } + if (length > 16) { + return writeLatin1String17To24(value, length); + } byte[] bytes = buffer; int start = position; int pos = start; @@ -713,38 +719,7 @@ private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { // Short compact strings dominate generated JSON writers. Copy fixed leading words plus an // overlapping tail so every byte is still validated, while common 10/20/30-byte values avoid // the branchy exact-tail sequence. - if (length > 24) { - long word0 = LittleEndian.getInt64(value, 0); - long word1 = LittleEndian.getInt64(value, 8); - long word2 = LittleEndian.getInt64(value, 16); - int tailOffset = length - Long.BYTES; - long tail = LittleEndian.getInt64(value, tailOffset); - if (!isJsonAsciiWord(word0) - || !isJsonAsciiWord(word1) - || !isJsonAsciiWord(word2) - || !isJsonAsciiWord(tail)) { - position = start; - return false; - } - LittleEndian.putInt64(bytes, pos, word0); - LittleEndian.putInt64(bytes, pos + 8, word1); - LittleEndian.putInt64(bytes, pos + 16, word2); - LittleEndian.putInt64(bytes, pos + tailOffset, tail); - pos += length; - } else if (length > 16) { - long word0 = LittleEndian.getInt64(value, 0); - long word1 = LittleEndian.getInt64(value, 8); - int tailOffset = length - Long.BYTES; - long tail = LittleEndian.getInt64(value, tailOffset); - if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1) || !isJsonAsciiWord(tail)) { - position = start; - return false; - } - LittleEndian.putInt64(bytes, pos, word0); - LittleEndian.putInt64(bytes, pos + 8, word1); - LittleEndian.putInt64(bytes, pos + tailOffset, tail); - pos += length; - } else if (length >= 8) { + if (length >= 8) { long word = LittleEndian.getInt64(value, 0); if (!isJsonAsciiWord(word)) { position = start; @@ -808,6 +783,55 @@ private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { return true; } + private boolean writeLatin1String25To31(byte[] value, int length) { + byte[] bytes = buffer; + int start = position; + int pos = start; + bytes[pos++] = (byte) '"'; + long word0 = LittleEndian.getInt64(value, 0); + long word1 = LittleEndian.getInt64(value, 8); + long word2 = LittleEndian.getInt64(value, 16); + int tailOffset = length - Long.BYTES; + long tail = LittleEndian.getInt64(value, tailOffset); + if (!isJsonAsciiWord(word0) + || !isJsonAsciiWord(word1) + || !isJsonAsciiWord(word2) + || !isJsonAsciiWord(tail)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + LittleEndian.putInt64(bytes, pos + 16, word2); + LittleEndian.putInt64(bytes, pos + tailOffset, tail); + pos += length; + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + + private boolean writeLatin1String17To24(byte[] value, int length) { + byte[] bytes = buffer; + int start = position; + int pos = start; + bytes[pos++] = (byte) '"'; + long word0 = LittleEndian.getInt64(value, 0); + long word1 = LittleEndian.getInt64(value, 8); + int tailOffset = length - Long.BYTES; + long tail = LittleEndian.getInt64(value, tailOffset); + if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1) || !isJsonAsciiWord(tail)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + LittleEndian.putInt64(bytes, pos + tailOffset, tail); + pos += length; + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + private boolean writeUtf16String(byte[] value) { int length = value.length; ensure((length >> 1) * 3 + 2); From 3f9a8b5da2296f998de3c98fb45d972b9b7e9e24 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 08:06:45 +0800 Subject: [PATCH 062/120] perf(json): clear only used pooled reader --- .../java/org/apache/fory/json/ForyJson.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index 449cd15a18..09d4c179e5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -138,7 +138,7 @@ public T fromJson(String json, Class type) { try { return castValue(readJavaStringValue(json, type, type, state), type); } finally { - state.clearReaders(); + state.clearStringReaders(); release(entry); } } @@ -151,7 +151,7 @@ public T fromJson(String json, TypeRef typeRef) { Object value = readJavaStringValue(json, typeRef.getType(), typeRef.getRawType(), state); return castValue(value, typeRef); } finally { - state.clearReaders(); + state.clearStringReaders(); release(entry); } } @@ -162,7 +162,7 @@ public T fromJson(byte[] bytes, Class type) { try { return castValue(readUtf8Value(state.utf8Reader(bytes, maxDepth), type, type, state), type); } finally { - state.clearReaders(); + state.clearUtf8Reader(); release(entry); } } @@ -177,7 +177,7 @@ public T fromJson(byte[] bytes, TypeRef typeRef) { state.utf8Reader(bytes, maxDepth), typeRef.getType(), typeRef.getRawType(), state); return castValue(value, typeRef); } finally { - state.clearReaders(); + state.clearUtf8Reader(); release(entry); } } @@ -351,12 +351,17 @@ private Utf8JsonReader utf8Reader(byte[] input, int maxDepth) { return utf8Reader; } - private void clearReaders() { + // Clear only readers reset by the current public parse entry; clearing the unused readers shows + // up on small byte-input parses and does not release additional retained input. + private void clearStringReaders() { latin1Reader.clearDepth(); utf16Reader.clearDepth(); - utf8Reader.clearDepth(); latin1Reader.clear(); utf16Reader.clear(); + } + + private void clearUtf8Reader() { + utf8Reader.clearDepth(); utf8Reader.clear(); } From ee6b6f1e75339bfa08a11d1299e044e27f35827a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 09:07:45 +0800 Subject: [PATCH 063/120] perf(json): parse compact doubles two digits at a time --- .../fory/json/reader/Utf8JsonReader.java | 132 +++++++++++++----- 1 file changed, 97 insertions(+), 35 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 136312a1fb..d10655ee6c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -44,6 +44,8 @@ public final class Utf8JsonReader extends JsonReader { private static final int INT_QUOTE_BYTES = 0x22222222; private static final long LONG_MAX_DIV_10 = Long.MAX_VALUE / 10; private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); + private static final long LONG_MAX_DIV_100 = Long.MAX_VALUE / 100; + private static final int LONG_MAX_MOD_100 = (int) (Long.MAX_VALUE % 100); private static final long LONG_MIN_DIV_10 = Long.MIN_VALUE / 10; private static final int LONG_MIN_LAST_DIGIT = (int) -(Long.MIN_VALUE % 10); private static final long EIGHT_DIGITS = 100_000_000L; @@ -749,7 +751,9 @@ private static int hexValue(int ch) { private double readDoubleToken() { // Keep the fast path exact: compact plain decimals convert through BigDecimal's long+scale - // path, while exponents, overflow, and longer precision stay on Java's full parser. + // path, while exponents, overflow, and longer precision stay on Java's full parser. The + // two-digit accumulator below only reduces parser loop work; it must keep the same overflow + // boundary and BigDecimal finish instead of accepting approximate double construction. byte[] bytes = input; int offset = position; int inputLength = bytes.length; @@ -776,36 +780,65 @@ private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength } } } else if (ch >= '1' && ch <= '9') { - do { - int digit = ch - '0'; - if (unscaled > (Long.MAX_VALUE - digit) / 10) { + unscaled = ch - '0'; + offset++; + while (offset + 1 < inputLength) { + int high = bytes[offset] - '0'; + int low = bytes[offset + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + break; + } + int pair = high * 10 + low; + if (unscaled > LONG_MAX_DIV_100 + || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { return readDoubleFallback(start); } - unscaled = unscaled * 10 + digit; - offset++; - if (offset >= inputLength) { - break; + unscaled = unscaled * 100 + pair; + offset += 2; + } + if (offset < inputLength) { + int digit = bytes[offset] - '0'; + if (digit >= 0 && digit <= 9) { + if (unscaled > LONG_MAX_DIV_10 + || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; } - ch = bytes[offset]; - } while (ch >= '0' && ch <= '9'); + } } else { return readDoubleFallback(start); } if (offset < inputLength && bytes[offset] == '.') { offset++; int fractionStart = offset; - while (offset < inputLength) { - ch = bytes[offset]; - if (ch < '0' || ch > '9') { + while (offset + 1 < inputLength) { + int high = bytes[offset] - '0'; + int low = bytes[offset + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { break; } - int digit = ch - '0'; - if (unscaled > (Long.MAX_VALUE - digit) / 10) { + int pair = high * 10 + low; + if (unscaled > LONG_MAX_DIV_100 + || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { return readDoubleFallback(start); } - unscaled = unscaled * 10 + digit; - scale++; - offset++; + unscaled = unscaled * 100 + pair; + scale += 2; + offset += 2; + } + if (offset < inputLength) { + int digit = bytes[offset] - '0'; + if (digit >= 0 && digit <= 9) { + if (unscaled > LONG_MAX_DIV_10 + || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } } if (offset == fractionStart) { return readDoubleFallback(start); @@ -833,36 +866,65 @@ private double readSignedDoubleToken(int start) { } } } else if (ch >= '1' && ch <= '9') { - do { - int digit = ch - '0'; - if (unscaled > (Long.MAX_VALUE - digit) / 10) { + unscaled = ch - '0'; + offset++; + while (offset + 1 < inputLength) { + int high = bytes[offset] - '0'; + int low = bytes[offset + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + break; + } + int pair = high * 10 + low; + if (unscaled > LONG_MAX_DIV_100 + || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { return readDoubleFallback(start); } - unscaled = unscaled * 10 + digit; - offset++; - if (offset >= inputLength) { - break; + unscaled = unscaled * 100 + pair; + offset += 2; + } + if (offset < inputLength) { + int digit = bytes[offset] - '0'; + if (digit >= 0 && digit <= 9) { + if (unscaled > LONG_MAX_DIV_10 + || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; } - ch = bytes[offset]; - } while (ch >= '0' && ch <= '9'); + } } else { return readDoubleFallback(start); } if (offset < inputLength && bytes[offset] == '.') { offset++; int fractionStart = offset; - while (offset < inputLength) { - ch = bytes[offset]; - if (ch < '0' || ch > '9') { + while (offset + 1 < inputLength) { + int high = bytes[offset] - '0'; + int low = bytes[offset + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { break; } - int digit = ch - '0'; - if (unscaled > (Long.MAX_VALUE - digit) / 10) { + int pair = high * 10 + low; + if (unscaled > LONG_MAX_DIV_100 + || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { return readDoubleFallback(start); } - unscaled = unscaled * 10 + digit; - scale++; - offset++; + unscaled = unscaled * 100 + pair; + scale += 2; + offset += 2; + } + if (offset < inputLength) { + int digit = bytes[offset] - '0'; + if (digit >= 0 && digit <= 9) { + if (unscaled > LONG_MAX_DIV_10 + || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } } if (offset == fractionStart) { return readDoubleFallback(start); From 03389d7014ff275a7737c08da8dd2777598c3d4e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 09:43:06 +0800 Subject: [PATCH 064/120] perf(json): split short latin1 string tail --- .../apache/fory/json/writer/Utf8JsonWriter.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 4dd8331c98..1c2d6597ff 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -736,7 +736,22 @@ private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { LittleEndian.putInt64(bytes, pos + tailOffset, tail); } pos += length; - } else if (length >= 4) { + } else { + // Keep the sub-8 tail outside this method so the common word-sized short-string path stays + // small enough to inline into generated object writers. + return writeLatin1String0To7(value, length); + } + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + + private boolean writeLatin1String0To7(byte[] value, int length) { + byte[] bytes = buffer; + int start = position; + int pos = start; + bytes[pos++] = (byte) '"'; + if (length >= 4) { int word = LittleEndian.getInt32(value, 0); if (!isJsonAsciiInt(word)) { position = start; From b8434899f0373619be37c5147b96d2151de516e5 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 10:02:35 +0800 Subject: [PATCH 065/120] perf(json): fast-path ascii escape checks --- .../fory/json/writer/Utf8JsonWriter.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 1c2d6597ff..7f35a0ee62 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -46,6 +46,9 @@ public final class Utf8JsonWriter extends JsonWriter { private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; private static final int INT_ASCII_CONTROL_OFFSET = 0x60606060; private static final int SHORT_ASCII_CONTROL_OFFSET = 0x6060; + private static final long ASCII_GT_QUOTE_OFFSET = 0x5D5D5D5D5D5D5D5DL; + private static final int INT_ASCII_GT_QUOTE_OFFSET = 0x5D5D5D5D; + private static final int SHORT_ASCII_GT_QUOTE_OFFSET = 0x5D5D; private static final long ONE_BYTES = 0x0101010101010101L; private static final int INT_ONE_BYTES = 0x01010101; private static final int SHORT_ONE_BYTES = 0x0101; @@ -1115,24 +1118,38 @@ private static boolean isJsonAsciiByte(byte value) { } private static boolean isJsonAsciiWord(long word) { + long notBackslashMask = ((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS; + // Common unescaped bytes are greater than '"' and not '\\'. The fallback keeps the exact + // quote, backslash, control, and high-bit predicate for the remaining byte values. + if ((notBackslashMask & (word + ASCII_GT_QUOTE_OFFSET)) == HIGH_BITS) { + return true; + } return (((word + ASCII_CONTROL_OFFSET) & ~word) & HIGH_BITS) == HIGH_BITS && (((word ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS - && (((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS; + && notBackslashMask == HIGH_BITS; } private static boolean isJsonAsciiInt(int word) { + int notBackslashMask = + ((word ^ INT_BACKSLASH_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS; + if ((notBackslashMask & (word + INT_ASCII_GT_QUOTE_OFFSET)) == INT_HIGH_BITS) { + return true; + } return (((word + INT_ASCII_CONTROL_OFFSET) & ~word) & INT_HIGH_BITS) == INT_HIGH_BITS && (((word ^ INT_QUOTE_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) == INT_HIGH_BITS - && (((word ^ INT_BACKSLASH_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) - == INT_HIGH_BITS; + && notBackslashMask == INT_HIGH_BITS; } private static boolean isJsonAsciiShort(int word) { + int notBackslashMask = + ((word ^ SHORT_BACKSLASH_BYTES_COMPLEMENT) + SHORT_ONE_BYTES) & SHORT_HIGH_BITS; + if ((notBackslashMask & (word + SHORT_ASCII_GT_QUOTE_OFFSET)) == SHORT_HIGH_BITS) { + return true; + } return (((word + SHORT_ASCII_CONTROL_OFFSET) & ~word) & SHORT_HIGH_BITS) == SHORT_HIGH_BITS && (((word ^ SHORT_QUOTE_BYTES_COMPLEMENT) + SHORT_ONE_BYTES) & SHORT_HIGH_BITS) == SHORT_HIGH_BITS - && (((word ^ SHORT_BACKSLASH_BYTES_COMPLEMENT) + SHORT_ONE_BYTES) & SHORT_HIGH_BITS) - == SHORT_HIGH_BITS; + && notBackslashMask == SHORT_HIGH_BITS; } private static int packUtf16Ascii(long word) { From 8fa8d59a6a0833800cb487adc8983526901910f6 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 11:02:05 +0800 Subject: [PATCH 066/120] perf(json): use exact tails for short latin1 writes --- .../fory/json/writer/Utf8JsonWriter.java | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 7f35a0ee62..f89febb66c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -719,9 +719,8 @@ private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { int start = position; int pos = start; bytes[pos++] = (byte) '"'; - // Short compact strings dominate generated JSON writers. Copy fixed leading words plus an - // overlapping tail so every byte is still validated, while common 10/20/30-byte values avoid - // the branchy exact-tail sequence. + // Short compact strings dominate generated JSON writers. Keep the 8-16 byte path exact here; + // longer short-string bands stay in helpers so this common path remains small. if (length >= 8) { long word = LittleEndian.getInt64(value, 0); if (!isJsonAsciiWord(word)) { @@ -729,16 +728,47 @@ private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { return false; } LittleEndian.putInt64(bytes, pos, word); - if (length > 8) { - int tailOffset = length - Long.BYTES; - long tail = LittleEndian.getInt64(value, tailOffset); + pos += 8; + int index = Long.BYTES; + if (index + Long.BYTES <= length) { + long tail = LittleEndian.getInt64(value, index); if (!isJsonAsciiWord(tail)) { position = start; return false; } - LittleEndian.putInt64(bytes, pos + tailOffset, tail); + LittleEndian.putInt64(bytes, pos, tail); + pos += Long.BYTES; + index += Long.BYTES; + } + if (index + Integer.BYTES <= length) { + int tail = LittleEndian.getInt32(value, index); + if (!isJsonAsciiInt(tail)) { + position = start; + return false; + } + LittleEndian.putInt32(bytes, pos, tail); + pos += Integer.BYTES; + index += Integer.BYTES; + } + if (index + Short.BYTES <= length) { + int tail = (value[index] & 0xFF) | ((value[index + 1] & 0xFF) << 8); + if (!isJsonAsciiShort(tail)) { + position = start; + return false; + } + bytes[pos] = (byte) tail; + bytes[pos + 1] = (byte) (tail >>> 8); + pos += Short.BYTES; + index += Short.BYTES; + } + if (index < length) { + byte tail = value[index]; + if (!isJsonAsciiByte(tail)) { + position = start; + return false; + } + bytes[pos++] = tail; } - pos += length; } else { // Keep the sub-8 tail outside this method so the common word-sized short-string path stays // small enough to inline into generated object writers. From 64ea55e43d08ce37d87060419f623b0ca6918eff Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 12:50:34 +0800 Subject: [PATCH 067/120] perf(json): streamline UTF8 reads --- .../apache/fory/json/codec/ArrayCodec.java | 57 +++++++------------ .../fory/json/reader/Utf8JsonReader.java | 45 ++++++++------- 2 files changed, 45 insertions(+), 57 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index aff3817589..a29b9f62e5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -313,29 +313,25 @@ public Object readUtf8( } rejectNull(reader); long v0 = reader.readLongValue(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new long[] {v0}; } rejectNull(reader); long v1 = reader.readLongValue(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new long[] {v0, v1}; } rejectNull(reader); long v2 = reader.readLongValue(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new long[] {v0, v1, v2}; } rejectNull(reader); long v3 = reader.readLongValue(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new long[] {v0, v1, v2, v3}; } @@ -345,29 +341,25 @@ public Object readUtf8( private long[] readUtf8Tail(Utf8JsonReader reader, long v0, long v1, long v2, long v3) { rejectNull(reader); long v4 = reader.readLongValue(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new long[] {v0, v1, v2, v3, v4}; } rejectNull(reader); long v5 = reader.readLongValue(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new long[] {v0, v1, v2, v3, v4, v5}; } rejectNull(reader); long v6 = reader.readLongValue(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new long[] {v0, v1, v2, v3, v4, v5, v6}; } rejectNull(reader); long v7 = reader.readLongValue(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new long[] {v0, v1, v2, v3, v4, v5, v6, v7}; } @@ -402,8 +394,7 @@ private long[] readUtf8LongTail( values = Arrays.copyOf(values, values.length << 1); } values[size++] = reader.readLongValue(); - } while (reader.consumeNextToken(',')); - reader.expectNextToken(']'); + } while (reader.consumeNextCommaOrEndArray()); reader.exitDepth(); return Arrays.copyOf(values, size); } @@ -859,26 +850,22 @@ public Object readUtf8( return new String[0]; } String v0 = reader.readNextNullableString(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new String[] {v0}; } String v1 = reader.readNextNullableString(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new String[] {v0, v1}; } String v2 = reader.readNextNullableString(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new String[] {v0, v1, v2}; } String v3 = reader.readNextNullableString(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new String[] {v0, v1, v2, v3}; } @@ -888,32 +875,27 @@ public Object readUtf8( private String[] readUtf8Tail( Utf8JsonReader reader, String v0, String v1, String v2, String v3) { String v4 = reader.readNextNullableString(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return stringArray5(v0, v1, v2, v3, v4); } String v5 = reader.readNextNullableString(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return stringArray6(v0, v1, v2, v3, v4, v5); } String v6 = reader.readNextNullableString(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return stringArray7(v0, v1, v2, v3, v4, v5, v6); } String v7 = reader.readNextNullableString(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return stringArray8(v0, v1, v2, v3, v4, v5, v6, v7); } String v8 = reader.readNextNullableString(); - if (!reader.consumeNextToken(',')) { - reader.expectNextToken(']'); + if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); return new String[] {v0, v1, v2, v3, v4, v5, v6, v7, v8}; } @@ -949,8 +931,7 @@ private String[] readUtf8LongTail( values = Arrays.copyOf(values, values.length << 1); } values[size++] = reader.readNextNullableString(); - } while (reader.consumeNextToken(',')); - reader.expectNextToken(']'); + } while (reader.consumeNextCommaOrEndArray()); reader.exitDepth(); return Arrays.copyOf(values, size); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index d10655ee6c..1a73cc6583 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -202,8 +202,14 @@ public boolean tryReadNextNullToken() { } private boolean tryReadNullLiteral() { - if (startsWithAscii("null")) { - position += 4; + byte[] bytes = input; + int offset = position; + if (offset + 3 < bytes.length + && bytes[offset] == 'n' + && bytes[offset + 1] == 'u' + && bytes[offset + 2] == 'l' + && bytes[offset + 3] == 'l') { + position = offset + 4; return true; } return false; @@ -229,11 +235,22 @@ public boolean readBooleanTokenValue() { } private boolean readBooleanToken() { - if (startsWithAscii("true")) { - position += 4; + byte[] bytes = input; + int offset = position; + if (offset + 3 < bytes.length + && bytes[offset] == 't' + && bytes[offset + 1] == 'r' + && bytes[offset + 2] == 'u' + && bytes[offset + 3] == 'e') { + position = offset + 4; return true; - } else if (startsWithAscii("false")) { - position += 5; + } else if (offset + 4 < bytes.length + && bytes[offset] == 'f' + && bytes[offset + 1] == 'a' + && bytes[offset + 2] == 'l' + && bytes[offset + 3] == 's' + && bytes[offset + 4] == 'e') { + position = offset + 5; return false; } throw error("Expected boolean"); @@ -1926,6 +1943,9 @@ private int continuation() { private void skipWhitespaceFast() { while (position < input.length) { int ch = input[position]; + if (ch > ' ') { + return; + } if (isWhitespace(ch)) { position++; } else { @@ -1938,19 +1958,6 @@ private static boolean isWhitespace(int ch) { return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; } - private boolean startsWithAscii(String value) { - int end = position + value.length(); - if (end > input.length) { - return false; - } - for (int i = 0; i < value.length(); i++) { - if (input[position + i] != value.charAt(i)) { - return false; - } - } - return true; - } - private void rejectLeadingDigitFast() { if (position < input.length) { int ch = input[position]; From 0576824995611dfd5a5c9589ccc28ec01478d7ab Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 14:21:26 +0800 Subject: [PATCH 068/120] perf(json): inline exact string array construction --- .../apache/fory/json/codec/ArrayCodec.java | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index a29b9f62e5..deb12eed3d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -877,22 +877,22 @@ private String[] readUtf8Tail( String v4 = reader.readNextNullableString(); if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); - return stringArray5(v0, v1, v2, v3, v4); + return new String[] {v0, v1, v2, v3, v4}; } String v5 = reader.readNextNullableString(); if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); - return stringArray6(v0, v1, v2, v3, v4, v5); + return new String[] {v0, v1, v2, v3, v4, v5}; } String v6 = reader.readNextNullableString(); if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); - return stringArray7(v0, v1, v2, v3, v4, v5, v6); + return new String[] {v0, v1, v2, v3, v4, v5, v6}; } String v7 = reader.readNextNullableString(); if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); - return stringArray8(v0, v1, v2, v3, v4, v5, v6, v7); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; } String v8 = reader.readNextNullableString(); if (!reader.consumeNextCommaOrEndArray()) { @@ -935,25 +935,6 @@ private String[] readUtf8LongTail( reader.exitDepth(); return Arrays.copyOf(values, size); } - - private static String[] stringArray5(String v0, String v1, String v2, String v3, String v4) { - return new String[] {v0, v1, v2, v3, v4}; - } - - private static String[] stringArray6( - String v0, String v1, String v2, String v3, String v4, String v5) { - return new String[] {v0, v1, v2, v3, v4, v5}; - } - - private static String[] stringArray7( - String v0, String v1, String v2, String v3, String v4, String v5, String v6) { - return new String[] {v0, v1, v2, v3, v4, v5, v6}; - } - - private static String[] stringArray8( - String v0, String v1, String v2, String v3, String v4, String v5, String v6, String v7) { - return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; - } } public static final class ObjectArrayCodec extends ArrayCodec { From a348a2adfd49c9415543840ddbcc58f214cb29b3 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 17:39:00 +0800 Subject: [PATCH 069/120] perf(java): speed up Fory JSON Latin1 scalar reads --- .../apache/fory/json/codec/ScalarCodecs.java | 50 ++ .../fory/json/reader/Latin1JsonReader.java | 617 ++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 30 + 3 files changed, 697 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 7902bf7134..271a75c17f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -593,6 +593,15 @@ public Object readUtf8( } return reader.readBigDecimal(); } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return reader.readBigDecimal(); + } } public static final class Float16Codec extends AbstractJsonCodec { @@ -781,6 +790,21 @@ public Object readUtf8( throw new ForyJsonException("Invalid " + typeInfo.rawType().getName() + " JSON string", e); } } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readUuid(); + } catch (ForyJsonException e) { + throw e; + } catch (RuntimeException e) { + throw new ForyJsonException("Invalid " + typeInfo.rawType().getName() + " JSON string", e); + } + } } public static final class LocaleCodec extends StringValueCodec { @@ -952,6 +976,19 @@ public Object readUtf8( return readStringValue(reader.readString(), typeInfo); } } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoLocalDate(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); + } + } } public static final class LocalTimeCodec extends StringValueCodec { @@ -1152,6 +1189,19 @@ public Object readUtf8( return readStringValue(reader.readString(), typeInfo); } } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoOffsetDateTime(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); + } + } } private static LocalDate parseIsoLocalDate(String value) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index 729d7159f1..dcb72621fb 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -19,6 +19,11 @@ package org.apache.fory.json.reader; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; @@ -37,6 +42,10 @@ public final class Latin1JsonReader extends JsonReader { private static final int INT_CONTROL_LIMIT_BYTES = 0x20202020; private static final long QUOTE_BYTES = 0x2222222222222222L; private static final int INT_QUOTE_BYTES = 0x22222222; + private static final long LONG_MAX_DIV_10 = Long.MAX_VALUE / 10; + private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); + private static final long LONG_MAX_DIV_100 = Long.MAX_VALUE / 100; + private static final int LONG_MAX_MOD_100 = (int) (Long.MAX_VALUE % 100); // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. // Latin1 string content and field-name hashing must keep unsigned byte conversion. @@ -460,6 +469,411 @@ private long readNegativeLongToken(int start) { return result; } + public BigDecimal readBigDecimal() { + skipWhitespaceFast(); + return readBigDecimalToken(); + } + + public UUID readUuid() { + skipWhitespaceFast(); + int mark = position; + try { + return readUuidToken(); + } catch (RuntimeException e) { + position = mark; + return UUID.fromString(readStringToken()); + } + } + + @Override + public double readDouble() { + skipWhitespaceFast(); + return readDoubleToken(); + } + + private BigDecimal readBigDecimalToken() { + byte[] bytes = input; + int offset = position; + int start = offset; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + int ch = bytes[offset]; + if (ch == '-') { + return readSignedBigDecimalToken(start); + } + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + position = offset; + rejectLeadingDigitFast(); + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readBigDecimalFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { + return readBigDecimalFallback(start); + } + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readBigDecimalFallback(start); + } + } + position = offset; + return BigDecimal.valueOf(unscaled, scale); + } + + private BigDecimal readSignedBigDecimalToken(int start) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + int ch = bytes[offset]; + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + position = offset; + rejectLeadingDigitFast(); + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readBigDecimalFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { + return readBigDecimalFallback(start); + } + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readBigDecimalFallback(start); + } + } + position = offset; + return BigDecimal.valueOf(-unscaled, scale); + } + + private BigDecimal readBigDecimalFallback(int start) { + position = start; + return new BigDecimal(readNumber()); + } + + private UUID readUuidToken() { + byte[] bytes = input; + int offset = position; + int start = offset + 1; + if (offset + 38 > bytes.length || bytes[offset] != '"') { + throw new IllegalArgumentException(); + } + if (bytes[start + 8] != '-' + || bytes[start + 13] != '-' + || bytes[start + 18] != '-' + || bytes[start + 23] != '-' + || bytes[start + 36] != '"') { + throw new IllegalArgumentException(); + } + long msb = parseHex(bytes, start, 8); + msb = (msb << 16) | parseHex(bytes, start + 9, 4); + msb = (msb << 16) | parseHex(bytes, start + 14, 4); + long lsb = parseHex(bytes, start + 19, 4); + lsb = (lsb << 48) | parseHex(bytes, start + 24, 12); + position = start + 37; + return new UUID(msb, lsb); + } + + private static long parseHex(byte[] bytes, int offset, int length) { + long value = 0; + for (int i = 0; i < length; i++) { + value = (value << 4) | hexValue(bytes[offset + i]); + } + return value; + } + + private static int hexValue(int ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + int lower = ch | 0x20; + if (lower >= 'a' && lower <= 'f') { + return lower - 'a' + 10; + } + throw new IllegalArgumentException(); + } + + private double readDoubleToken() { + byte[] bytes = input; + int offset = position; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readDoubleFallback(offset); + } + int ch = bytes[offset]; + if (ch == '-') { + return readSignedDoubleToken(offset); + } + return readPositiveDoubleToken(bytes, offset, inputLength, ch); + } + + private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength, int ch) { + int start = offset; + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readDoubleFallback(start); + } + } + } else if (ch >= '1' && ch <= '9') { + unscaled = ch - '0'; + offset++; + while (offset + 1 < inputLength) { + int high = bytes[offset] - '0'; + int low = bytes[offset + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + break; + } + int pair = high * 10 + low; + if (unscaled > LONG_MAX_DIV_100 + || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 100 + pair; + offset += 2; + } + if (offset < inputLength) { + int digit = bytes[offset] - '0'; + if (digit >= 0 && digit <= 9) { + if (unscaled > LONG_MAX_DIV_10 + || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + } + } + } else { + return readDoubleFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset + 1 < inputLength) { + int high = bytes[offset] - '0'; + int low = bytes[offset + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + break; + } + int pair = high * 10 + low; + if (unscaled > LONG_MAX_DIV_100 + || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 100 + pair; + scale += 2; + offset += 2; + } + if (offset < inputLength) { + int digit = bytes[offset] - '0'; + if (digit >= 0 && digit <= 9) { + if (unscaled > LONG_MAX_DIV_10 + || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + } + if (offset == fractionStart) { + return readDoubleFallback(start); + } + } + return finishDoubleToken(bytes, offset, inputLength, start, unscaled, scale); + } + + private double readSignedDoubleToken(int start) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + int ch = bytes[offset]; + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readDoubleFallback(start); + } + } + } else if (ch >= '1' && ch <= '9') { + unscaled = ch - '0'; + offset++; + while (offset + 1 < inputLength) { + int high = bytes[offset] - '0'; + int low = bytes[offset + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + break; + } + int pair = high * 10 + low; + if (unscaled > LONG_MAX_DIV_100 + || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 100 + pair; + offset += 2; + } + if (offset < inputLength) { + int digit = bytes[offset] - '0'; + if (digit >= 0 && digit <= 9) { + if (unscaled > LONG_MAX_DIV_10 + || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + } + } + } else { + return readDoubleFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset + 1 < inputLength) { + int high = bytes[offset] - '0'; + int low = bytes[offset + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + break; + } + int pair = high * 10 + low; + if (unscaled > LONG_MAX_DIV_100 + || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 100 + pair; + scale += 2; + offset += 2; + } + if (offset < inputLength) { + int digit = bytes[offset] - '0'; + if (digit >= 0 && digit <= 9) { + if (unscaled > LONG_MAX_DIV_10 + || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + } + if (offset == fractionStart) { + return readDoubleFallback(start); + } + } + return finishSignedDoubleToken(bytes, offset, inputLength, start, unscaled, scale); + } + + private double finishDoubleToken( + byte[] bytes, int offset, int inputLength, int start, long unscaled, int scale) { + if (offset < inputLength) { + int ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readDoubleFallback(start); + } + } + position = offset; + return BigDecimal.valueOf(unscaled, scale).doubleValue(); + } + + private double finishSignedDoubleToken( + byte[] bytes, int offset, int inputLength, int start, long unscaled, int scale) { + if (offset < inputLength) { + int ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readDoubleFallback(start); + } + } + position = offset; + if (unscaled == 0) { + return -0.0d; + } + return BigDecimal.valueOf(-unscaled, scale).doubleValue(); + } + + private double readDoubleFallback(int start) { + position = start; + return Double.parseDouble(readNumber()); + } + @Override public int readFieldNameInt() { skipWhitespaceFast(); @@ -630,6 +1044,28 @@ public String readNullableStringToken() { return readStringToken(); } + public LocalDate readIsoLocalDate() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoLocalDateToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + + public OffsetDateTime readIsoOffsetDateTime() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoOffsetDateTimeToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + private String readStringToken() { byte[] bytes = input; int inputLength = bytes.length; @@ -684,6 +1120,187 @@ private String readStringToken() { throw error("Unterminated string"); } + private LocalDate readIsoLocalDateToken() { + byte[] bytes = input; + int offset = position; + int length = bytes.length; + if (offset + 12 > length || bytes[offset++] != '"') { + throw error("Expected string"); + } + int dateStart = offset; + if (bytes[dateStart + 4] != '-' || bytes[dateStart + 7] != '-') { + throw new IllegalArgumentException(); + } + int year = parse4(bytes, dateStart); + int month = parse2(bytes, dateStart + 5); + int day = parse2(bytes, dateStart + 8); + int end = dateStart + 10; + int ch = bytes[end]; + if (ch == '"') { + position = end + 1; + return LocalDate.of(year, month, day); + } + if (ch == 'T') { + position = scanSimpleStringTail(bytes, end + 1); + return LocalDate.of(year, month, day); + } + throw new IllegalArgumentException(); + } + + private OffsetDateTime readIsoOffsetDateTimeToken() { + byte[] bytes = input; + int offset = position; + int length = bytes.length; + if (offset + 19 > length || bytes[offset++] != '"') { + throw error("Expected string"); + } + int start = offset; + if (bytes[start + 4] != '-' + || bytes[start + 7] != '-' + || bytes[start + 10] != 'T' + || bytes[start + 13] != ':') { + throw new IllegalArgumentException(); + } + int year = parse4(bytes, start); + int month = parse2(bytes, start + 5); + int day = parse2(bytes, start + 8); + int hour = parse2(bytes, start + 11); + int minute = parse2(bytes, start + 14); + return readIsoOffsetDateTimeTail(bytes, start + 16, length, year, month, day, hour, minute); + } + + private OffsetDateTime readIsoOffsetDateTimeTail( + byte[] bytes, int index, int length, int year, int month, int day, int hour, int minute) { + int second = 0; + int nano = 0; + if (index < length && bytes[index] == ':') { + second = parse2(bytes, index + 1); + index += 3; + if (index < length && bytes[index] == '.') { + int fractionStart = index + 1; + int fractionEnd = fractionStart; + while (fractionEnd < length && isDigit(bytes[fractionEnd])) { + fractionEnd++; + } + if (fractionEnd == fractionStart || fractionEnd - fractionStart > 9) { + throw new IllegalArgumentException(); + } + nano = parseNano(bytes, fractionStart, fractionEnd); + index = fractionEnd; + } + } + if (index < length && bytes[index] == 'Z') { + if (index + 1 >= length || bytes[index + 1] != '"') { + throw new IllegalArgumentException(); + } + position = index + 2; + return OffsetDateTime.of(year, month, day, hour, minute, second, nano, ZoneOffset.UTC); + } + return readIsoOffsetDateTimeOffsetTail( + bytes, index, length, year, month, day, hour, minute, second, nano); + } + + private OffsetDateTime readIsoOffsetDateTimeOffsetTail( + byte[] bytes, + int index, + int length, + int year, + int month, + int day, + int hour, + int minute, + int second, + int nano) { + long offsetAndEnd = parseOffsetAndEnd(bytes, index, length); + position = (int) offsetAndEnd; + return OffsetDateTime.of( + year, + month, + day, + hour, + minute, + second, + nano, + ZoneOffset.ofTotalSeconds((int) (offsetAndEnd >> 32))); + } + + private int scanSimpleStringTail(byte[] bytes, int offset) { + int length = bytes.length; + while (offset < length) { + int b = bytes[offset++]; + if (b == '"') { + return offset; + } + if (b == '\\' || b < 0x20 || b < 0) { + throw new IllegalArgumentException(); + } + } + throw error("Unterminated string"); + } + + private static long parseOffsetAndEnd(byte[] bytes, int index, int length) { + int offset = bytes[index]; + if (offset == 'Z') { + if (index + 1 >= length || bytes[index + 1] != '"') { + throw new IllegalArgumentException(); + } + return ((long) (index + 2)) & 0xFFFF_FFFFL; + } + if (offset != '+' && offset != '-') { + throw new IllegalArgumentException(); + } + if (index + 6 >= length || bytes[index + 3] != ':') { + throw new IllegalArgumentException(); + } + int hour = parse2(bytes, index + 1); + int minute = parse2(bytes, index + 4); + int second = 0; + int end = index + 6; + if (bytes[end] == ':') { + if (end + 3 >= length) { + throw new IllegalArgumentException(); + } + second = parse2(bytes, end + 1); + end += 3; + } + if (bytes[end] != '"') { + throw new IllegalArgumentException(); + } + int total = hour * 3600 + minute * 60 + second; + if (offset == '-') { + total = -total; + } + return ((long) total << 32) | ((long) (end + 1) & 0xFFFF_FFFFL); + } + + private static int parseNano(byte[] bytes, int start, int end) { + int nano = 0; + for (int i = start; i < end; i++) { + nano = nano * 10 + bytes[i] - '0'; + } + for (int i = end - start; i < 9; i++) { + nano *= 10; + } + return nano; + } + + private static int parse4(byte[] bytes, int index) { + return parse2(bytes, index) * 100 + parse2(bytes, index + 2); + } + + private static int parse2(byte[] bytes, int index) { + int high = bytes[index] - '0'; + int low = bytes[index + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + throw new IllegalArgumentException(); + } + return high * 10 + low; + } + + private static boolean isDigit(byte b) { + return b >= '0' && b <= '9'; + } + private String readStringStop(int start, int stop, int ch) { position = stop + 1; if (ch == '\\') { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index a0e3e356f0..11aae896e5 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -177,6 +177,16 @@ public void readUtf8DoubleTokens() { () -> new Utf8JsonReader("01.5".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue()); } + @Test + public void readLatin1DoubleTokens() { + assertEquals(new Latin1JsonReader("12.375").readDouble(), 12.375d); + assertEquals( + Double.doubleToRawLongBits(new Latin1JsonReader("-0.0").readDouble()), + Double.doubleToRawLongBits(-0.0d)); + assertEquals(new Latin1JsonReader("1.25e2").readDouble(), 125.0d); + assertThrows(ForyJsonException.class, () -> new Latin1JsonReader("01.5").readDouble()); + } + @Test public void readUtf8LongBlocks() { assertEquals( @@ -342,6 +352,7 @@ public void readScalarRoots() { ForyJson json = ForyJson.builder().build(); assertEquals(json.fromJson("7", int.class), Integer.valueOf(7)); assertEquals(json.fromJson("true", boolean.class), Boolean.TRUE); + assertEquals(json.fromJson("0.100", BigDecimal.class), new BigDecimal("0.100")); assertEquals(json.fromJson("\"fory\"".getBytes(StandardCharsets.UTF_8), String.class), "fory"); assertEquals( json.fromJson("\"\uD83D\uDE00\u1234\"".getBytes(StandardCharsets.UTF_8), String.class), @@ -358,6 +369,9 @@ public void readScalarRoots() { "\"123e4567-e89b-12d3-a456-426614174000\"".getBytes(StandardCharsets.UTF_8), UUID.class), UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); + assertEquals( + json.fromJson("\"123e4567-e89b-12d3-a456-426614174000\"", UUID.class), + UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); } @Test @@ -374,6 +388,22 @@ public void readGeneratedUtf8BigDecimal() { assertEquals(fields.decimal, new BigDecimal("0.12345678901234567")); } + @Test + public void readGeneratedLatin1Scalars() { + ForyJson json = ForyJson.builder().build(); + String input = + "{\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"," + + "\"decimal\":0.12345678901234567," + + "\"date\":\"2024-02-03\"," + + "\"timestamp\":\"2024-02-03T04:05:06.123456789Z\"}"; + Utf8ScalarFields fields = json.fromJson(input, Utf8ScalarFields.class); + assertEquals(fields.uuid, UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); + assertEquals(fields.decimal, new BigDecimal("0.12345678901234567")); + assertEquals(fields.date, LocalDate.of(2024, 2, 3)); + assertEquals( + fields.timestamp, OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.UTC)); + } + @Test public void readUntypedLargeInteger() { ForyJson json = ForyJson.builder().build(); From 99e9d19837e1ebff3f4987af15077da5b6e8d84f Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 17:55:01 +0800 Subject: [PATCH 070/120] perf(java): speed up Fory JSON Latin1 string scanning --- .../fory/json/reader/Latin1JsonReader.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index dcb72621fb..d424b56fdb 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -1074,6 +1074,31 @@ private String readStringToken() { } int start = position; int offset = start; + int doubleWordEnd = inputLength - (Long.BYTES << 1); + while (offset <= doubleWordEnd) { + long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); + int ch = bytes[stop] & 0xFF; + if (ch == '"') { + position = stop + 1; + return newLatin1String(start, stop); + } + return readStringStop(start, stop, ch); + } + int nextOffset = offset + Long.BYTES; + stopMask = stringStopMask(LittleEndian.getInt64(bytes, nextOffset)); + if (stopMask != 0) { + int stop = nextOffset + (Long.numberOfTrailingZeros(stopMask) >>> 3); + int ch = bytes[stop] & 0xFF; + if (ch == '"') { + position = stop + 1; + return newLatin1String(start, stop); + } + return readStringStop(start, stop, ch); + } + offset = nextOffset + Long.BYTES; + } int wordEnd = inputLength - Long.BYTES; while (offset <= wordEnd) { long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); @@ -1089,6 +1114,11 @@ private String readStringToken() { } return readStringStop(start, stop, ch); } + return readStringTokenTail(start, offset, inputLength); + } + + private String readStringTokenTail(int start, int offset, int inputLength) { + byte[] bytes = input; if (offset + Integer.BYTES <= inputLength) { int stopMask = stringStopMask(LittleEndian.getInt32(bytes, offset)); if (stopMask == 0) { From 49e8b086ba7e7e4659ca8ca9d1e39ab76d09aa30 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 18:56:31 +0800 Subject: [PATCH 071/120] perf(java): shrink Latin1 JSON string reader --- .../fory/json/reader/Latin1JsonReader.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index d424b56fdb..386ce71329 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -1081,8 +1081,7 @@ private String readStringToken() { int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); int ch = bytes[stop] & 0xFF; if (ch == '"') { - position = stop + 1; - return newLatin1String(start, stop); + return closeLatin1String(start, stop); } return readStringStop(start, stop, ch); } @@ -1092,8 +1091,7 @@ private String readStringToken() { int stop = nextOffset + (Long.numberOfTrailingZeros(stopMask) >>> 3); int ch = bytes[stop] & 0xFF; if (ch == '"') { - position = stop + 1; - return newLatin1String(start, stop); + return closeLatin1String(start, stop); } return readStringStop(start, stop, ch); } @@ -1109,8 +1107,7 @@ private String readStringToken() { int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); int ch = bytes[stop] & 0xFF; if (ch == '"') { - position = stop + 1; - return newLatin1String(start, stop); + return closeLatin1String(start, stop); } return readStringStop(start, stop, ch); } @@ -1127,8 +1124,7 @@ private String readStringTokenTail(int start, int offset, int inputLength) { int stop = offset + (Integer.numberOfTrailingZeros(stopMask) >>> 3); int ch = bytes[stop] & 0xFF; if (ch == '"') { - position = stop + 1; - return newLatin1String(start, stop); + return closeLatin1String(start, stop); } return readStringStop(start, stop, ch); } @@ -1136,8 +1132,7 @@ private String readStringTokenTail(int start, int offset, int inputLength) { while (offset < inputLength) { int ch = bytes[offset++] & 0xFF; if (ch == '"') { - position = offset; - return newLatin1String(start, offset - 1); + return closeLatin1String(start, offset - 1); } if (ch == '\\') { return readStringStop(start, offset - 1, ch); @@ -1715,6 +1710,11 @@ protected String slice(int start, int end) { return newLatin1String(start, end); } + private String closeLatin1String(int start, int end) { + position = end + 1; + return newLatin1String(start, end); + } + private String newLatin1String(int start, int end) { int length = end - start; byte[] bytes = new byte[length]; From d068eda372c1663f006c248af0eaeb50fff91fc6 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 19:49:32 +0800 Subject: [PATCH 072/120] perf(java): route ASCII JSON strings through UTF-8 reader --- .../java/org/apache/fory/json/ForyJson.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index 09d4c179e5..bdd4547588 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -33,6 +33,7 @@ import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.memory.LittleEndian; import org.apache.fory.reflect.TypeRef; import org.apache.fory.serializer.StringSerializer; @@ -42,6 +43,7 @@ public final class ForyJson { private static final int INITIAL_BUFFER_SIZE = 8192; private static final int PRIMARY_SLOT = -1; private static final int TEMPORARY_SLOT = -2; + private static final long ASCII_HIGH_BITS = 0x8080808080808080L; private static final int DEFAULT_POOL_SIZE = Math.max(1, Runtime.getRuntime().availableProcessors() * 4); @@ -256,6 +258,11 @@ private Object readJavaStringValue(String json, Type type, Class fallback, Js if (StringSerializer.isBytesBackedString()) { byte coder = StringSerializer.getStringCoder(json); if (StringSerializer.isLatin1Coder(coder)) { + byte[] bytes = StringSerializer.getStringBytes(json); + // ASCII Latin1 input is byte-identical to UTF-8; raw high-bit Latin1 must keep this path. + if (isAscii(bytes)) { + return readUtf8Value(state.utf8Reader(bytes, maxDepth), type, fallback, state); + } return readLatin1Value(state.latin1Reader(json, maxDepth), type, fallback, state); } if (StringSerializer.isUtf16Coder(coder)) { @@ -265,6 +272,25 @@ private Object readJavaStringValue(String json, Type type, Class fallback, Js return readUtf16Value(state.utf16Reader(json, maxDepth), type, fallback, state); } + private static boolean isAscii(byte[] bytes) { + int length = bytes.length; + int offset = 0; + int wordEnd = length - Long.BYTES; + while (offset <= wordEnd) { + if ((LittleEndian.getInt64(bytes, offset) & ASCII_HIGH_BITS) != 0) { + return false; + } + offset += Long.BYTES; + } + while (offset < length) { + if (bytes[offset] < 0) { + return false; + } + offset++; + } + return true; + } + private Object readLatin1Value( Latin1JsonReader reader, Type type, Class fallback, JsonState state) { JsonTypeResolver resolver = state.typeResolver; From 5c85b90136d943c58fb82688ef60cb5360fe4c70 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 21:22:44 +0800 Subject: [PATCH 073/120] fix(java): keep JSON string input on string readers --- .../fory/serializer/StringSerializer.java | 32 +++++++++++ .../java/org/apache/fory/json/ForyJson.java | 57 ++++++++++--------- .../fory/json/reader/Utf16JsonReader.java | 15 +++++ .../org/apache/fory/json/JsonScalarTest.java | 27 +++++++++ .../org/apache/fory/json/JsonStringTest.java | 11 ++++ 5 files changed, 115 insertions(+), 27 deletions(-) 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 2ad935101f..3e9d61e934 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 @@ -442,6 +442,38 @@ public static byte[] getStringBytes(String value) { return (byte[]) getStringValue(value); } + @Internal + public static void copyStringCharsToBytes(String value, byte[] target) { + int length = value.length(); + if (length > (Integer.MAX_VALUE >>> 1)) { + throw new IllegalArgumentException("String is too large"); + } + int numBytes = length << 1; + if (target.length < numBytes) { + throw new IllegalArgumentException("Target byte array is too small"); + } + if (STRING_VALUE_FIELD_IS_CHARS) { + char[] chars = (char[]) getStringValue(value); + int offset = STRING_HAS_COUNT_OFFSET ? getStringOffset(value) : 0; + PlatformStringUtils.copyCharsToBytes(chars, offset, target, 0, numBytes); + return; + } + int byteIndex = 0; + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + target[byteIndex++] = (byte) c; + target[byteIndex++] = (byte) (c >>> 8); + } + } else { + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + target[byteIndex++] = (byte) (c >>> 8); + target[byteIndex++] = (byte) c; + } + } + } + @Internal public static byte getStringCoder(String value) { return PlatformStringUtils.getStringCoder(value); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index bdd4547588..608a5da187 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -33,7 +33,6 @@ import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; -import org.apache.fory.memory.LittleEndian; import org.apache.fory.reflect.TypeRef; import org.apache.fory.serializer.StringSerializer; @@ -41,9 +40,10 @@ public final class ForyJson { private static final int PREFERRED_SLOT_RETRIES = 2; private static final int INITIAL_BUFFER_SIZE = 8192; + private static final int RETAINED_UTF16_BYTES = 64 * 1024; private static final int PRIMARY_SLOT = -1; private static final int TEMPORARY_SLOT = -2; - private static final long ASCII_HIGH_BITS = 0x8080808080808080L; + private static final byte[] EMPTY_BYTES = new byte[0]; private static final int DEFAULT_POOL_SIZE = Math.max(1, Runtime.getRuntime().availableProcessors() * 4); @@ -258,37 +258,15 @@ private Object readJavaStringValue(String json, Type type, Class fallback, Js if (StringSerializer.isBytesBackedString()) { byte coder = StringSerializer.getStringCoder(json); if (StringSerializer.isLatin1Coder(coder)) { - byte[] bytes = StringSerializer.getStringBytes(json); - // ASCII Latin1 input is byte-identical to UTF-8; raw high-bit Latin1 must keep this path. - if (isAscii(bytes)) { - return readUtf8Value(state.utf8Reader(bytes, maxDepth), type, fallback, state); - } + // Keep String input on its reader owner even when ASCII Latin1 bytes match UTF-8; + // custom JsonCodec implementations can observe readLatin1/readUtf8 dispatch. return readLatin1Value(state.latin1Reader(json, maxDepth), type, fallback, state); } if (StringSerializer.isUtf16Coder(coder)) { return readUtf16Value(state.utf16Reader(json, maxDepth), type, fallback, state); } } - return readUtf16Value(state.utf16Reader(json, maxDepth), type, fallback, state); - } - - private static boolean isAscii(byte[] bytes) { - int length = bytes.length; - int offset = 0; - int wordEnd = length - Long.BYTES; - while (offset <= wordEnd) { - if ((LittleEndian.getInt64(bytes, offset) & ASCII_HIGH_BITS) != 0) { - return false; - } - offset += Long.BYTES; - } - while (offset < length) { - if (bytes[offset] < 0) { - return false; - } - offset++; - } - return true; + return readUtf16Value(state.legacyUtf16Reader(json, maxDepth), type, fallback, state); } private Object readLatin1Value( @@ -346,6 +324,7 @@ private static final class JsonState { private final Latin1JsonReader latin1Reader; private final Utf16JsonReader utf16Reader; private final JsonTypeResolver typeResolver; + private byte[] legacyUtf16Bytes; private Type lastRootType; private Class lastRootFallback; private JsonTypeInfo lastRootInfo; @@ -357,6 +336,7 @@ private JsonState(boolean writeNullFields, JsonSharedRegistry sharedRegistry) { latin1Reader = new Latin1JsonReader(); utf16Reader = new Utf16JsonReader(); typeResolver = new JsonTypeResolver(sharedRegistry); + legacyUtf16Bytes = EMPTY_BYTES; } private Latin1JsonReader latin1Reader(String input, int maxDepth) { @@ -371,6 +351,29 @@ private Utf16JsonReader utf16Reader(String input, int maxDepth) { return utf16Reader; } + private Utf16JsonReader legacyUtf16Reader(String input, int maxDepth) { + int length = input.length(); + if (length > (Integer.MAX_VALUE >>> 1)) { + throw new IllegalArgumentException("String is too large"); + } + int numBytes = length << 1; + byte[] bytes; + if (numBytes <= RETAINED_UTF16_BYTES) { + bytes = legacyUtf16Bytes; + if (bytes.length < numBytes) { + bytes = new byte[Math.max(numBytes, INITIAL_BUFFER_SIZE)]; + legacyUtf16Bytes = bytes; + } + } else { + bytes = new byte[numBytes]; + } + // Legacy char[]-backed Strings are converted once so parsing still uses UTF16 byte loads. + StringSerializer.copyStringCharsToBytes(input, bytes); + utf16Reader.resetUtf16Bytes(input, bytes); + utf16Reader.resetDepth(maxDepth); + return utf16Reader; + } + private Utf8JsonReader utf8Reader(byte[] input, int maxDepth) { utf8Reader.reset(input); utf8Reader.resetDepth(maxDepth); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index b2f0f6a058..38f30a972a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -56,6 +56,21 @@ public Utf16JsonReader reset(String input) { return this; } + public Utf16JsonReader resetUtf16Bytes(String input, byte[] bytes) { + int length = input.length(); + if (length > (Integer.MAX_VALUE >>> 1)) { + throw new IllegalArgumentException("String is too large"); + } + if (bytes.length < (length << 1)) { + throw new IllegalArgumentException("UTF16 byte array is too small"); + } + this.input = input; + this.bytes = bytes; + this.length = length; + position = 0; + return this; + } + public void clear() { input = ""; bytes = null; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 11aae896e5..82233a7af2 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -55,6 +55,7 @@ import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.reflect.TypeRef; +import org.apache.fory.serializer.StringSerializer; import org.testng.annotations.Test; public class JsonScalarTest extends ForyJsonTestModels { @@ -512,6 +513,32 @@ public void objectFieldUsesUtf8Codec() { assertEquals(holder.value.mode, "utf8"); } + @Test + public void byteInputUsesUtf8Codec() { + ForyJson json = + ForyJson.builder().registerCodec(ModeAwareValue.class, new ModeAwareCodec()).build(); + ModeAwareValue value = + json.fromJson("{}".getBytes(StandardCharsets.UTF_8), ModeAwareValue.class); + assertEquals(value.mode, "utf8"); + } + + @Test + public void stringInputUsesLatin1Codec() { + ForyJson json = + ForyJson.builder().registerCodec(ModeAwareValue.class, new ModeAwareCodec()).build(); + ModeAwareValue value = json.fromJson("{}", ModeAwareValue.class); + String expected = StringSerializer.isBytesBackedString() ? "latin1" : "utf16"; + assertEquals(value.mode, expected); + } + + @Test + public void stringInputUsesUtf16Codec() { + ForyJson json = + ForyJson.builder().registerCodec(ModeAwareValue.class, new ModeAwareCodec()).build(); + ModeAwareValue value = json.fromJson("{\"ignored\":\"\u0100\"}", ModeAwareValue.class); + assertEquals(value.mode, "utf16"); + } + @Test public void wrapStringScalarParseErrors() { ForyJson json = ForyJson.builder().build(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index d5a6ebf9af..0be47898f9 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -34,6 +34,7 @@ import org.apache.fory.json.data.UnicodeKind; import org.apache.fory.json.data.UnicodeMatrix; import org.apache.fory.json.data.UnicodeValues; +import org.apache.fory.json.reader.Utf16JsonReader; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.serializer.StringSerializer; import org.testng.annotations.Test; @@ -63,6 +64,16 @@ public void writeUtf16StringText() { assertEquals(json.fromJson(json.toJsonBytes(values), UnicodeValues.class).tags, values.tags); } + @Test + public void readCachedUtf16Bytes() { + String input = "\"music \uD834\uDD1E\""; + byte[] bytes = new byte[input.length() << 1]; + StringSerializer.copyStringCharsToBytes(input, bytes); + Utf16JsonReader reader = new Utf16JsonReader().resetUtf16Bytes(input, bytes); + assertEquals(reader.readString(), "music \uD834\uDD1E"); + reader.finish(); + } + @Test public void writeUtf16Char() { ForyJson json = ForyJson.builder().build(); From a80791ba2f62ccf0fdb88c7ac2e5c8065c7c81a4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 22:31:26 +0800 Subject: [PATCH 074/120] perf(java): add exact JSON string array reads --- .../apache/fory/json/codec/ArrayCodec.java | 320 +++++++++++++++++- .../apache/fory/json/JsonContainerTest.java | 39 +++ 2 files changed, 343 insertions(+), 16 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index deb12eed3d..1f8fb59ceb 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -259,16 +259,88 @@ public Object readLatin1( reader.exitDepth(); return new long[0]; } - long[] values = new long[8]; - int size = 0; + rejectNull(reader); + long v0 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0}; + } + rejectNull(reader); + long v1 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1}; + } + rejectNull(reader); + long v2 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2}; + } + rejectNull(reader); + long v3 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3}; + } + return readLatin1Tail(reader, v0, v1, v2, v3); + } + + private long[] readLatin1Tail(Latin1JsonReader reader, long v0, long v1, long v2, long v3) { + rejectNull(reader); + long v4 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4}; + } + rejectNull(reader); + long v5 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5}; + } + rejectNull(reader); + long v6 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6}; + } + rejectNull(reader); + long v7 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + return readLatin1LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7); + } + + private long[] readLatin1LongTail( + Latin1JsonReader reader, + long v0, + long v1, + long v2, + long v3, + long v4, + long v5, + long v6, + long v7) { + long[] values = new long[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + int size = 8; do { rejectNull(reader); if (size == values.length) { values = Arrays.copyOf(values, values.length << 1); } values[size++] = reader.readLongValue(); - } while (reader.consumeNextToken(',')); - reader.expectNextToken(']'); + } while (reader.consumeNextCommaOrEndArray()); reader.exitDepth(); return Arrays.copyOf(values, size); } @@ -285,16 +357,88 @@ public Object readUtf16( reader.exitDepth(); return new long[0]; } - long[] values = new long[8]; - int size = 0; + rejectNull(reader); + long v0 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0}; + } + rejectNull(reader); + long v1 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1}; + } + rejectNull(reader); + long v2 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2}; + } + rejectNull(reader); + long v3 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3}; + } + return readUtf16Tail(reader, v0, v1, v2, v3); + } + + private long[] readUtf16Tail(Utf16JsonReader reader, long v0, long v1, long v2, long v3) { + rejectNull(reader); + long v4 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4}; + } + rejectNull(reader); + long v5 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5}; + } + rejectNull(reader); + long v6 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6}; + } + rejectNull(reader); + long v7 = reader.readLongValue(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + return readUtf16LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7); + } + + private long[] readUtf16LongTail( + Utf16JsonReader reader, + long v0, + long v1, + long v2, + long v3, + long v4, + long v5, + long v6, + long v7) { + long[] values = new long[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + int size = 8; do { rejectNull(reader); if (size == values.length) { values = Arrays.copyOf(values, values.length << 1); } values[size++] = reader.readLongValue(); - } while (reader.consumeNextToken(',')); - reader.expectNextToken(']'); + } while (reader.consumeNextCommaOrEndArray()); reader.exitDepth(); return Arrays.copyOf(values, size); } @@ -799,15 +943,87 @@ public Object readLatin1( reader.exitDepth(); return new String[0]; } - String[] values = new String[8]; - int size = 0; + String v0 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0}; + } + String v1 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1}; + } + String v2 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2}; + } + String v3 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3}; + } + return readLatin1Tail(reader, v0, v1, v2, v3); + } + + private String[] readLatin1Tail( + Latin1JsonReader reader, String v0, String v1, String v2, String v3) { + String v4 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4}; + } + String v5 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5}; + } + String v6 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6}; + } + String v7 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + String v8 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7, v8}; + } + return readLatin1LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7, v8); + } + + private String[] readLatin1LongTail( + Latin1JsonReader reader, + String v0, + String v1, + String v2, + String v3, + String v4, + String v5, + String v6, + String v7, + String v8) { + String[] values = new String[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + values[8] = v8; + int size = 9; do { if (size == values.length) { values = Arrays.copyOf(values, values.length << 1); } values[size++] = reader.readNextNullableString(); - } while (reader.consumeNextToken(',')); - reader.expectNextToken(']'); + } while (reader.consumeNextCommaOrEndArray()); reader.exitDepth(); return Arrays.copyOf(values, size); } @@ -824,15 +1040,87 @@ public Object readUtf16( reader.exitDepth(); return new String[0]; } - String[] values = new String[8]; - int size = 0; + String v0 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0}; + } + String v1 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1}; + } + String v2 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2}; + } + String v3 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3}; + } + return readUtf16Tail(reader, v0, v1, v2, v3); + } + + private String[] readUtf16Tail( + Utf16JsonReader reader, String v0, String v1, String v2, String v3) { + String v4 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4}; + } + String v5 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5}; + } + String v6 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6}; + } + String v7 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + String v8 = reader.readNextNullableString(); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7, v8}; + } + return readUtf16LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7, v8); + } + + private String[] readUtf16LongTail( + Utf16JsonReader reader, + String v0, + String v1, + String v2, + String v3, + String v4, + String v5, + String v6, + String v7, + String v8) { + String[] values = new String[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + values[8] = v8; + int size = 9; do { if (size == values.length) { values = Arrays.copyOf(values, values.length << 1); } values[size++] = reader.readNextNullableString(); - } while (reader.consumeNextToken(',')); - reader.expectNextToken(']'); + } while (reader.consumeNextCommaOrEndArray()); reader.exitDepth(); return Arrays.copyOf(values, size); } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index d441b9de3a..02c4a27551 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -304,6 +304,22 @@ public void readUtf8StringArrays() { new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i"}); } + @Test + public void readStringInputStringArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.fromJson("[\"a\"]", String[].class), new String[] {"a"}); + assertEquals( + json.fromJson("[\"a\",null,\"b\",\"c\",\"d\",\"e\",\"f\",\"g\"]", String[].class), + new String[] {"a", null, "b", "c", "d", "e", "f", "g"}); + assertEquals( + json.fromJson("[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\",\"h\",\"i\"]", String[].class), + new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i"}); + assertEquals( + json.fromJson( + "[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\",\"h\",\"i\",\"\u0100\"]", String[].class), + new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i", "\u0100"}); + } + @Test public void readUtf8LongArrays() { ForyJson json = ForyJson.builder().build(); @@ -320,6 +336,24 @@ public void readUtf8LongArrays() { () -> json.fromJson("[1,null]".getBytes(StandardCharsets.UTF_8), long[].class)); } + @Test + public void readStringInputLongArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.fromJson("[7]", long[].class), new long[] {7L}); + assertEquals( + json.fromJson("[1,2,3,4,5,6,7,8]", long[].class), + new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L}); + assertEquals( + json.fromJson("[1,2,3,4,5,6,7,8,9]", long[].class), + new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L}); + LongArrayUtf16Root root = + json.fromJson( + "{\"values\":[1,2,3,4,5,6,7,8,9],\"text\":\"\u0100\"}", LongArrayUtf16Root.class); + assertEquals(root.values, new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L}); + assertEquals(root.text, "\u0100"); + assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", long[].class)); + } + @Test public void writeReadBoxedPrimitiveArrays() { ForyJson json = ForyJson.builder().build(); @@ -478,6 +512,11 @@ public static final class AtomicArrayFields { public AtomicReferenceArray refs = new AtomicReferenceArray<>(new String[] {"b", null}); } + public static final class LongArrayUtf16Root { + public long[] values; + public String text; + } + private static String arrayNodeJson(int depth) { return "[" + nodeJson(0, depth) + "]"; } From 62b862ea4711e64692429468892f882ea4079453 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 5 Jul 2026 23:03:43 +0800 Subject: [PATCH 075/120] perf(java): streamline JSON Latin1 string reads --- .../fory/json/reader/Latin1JsonReader.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index 386ce71329..200d99b29d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -1024,7 +1024,7 @@ public String readNextNullableString() { if (ch == 'n' && tryReadNullLiteral()) { return null; } - if (!isWhitespace(ch)) { + if (ch > ' ' || !isWhitespace(ch)) { return readStringToken(); } } @@ -1079,9 +1079,10 @@ private String readStringToken() { long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); if (stopMask != 0) { int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); - int ch = bytes[stop] & 0xFF; + int ch = bytes[stop]; if (ch == '"') { - return closeLatin1String(start, stop); + position = stop + 1; + return newLatin1String(start, stop); } return readStringStop(start, stop, ch); } @@ -1089,9 +1090,10 @@ private String readStringToken() { stopMask = stringStopMask(LittleEndian.getInt64(bytes, nextOffset)); if (stopMask != 0) { int stop = nextOffset + (Long.numberOfTrailingZeros(stopMask) >>> 3); - int ch = bytes[stop] & 0xFF; + int ch = bytes[stop]; if (ch == '"') { - return closeLatin1String(start, stop); + position = stop + 1; + return newLatin1String(start, stop); } return readStringStop(start, stop, ch); } @@ -1105,9 +1107,10 @@ private String readStringToken() { continue; } int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); - int ch = bytes[stop] & 0xFF; + int ch = bytes[stop]; if (ch == '"') { - return closeLatin1String(start, stop); + position = stop + 1; + return newLatin1String(start, stop); } return readStringStop(start, stop, ch); } @@ -1122,9 +1125,10 @@ private String readStringTokenTail(int start, int offset, int inputLength) { offset += Integer.BYTES; } else { int stop = offset + (Integer.numberOfTrailingZeros(stopMask) >>> 3); - int ch = bytes[stop] & 0xFF; + int ch = bytes[stop]; if (ch == '"') { - return closeLatin1String(start, stop); + position = stop + 1; + return newLatin1String(start, stop); } return readStringStop(start, stop, ch); } @@ -1132,7 +1136,8 @@ private String readStringTokenTail(int start, int offset, int inputLength) { while (offset < inputLength) { int ch = bytes[offset++] & 0xFF; if (ch == '"') { - return closeLatin1String(start, offset - 1); + position = offset; + return newLatin1String(start, offset - 1); } if (ch == '\\') { return readStringStop(start, offset - 1, ch); @@ -1710,11 +1715,6 @@ protected String slice(int start, int end) { return newLatin1String(start, end); } - private String closeLatin1String(int start, int end) { - position = end + 1; - return newLatin1String(start, end); - } - private String newLatin1String(int start, int end) { int length = end - start; byte[] bytes = new byte[length]; From 4d28c14b594aec7495f7e3d57c5119f094636956 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 00:35:05 +0800 Subject: [PATCH 076/120] perf(java): tune JSON reader group splitting --- .../fory/json/codegen/JsonReaderCodegen.java | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 5721fee7a1..3b2c1bc29b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -878,33 +878,38 @@ private static String readFieldMethod(String readMethod, int start) { } private static int readGroupEnd(JsonFieldInfo[] properties, int start) { - int weight = 0; - int index = start; - while (index < properties.length) { - int fieldWeight = readFieldWeight(properties[index]); - if (index > start && weight + fieldWeight > READ_FIELD_GROUP_SIZE) { - break; - } - weight += fieldWeight; - index++; - if (fieldWeight >= READ_FIELD_GROUP_SIZE) { - break; - } + int end = start + 1; + while (end < properties.length + && end - start < READ_FIELD_GROUP_SIZE + && canPairReadFields(properties[end - 1], properties[end])) { + end++; } - return index; + return end; } - private static int readFieldWeight(JsonFieldInfo property) { - switch (property.readKind()) { - case ARRAY: - case COLLECTION: - case MAP: - case OBJECT: - case ENUM: - return READ_FIELD_GROUP_SIZE; - default: - return 1; + private static boolean canPairReadFields(JsonFieldInfo left, JsonFieldInfo right) { + JsonFieldKind leftKind = left.readKind(); + JsonFieldKind rightKind = right.readKind(); + if (leftKind == null || rightKind == null) { + return false; + } + // Fast-read fallback branches duplicate some field reads in each group. Keep method-size + // estimation conservative so generated helpers stay close to the C2-friendly target. + if (leftKind == JsonFieldKind.ENUM + || rightKind == JsonFieldKind.ENUM + || leftKind == JsonFieldKind.COLLECTION + || rightKind == JsonFieldKind.COLLECTION + || leftKind == JsonFieldKind.MAP + || rightKind == JsonFieldKind.MAP) { + return false; } + if (leftKind == JsonFieldKind.ARRAY || rightKind == JsonFieldKind.ARRAY) { + return false; + } + if (leftKind == JsonFieldKind.OBJECT || rightKind == JsonFieldKind.OBJECT) { + return leftKind == JsonFieldKind.BOOLEAN || rightKind == JsonFieldKind.BOOLEAN; + } + return true; } private static String readMethod(int readerMode) { From e9814e4b8f4a71e1d50a5cc6da457fb4d47549ed Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 01:51:48 +0800 Subject: [PATCH 077/120] perf(java): use Latin1 JSON double token reads --- .../fory/json/codegen/JsonReaderCodegen.java | 2 +- .../apache/fory/json/reader/Latin1JsonReader.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 3b2c1bc29b..cd75a9d798 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -1329,7 +1329,7 @@ private static String readLongMethod(int readerMode, boolean tokenValueRead) { } private static String readDoubleMethod(int readerMode, boolean tokenValueRead) { - if (readerMode == UTF8_READER) { + if (readerMode == LATIN1_READER || readerMode == UTF8_READER) { return tokenValueRead ? "readDoubleTokenValue" : "readNextDoubleValue"; } return "readDouble"; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index 200d99b29d..c5606f401b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -491,6 +491,20 @@ public double readDouble() { return readDoubleToken(); } + public double readNextDoubleValue() { + if (position < input.length) { + int ch = input[position]; + if (ch > ' ' || !isWhitespace(ch)) { + return readDoubleToken(); + } + } + return readDouble(); + } + + public double readDoubleTokenValue() { + return readDoubleToken(); + } + private BigDecimal readBigDecimalToken() { byte[] bytes = input; int offset = position; From 7e0ee96242bd565bedfb922e8cd285af0c744460 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 02:49:46 +0800 Subject: [PATCH 078/120] perf(java): parse Latin1 JSON longs by blocks --- .../fory/json/reader/Latin1JsonReader.java | 99 +++++++++++++++---- .../org/apache/fory/json/JsonScalarTest.java | 16 +++ 2 files changed, 98 insertions(+), 17 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index c5606f401b..5591299a65 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -46,6 +46,12 @@ public final class Latin1JsonReader extends JsonReader { private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); private static final long LONG_MAX_DIV_100 = Long.MAX_VALUE / 100; private static final int LONG_MAX_MOD_100 = (int) (Long.MAX_VALUE % 100); + private static final long LONG_MIN_DIV_10 = Long.MIN_VALUE / 10; + private static final int LONG_MIN_LAST_DIGIT = (int) -(Long.MIN_VALUE % 10); + private static final long EIGHT_DIGITS = 100_000_000L; + private static final long ASCII_ZEROES = 0x3030_3030_3030_3030L; + private static final long ASCII_NINES = 0x3939_3939_3939_3939L; + private static final long ASCII_HIGH_BITS = 0x8080_8080_8080_8080L; // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. // Latin1 string content and field-name hashing must keep unsigned byte conversion. @@ -393,6 +399,16 @@ private long readLongToken() { if (safeEnd > inputLength) { safeEnd = inputLength; } + int block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS + block; + offset += 8; + block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS + block; + offset += 8; + } + } while (offset < safeEnd) { ch = bytes[offset]; if (ch < '0' || ch > '9') { @@ -419,7 +435,7 @@ private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, lon break; } int digit = ch - '0'; - if (result > (Long.MAX_VALUE - digit) / 10) { + if (result > LONG_MAX_DIV_10 || (result == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { position = offset; throw error("Long overflow"); } @@ -432,15 +448,15 @@ private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, lon } private long readNegativeLongToken(int start) { - position = start + 1; - long result = 0; - long limit = Long.MIN_VALUE; - if (position >= input.length) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { throw error("Expected digit"); } - int ch = input[position]; + int ch = bytes[offset]; if (ch == '0') { - position++; + position = offset + 1; rejectLeadingDigitFast(); rejectFractionOrExponentFast(); return 0; @@ -448,27 +464,76 @@ private long readNegativeLongToken(int start) { if (ch < '1' || ch > '9') { throw error("Expected digit"); } - long multmin = limit / 10; - while (position < input.length) { - ch = input[position]; + long result = '0' - ch; + offset++; + int safeEnd = offset + 17; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + int block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS - block; + offset += 8; + block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS - block; + offset += 8; + } + } + while (offset < safeEnd) { + ch = bytes[offset]; if (ch < '0' || ch > '9') { break; } - int digit = ch - '0'; - if (result < multmin) { - throw error("Long overflow"); + result = result * 10 - (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readNegativeLongTail(bytes, offset, inputLength, result); } - result *= 10; - if (result < Long.MIN_VALUE + digit) { + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readNegativeLongTail(byte[] bytes, int offset, int inputLength, long result) { + while (offset < inputLength) { + int ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result < LONG_MIN_DIV_10 || (result == LONG_MIN_DIV_10 && digit > LONG_MIN_LAST_DIGIT)) { + position = offset; throw error("Long overflow"); } - result -= digit; - position++; + result = result * 10 - digit; + offset++; } + position = offset; rejectFractionOrExponentFast(); return result; } + private static int parseEightDigits(byte[] bytes, int offset, int safeEnd) { + if (offset + 8 > safeEnd) { + return -1; + } + // Keep the compact byte-lane arithmetic local to the byte-backed readers so generated parse + // call sites can inline long tokens without eight separate digit checks. + long chunk = LittleEndian.getInt64(bytes, offset); + long digits = chunk - ASCII_ZEROES; + if (((digits | (ASCII_NINES - chunk)) & ASCII_HIGH_BITS) != 0) { + return -1; + } + long pairs = (digits * 10 + (digits >>> 8)) & 0x00FF_00FF_00FF_00FFL; + long quads = (pairs * 100 + (pairs >>> 16)) & 0x0000_FFFF_0000_FFFFL; + return (int) ((quads & 0xFFFF) * 10_000 + (quads >>> 32)); + } + public BigDecimal readBigDecimal() { skipWhitespaceFast(); return readBigDecimalToken(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 82233a7af2..fe401751c3 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -213,6 +213,22 @@ public void readUtf8LongBlocks() { .readLongTokenValue()); } + @Test + public void readLatin1LongBlocks() { + assertEquals( + new Latin1JsonReader("123456789012345678").readLongTokenValue(), 123456789012345678L); + assertEquals( + new Latin1JsonReader("-123456789012345678").readLongTokenValue(), -123456789012345678L); + assertEquals(new Latin1JsonReader("9223372036854775807").readLongTokenValue(), Long.MAX_VALUE); + assertEquals(new Latin1JsonReader("-9223372036854775808").readLongTokenValue(), Long.MIN_VALUE); + assertThrows( + ForyJsonException.class, + () -> new Latin1JsonReader("9223372036854775808").readLongTokenValue()); + assertThrows( + ForyJsonException.class, + () -> new Latin1JsonReader("-9223372036854775809").readLongTokenValue()); + } + @Test public void writeNumericBoundaries() { ForyJson json = ForyJson.builder().build(); From 6c8a110f91dc4bb0a6432a0485e6166fcc0d07d7 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 05:39:34 +0800 Subject: [PATCH 079/120] perf(java): speed up latin1 json string reads --- .../fory/json/reader/Latin1JsonReader.java | 17 ++++++++++++++--- .../org/apache/fory/json/JsonStringTest.java | 4 ++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index 5591299a65..cd8e76ac44 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -1155,7 +1155,7 @@ private String readStringToken() { int offset = start; int doubleWordEnd = inputLength - (Long.BYTES << 1); while (offset <= doubleWordEnd) { - long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + long stopMask = asciiStringStopMask(LittleEndian.getInt64(bytes, offset)); if (stopMask != 0) { int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); int ch = bytes[stop]; @@ -1166,7 +1166,7 @@ private String readStringToken() { return readStringStop(start, stop, ch); } int nextOffset = offset + Long.BYTES; - stopMask = stringStopMask(LittleEndian.getInt64(bytes, nextOffset)); + stopMask = asciiStringStopMask(LittleEndian.getInt64(bytes, nextOffset)); if (stopMask != 0) { int stop = nextOffset + (Long.numberOfTrailingZeros(stopMask) >>> 3); int ch = bytes[stop]; @@ -1180,7 +1180,7 @@ private String readStringToken() { } int wordEnd = inputLength - Long.BYTES; while (offset <= wordEnd) { - long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + long stopMask = asciiStringStopMask(LittleEndian.getInt64(bytes, offset)); if (stopMask == 0) { offset += Long.BYTES; continue; @@ -1411,6 +1411,9 @@ private static boolean isDigit(byte b) { } private String readStringStop(int start, int stop, int ch) { + if (ch < 0) { + return readStringTokenTail(start, stop, input.length); + } position = stop + 1; if (ch == '\\') { StringBuilder builder = newStringBuilder(start, stop); @@ -1425,6 +1428,14 @@ private StringBuilder newStringBuilder(int start, int stop) { return new StringBuilder(Math.max(16, stop - start + 16)); } + private static long asciiStringStopMask(long word) { + // ASCII segments can use the cheaper UTF-8-style syntax scan. High-bit bytes stop this fast + // path and continue through the precise Latin1 scanner, where non-ASCII Latin1 remains valid + // and raw control bytes still fail. + long syntaxStop = ((word ^ QUOTE_BYTES) - BYTE_ONES) | ((word ^ BACKSLASH_BYTES) - BYTE_ONES); + return (syntaxStop | word | (word - CONTROL_LIMIT_BYTES)) & BYTE_HIGH_BITS; + } + private static long stringStopMask(long word) { return byteMatchMask(word, QUOTE_BYTES) | byteMatchMask(word, BACKSLASH_BYTES) diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index 0be47898f9..67767e6b3e 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -262,6 +262,10 @@ public void readStringScanBoundaries() { assertEquals(json.fromJson(input, String.class), value); assertEquals(json.fromJson(input.getBytes(StandardCharsets.UTF_8), String.class), value); } + for (int length : new int[] {7, 8, 15, 16, 23, 24, 31, 32}) { + String value = repeat('a', length) + "\u00e9" + repeat('b', 3); + assertEquals(json.fromJson("\"" + value + "\"", String.class), value); + } String escapedValue = repeat('b', 16) + "\n"; String escapedInput = "\"" + repeat('b', 16) + "\\n\""; assertEquals(json.fromJson(escapedInput, String.class), escapedValue); From 72dd8d17cfe7650dc2d6c436604316775430754e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 07:48:26 +0800 Subject: [PATCH 080/120] perf(java): optimize JSON string collection reads --- .../fory/json/codec/CollectionCodec.java | 518 ++++++++++++++++++ 1 file changed, 518 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index a0af5b054a..698551d5ab 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -235,6 +235,9 @@ public final Object readLatin1( @Override public final Object readLatin1NonNull( Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readLatin1ArrayListNonNull(reader); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -259,6 +262,9 @@ public final Object readUtf16( @Override public final Object readUtf16NonNull( Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readUtf16ArrayListNonNull(reader); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -298,6 +304,240 @@ public final Object readUtf8NonNull( return collection; } + private ArrayList readLatin1ArrayListNonNull(Latin1JsonReader reader) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableLatin1Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableLatin1Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableLatin1Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableLatin1Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + return readLatin1ArrayListTail(reader, e0, e1, e2, e3); + } + + private ArrayList readLatin1ArrayListTail( + Latin1JsonReader reader, Object e0, Object e1, Object e2, Object e3) { + Object e4 = readNullableLatin1Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableLatin1Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + return readLatin1ArrayListLongTail(reader, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readLatin1ArrayListLongTail( + Latin1JsonReader reader, Object e0, Object e1, Object e2, Object e3, Object e4, Object e5) { + Object e6 = readNullableLatin1Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableLatin1Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableLatin1Element(reader)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + + private ArrayList readUtf16ArrayListNonNull(Utf16JsonReader reader) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableUtf16Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableUtf16Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableUtf16Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableUtf16Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + return readUtf16ArrayListTail(reader, e0, e1, e2, e3); + } + + private ArrayList readUtf16ArrayListTail( + Utf16JsonReader reader, Object e0, Object e1, Object e2, Object e3) { + Object e4 = readNullableUtf16Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableUtf16Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + return readUtf16ArrayListLongTail(reader, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readUtf16ArrayListLongTail( + Utf16JsonReader reader, Object e0, Object e1, Object e2, Object e3, Object e4, Object e5) { + Object e6 = readNullableUtf16Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableUtf16Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableUtf16Element(reader)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + private ArrayList readUtf8ArrayListNonNull(Utf8JsonReader reader) { reader.enterDepth(); reader.expectNextToken('['); @@ -700,6 +940,9 @@ public Object readLatin1( @Override public Object readLatin1NonNull( Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readLatin1ArrayListNonNull(reader, resolver); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -727,6 +970,9 @@ public Object readUtf16( @Override public Object readUtf16NonNull( Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readUtf16ArrayListNonNull(reader, resolver); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -772,6 +1018,266 @@ public Object readUtf8NonNull( return collection; } + private ArrayList readLatin1ArrayListNonNull( + Latin1JsonReader reader, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableLatin1Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableLatin1Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableLatin1Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableLatin1Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + return readLatin1ArrayListTail(reader, resolver, e0, e1, e2, e3); + } + + private ArrayList readLatin1ArrayListTail( + Latin1JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3) { + Object e4 = readNullableLatin1Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableLatin1Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + return readLatin1ArrayListLongTail(reader, resolver, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readLatin1ArrayListLongTail( + Latin1JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3, + Object e4, + Object e5) { + Object e6 = readNullableLatin1Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableLatin1Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableLatin1Element(reader, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + + private ArrayList readUtf16ArrayListNonNull( + Utf16JsonReader reader, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableUtf16Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableUtf16Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableUtf16Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableUtf16Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + return readUtf16ArrayListTail(reader, resolver, e0, e1, e2, e3); + } + + private ArrayList readUtf16ArrayListTail( + Utf16JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3) { + Object e4 = readNullableUtf16Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableUtf16Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + return readUtf16ArrayListLongTail(reader, resolver, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readUtf16ArrayListLongTail( + Utf16JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3, + Object e4, + Object e5) { + Object e6 = readNullableUtf16Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableUtf16Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableUtf16Element(reader, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + private ArrayList readUtf8ArrayListNonNull( Utf8JsonReader reader, JsonTypeResolver resolver) { reader.enterDepth(); @@ -902,6 +1408,18 @@ private ArrayList readUtf8ArrayListLongTail( return list; } + private Object readNullableLatin1Element(Latin1JsonReader reader, JsonTypeResolver resolver) { + return reader.tryReadNextNullToken() + ? null + : elementCodec.readLatin1NonNull(reader, elementTypeInfo, resolver); + } + + private Object readNullableUtf16Element(Utf16JsonReader reader, JsonTypeResolver resolver) { + return reader.tryReadNextNullToken() + ? null + : elementCodec.readUtf16NonNull(reader, elementTypeInfo, resolver); + } + private Object readNullableUtf8Element(Utf8JsonReader reader, JsonTypeResolver resolver) { return reader.tryReadNextNullToken() ? null From 5e32ec7ac7b3426293b860deb9374f614bd0aa18 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 11:43:40 +0800 Subject: [PATCH 081/120] fix(java): fix Fory JSON CI failures --- .../apache/fory/json/meta/JsonFieldInfo.java | 32 +++++++++---------- .../fory/json/reader/Latin1JsonReader.java | 10 ++++++ .../fory/json/writer/Utf8JsonWriter.java | 6 ++-- .../fory/json/JsonGeneratedCodecTest.java | 8 +++-- .../org/apache/fory/json/JsonScalarTest.java | 31 ++++++++++++------ 5 files changed, 56 insertions(+), 31 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index b1a4a148f8..9f583171e0 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -198,10 +198,18 @@ public Type writeType() { return writeType; } + private static Type writeType(Field field, Method getter) { + return getter == null ? fieldType(field) : getter.getGenericReturnType(); + } + public Class writeRawType() { return writeRawType; } + private static Class writeRawType(Field field, Method getter) { + return getter == null ? fieldRawType(field) : getter.getReturnType(); + } + public JsonFieldKind writeKind() { return writeKind; } @@ -238,6 +246,10 @@ public Type readType() { return readType; } + private static Type readType(Field field, Method setter) { + return setter == null ? fieldType(field) : setter.getGenericParameterTypes()[0]; + } + public Field readField() { return readField; } @@ -250,6 +262,10 @@ public Class readRawType() { return readRawType; } + private static Class readRawType(Field field, Method setter) { + return setter == null ? fieldRawType(field) : setter.getParameterTypes()[0]; + } + public JsonFieldKind readKind() { return readKind; } @@ -266,22 +282,6 @@ private static Class fieldRawType(Field field) { return field == null ? null : field.getType(); } - private static Type writeType(Field field, Method getter) { - return getter == null ? fieldType(field) : getter.getGenericReturnType(); - } - - private static Class writeRawType(Field field, Method getter) { - return getter == null ? fieldRawType(field) : getter.getReturnType(); - } - - private static Type readType(Field field, Method setter) { - return setter == null ? fieldType(field) : setter.getGenericParameterTypes()[0]; - } - - private static Class readRawType(Field field, Method setter) { - return setter == null ? fieldRawType(field) : setter.getParameterTypes()[0]; - } - public void resolveTypes(JsonTypeResolver typeResolver) { if (writeRawType != null) { writeTypeInfo = typeResolver.getTypeInfo(writeType, writeRawType); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index cd8e76ac44..af4182572a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -61,10 +61,20 @@ public Latin1JsonReader() { input = EMPTY_BYTES; } + public Latin1JsonReader(byte[] input) { + this.input = input; + } + public Latin1JsonReader(String input) { reset(input); } + public Latin1JsonReader reset(byte[] input) { + this.input = input; + position = 0; + return this; + } + public Latin1JsonReader reset(String input) { if (!StringSerializer.isBytesBackedString()) { throw new IllegalStateException("Latin1JsonReader requires byte-backed strings"); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index f89febb66c..641fdbc0f3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -229,7 +229,7 @@ public void writeLocalDate(LocalDate value) { byte[] bytes = buffer; int pos = position; bytes[pos++] = (byte) '"'; - pos = writeLocalDate(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + pos = writeLocalDateBytes(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); bytes[pos++] = (byte) '"'; position = pos; } @@ -244,7 +244,7 @@ public void writeOffsetDateTime(OffsetDateTime value) { byte[] bytes = buffer; int pos = position; bytes[pos++] = (byte) '"'; - pos = writeLocalDate(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + pos = writeLocalDateBytes(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); bytes[pos++] = (byte) 'T'; pos = writeTime( @@ -1271,7 +1271,7 @@ private static int writeHex(byte[] bytes, int pos, long value, int shift, int co return pos; } - private static int writeLocalDate(byte[] bytes, int pos, int year, int month, int day) { + private static int writeLocalDateBytes(byte[] bytes, int pos, int year, int month, int day) { pos = writePadded4(bytes, pos, year); bytes[pos++] = (byte) '-'; pos = writeTwoDigits(bytes, pos, month); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index cf434e72f4..916b50872e 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -169,7 +169,7 @@ public void readLongAsciiFieldToken() { assertTrue(utf8.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); assertEquals(utf8.readNullableStringToken(), "apple"); - Latin1JsonReader latin1 = new Latin1JsonReader(token + "\"pear\""); + Latin1JsonReader latin1 = new Latin1JsonReader(latin1Bytes(token + "\"pear\"")); assertTrue(latin1.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); assertEquals(latin1.readNullableStringToken(), "pear"); @@ -183,7 +183,7 @@ public void readLongAsciiFieldToken() { tailUtf8.tryReadNextFieldNameToken8( tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); assertEquals(tailUtf8.readIntTokenValue(), 1); - Latin1JsonReader tailLatin1 = new Latin1JsonReader(tailToken + "2"); + Latin1JsonReader tailLatin1 = new Latin1JsonReader(latin1Bytes(tailToken + "2")); assertTrue( tailLatin1.tryReadNextFieldNameToken8( tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); @@ -253,6 +253,10 @@ private static void assertLongAsciiFields(LongAsciiFields value) { assertEquals(value.shortName, "core"); } + private static byte[] latin1Bytes(String value) { + return value.getBytes(StandardCharsets.ISO_8859_1); + } + public static class LongAsciiFields { public String registered; public double longitude; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index fe401751c3..46bc32c527 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -180,12 +180,13 @@ public void readUtf8DoubleTokens() { @Test public void readLatin1DoubleTokens() { - assertEquals(new Latin1JsonReader("12.375").readDouble(), 12.375d); + assertEquals(new Latin1JsonReader(latin1Bytes("12.375")).readDouble(), 12.375d); assertEquals( - Double.doubleToRawLongBits(new Latin1JsonReader("-0.0").readDouble()), + Double.doubleToRawLongBits(new Latin1JsonReader(latin1Bytes("-0.0")).readDouble()), Double.doubleToRawLongBits(-0.0d)); - assertEquals(new Latin1JsonReader("1.25e2").readDouble(), 125.0d); - assertThrows(ForyJsonException.class, () -> new Latin1JsonReader("01.5").readDouble()); + assertEquals(new Latin1JsonReader(latin1Bytes("1.25e2")).readDouble(), 125.0d); + assertThrows( + ForyJsonException.class, () -> new Latin1JsonReader(latin1Bytes("01.5")).readDouble()); } @Test @@ -216,17 +217,23 @@ public void readUtf8LongBlocks() { @Test public void readLatin1LongBlocks() { assertEquals( - new Latin1JsonReader("123456789012345678").readLongTokenValue(), 123456789012345678L); + new Latin1JsonReader(latin1Bytes("123456789012345678")).readLongTokenValue(), + 123456789012345678L); assertEquals( - new Latin1JsonReader("-123456789012345678").readLongTokenValue(), -123456789012345678L); - assertEquals(new Latin1JsonReader("9223372036854775807").readLongTokenValue(), Long.MAX_VALUE); - assertEquals(new Latin1JsonReader("-9223372036854775808").readLongTokenValue(), Long.MIN_VALUE); + new Latin1JsonReader(latin1Bytes("-123456789012345678")).readLongTokenValue(), + -123456789012345678L); + assertEquals( + new Latin1JsonReader(latin1Bytes("9223372036854775807")).readLongTokenValue(), + Long.MAX_VALUE); + assertEquals( + new Latin1JsonReader(latin1Bytes("-9223372036854775808")).readLongTokenValue(), + Long.MIN_VALUE); assertThrows( ForyJsonException.class, - () -> new Latin1JsonReader("9223372036854775808").readLongTokenValue()); + () -> new Latin1JsonReader(latin1Bytes("9223372036854775808")).readLongTokenValue()); assertThrows( ForyJsonException.class, - () -> new Latin1JsonReader("-9223372036854775809").readLongTokenValue()); + () -> new Latin1JsonReader(latin1Bytes("-9223372036854775809")).readLongTokenValue()); } @Test @@ -576,6 +583,10 @@ public static final class ClassFieldHolder { public Class type = String.class; } + private static byte[] latin1Bytes(String value) { + return value.getBytes(StandardCharsets.ISO_8859_1); + } + public static final class ClassArrayFields { public Class[] types = new Class[] {String.class}; } From 1ba1ed712b5cf73b9922b19e852b6207157fd5de Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 13:02:43 +0800 Subject: [PATCH 082/120] perf(java): optimize Fory JSON UTF16 temporal reads --- .../apache/fory/json/codec/ScalarCodecs.java | 26 +++ .../fory/json/reader/Utf16JsonReader.java | 203 ++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 36 ++++ 3 files changed, 265 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 271a75c17f..2380a4c933 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -989,6 +989,19 @@ public Object readLatin1( return readStringValue(reader.readString(), typeInfo); } } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoLocalDate(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); + } + } } public static final class LocalTimeCodec extends StringValueCodec { @@ -1202,6 +1215,19 @@ public Object readLatin1( return readStringValue(reader.readString(), typeInfo); } } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoOffsetDateTime(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); + } + } } private static LocalDate parseIsoLocalDate(String value) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index 38f30a972a..4b08a27a36 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -19,6 +19,9 @@ package org.apache.fory.json.reader; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; @@ -615,6 +618,28 @@ public String readNextNullableString() { return readNullableString(); } + public LocalDate readIsoLocalDate() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoLocalDateToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + + public OffsetDateTime readIsoOffsetDateTime() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoOffsetDateTimeToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + private String readStringToken() { if (position >= length || charAtFast(position++) != '"') { throw error("Expected string"); @@ -650,6 +675,184 @@ private String readStringToken() { throw error("Unterminated string"); } + private LocalDate readIsoLocalDateToken() { + int offset = position; + int inputLength = length; + if (offset + 12 > inputLength || charAtFast(offset++) != '"') { + throw error("Expected string"); + } + int dateStart = offset; + if (charAtFast(dateStart + 4) != '-' || charAtFast(dateStart + 7) != '-') { + throw new IllegalArgumentException(); + } + int year = parse4(dateStart); + int month = parse2(dateStart + 5); + int day = parse2(dateStart + 8); + int end = dateStart + 10; + char ch = charAtFast(end); + if (ch == '"') { + position = end + 1; + return LocalDate.of(year, month, day); + } + if (ch == 'T') { + position = scanSimpleStringTail(end + 1); + return LocalDate.of(year, month, day); + } + throw new IllegalArgumentException(); + } + + private OffsetDateTime readIsoOffsetDateTimeToken() { + int offset = position; + int inputLength = length; + if (offset + 19 > inputLength || charAtFast(offset++) != '"') { + throw error("Expected string"); + } + int start = offset; + if (charAtFast(start + 4) != '-' + || charAtFast(start + 7) != '-' + || charAtFast(start + 10) != 'T' + || charAtFast(start + 13) != ':') { + throw new IllegalArgumentException(); + } + int year = parse4(start); + int month = parse2(start + 5); + int day = parse2(start + 8); + int hour = parse2(start + 11); + int minute = parse2(start + 14); + return readIsoOffsetDateTimeTail(start + 16, inputLength, year, month, day, hour, minute); + } + + private OffsetDateTime readIsoOffsetDateTimeTail( + int index, int inputLength, int year, int month, int day, int hour, int minute) { + int second = 0; + int nano = 0; + if (index < inputLength && charAtFast(index) == ':') { + second = parse2(index + 1); + index += 3; + if (index < inputLength && charAtFast(index) == '.') { + int fractionStart = index + 1; + int fractionEnd = fractionStart; + while (fractionEnd < inputLength && isDigit(charAtFast(fractionEnd))) { + fractionEnd++; + } + if (fractionEnd == fractionStart || fractionEnd - fractionStart > 9) { + throw new IllegalArgumentException(); + } + nano = parseNano(fractionStart, fractionEnd); + index = fractionEnd; + } + } + if (index < inputLength && charAtFast(index) == 'Z') { + if (index + 1 >= inputLength || charAtFast(index + 1) != '"') { + throw new IllegalArgumentException(); + } + position = index + 2; + return OffsetDateTime.of(year, month, day, hour, minute, second, nano, ZoneOffset.UTC); + } + return readIsoOffsetDateTimeOffsetTail( + index, inputLength, year, month, day, hour, minute, second, nano); + } + + private OffsetDateTime readIsoOffsetDateTimeOffsetTail( + int index, + int inputLength, + int year, + int month, + int day, + int hour, + int minute, + int second, + int nano) { + long offsetAndEnd = parseOffsetAndEnd(index, inputLength); + position = (int) offsetAndEnd; + return OffsetDateTime.of( + year, + month, + day, + hour, + minute, + second, + nano, + ZoneOffset.ofTotalSeconds((int) (offsetAndEnd >> 32))); + } + + private int scanSimpleStringTail(int offset) { + int inputLength = length; + while (offset < inputLength) { + char ch = charAtFast(offset++); + if (ch == '"') { + return offset; + } + if (ch == '\\' || ch < 0x20 || Character.isSurrogate(ch)) { + throw new IllegalArgumentException(); + } + } + throw error("Unterminated string"); + } + + private long parseOffsetAndEnd(int index, int inputLength) { + char offset = charAtFast(index); + if (offset == 'Z') { + if (index + 1 >= inputLength || charAtFast(index + 1) != '"') { + throw new IllegalArgumentException(); + } + return ((long) (index + 2)) & 0xFFFF_FFFFL; + } + if (offset != '+' && offset != '-') { + throw new IllegalArgumentException(); + } + if (index + 6 >= inputLength || charAtFast(index + 3) != ':') { + throw new IllegalArgumentException(); + } + int hour = parse2(index + 1); + int minute = parse2(index + 4); + int second = 0; + int end = index + 6; + if (charAtFast(end) == ':') { + if (end + 3 >= inputLength) { + throw new IllegalArgumentException(); + } + second = parse2(end + 1); + end += 3; + } + if (charAtFast(end) != '"') { + throw new IllegalArgumentException(); + } + int total = hour * 3600 + minute * 60 + second; + if (offset == '-') { + total = -total; + } + return ((long) total << 32) | ((long) (end + 1) & 0xFFFF_FFFFL); + } + + private int parseNano(int start, int end) { + int nano = 0; + for (int i = start; i < end; i++) { + nano = nano * 10 + charAtFast(i) - '0'; + } + for (int i = end - start; i < 9; i++) { + nano *= 10; + } + return nano; + } + + private int parse4(int index) { + return parse2(index) * 100 + parse2(index + 2); + } + + private int parse2(int index) { + int high = charAtFast(index) - '0'; + int low = charAtFast(index + 1) - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + throw new IllegalArgumentException(); + } + return high * 10 + low; + } + + private static boolean isDigit(char ch) { + return ch >= '0' && ch <= '9'; + } + @Override public JsonFieldInfo readField(JsonFieldTable table) { return table.get(readFieldNameHash()); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 46bc32c527..b5885f80d5 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -527,6 +527,30 @@ public void readOffsetDateTime() { assertEquals(fields.value, minutePrecision); } + @Test + public void readUtf16TemporalScalars() { + LocalDate date = LocalDate.of(2023, 7, 2); + Utf16JsonReader dateReader = utf16Reader("\"2023-07-02T16:00:00.000Z\""); + assertEquals(dateReader.readIsoLocalDate(), date); + dateReader.finish(); + + OffsetDateTime timestamp = + OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.ofHoursMinutes(8, 30)); + Utf16JsonReader timestampReader = utf16Reader("\"2024-02-03T04:05:06.123456789+08:30\""); + assertEquals(timestampReader.readIsoOffsetDateTime(), timestamp); + timestampReader.finish(); + + ForyJson json = ForyJson.builder().build(); + Utf16TemporalFields fields = + json.fromJson( + "{\"text\":\"中文\",\"date\":\"2023-07-02\"," + + "\"timestamp\":\"2024-02-03T04:05:06.123456789+08:30\"}", + Utf16TemporalFields.class); + assertEquals(fields.text, "中文"); + assertEquals(fields.date, date); + assertEquals(fields.timestamp, timestamp); + } + @Test public void objectFieldUsesUtf8Codec() { ForyJson json = @@ -611,6 +635,12 @@ public static final class Utf8ScalarFields { public OffsetDateTime timestamp; } + public static final class Utf16TemporalFields { + public String text; + public LocalDate date; + public OffsetDateTime timestamp; + } + public static final class ModeAwareHolder { public ModeAwareValue value; } @@ -666,4 +696,10 @@ public Object readUtf8( return new ModeAwareValue("utf8"); } } + + private static Utf16JsonReader utf16Reader(String input) { + byte[] bytes = new byte[input.length() << 1]; + StringSerializer.copyStringCharsToBytes(input, bytes); + return new Utf16JsonReader().resetUtf16Bytes(input, bytes); + } } From c9b60ed1ddf4babd5149b2e10a96a30a955b557d Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 13:58:40 +0800 Subject: [PATCH 083/120] perf(java): speed up JSON string writer after UTF16 upgrade --- .../fory/json/writer/StringJsonWriter.java | 55 +++++++++++++++++++ .../org/apache/fory/json/JsonStringTest.java | 13 +++++ 2 files changed, 68 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index c273195f9d..5ef914a65f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -645,6 +645,13 @@ private void writeEscapedChar(char ch) { } private void writeStringUtf16(String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + if (StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(value))) { + writeLatin1StringUtf16(bytes); + return; + } + } int length = value.length(); ensure((length + 2) << 1); writeUtf16ByteNoEnsure((byte) '"'); @@ -660,6 +667,44 @@ private void writeStringUtf16(String value) { writeByteRaw((byte) '"'); } + private void writeLatin1StringUtf16(byte[] value) { + int length = value.length; + ensure((length + 2) << 1); + writeUtf16ByteNoEnsure((byte) '"'); + byte[] target = buffer; + int pos = position; + int i = 0; + int upperBound = length & ~7; + for (; i < upperBound; i += 8) { + long word = LittleEndian.getInt64(value, i); + if (!isJsonAsciiWord(word)) { + break; + } + putLatin1WordAsUtf16(target, pos, word); + pos += 16; + } + while (i < length) { + byte ch = value[i]; + if (isJsonLatin1Byte(ch)) { + pos = putUtf16Char(target, pos, (char) (ch & 0xff)); + i++; + } else { + position = pos; + writeLatin1StringUtf16Slow(value, i, length); + return; + } + } + position = pos; + writeUtf16ByteNoEnsure((byte) '"'); + } + + private void writeLatin1StringUtf16Slow(byte[] value, int index, int length) { + for (int i = index; i < length; i++) { + writeEscapedChar((char) (value[i] & 0xff)); + } + writeByteRaw((byte) '"'); + } + private void writeStringUtf16Slow(String value, int index, int length) { for (int i = index; i < length; i++) { char ch = value.charAt(i); @@ -933,6 +978,16 @@ private static long spreadLatin1ToUtf16(long value) { return (value | (value << 8)) & UTF16_BYTE_MASK; } + private static void putLatin1WordAsUtf16(byte[] target, int offset, long word) { + if (LITTLE_ENDIAN) { + LittleEndian.putInt64(target, offset, spreadLatin1ToUtf16(word & 0xFFFFFFFFL)); + LittleEndian.putInt64(target, offset + 8, spreadLatin1ToUtf16(word >>> 32)); + } else { + LittleEndian.putInt64(target, offset, spreadLatin1ToUtf16(word & 0xFFFFFFFFL) << 8); + LittleEndian.putInt64(target, offset + 8, spreadLatin1ToUtf16(word >>> 32) << 8); + } + } + private static boolean isJsonLatin1(char ch) { return ch > 0x1F && ch <= 0xff && ch != '"' && ch != '\\'; } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index 67767e6b3e..d276d27bfe 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -123,6 +123,19 @@ public void stringWriterResetAfterMaterialize() { } } + @Test + public void stringWriterLatin1AfterUtf16() { + StringJsonWriter writer = new StringJsonWriter(false, new byte[16]); + writer.writeArrayStart(); + writer.writeStringElement(0, "你好"); + writer.writeStringElement(1, "http://example.com/keynote.jpg"); + writer.writeStringElement(2, "café"); + writer.writeStringElement(3, "line\nbreak"); + writer.writeArrayEnd(); + assertEquals( + writer.toJson(), "[\"你好\",\"http://example.com/keynote.jpg\",\"café\",\"line\\nbreak\"]"); + } + @Test public void stringWriterShrinksOnReset() throws Exception { StringJsonWriter writer = new StringJsonWriter(false, new byte[16]); From 6be89edddca4c3fed122d35b2cc141018b66cf32 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 14:23:19 +0800 Subject: [PATCH 084/120] perf(java): speed up JSON UTF16 field probes --- .../fory/json/codegen/JsonReaderCodegen.java | 41 +++++- .../fory/json/reader/Utf16JsonReader.java | 138 ++++++++++++++++++ .../org/apache/fory/json/JsonStringTest.java | 38 +++++ 3 files changed, 216 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index cd75a9d798..1ffe3a4693 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -43,6 +43,7 @@ import org.apache.fory.json.reader.Utf8ObjectReader; import org.apache.fory.json.resolver.JsonTypeInfo; import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.reflect.TypeRef; final class JsonReaderCodegen { @@ -53,6 +54,9 @@ final class JsonReaderCodegen { private static final int MIN_SPLIT_READ_FIELDS = 12; private static final int READ_FIELD_GROUP_SIZE = 2; private static final int READ_FIELD_SWITCH_SIZE = 8; + private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; + private static final long UTF16_PAIR_MASK = 0x0000FFFF0000FFFFL; + private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL; private final JsonCodegen codegen; @@ -854,7 +858,7 @@ private static boolean isAsciiName(String name) { int length = name.length(); for (int i = 0; i < length; i++) { char ch = name.charAt(i); - if (ch == 0 || ch > 0xFF) { + if (ch == '"' || ch == '\\' || ch < 0x20 || ch > 0xFF) { return false; } } @@ -1400,6 +1404,23 @@ private static Expression tryReadNextFieldNameColon(int readerMode, JsonFieldInf Expression.Literal.ofInt(tokenLength)) .inline(); } + if (readerMode == UTF16_READER) { + String name = property.name(); + int length = name.length(); + int tailLength = Math.max(0, length - 4); + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextFieldNameUtf16", + TypeRef.of(boolean.class), + Expression.Literal.ofLong(property.nameHash()), + Expression.Literal.ofLong(packedNameMask(length)), + Expression.Literal.ofLong(utf16NameWord(name, 0, Math.min(length, 4))), + Expression.Literal.ofLong(utf16WordMask(Math.min(length, 4))), + Expression.Literal.ofLong(tailLength == 0 ? 0 : utf16NameWord(name, 4, tailLength)), + Expression.Literal.ofLong(tailLength == 0 ? 0 : utf16WordMask(tailLength)), + Expression.Literal.ofInt(length)) + .inline(); + } return new Expression.Invoke( readerRef(readerMode), "tryReadNextFieldNameColon", @@ -1410,6 +1431,24 @@ private static Expression tryReadNextFieldNameColon(int readerMode, JsonFieldInf .inline(); } + private static long utf16NameWord(String name, int start, int length) { + long value = 0; + for (int i = 0; i < length; i++) { + value |= (long) (name.charAt(start + i) & 0xFF) << (i << 3); + } + long word = spreadLatin1ToUtf16(value); + return LITTLE_ENDIAN ? word : word << 8; + } + + private static long utf16WordMask(int length) { + return length == 4 ? -1L : (1L << (length << 4)) - 1; + } + + private static long spreadLatin1ToUtf16(long value) { + value = (value | (value << 16)) & UTF16_PAIR_MASK; + return (value | (value << 8)) & UTF16_BYTE_MASK; + } + private static Expression tryReadNextStringToken(int readerMode, String token) { int tokenLength = token.length(); int suffixLength = JsonAsciiToken.suffixLength(tokenLength); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index 4b08a27a36..7c0c9797cb 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -25,9 +25,19 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; +import org.apache.fory.memory.LittleEndian; +import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.serializer.StringSerializer; public final class Utf16JsonReader extends JsonReader { + private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; + private static final long BYTE_ONES = 0x0101010101010101L; + private static final long BYTE_HIGH_BITS = 0x8080808080808080L; + private static final long BACKSLASH_BYTES = 0x5c5c5c5c5c5c5c5cL; + private static final long CONTROL_LIMIT_BYTES = 0x2020202020202020L; + private static final long QUOTE_BYTES = 0x2222222222222222L; + private static final long UTF16_PAIR_MASK = 0x0000FFFF0000FFFFL; + private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL; private String input; private byte[] bytes; private int length; @@ -895,8 +905,68 @@ public boolean tryReadNextFieldNameColon( return tryReadFieldNameColon(expectedHash, expectedMask, expectedLength); } + public boolean tryReadNextFieldNameUtf16( + long expectedHash, + long expectedMask, + long firstWord, + long firstMask, + long secondWord, + long secondMask, + int expectedLength) { + byte[] localBytes = bytes; + if (localBytes == null) { + return tryReadNextFieldNameColon(expectedHash, expectedMask, expectedLength); + } + int mark = position; + if (mark < length) { + if (utf16CharEquals(localBytes, mark << 1, '"')) { + return tryReadUtf16FieldNameAt( + localBytes, + mark, + expectedHash, + expectedLength, + firstWord, + firstMask, + secondWord, + secondMask); + } + if (!isWhitespace(charAtFast(mark))) { + return false; + } + } + skipWhitespaceFast(); + return tryReadUtf16FieldNameAt( + localBytes, + mark, + expectedHash, + expectedLength, + firstWord, + firstMask, + secondWord, + secondMask); + } + private boolean tryReadFieldNameColonAt( int mark, long expectedHash, long expectedMask, int expectedLength) { + byte[] localBytes = bytes; + if (localBytes != null + && expectedLength > 0 + && expectedLength <= Long.BYTES + && isJsonPackedName(expectedHash, expectedMask)) { + return tryReadUtf16FieldNameAt( + localBytes, + mark, + expectedHash, + expectedLength, + packedUtf16Word(expectedHash), + utf16WordMask(Math.min(expectedLength, 4)), + expectedLength > 4 ? packedUtf16Word(expectedHash >>> 32) : 0, + expectedLength > 4 ? utf16WordMask(expectedLength - 4) : 0); + } + return tryReadFieldNameColonLoop(mark, expectedHash, expectedLength); + } + + private boolean tryReadFieldNameColonLoop(int mark, long expectedHash, int expectedLength) { int offset = position; int end = offset + expectedLength + 1; if (end < length && charAtFast(offset++) == '"') { @@ -923,6 +993,74 @@ private boolean tryReadFieldNameColonAt( return false; } + private boolean tryReadUtf16FieldNameAt( + byte[] localBytes, + int mark, + long expectedHash, + int expectedLength, + long firstWord, + long firstMask, + long secondWord, + long secondMask) { + int offset = position; + int quoteOffset = offset + expectedLength + 1; + int nameOffset = (offset + 1) << 1; + if ((nameOffset + Long.BYTES > localBytes.length) + || (secondMask != 0 && nameOffset + (Long.BYTES << 1) > localBytes.length)) { + return tryReadFieldNameColonLoop(mark, expectedHash, expectedLength); + } + if (quoteOffset < length + && utf16CharEquals(localBytes, offset << 1, '"') + && (LittleEndian.getInt64(localBytes, nameOffset) & firstMask) == firstWord + && (secondMask == 0 + || (LittleEndian.getInt64(localBytes, nameOffset + Long.BYTES) & secondMask) + == secondWord) + && utf16CharEquals(localBytes, quoteOffset << 1, '"')) { + int colonOffset = quoteOffset + 1; + if (colonOffset < length && utf16CharEquals(localBytes, colonOffset << 1, ':')) { + position = colonOffset + 1; + } else { + readFieldNameColon(colonOffset); + } + return true; + } + position = mark; + return false; + } + + private static boolean isJsonPackedName(long packed, long mask) { + long value = (packed & mask) | (~mask & CONTROL_LIMIT_BYTES); + long control = (value - CONTROL_LIMIT_BYTES) & ~value & BYTE_HIGH_BITS; + return (control | byteMatchMask(value, QUOTE_BYTES) | byteMatchMask(value, BACKSLASH_BYTES)) + == 0; + } + + private static long byteMatchMask(long word, long repeatedByte) { + long match = word ^ repeatedByte; + return (match - BYTE_ONES) & ~match & BYTE_HIGH_BITS; + } + + private static long packedUtf16Word(long packed) { + long word = spreadLatin1ToUtf16(packed & 0xFFFFFFFFL); + return LITTLE_ENDIAN ? word : word << 8; + } + + private static long utf16WordMask(int length) { + return length == 4 ? -1L : (1L << (length << 4)) - 1; + } + + private static long spreadLatin1ToUtf16(long value) { + value = (value | (value << 16)) & UTF16_PAIR_MASK; + return (value | (value << 8)) & UTF16_BYTE_MASK; + } + + private static boolean utf16CharEquals(byte[] localBytes, int byteOffset, int expected) { + if (LITTLE_ENDIAN) { + return (localBytes[byteOffset] & 0xFF) == expected && localBytes[byteOffset + 1] == 0; + } + return localBytes[byteOffset] == 0 && (localBytes[byteOffset + 1] & 0xFF) == expected; + } + private void readFieldNameColon(int colonOffset) { position = colonOffset; expectNextToken(':'); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index d276d27bfe..416160511d 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -20,6 +20,7 @@ package org.apache.fory.json; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; @@ -34,6 +35,7 @@ import org.apache.fory.json.data.UnicodeKind; import org.apache.fory.json.data.UnicodeMatrix; import org.apache.fory.json.data.UnicodeValues; +import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.reader.Utf16JsonReader; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.serializer.StringSerializer; @@ -74,6 +76,32 @@ public void readCachedUtf16Bytes() { reader.finish(); } + @Test + public void readUtf16FieldNameProbe() { + long hash = JsonFieldNameHash.hash("duration"); + int length = "duration".length(); + long mask = packedNameMask(length); + + Utf16JsonReader direct = utf16Reader("\"duration\":123"); + assertTrue(direct.tryReadNextFieldNameColon(hash, mask, length)); + assertEquals(direct.readNextIntValue(), 123); + direct.finish(); + + Utf16JsonReader mismatch = utf16Reader("\"durable\":7"); + assertFalse(mismatch.tryReadNextFieldNameColon(hash, mask, length)); + assertEquals(mismatch.readFieldNameHash(), JsonFieldNameHash.hash("durable")); + mismatch.expectNextToken(':'); + assertEquals(mismatch.readNextIntValue(), 7); + mismatch.finish(); + + Utf16JsonReader escaped = utf16Reader("\"dur\\u0061tion\":9"); + assertFalse(escaped.tryReadNextFieldNameColon(hash, mask, length)); + assertEquals(escaped.readFieldNameHash(), hash); + escaped.expectNextToken(':'); + assertEquals(escaped.readNextIntValue(), 9); + escaped.finish(); + } + @Test public void writeUtf16Char() { ForyJson json = ForyJson.builder().build(); @@ -315,4 +343,14 @@ private static int writerBufferLength(StringJsonWriter writer) throws Exception field.setAccessible(true); return ((byte[]) field.get(writer)).length; } + + private static Utf16JsonReader utf16Reader(String input) { + byte[] bytes = new byte[input.length() << 1]; + StringSerializer.copyStringCharsToBytes(input, bytes); + return new Utf16JsonReader().resetUtf16Bytes(input, bytes); + } + + private static long packedNameMask(int length) { + return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L; + } } From a54efd4a09f5c7e20f801188d037e60babaac987 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 14:33:20 +0800 Subject: [PATCH 085/120] perf(java): scan JSON UTF16 strings by word --- .../fory/json/reader/Utf16JsonReader.java | 105 +++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index 7c0c9797cb..d5bf51be0c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -38,6 +38,13 @@ public final class Utf16JsonReader extends JsonReader { private static final long QUOTE_BYTES = 0x2222222222222222L; private static final long UTF16_PAIR_MASK = 0x0000FFFF0000FFFFL; private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL; + private static final long UTF16_ONES = 0x0001000100010001L; + private static final long UTF16_HIGH_BITS = 0x8000800080008000L; + private static final long UTF16_QUOTE_CHARS = 0x0022002200220022L; + private static final long UTF16_BACKSLASH_CHARS = 0x005c005c005c005cL; + private static final long UTF16_CONTROL_LIMIT = 0x0020002000200020L; + private static final long UTF16_SURROGATE_MASK = 0xf800f800f800f800L; + private static final long UTF16_SURROGATE_PREFIX = 0xd800d800d800d800L; private String input; private byte[] bytes; private int length; @@ -655,7 +662,89 @@ private String readStringToken() { throw error("Expected string"); } int start = position; - StringBuilder builder = null; + if (LITTLE_ENDIAN && bytes != null) { + return readUtf16StringToken(start); + } + return readStringLoop(start, null); + } + + private String readUtf16StringToken(int start) { + byte[] localBytes = bytes; + int inputLength = length; + int offset = start; + int doubleWordEnd = inputLength - 8; + while (offset <= doubleWordEnd) { + long stopMask = utf16StringStopMask(LittleEndian.getInt64(localBytes, offset << 1)); + if (stopMask != 0) { + int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 4); + String value = readUtf16StringStop(start, stop, charAtFast(stop)); + if (value != null) { + return value; + } + offset = position; + continue; + } + int nextOffset = offset + 4; + stopMask = utf16StringStopMask(LittleEndian.getInt64(localBytes, nextOffset << 1)); + if (stopMask != 0) { + int stop = nextOffset + (Long.numberOfTrailingZeros(stopMask) >>> 4); + String value = readUtf16StringStop(start, stop, charAtFast(stop)); + if (value != null) { + return value; + } + offset = position; + continue; + } + offset = nextOffset + 4; + } + int wordEnd = inputLength - 4; + while (offset <= wordEnd) { + long stopMask = utf16StringStopMask(LittleEndian.getInt64(localBytes, offset << 1)); + if (stopMask == 0) { + offset += 4; + continue; + } + int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 4); + String value = readUtf16StringStop(start, stop, charAtFast(stop)); + if (value != null) { + return value; + } + offset = position; + } + position = offset; + return readStringLoop(start, null); + } + + private String readUtf16StringStop(int start, int stop, char ch) { + if (ch == '"') { + position = stop + 1; + return input.substring(start, stop); + } + if (ch == '\\') { + StringBuilder builder = new StringBuilder(Math.max(16, stop - start + 16)); + builder.append(input, start, stop); + position = stop + 1; + appendEscape(builder); + return readStringLoop(position, builder); + } + if (ch < 0x20) { + position = stop; + throw error("Control character in string"); + } + if (Character.isHighSurrogate(ch)) { + int lowOffset = stop + 1; + if (lowOffset >= length || !Character.isLowSurrogate(charAtFast(lowOffset))) { + position = stop; + throw error("Unpaired high surrogate in string"); + } + position = lowOffset + 1; + return null; + } + position = stop; + throw error("Unpaired low surrogate in string"); + } + + private String readStringLoop(int start, StringBuilder builder) { while (position < length) { char ch = charAtFast(position++); if (ch == '"') { @@ -1040,6 +1129,20 @@ private static long byteMatchMask(long word, long repeatedByte) { return (match - BYTE_ONES) & ~match & BYTE_HIGH_BITS; } + private static long utf16StringStopMask(long word) { + long syntaxStop = + utf16CharMatchMask(word, UTF16_QUOTE_CHARS) + | utf16CharMatchMask(word, UTF16_BACKSLASH_CHARS); + long controlStop = (word - UTF16_CONTROL_LIMIT) & ~word & UTF16_HIGH_BITS; + long surrogateStop = utf16CharMatchMask(word & UTF16_SURROGATE_MASK, UTF16_SURROGATE_PREFIX); + return syntaxStop | controlStop | surrogateStop; + } + + private static long utf16CharMatchMask(long word, long repeatedChar) { + long match = word ^ repeatedChar; + return (match - UTF16_ONES) & ~match & UTF16_HIGH_BITS; + } + private static long packedUtf16Word(long packed) { long word = spreadLatin1ToUtf16(packed & 0xFFFFFFFFL); return LITTLE_ENDIAN ? word : word << 8; From 86d066b9df6f9e8fa2211905dbea26bb3f8ef8f9 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 14:58:01 +0800 Subject: [PATCH 086/120] perf(java): speed up JSON UTF16 string writes --- .../fory/json/writer/StringJsonWriter.java | 62 ++++++++++++++++++- .../org/apache/fory/json/JsonStringTest.java | 10 +++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 5ef914a65f..251886d507 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -52,6 +52,13 @@ public final class StringJsonWriter extends JsonWriter { private static final int[] DIGIT_QUADS = new int[10000]; private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL; private static final long UTF16_PAIR_MASK = 0x0000FFFF0000FFFFL; + private static final long UTF16_ONES = 0x0001000100010001L; + private static final long UTF16_HIGH_BITS = 0x8000800080008000L; + private static final long UTF16_QUOTE_CHARS = 0x0022002200220022L; + private static final long UTF16_BACKSLASH_CHARS = 0x005c005c005c005cL; + private static final long UTF16_CONTROL_LIMIT = 0x0020002000200020L; + private static final long UTF16_SURROGATE_MASK = 0xf800f800f800f800L; + private static final long UTF16_SURROGATE_PREFIX = 0xd800d800d800d800L; private static final boolean STRING_BYTES_BACKED = StringSerializer.isBytesBackedString(); private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; @@ -647,10 +654,15 @@ private void writeEscapedChar(char ch) { private void writeStringUtf16(String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - if (StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(value))) { + byte stringCoder = StringSerializer.getStringCoder(value); + if (StringSerializer.isLatin1Coder(stringCoder)) { writeLatin1StringUtf16(bytes); return; } + if (LITTLE_ENDIAN && StringSerializer.isUtf16Coder(stringCoder)) { + writeUtf16StringBytes(value, bytes); + return; + } } int length = value.length(); ensure((length + 2) << 1); @@ -667,6 +679,40 @@ private void writeStringUtf16(String value) { writeByteRaw((byte) '"'); } + private void writeUtf16StringBytes(String value, byte[] source) { + int length = value.length(); + int byteLength = length << 1; + ensure(byteLength + 4); + writeUtf16ByteNoEnsure((byte) '"'); + byte[] target = buffer; + int pos = position; + int index = 0; + int wordEnd = byteLength & ~7; + for (; index < wordEnd; index += 8) { + long word = LittleEndian.getInt64(source, index); + if (utf16StringStopMask(word) != 0) { + position = pos; + writeStringUtf16Slow(value, index >>> 1, length); + return; + } + LittleEndian.putInt64(target, pos, word); + pos += 8; + } + for (; index < byteLength; index += 2) { + char ch = (char) ((source[index] & 0xff) | ((source[index + 1] & 0xff) << 8)); + if (!isJsonUtf16(ch)) { + position = pos; + writeStringUtf16Slow(value, index >>> 1, length); + return; + } + target[pos] = source[index]; + target[pos + 1] = source[index + 1]; + pos += 2; + } + position = pos; + writeUtf16ByteNoEnsure((byte) '"'); + } + private void writeLatin1StringUtf16(byte[] value) { int length = value.length; ensure((length + 2) << 1); @@ -978,6 +1024,20 @@ private static long spreadLatin1ToUtf16(long value) { return (value | (value << 8)) & UTF16_BYTE_MASK; } + private static long utf16StringStopMask(long word) { + long syntaxStop = + utf16CharMatchMask(word, UTF16_QUOTE_CHARS) + | utf16CharMatchMask(word, UTF16_BACKSLASH_CHARS); + long controlStop = (word - UTF16_CONTROL_LIMIT) & ~word & UTF16_HIGH_BITS; + long surrogateStop = utf16CharMatchMask(word & UTF16_SURROGATE_MASK, UTF16_SURROGATE_PREFIX); + return syntaxStop | controlStop | surrogateStop; + } + + private static long utf16CharMatchMask(long word, long repeatedChar) { + long match = word ^ repeatedChar; + return (match - UTF16_ONES) & ~match & UTF16_HIGH_BITS; + } + private static void putLatin1WordAsUtf16(byte[] target, int offset, long word) { if (LITTLE_ENDIAN) { LittleEndian.putInt64(target, offset, spreadLatin1ToUtf16(word & 0xFFFFFFFFL)); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index 416160511d..b2c44fd77f 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -164,6 +164,16 @@ public void stringWriterLatin1AfterUtf16() { writer.toJson(), "[\"你好\",\"http://example.com/keynote.jpg\",\"café\",\"line\\nbreak\"]"); } + @Test + public void stringWriterUtf16Escapes() { + StringJsonWriter writer = new StringJsonWriter(false, new byte[16]); + writer.writeArrayStart(); + writer.writeStringElement(0, "你好"); + writer.writeStringElement(1, "前缀\"\\\\\n\u1234"); + writer.writeArrayEnd(); + assertEquals(writer.toJson(), "[\"你好\",\"前缀\\\"\\\\\\\\\\n\u1234\"]"); + } + @Test public void stringWriterShrinksOnReset() throws Exception { StringJsonWriter writer = new StringJsonWriter(false, new byte[16]); From aac2e54c37a30918ba4cb81d9c8611bc22667541 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 15:09:31 +0800 Subject: [PATCH 087/120] perf(java): inline JSON UTF16 string scan --- .../fory/json/writer/StringJsonWriter.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 251886d507..3ce80097ed 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -690,7 +690,15 @@ private void writeUtf16StringBytes(String value, byte[] source) { int wordEnd = byteLength & ~7; for (; index < wordEnd; index += 8) { long word = LittleEndian.getInt64(source, index); - if (utf16StringStopMask(word) != 0) { + long quote = word ^ UTF16_QUOTE_CHARS; + long backslash = word ^ UTF16_BACKSLASH_CHARS; + long surrogate = (word & UTF16_SURROGATE_MASK) ^ UTF16_SURROGATE_PREFIX; + long stop = + ((quote - UTF16_ONES) & ~quote & UTF16_HIGH_BITS) + | ((backslash - UTF16_ONES) & ~backslash & UTF16_HIGH_BITS) + | ((word - UTF16_CONTROL_LIMIT) & ~word & UTF16_HIGH_BITS) + | ((surrogate - UTF16_ONES) & ~surrogate & UTF16_HIGH_BITS); + if (stop != 0) { position = pos; writeStringUtf16Slow(value, index >>> 1, length); return; @@ -1024,20 +1032,6 @@ private static long spreadLatin1ToUtf16(long value) { return (value | (value << 8)) & UTF16_BYTE_MASK; } - private static long utf16StringStopMask(long word) { - long syntaxStop = - utf16CharMatchMask(word, UTF16_QUOTE_CHARS) - | utf16CharMatchMask(word, UTF16_BACKSLASH_CHARS); - long controlStop = (word - UTF16_CONTROL_LIMIT) & ~word & UTF16_HIGH_BITS; - long surrogateStop = utf16CharMatchMask(word & UTF16_SURROGATE_MASK, UTF16_SURROGATE_PREFIX); - return syntaxStop | controlStop | surrogateStop; - } - - private static long utf16CharMatchMask(long word, long repeatedChar) { - long match = word ^ repeatedChar; - return (match - UTF16_ONES) & ~match & UTF16_HIGH_BITS; - } - private static void putLatin1WordAsUtf16(byte[] target, int offset, long word) { if (LITTLE_ENDIAN) { LittleEndian.putInt64(target, offset, spreadLatin1ToUtf16(word & 0xFFFFFFFFL)); From 3fb712a2018ceda3b7156b9f3a1ae0ae833c2ec6 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 15:21:37 +0800 Subject: [PATCH 088/120] perf(java): compact JSON ASCII word predicate --- .../java/org/apache/fory/json/writer/StringJsonWriter.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 3ce80097ed..caf6768df3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -1056,9 +1056,10 @@ private static boolean isJsonLatin1Byte(byte value) { } private static boolean isJsonAsciiWord(long word) { - return (((word + ASCII_CONTROL_OFFSET) & ~word) & HIGH_BITS) == HIGH_BITS - && (((word ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS - && (((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS; + long control = ((word + ASCII_CONTROL_OFFSET) & ~word) & HIGH_BITS; + long quote = ((word ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS; + long backslash = ((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS; + return (control & quote & backslash) == HIGH_BITS; } private static boolean isJsonAsciiInt(int word) { From 902bc23c8f02c337b80da92db497347039b81d04 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 16:11:11 +0800 Subject: [PATCH 089/120] perf(java): speed up JSON UTF16 field prefixes --- .../fory/json/codegen/JsonWriterCodegen.java | 111 ++++++++++++-- .../apache/fory/json/meta/JsonFieldInfo.java | 27 ++++ .../fory/json/writer/StringJsonWriter.java | 145 ++++++++++++++++++ 3 files changed, 269 insertions(+), 14 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index 3797c7bd3f..dc5a241b85 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -128,6 +128,8 @@ String genWriterCode( } else { ctx.addField(byte[].class, "s" + i); ctx.addField(byte[].class, "sc" + i); + ctx.addField(byte[].class, "s16" + i); + ctx.addField(byte[].class, "sc16" + i); } } } @@ -207,6 +209,17 @@ private Expression writerConstructorExpression(JsonFieldInfo[] properties, boole new Reference("this.sc" + i, TypeRef.of(byte[].class)), new Expression.Invoke(property, "stringCommaNamePrefix", TypeRef.of(byte[].class)) .inline())); + expressions.add( + new Expression.Assign( + new Reference("this.s16" + i, TypeRef.of(byte[].class)), + new Expression.Invoke(property, "stringUtf16NamePrefix", TypeRef.of(byte[].class)) + .inline())); + expressions.add( + new Expression.Assign( + new Reference("this.sc16" + i, TypeRef.of(byte[].class)), + new Expression.Invoke( + property, "stringUtf16CommaNamePrefix", TypeRef.of(byte[].class)) + .inline())); } } } @@ -324,6 +337,14 @@ private static Expression writeObjectStartPrimitive( return new Expression.Invoke( writer, "writeObjectIntField", packedPrefixArgs(property, false, value)); } + if (!utf8) { + return new Expression.Invoke( + writer, + "writeObjectIntField", + prefixRef(false, false, 0), + utf16PrefixRef(false, 0), + value); + } return new Expression.Invoke( writer, "writeObjectIntField", prefixRef(utf8, false, 0), value); case LONG: @@ -331,6 +352,14 @@ private static Expression writeObjectStartPrimitive( return new Expression.Invoke( writer, "writeObjectLongField", packedPrefixArgs(property, false, value)); } + if (!utf8) { + return new Expression.Invoke( + writer, + "writeObjectLongField", + prefixRef(false, false, 0), + utf16PrefixRef(false, 0), + value); + } return new Expression.Invoke( writer, "writeObjectLongField", prefixRef(utf8, false, 0), value); default: @@ -425,6 +454,10 @@ private static Expression writeNumberField( if (utf8 && canPackPrefix(property, true)) { return new Expression.Invoke(writer, writerMethod, packedPrefixArgs(property, true, value)); } + if (!utf8) { + return new Expression.Invoke( + writer, writerMethod, prefixRef(false, true, id), utf16PrefixRef(true, id), value); + } return new Expression.Invoke(writer, writerMethod, prefixRef(utf8, true, id), value); } if (utf8 && canPackSinglePrefix(property, false) && canPackSinglePrefix(property, true)) { @@ -435,13 +468,23 @@ writer, writerMethod, packedDynamicPrefixArgs(property, index, value)), } Expression.ListExpression expressions = new Expression.ListExpression( - new Expression.Invoke( - writer, - writerMethod, - prefixRef(utf8, false, id), - prefixRef(utf8, true, id), - index, - value)); + utf8 + ? new Expression.Invoke( + writer, + writerMethod, + prefixRef(true, false, id), + prefixRef(true, true, id), + index, + value) + : new Expression.Invoke( + writer, + writerMethod, + prefixRef(false, false, id), + prefixRef(false, true, id), + utf16PrefixRef(false, id), + utf16PrefixRef(true, id), + index, + value)); expressions.add(increment(index)); return expressions; } @@ -459,6 +502,14 @@ private static Expression writeStringField( return new Expression.Invoke( writer, "writeStringField", packedPrefixArgs(property, true, value)); } + if (!utf8) { + return new Expression.Invoke( + writer, + "writeStringField", + prefixRef(false, true, id), + utf16PrefixRef(true, id), + value); + } return new Expression.Invoke(writer, "writeStringField", prefixRef(utf8, true, id), value); } if (utf8 && canPackSinglePrefix(property, false) && canPackSinglePrefix(property, true)) { @@ -471,13 +522,23 @@ private static Expression writeStringField( } Expression.ListExpression expressions = new Expression.ListExpression( - new Expression.Invoke( - writer, - "writeStringField", - prefixRef(utf8, false, id), - prefixRef(utf8, true, id), - index, - value)); + utf8 + ? new Expression.Invoke( + writer, + "writeStringField", + prefixRef(true, false, id), + prefixRef(true, true, id), + index, + value) + : new Expression.Invoke( + writer, + "writeStringField", + prefixRef(false, false, id), + prefixRef(false, true, id), + utf16PrefixRef(false, id), + utf16PrefixRef(true, id), + index, + value)); expressions.add(increment(index)); return expressions; } @@ -492,6 +553,24 @@ private static Expression writeFieldName( if (commaKnown && utf8 && canPackPrefix(property, true)) { return new Expression.Invoke(writer, "writeRawValue", packedPrefixArgs(property, true)); } + if (!utf8 && commaKnown) { + return new Expression.Invoke( + writer, "writeRawValue", prefixRef(false, true, id), utf16PrefixRef(true, id)); + } + if (!utf8) { + Expression.ListExpression expressions = + new Expression.ListExpression( + new Expression.Invoke( + writer, + "writeRawValue", + prefixRef(false, false, id), + prefixRef(false, true, id), + utf16PrefixRef(false, id), + utf16PrefixRef(true, id), + index)); + expressions.add(increment(index)); + return expressions; + } Expression prefix = commaKnown ? prefixRef(utf8, true, id) @@ -764,6 +843,10 @@ private static Reference prefixRef(boolean utf8, boolean comma, int id) { return fieldRef(prefix + id, byte[].class); } + private static Reference utf16PrefixRef(boolean comma, int id) { + return fieldRef((comma ? "sc16" : "s16") + id, byte[].class); + } + private static Expression commaFlag(boolean commaKnown, Expression index) { return commaKnown ? Expression.Literal.True : ne(index, Expression.Literal.ofInt(0)); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index 9f583171e0..6e30658a49 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -34,6 +34,7 @@ import org.apache.fory.json.writer.JsonWriter; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.memory.NativeByteOrder; public final class JsonFieldInfo { private static final int KIND_BOOLEAN = 1; @@ -75,6 +76,8 @@ public final class JsonFieldInfo { private final Class readElementRawType; private final byte[] stringNamePrefix; private final byte[] stringCommaNamePrefix; + private final byte[] stringUtf16NamePrefix; + private final byte[] stringUtf16CommaNamePrefix; private final byte[] utf8NamePrefix; private final byte[] utf8CommaNamePrefix; private final byte[][] stringEnumValues; @@ -135,6 +138,8 @@ public JsonFieldInfo( String utf8Prefix = JsonStringEscaper.escapedNamePrefix(name, false); stringNamePrefix = stringPrefix.getBytes(StandardCharsets.ISO_8859_1); stringCommaNamePrefix = ("," + stringPrefix).getBytes(StandardCharsets.ISO_8859_1); + stringUtf16NamePrefix = toUtf16Bytes(stringNamePrefix); + stringUtf16CommaNamePrefix = toUtf16Bytes(stringCommaNamePrefix); utf8NamePrefix = utf8Prefix.getBytes(StandardCharsets.UTF_8); utf8CommaNamePrefix = ("," + utf8Prefix).getBytes(StandardCharsets.UTF_8); stringEnumValues = writeKind == JsonFieldKind.ENUM ? stringEnumValues(writeRawType) : null; @@ -330,6 +335,14 @@ public byte[] stringCommaNamePrefix() { return stringCommaNamePrefix; } + public byte[] stringUtf16NamePrefix() { + return stringUtf16NamePrefix; + } + + public byte[] stringUtf16CommaNamePrefix() { + return stringUtf16CommaNamePrefix; + } + public byte[] utf8NamePrefix() { return utf8NamePrefix; } @@ -1140,4 +1153,18 @@ private static byte[] join(byte[] prefix, byte[] token) { System.arraycopy(token, 0, joined, prefix.length, token.length); return joined; } + + private static byte[] toUtf16Bytes(byte[] latin1) { + byte[] utf16 = new byte[latin1.length << 1]; + if (NativeByteOrder.IS_LITTLE_ENDIAN) { + for (int i = 0, j = 0; i < latin1.length; i++, j += 2) { + utf16[j] = latin1[i]; + } + } else { + for (int i = 0, j = 0; i < latin1.length; i++, j += 2) { + utf16[j + 1] = latin1[i]; + } + } + return utf16; + } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index caf6768df3..42007af112 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -277,6 +277,20 @@ public void writeIntField(byte[] namePrefix, byte[] commaNamePrefix, int index, writeIntField(prefix, value); } + public void writeIntField( + byte[] namePrefix, + byte[] commaNamePrefix, + byte[] utf16NamePrefix, + byte[] utf16CommaNamePrefix, + int index, + int value) { + if (coder == LATIN1) { + writeIntField(index == 0 ? namePrefix : commaNamePrefix, value); + return; + } + writeIntFieldUtf16Value(index == 0 ? utf16NamePrefix : utf16CommaNamePrefix, value); + } + public void writeIntField(byte[] prefix, int value) { if (coder == LATIN1) { writeIntFieldLatin1(prefix, value); @@ -285,6 +299,14 @@ public void writeIntField(byte[] prefix, int value) { writeIntFieldUtf16(prefix, value); } + public void writeIntField(byte[] prefix, byte[] utf16Prefix, int value) { + if (coder == LATIN1) { + writeIntFieldLatin1(prefix, value); + return; + } + writeIntFieldUtf16Value(utf16Prefix, value); + } + private void writeIntFieldLatin1(byte[] prefix, int value) { ensure(prefix.length + 11); writeRawLatin1NoEnsure(prefix); @@ -297,6 +319,12 @@ private void writeIntFieldUtf16(byte[] prefix, int value) { writeIntUtf16NoEnsure(value); } + private void writeIntFieldUtf16Value(byte[] utf16Prefix, int value) { + ensure(utf16Prefix.length + 22); + writeRawUtf16ValueNoEnsure(utf16Prefix); + writeIntUtf16NoEnsure(value); + } + public void writeObjectIntField(byte[] namePrefix, int value) { if (coder == LATIN1) { writeObjectIntFieldLatin1(namePrefix, value); @@ -305,6 +333,14 @@ public void writeObjectIntField(byte[] namePrefix, int value) { writeObjectIntFieldUtf16(namePrefix, value); } + public void writeObjectIntField(byte[] namePrefix, byte[] utf16NamePrefix, int value) { + if (coder == LATIN1) { + writeObjectIntFieldLatin1(namePrefix, value); + return; + } + writeObjectIntFieldUtf16Value(utf16NamePrefix, value); + } + private void writeObjectIntFieldLatin1(byte[] namePrefix, int value) { ensure(namePrefix.length + 12); buffer[position++] = (byte) '{'; @@ -319,11 +355,32 @@ private void writeObjectIntFieldUtf16(byte[] namePrefix, int value) { writeIntUtf16NoEnsure(value); } + private void writeObjectIntFieldUtf16Value(byte[] utf16NamePrefix, int value) { + ensure(utf16NamePrefix.length + 24); + writeUtf16ByteNoEnsure((byte) '{'); + writeRawUtf16ValueNoEnsure(utf16NamePrefix); + writeIntUtf16NoEnsure(value); + } + public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, long value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeLongField(prefix, value); } + public void writeLongField( + byte[] namePrefix, + byte[] commaNamePrefix, + byte[] utf16NamePrefix, + byte[] utf16CommaNamePrefix, + int index, + long value) { + if (coder == LATIN1) { + writeLongField(index == 0 ? namePrefix : commaNamePrefix, value); + return; + } + writeLongFieldUtf16Value(index == 0 ? utf16NamePrefix : utf16CommaNamePrefix, value); + } + public void writeLongField(byte[] prefix, long value) { if (coder == LATIN1) { writeLongFieldLatin1(prefix, value); @@ -332,6 +389,14 @@ public void writeLongField(byte[] prefix, long value) { writeLongFieldUtf16(prefix, value); } + public void writeLongField(byte[] prefix, byte[] utf16Prefix, long value) { + if (coder == LATIN1) { + writeLongFieldLatin1(prefix, value); + return; + } + writeLongFieldUtf16Value(utf16Prefix, value); + } + private void writeLongFieldLatin1(byte[] prefix, long value) { ensure(prefix.length + 20); writeRawLatin1NoEnsure(prefix); @@ -344,6 +409,12 @@ private void writeLongFieldUtf16(byte[] prefix, long value) { writeLongUtf16NoEnsure(value); } + private void writeLongFieldUtf16Value(byte[] utf16Prefix, long value) { + ensure(utf16Prefix.length + 40); + writeRawUtf16ValueNoEnsure(utf16Prefix); + writeLongUtf16NoEnsure(value); + } + public void writeObjectLongField(byte[] namePrefix, long value) { if (coder == LATIN1) { writeObjectLongFieldLatin1(namePrefix, value); @@ -352,6 +423,14 @@ public void writeObjectLongField(byte[] namePrefix, long value) { writeObjectLongFieldUtf16(namePrefix, value); } + public void writeObjectLongField(byte[] namePrefix, byte[] utf16NamePrefix, long value) { + if (coder == LATIN1) { + writeObjectLongFieldLatin1(namePrefix, value); + return; + } + writeObjectLongFieldUtf16Value(utf16NamePrefix, value); + } + private void writeObjectLongFieldLatin1(byte[] namePrefix, long value) { ensure(namePrefix.length + 21); buffer[position++] = (byte) '{'; @@ -366,11 +445,32 @@ private void writeObjectLongFieldUtf16(byte[] namePrefix, long value) { writeLongUtf16NoEnsure(value); } + private void writeObjectLongFieldUtf16Value(byte[] utf16NamePrefix, long value) { + ensure(utf16NamePrefix.length + 42); + writeUtf16ByteNoEnsure((byte) '{'); + writeRawUtf16ValueNoEnsure(utf16NamePrefix); + writeLongUtf16NoEnsure(value); + } + public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeStringField(prefix, value); } + public void writeStringField( + byte[] namePrefix, + byte[] commaNamePrefix, + byte[] utf16NamePrefix, + byte[] utf16CommaNamePrefix, + int index, + String value) { + if (coder == LATIN1) { + writeStringField(index == 0 ? namePrefix : commaNamePrefix, value); + return; + } + writeStringFieldUtf16Value(index == 0 ? utf16NamePrefix : utf16CommaNamePrefix, value); + } + public void writeStringField(byte[] prefix, String value) { if (coder == LATIN1) { writeStringFieldLatin1(prefix, value); @@ -379,6 +479,14 @@ public void writeStringField(byte[] prefix, String value) { writeStringFieldUtf16(prefix, value); } + public void writeStringField(byte[] prefix, byte[] utf16Prefix, String value) { + if (coder == LATIN1) { + writeStringFieldLatin1(prefix, value); + return; + } + writeStringFieldUtf16Value(utf16Prefix, value); + } + private void writeStringFieldLatin1(byte[] prefix, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); @@ -401,6 +509,12 @@ private void writeStringFieldUtf16(byte[] prefix, String value) { writeString(value); } + private void writeStringFieldUtf16Value(byte[] utf16Prefix, String value) { + ensure(utf16Prefix.length); + writeRawUtf16ValueNoEnsure(utf16Prefix); + writeStringUtf16(value); + } + public void writeStringElement(int index, String value) { int comma = index == 0 ? 0 : 1; if (value == null) { @@ -461,6 +575,27 @@ public void writeRawValue(byte[] value) { writeRaw(value); } + public void writeRawValue(byte[] value, byte[] utf16Value) { + if (coder == LATIN1) { + writeRaw(value); + return; + } + writeRawUtf16Value(utf16Value); + } + + public void writeRawValue( + byte[] namePrefix, + byte[] commaNamePrefix, + byte[] utf16NamePrefix, + byte[] utf16CommaNamePrefix, + int index) { + if (coder == LATIN1) { + writeRaw(index == 0 ? namePrefix : commaNamePrefix); + return; + } + writeRawUtf16Value(index == 0 ? utf16NamePrefix : utf16CommaNamePrefix); + } + @Override public void writeObjectStart() { writeByteRaw((byte) '{'); @@ -867,6 +1002,16 @@ private void writeRawUtf16(byte[] bytes) { writeRawUtf16NoEnsure(bytes); } + private void writeRawUtf16Value(byte[] bytes) { + ensure(bytes.length); + writeRawUtf16ValueNoEnsure(bytes); + } + + private void writeRawUtf16ValueNoEnsure(byte[] bytes) { + System.arraycopy(bytes, 0, buffer, position, bytes.length); + position += bytes.length; + } + private void writeRawUtf16NoEnsure(byte[] bytes) { byte[] target = buffer; int pos = position; From e8686eca4ecaa052636cb8d282ccaa9ae6e427b1 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 16:19:16 +0800 Subject: [PATCH 090/120] perf(java): speed up JSON string list writes --- .../fory/json/codegen/JsonWriterCodegen.java | 13 +---------- .../fory/json/writer/StringJsonWriter.java | 22 +++++++++++++++++++ .../fory/json/JsonGeneratedCodecTest.java | 3 ++- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index dc5a241b85..6ec2bc7b38 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -784,18 +784,7 @@ private static Expression writeExactUtf8Array( private static Expression writeStringCollection( Expression value, boolean utf8, Expression writer) { - if (utf8) { - return new Expression.Invoke(writer, "writeStringCollection", value); - } - return new Expression.ListExpression( - new Expression.Invoke(writer, "writeArrayStart"), - new Expression.ForEach( - value, - TypeRef.of(String.class), - true, - (index, element) -> - new Expression.Invoke(writer, "writeStringElement", index, element)), - new Expression.Invoke(writer, "writeArrayEnd")); + return new Expression.Invoke(writer, "writeStringCollection", value); } private Expression writeScalar(JsonFieldKind kind, Expression value, Expression writer) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 42007af112..15474bc824 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -20,7 +20,9 @@ package org.apache.fory.json.writer; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.memory.LittleEndian; @@ -571,6 +573,26 @@ private void writeStringElementUtf16(int comma, String value) { writeStringUtf16(value); } + public void writeStringCollection(Collection values) { + writeArrayStart(); + if (values.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) values; + int size = list.size(); + if (size != 0) { + writeStringElement(0, list.get(0)); + for (int i = 1; i < size; i++) { + writeStringElement(1, list.get(i)); + } + } + } else { + int index = 0; + for (String value : values) { + writeStringElement(index++, value); + } + } + writeArrayEnd(); + } + public void writeRawValue(byte[] value) { writeRaw(value); } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index 916b50872e..e693758f42 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -26,6 +26,7 @@ import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.atomic.AtomicReference; import org.apache.fory.json.codec.BaseObjectCodec; @@ -69,7 +70,7 @@ public void writeGeneratedTokenChanges() { assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), first); value.count = 7; value.name = "beta"; - value.tags = Arrays.asList("z", "x"); + value.tags = new ArrayList<>(Arrays.asList("z", "x")); value.total = 9; String second = "{\"count\":7,\"name\":\"beta\",\"tags\":[\"z\",\"x\"],\"total\":9}"; assertEquals(json.toJson(value), second); From 120f1b5a4a7ab5b31332caa2083af88c1a528879 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 16:49:23 +0800 Subject: [PATCH 091/120] perf(java): speed up JSON UTF16 numeric writes --- .../fory/json/writer/StringJsonWriter.java | 134 ++++++++++-------- .../org/apache/fory/json/JsonScalarTest.java | 63 ++++++++ 2 files changed, 135 insertions(+), 62 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 15474bc824..a7b08bf09d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -37,6 +37,7 @@ public final class StringJsonWriter extends JsonWriter { private static final byte[] MIN_INT_BYTES = "-2147483648".getBytes(StandardCharsets.ISO_8859_1); private static final byte[] MIN_LONG_BYTES = "-9223372036854775808".getBytes(StandardCharsets.ISO_8859_1); + private static final long DECIMAL_8 = 100_000_000L; private static final long HIGH_BITS = 0x8080808080808080L; private static final int INT_HIGH_BITS = 0x80808080; private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; @@ -47,9 +48,6 @@ public final class StringJsonWriter extends JsonWriter { private static final int INT_QUOTE_BYTES_COMPLEMENT = ~0x22222222; private static final long BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C5C5C5C5CL; private static final int INT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C; - private static final byte[] DIGIT_HUNDREDS = new byte[1000]; - private static final byte[] DIGIT_TENS = new byte[1000]; - private static final byte[] DIGIT_ONES = new byte[1000]; private static final int[] DIGIT_TRIPLES = new int[1000]; private static final int[] DIGIT_QUADS = new int[10000]; private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL; @@ -69,9 +67,6 @@ public final class StringJsonWriter extends JsonWriter { int c0 = '0' + i / 100; int c1 = '0' + (i / 10) % 10; int c2 = '0' + i % 10; - DIGIT_HUNDREDS[i] = (byte) c0; - DIGIT_TENS[i] = (byte) c1; - DIGIT_ONES[i] = (byte) c2; int skip = i < 10 ? 2 : i < 100 ? 1 : 0; DIGIT_TRIPLES[i] = skip | (c0 << 8) | (c1 << 16) | (c2 << 24); } @@ -1088,19 +1083,6 @@ private void reverse(int start, int end) { } } - private void reverseUtf16Chars(int start, int end) { - while (start < end) { - byte b0 = buffer[start]; - byte b1 = buffer[start + 1]; - buffer[start] = buffer[end]; - buffer[start + 1] = buffer[end + 1]; - buffer[end] = b0; - buffer[end + 1] = b1; - start += 2; - end -= 2; - } - } - private void writeUtf16ByteNoEnsure(byte value) { position = putUtf16Char(buffer, position, (char) (value & 0xff)); } @@ -1295,11 +1277,13 @@ private void writeIntUtf16NoEnsure(int value) { writeRawUtf16NoEnsure(MIN_INT_BYTES); return; } + byte[] bytes = buffer; + int pos = position; if (value < 0) { - writeUtf16ByteNoEnsure((byte) '-'); + pos = putUtf16Byte(bytes, pos, (byte) '-'); value = -value; } - writePositiveIntUtf16NoEnsure(value); + position = writePositiveIntUtf16(bytes, pos, value); } private void writeLongUtf16NoEnsure(long value) { @@ -1307,20 +1291,13 @@ private void writeLongUtf16NoEnsure(long value) { writeRawUtf16NoEnsure(MIN_LONG_BYTES); return; } + byte[] bytes = buffer; + int pos = position; if (value < 0) { - writeUtf16ByteNoEnsure((byte) '-'); + pos = putUtf16Byte(bytes, pos, (byte) '-'); value = -value; } - if (value <= Integer.MAX_VALUE) { - writePositiveIntUtf16NoEnsure((int) value); - return; - } - int start = position; - do { - writeUtf16CharNoEnsure((char) ('0' + value % 10)); - value /= 10; - } while (value != 0); - reverseUtf16Chars(start, position - 2); + position = writePositiveLongUtf16(bytes, pos, value); } private void writePositiveIntNoEnsure(int value) { @@ -1347,23 +1324,40 @@ private void writePositiveIntNoEnsure(int value) { position = writePadded8(bytes, pos, middle, low); } - private void writePositiveIntUtf16NoEnsure(int value) { + private static int writePositiveIntUtf16(byte[] bytes, int pos, int value) { if (value < 10000) { - writeIntUpTo4Utf16(value); - return; + return writeIntUpTo4Utf16(bytes, pos, value); } int high = divide10000(value); int low = value - high * 10000; if (high < 10000) { - writeIntUpTo4Utf16(high); - writePadded4Utf16(low); - return; + if (high >= 1000) { + return writePadded8Utf16(bytes, pos, high, low); + } + pos = writeIntUpTo4Utf16(bytes, pos, high); + return writePadded4Utf16(bytes, pos, low); } int top = divide10000(high); int middle = high - top * 10000; - writeIntUpTo4Utf16(top); - writePadded4Utf16(middle); - writePadded4Utf16(low); + pos = writeIntUpTo4Utf16(bytes, pos, top); + return writePadded8Utf16(bytes, pos, middle, low); + } + + private static int writePositiveLongUtf16(byte[] bytes, int pos, long value) { + if (value <= Integer.MAX_VALUE) { + return writePositiveIntUtf16(bytes, pos, (int) value); + } + long high = value / DECIMAL_8; + int low = (int) (value - high * DECIMAL_8); + if (high < DECIMAL_8) { + pos = writePositiveIntUtf16(bytes, pos, (int) high); + return writePadded8Utf16(bytes, pos, low); + } + long top = high / DECIMAL_8; + int middle = (int) (high - top * DECIMAL_8); + pos = writePositiveIntUtf16(bytes, pos, (int) top); + pos = writePadded8Utf16(bytes, pos, middle); + return writePadded8Utf16(bytes, pos, low); } private static int divide10000(int value) { @@ -1395,32 +1389,48 @@ private static int writePadded8(byte[] bytes, int pos, int high, int low) { return pos + 8; } - private void writeIntUpTo4Utf16(int value) { - if (value < 10) { - writeUtf16ByteNoEnsure(DIGIT_ONES[value]); - } else if (value < 100) { - writeUtf16ByteNoEnsure(DIGIT_TENS[value]); - writeUtf16ByteNoEnsure(DIGIT_ONES[value]); - } else if (value < 1000) { - writePadded3Utf16(value); - } else { - writePadded4Utf16(value); + private static int writeIntUpTo4Utf16(byte[] bytes, int pos, int value) { + if (value < 1000) { + return writeIntUpTo3Utf16(bytes, pos, value); } + return writePadded4Utf16(bytes, pos, value); + } + + private static int writeIntUpTo3Utf16(byte[] bytes, int pos, int value) { + int digits = DIGIT_TRIPLES[value]; + int skip = digits & 0xFF; + long latin1 = (digits >>> ((skip + 1) << 3)) & 0xFFFFFFFFL; + long utf16 = spreadLatin1ToUtf16(latin1); + LittleEndian.putInt64(bytes, pos, LITTLE_ENDIAN ? utf16 : utf16 << 8); + return pos + ((3 - skip) << 1); + } + + private static int writePadded4Utf16(byte[] bytes, int pos, int value) { + long digits = spreadLatin1ToUtf16(DIGIT_QUADS[value] & 0xFFFFFFFFL); + LittleEndian.putInt64(bytes, pos, LITTLE_ENDIAN ? digits : digits << 8); + return pos + 8; } - private void writePadded3Utf16(int value) { - writeUtf16ByteNoEnsure(DIGIT_HUNDREDS[value]); - writeUtf16ByteNoEnsure(DIGIT_TENS[value]); - writeUtf16ByteNoEnsure(DIGIT_ONES[value]); + private static int writePadded8Utf16(byte[] bytes, int pos, int high, int low) { + pos = writePadded4Utf16(bytes, pos, high); + return writePadded4Utf16(bytes, pos, low); + } + + private static int writePadded8Utf16(byte[] bytes, int pos, int value) { + int high = divide10000(value); + int low = value - high * 10000; + return writePadded8Utf16(bytes, pos, high, low); } - private void writePadded4Utf16(int value) { - int high = value / 100; - int low = value - high * 100; - writeUtf16ByteNoEnsure((byte) ('0' + high / 10)); - writeUtf16ByteNoEnsure((byte) ('0' + high % 10)); - writeUtf16ByteNoEnsure((byte) ('0' + low / 10)); - writeUtf16ByteNoEnsure((byte) ('0' + low % 10)); + private static int putUtf16Byte(byte[] bytes, int pos, byte value) { + if (LITTLE_ENDIAN) { + bytes[pos] = value; + bytes[pos + 1] = 0; + } else { + bytes[pos] = 0; + bytes[pos + 1] = value; + } + return pos + 2; } private static char hex(int value) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index b5885f80d5..339bd7d879 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -254,6 +254,51 @@ public void writeNumericBoundaries() { assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); } + @Test + public void writeUtf16Numbers() { + StringJsonWriter writer = new StringJsonWriter(false); + writer.writeArrayStart(); + writer.writeString(ZH_TEXT); + writer.writeComma(1); + writer.writeLong(Long.MAX_VALUE); + writer.writeComma(2); + writer.writeLong(2_147_483_648L); + writer.writeComma(3); + writer.writeInt(Integer.MIN_VALUE); + writer.writeArrayEnd(); + assertEquals( + writer.toJson(), "[\"" + ZH_TEXT + "\",9223372036854775807,2147483648,-2147483648]"); + + ForyJson json = ForyJson.builder().build(); + Utf16NumericFields value = new Utf16NumericFields(); + value.prefix = ZH_TEXT; + value.zero = 0; + value.one = 7; + value.twoDigits = 42; + value.threeDigits = 321; + value.fourDigits = 9999; + value.fiveDigits = 10000; + value.eightDigits = 99999999; + value.nineDigits = 100000000; + value.intMax = Integer.MAX_VALUE; + value.intMin = Integer.MIN_VALUE; + value.aroundIntMax = 2_147_483_648L; + value.longMax = Long.MAX_VALUE; + value.longMin = Long.MIN_VALUE; + value.negative = -12345; + String expected = + "{\"prefix\":\"" + + ZH_TEXT + + "\",\"zero\":0,\"one\":7,\"twoDigits\":42,\"threeDigits\":321," + + "\"fourDigits\":9999,\"fiveDigits\":10000,\"eightDigits\":99999999," + + "\"nineDigits\":100000000,\"intMax\":2147483647," + + "\"intMin\":-2147483648,\"aroundIntMax\":2147483648," + + "\"longMax\":9223372036854775807,\"longMin\":-9223372036854775808," + + "\"negative\":-12345}"; + assertEquals(json.toJson(value), expected); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + } + @Test public void writeReadCoreScalarFields() { ForyJson json = ForyJson.builder().build(); @@ -641,6 +686,24 @@ public static final class Utf16TemporalFields { public OffsetDateTime timestamp; } + public static final class Utf16NumericFields { + public String prefix; + public int zero; + public int one; + public int twoDigits; + public int threeDigits; + public int fourDigits; + public int fiveDigits; + public int eightDigits; + public int nineDigits; + public int intMax; + public int intMin; + public long aroundIntMax; + public long longMax; + public long longMin; + public int negative; + } + public static final class ModeAwareHolder { public ModeAwareValue value; } From 0a93d0a9e530b7226388603d26ff72bca5e6927a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 17:16:38 +0800 Subject: [PATCH 092/120] perf(java): speed up JSON UTF16 string transition --- .../fory/json/writer/StringJsonWriter.java | 39 ++++++++++++++++++- .../org/apache/fory/json/JsonStringTest.java | 4 +- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index a7b08bf09d..eb93100781 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -215,6 +215,12 @@ private void writeStringLatin1(String value) { writeLatin1StringNoEnsure(bytes); return; } + if (LITTLE_ENDIAN && StringSerializer.isUtf16Coder(stringCoder)) { + int length = value.length(); + upgradeToUtf16((position << 1) + ((length + 2) << 1)); + writeUtf16StringBytes(value, bytes); + return; + } } ensure(value.length() * 6 + 2); writeStringCharsNoEnsure(value); @@ -478,13 +484,17 @@ public void writeStringField(byte[] prefix, String value) { public void writeStringField(byte[] prefix, byte[] utf16Prefix, String value) { if (coder == LATIN1) { - writeStringFieldLatin1(prefix, value); + writeStringFieldLatin1(prefix, utf16Prefix, value); return; } writeStringFieldUtf16Value(utf16Prefix, value); } private void writeStringFieldLatin1(byte[] prefix, String value) { + writeStringFieldLatin1(prefix, null, value); + } + + private void writeStringFieldLatin1(byte[] prefix, byte[] utf16Prefix, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); byte stringCoder = StringSerializer.getStringCoder(value); @@ -494,6 +504,21 @@ private void writeStringFieldLatin1(byte[] prefix, String value) { writeLatin1StringNoEnsure(bytes); return; } + if (LITTLE_ENDIAN && StringSerializer.isUtf16Coder(stringCoder)) { + int length = value.length(); + int utf16PrefixLength = utf16Prefix == null ? prefix.length << 1 : utf16Prefix.length; + // A compact UTF16 input cannot remain in a Latin1 JSON String. Upgrade before writing the + // pending prefix so it is emitted once in the final coder instead of being written as + // Latin1 and widened immediately by the first non-Latin1 value character. + upgradeToUtf16((position << 1) + utf16PrefixLength + ((length + 2) << 1)); + if (utf16Prefix == null) { + writeRawUtf16NoEnsure(prefix); + } else { + writeRawUtf16ValueNoEnsure(utf16Prefix); + } + writeUtf16StringBytes(value, bytes); + return; + } } ensure(prefix.length + value.length() * 6 + 2); writeRawLatin1NoEnsure(prefix); @@ -544,7 +569,8 @@ private void writeNullStringElement(int comma) { private void writeStringElementLatin1(int comma, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - if (StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(value))) { + byte stringCoder = StringSerializer.getStringCoder(value); + if (StringSerializer.isLatin1Coder(stringCoder)) { ensure(comma + bytes.length + 2); if (comma != 0) { buffer[position++] = ','; @@ -552,6 +578,15 @@ private void writeStringElementLatin1(int comma, String value) { writeLatin1StringNoEnsure(bytes); return; } + if (LITTLE_ENDIAN && StringSerializer.isUtf16Coder(stringCoder)) { + int length = value.length(); + upgradeToUtf16((position << 1) + ((comma + length + 2) << 1)); + if (comma != 0) { + writeUtf16ByteNoEnsure((byte) ','); + } + writeUtf16StringBytes(value, bytes); + return; + } } ensure(comma + value.length() * 6 + 2); if (comma != 0) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index b2c44fd77f..c35ce1eb80 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -177,8 +177,8 @@ public void stringWriterUtf16Escapes() { @Test public void stringWriterShrinksOnReset() throws Exception { StringJsonWriter writer = new StringJsonWriter(false, new byte[16]); - writer.writeString(repeat('a', 9000) + "你好,Fory"); - assertTrue(writerBufferLength(writer) > 8192); + writer.writeString(repeat('a', 40000) + "你好,Fory"); + assertTrue(writerBufferLength(writer) > 65536); writer.toJson(); writer.reset(); assertEquals(writerBufferLength(writer), 65536); From e1a0295f232e7715844ad87c9b6cd1685dd7d547 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 18:12:07 +0800 Subject: [PATCH 093/120] perf(java): cache JSON UTF16 enum field values --- .../fory/json/codegen/JsonWriterCodegen.java | 33 ++++++++++-- .../apache/fory/json/meta/JsonFieldInfo.java | 53 +++++++++++++++++-- .../fory/json/writer/StringJsonWriter.java | 13 +++++ 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index 6ec2bc7b38..05b47c3d58 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -636,7 +636,11 @@ private Expression writeValue( return writeStringField(property, id, value, utf8, commaKnown, index, writer); case ENUM: return writeRawFieldValue( - commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index), writer); + commaKnown, + index, + enumFieldValue(id, value, utf8, commaKnown, index), + utf8 ? null : stringUtf16EnumFieldValue(id, value, commaKnown, index), + writer); case FLOAT: case DOUBLE: case CHAR: @@ -661,8 +665,20 @@ private Expression writeValue( private static Expression writeRawFieldValue( boolean commaKnown, Expression index, Expression value, Expression writer) { - Expression.ListExpression expressions = - new Expression.ListExpression(new Expression.Invoke(writer, "writeRawValue", value)); + return writeRawFieldValue(commaKnown, index, value, null, writer); + } + + private static Expression writeRawFieldValue( + boolean commaKnown, + Expression index, + Expression value, + Expression utf16Value, + Expression writer) { + Expression write = + utf16Value == null + ? new Expression.Invoke(writer, "writeRawValue", value) + : new Expression.Invoke(writer, "writeRawValue", value, utf16Value); + Expression.ListExpression expressions = new Expression.ListExpression(write); if (!commaKnown) { expressions.add(increment(index)); } @@ -735,6 +751,17 @@ private static Expression enumFieldValue( .inline(); } + private static Expression stringUtf16EnumFieldValue( + int id, Expression value, boolean commaKnown, Expression index) { + return new Expression.Invoke( + fieldRef("p" + id, JsonFieldInfo.class), + "stringUtf16EnumFieldValue", + TypeRef.of(byte[].class), + value, + commaFlag(commaKnown, index)) + .inline(); + } + private static Expression writeCodec( int id, Expression value, boolean utf8, Expression writer, Expression typeResolver) { return new Expression.Invoke( diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index 6e30658a49..b3b706e658 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -84,6 +84,8 @@ public final class JsonFieldInfo { private final byte[][] stringElementEnumValues; private final byte[][] stringEnumNameValues; private final byte[][] stringEnumCommaValues; + private final byte[][] stringUtf16EnumNameValues; + private final byte[][] stringUtf16EnumCommaValues; private final byte[][] utf8EnumValues; private final byte[][] utf8ElementEnumValues; private final byte[][] utf8EnumNameValues; @@ -92,6 +94,10 @@ public final class JsonFieldInfo { private final byte[] stringTrueCommaToken; private final byte[] stringFalseNameToken; private final byte[] stringFalseCommaToken; + private final byte[] stringUtf16TrueNameToken; + private final byte[] stringUtf16TrueCommaToken; + private final byte[] stringUtf16FalseNameToken; + private final byte[] stringUtf16FalseCommaToken; private final byte[] utf8TrueNameToken; private final byte[] utf8TrueCommaToken; private final byte[] utf8FalseNameToken; @@ -149,6 +155,14 @@ public JsonFieldInfo( writeKind == JsonFieldKind.ENUM ? fieldValues(stringCommaNamePrefix, stringEnumValues) : null; + stringUtf16EnumNameValues = + writeKind == JsonFieldKind.ENUM + ? utf16FieldValues(stringUtf16NamePrefix, stringEnumValues) + : null; + stringUtf16EnumCommaValues = + writeKind == JsonFieldKind.ENUM + ? utf16FieldValues(stringUtf16CommaNamePrefix, stringEnumValues) + : null; stringElementEnumValues = writeElementRawType != null && writeElementRawType.isEnum() ? stringEnumValues(writeElementRawType) @@ -167,6 +181,10 @@ public JsonFieldInfo( stringTrueCommaToken = join(stringCommaNamePrefix, TRUE_BYTES); stringFalseNameToken = join(stringNamePrefix, FALSE_BYTES); stringFalseCommaToken = join(stringCommaNamePrefix, FALSE_BYTES); + stringUtf16TrueNameToken = join(stringUtf16NamePrefix, toUtf16Bytes(TRUE_BYTES)); + stringUtf16TrueCommaToken = join(stringUtf16CommaNamePrefix, toUtf16Bytes(TRUE_BYTES)); + stringUtf16FalseNameToken = join(stringUtf16NamePrefix, toUtf16Bytes(FALSE_BYTES)); + stringUtf16FalseCommaToken = join(stringUtf16CommaNamePrefix, toUtf16Bytes(FALSE_BYTES)); utf8TrueNameToken = join(utf8NamePrefix, TRUE_BYTES); utf8TrueCommaToken = join(utf8CommaNamePrefix, TRUE_BYTES); utf8FalseNameToken = join(utf8NamePrefix, FALSE_BYTES); @@ -176,6 +194,10 @@ public JsonFieldInfo( stringTrueCommaToken = null; stringFalseNameToken = null; stringFalseCommaToken = null; + stringUtf16TrueNameToken = null; + stringUtf16TrueCommaToken = null; + stringUtf16FalseNameToken = null; + stringUtf16FalseCommaToken = null; utf8TrueNameToken = null; utf8TrueCommaToken = null; utf8FalseNameToken = null; @@ -373,12 +395,22 @@ public byte[] stringEnumFieldValue(Enum value, boolean comma) { return (comma ? stringEnumCommaValues : stringEnumNameValues)[value.ordinal()]; } + public byte[] stringUtf16EnumFieldValue(Enum value, boolean comma) { + return (comma ? stringUtf16EnumCommaValues : stringUtf16EnumNameValues)[value.ordinal()]; + } + public byte[] stringBooleanFieldValue(boolean value, boolean comma) { return value ? (comma ? stringTrueCommaToken : stringTrueNameToken) : (comma ? stringFalseCommaToken : stringFalseNameToken); } + public byte[] stringUtf16BooleanFieldValue(boolean value, boolean comma) { + return value + ? (comma ? stringUtf16TrueCommaToken : stringUtf16TrueNameToken) + : (comma ? stringUtf16FalseCommaToken : stringUtf16FalseNameToken); + } + public byte[] utf8ElementEnumValue(Enum value) { return utf8ElementEnumValues[value.ordinal()]; } @@ -477,7 +509,10 @@ public boolean writeString( if (!writeRawType.isPrimitive()) { return writeStringScalar(writer, object, index); } - writer.writeRawValue(stringBooleanFieldValue(writeAccessor.getBoolean(object), index != 0)); + boolean booleanValue = writeAccessor.getBoolean(object); + writer.writeRawValue( + stringBooleanFieldValue(booleanValue, index != 0), + stringUtf16BooleanFieldValue(booleanValue, index != 0)); return true; case KIND_BYTE: if (!writeRawType.isPrimitive()) { @@ -670,7 +705,10 @@ private boolean writeStringScalar(StringJsonWriter writer, Object object, int in } switch (writeKind) { case BOOLEAN: - writer.writeRawValue(stringBooleanFieldValue(((Boolean) value).booleanValue(), index != 0)); + boolean booleanValue = ((Boolean) value).booleanValue(); + writer.writeRawValue( + stringBooleanFieldValue(booleanValue, index != 0), + stringUtf16BooleanFieldValue(booleanValue, index != 0)); return true; case BYTE: writer.writeIntField( @@ -754,7 +792,8 @@ private boolean writeStringEnum(StringJsonWriter writer, Object object, int inde writer.writeFieldName(this, index); writer.writeNull(); } else { - writer.writeRawValue(stringEnumFieldValue(value, index != 0)); + writer.writeRawValue( + stringEnumFieldValue(value, index != 0), stringUtf16EnumFieldValue(value, index != 0)); } return true; } @@ -1147,6 +1186,14 @@ private static byte[][] fieldValues(byte[] prefix, byte[][] values) { return fieldValues; } + private static byte[][] utf16FieldValues(byte[] utf16Prefix, byte[][] values) { + byte[][] fieldValues = new byte[values.length][]; + for (int i = 0; i < values.length; i++) { + fieldValues[i] = join(utf16Prefix, toUtf16Bytes(values[i])); + } + return fieldValues; + } + private static byte[] join(byte[] prefix, byte[] token) { byte[] joined = new byte[prefix.length + token.length]; System.arraycopy(prefix, 0, joined, 0, prefix.length); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index eb93100781..cd9d935097 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -924,6 +924,14 @@ private void writeLatin1StringUtf16(byte[] value) { putLatin1WordAsUtf16(target, pos, word); pos += 16; } + if (i + 4 <= length) { + int word = LittleEndian.getInt32(value, i); + if (isJsonAsciiInt(word)) { + putLatin1IntAsUtf16(target, pos, word); + pos += 8; + i += 4; + } + } while (i < length) { byte ch = value[i]; if (isJsonLatin1Byte(ch)) { @@ -1226,6 +1234,11 @@ private static void putLatin1WordAsUtf16(byte[] target, int offset, long word) { } } + private static void putLatin1IntAsUtf16(byte[] target, int offset, int word) { + long value = spreadLatin1ToUtf16(word & 0xFFFFFFFFL); + LittleEndian.putInt64(target, offset, LITTLE_ENDIAN ? value : value << 8); + } + private static boolean isJsonLatin1(char ch) { return ch > 0x1F && ch <= 0xff && ch != '"' && ch != '\\'; } From ce6b220b659f0c0a5671702c79e9ff7cb812d9af Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 18:45:33 +0800 Subject: [PATCH 094/120] perf(java): split medium JSON readers --- .../java/org/apache/fory/json/codegen/JsonReaderCodegen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 1ffe3a4693..53bb1818ec 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -51,7 +51,7 @@ final class JsonReaderCodegen { private static final int LATIN1_READER = JsonCodegen.LATIN1_READER; private static final int UTF16_READER = JsonCodegen.UTF16_READER; private static final int UTF8_READER = JsonCodegen.UTF8_READER; - private static final int MIN_SPLIT_READ_FIELDS = 12; + private static final int MIN_SPLIT_READ_FIELDS = 8; private static final int READ_FIELD_GROUP_SIZE = 2; private static final int READ_FIELD_SWITCH_SIZE = 8; private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; From 4325e16670dc7e12ee7da426b46872fc9276f9fa Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 18:59:16 +0800 Subject: [PATCH 095/120] perf(java): speed up UTF16 JSON string reads --- .../fory/serializer/StringSerializer.java | 5 ++ .../fory/json/reader/Utf16JsonReader.java | 59 ++++++++++++++++--- 2 files changed, 56 insertions(+), 8 deletions(-) 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 3e9d61e934..704b603fd2 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 @@ -1001,6 +1001,11 @@ public static String newLatin1StringZeroCopy(byte[] data) { return newBytesStringZeroCopy(LATIN1, data); } + @Internal + public static String newUtf16StringZeroCopy(byte[] data) { + return newBytesStringZeroCopy(UTF16, data); + } + private static String newBytesStringSlow(byte coder, byte[] data) { if (coder == LATIN1) { return new String(data, StandardCharsets.ISO_8859_1); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index d5bf51be0c..d0dc65d778 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -43,6 +43,7 @@ public final class Utf16JsonReader extends JsonReader { private static final long UTF16_QUOTE_CHARS = 0x0022002200220022L; private static final long UTF16_BACKSLASH_CHARS = 0x005c005c005c005cL; private static final long UTF16_CONTROL_LIMIT = 0x0020002000200020L; + private static final long UTF16_NON_LATIN_BYTES = 0xFF00FF00FF00FF00L; private static final long UTF16_SURROGATE_MASK = 0xf800f800f800f800L; private static final long UTF16_SURROGATE_PREFIX = 0xd800d800d800d800L; private String input; @@ -672,53 +673,81 @@ private String readUtf16StringToken(int start) { byte[] localBytes = bytes; int inputLength = length; int offset = start; + boolean nonLatin = false; int doubleWordEnd = inputLength - 8; while (offset <= doubleWordEnd) { - long stopMask = utf16StringStopMask(LittleEndian.getInt64(localBytes, offset << 1)); + long word = LittleEndian.getInt64(localBytes, offset << 1); + long stopMask = utf16StringStopMask(word); if (stopMask != 0) { int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 4); - String value = readUtf16StringStop(start, stop, charAtFast(stop)); + boolean currentNonLatin = hasNonLatinBeforeStop(nonLatin, word, stop - offset); + char ch = charAtFast(stop); + if (Character.isHighSurrogate(ch)) { + currentNonLatin = true; + } + String value = readUtf16StringStop(localBytes, start, stop, currentNonLatin, ch); if (value != null) { return value; } + nonLatin = currentNonLatin; offset = position; continue; } + nonLatin |= (word & UTF16_NON_LATIN_BYTES) != 0; int nextOffset = offset + 4; - stopMask = utf16StringStopMask(LittleEndian.getInt64(localBytes, nextOffset << 1)); + word = LittleEndian.getInt64(localBytes, nextOffset << 1); + stopMask = utf16StringStopMask(word); if (stopMask != 0) { int stop = nextOffset + (Long.numberOfTrailingZeros(stopMask) >>> 4); - String value = readUtf16StringStop(start, stop, charAtFast(stop)); + boolean currentNonLatin = hasNonLatinBeforeStop(nonLatin, word, stop - nextOffset); + char ch = charAtFast(stop); + if (Character.isHighSurrogate(ch)) { + currentNonLatin = true; + } + String value = readUtf16StringStop(localBytes, start, stop, currentNonLatin, ch); if (value != null) { return value; } + nonLatin = currentNonLatin; offset = position; continue; } + nonLatin |= (word & UTF16_NON_LATIN_BYTES) != 0; offset = nextOffset + 4; } int wordEnd = inputLength - 4; while (offset <= wordEnd) { - long stopMask = utf16StringStopMask(LittleEndian.getInt64(localBytes, offset << 1)); + long word = LittleEndian.getInt64(localBytes, offset << 1); + long stopMask = utf16StringStopMask(word); if (stopMask == 0) { + nonLatin |= (word & UTF16_NON_LATIN_BYTES) != 0; offset += 4; continue; } int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 4); - String value = readUtf16StringStop(start, stop, charAtFast(stop)); + boolean currentNonLatin = hasNonLatinBeforeStop(nonLatin, word, stop - offset); + char ch = charAtFast(stop); + if (Character.isHighSurrogate(ch)) { + currentNonLatin = true; + } + String value = readUtf16StringStop(localBytes, start, stop, currentNonLatin, ch); if (value != null) { return value; } + nonLatin = currentNonLatin; offset = position; } position = offset; return readStringLoop(start, null); } - private String readUtf16StringStop(int start, int stop, char ch) { + private String readUtf16StringStop( + byte[] localBytes, int start, int stop, boolean nonLatin, char ch) { if (ch == '"') { position = stop + 1; - return input.substring(start, stop); + // Preserve Java compact-string invariants: a LATIN1-representable token must keep the JDK's + // normal LATIN1 representation, otherwise equality/hash behavior can differ for map/set keys. + return nonLatin ? newUtf16String(localBytes, start, stop) : input.substring(start, stop); } if (ch == '\\') { StringBuilder builder = new StringBuilder(Math.max(16, stop - start + 16)); @@ -744,6 +773,20 @@ private String readUtf16StringStop(int start, int stop, char ch) { throw error("Unpaired low surrogate in string"); } + private static boolean hasNonLatinBeforeStop(boolean previousNonLatin, long word, int chars) { + if (previousNonLatin) { + return true; + } + long mask = chars == 4 ? -1L : (1L << (chars << 4)) - 1; + return (word & mask & UTF16_NON_LATIN_BYTES) != 0; + } + + private static String newUtf16String(byte[] localBytes, int start, int stop) { + byte[] valueBytes = new byte[(stop - start) << 1]; + System.arraycopy(localBytes, start << 1, valueBytes, 0, valueBytes.length); + return StringSerializer.newUtf16StringZeroCopy(valueBytes); + } + private String readStringLoop(int start, StringBuilder builder) { while (position < length) { char ch = charAtFast(position++); From f32554b5a585f8402159f5b19177d4aa8e53642e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 6 Jul 2026 22:08:17 +0800 Subject: [PATCH 096/120] perf(java): use UTF16 JSON token reads in codegen --- .../fory/json/codegen/JsonReaderCodegen.java | 22 +++++-- .../fory/json/reader/Utf16JsonReader.java | 66 +++++++++++++++++++ .../org/apache/fory/json/JsonStringTest.java | 40 +++++++++++ 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 53bb1818ec..3a42c1126a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -624,7 +624,7 @@ private Expression fastReadField( boolean record) { if (isDirectName(readerMode, properties[index].name())) { return statementIf( - tryReadNextFieldNameColon(readerMode, properties[index]), + tryReadNextFieldNameColon(readerMode, properties[index], usesTokenValueRead(readerMode)), new Expression.ListExpression( readField( builder, @@ -690,7 +690,8 @@ private Expression nextDirectFallback( int nextIndex = index + 1; if (nextIndex < groupEnd && isDirectName(readerMode, properties[nextIndex].name())) { return statementIf( - tryReadNextFieldNameColon(readerMode, properties[nextIndex]), + tryReadNextFieldNameColon( + readerMode, properties[nextIndex], usesTokenValueRead(readerMode)), new Expression.ListExpression( readField( builder, @@ -1347,7 +1348,7 @@ private static String readStringMethod(int readerMode, boolean tokenValueRead) { } private static boolean usesTokenValueRead(int readerMode) { - return readerMode == LATIN1_READER || readerMode == UTF8_READER; + return readerMode == LATIN1_READER || readerMode == UTF16_READER || readerMode == UTF8_READER; } private static Expression readFieldNameHash(int readerMode, String namePrefix) { @@ -1365,7 +1366,8 @@ private static Expression fieldIndexInvoke(Expression fieldHash) { .inline(); } - private static Expression tryReadNextFieldNameColon(int readerMode, JsonFieldInfo property) { + private static Expression tryReadNextFieldNameColon( + int readerMode, JsonFieldInfo property, boolean tokenValueRead) { if (readerMode == LATIN1_READER || readerMode == UTF8_READER) { String name = property.name(); String token = fieldNameToken(name); @@ -1408,6 +1410,18 @@ private static Expression tryReadNextFieldNameColon(int readerMode, JsonFieldInf String name = property.name(); int length = name.length(); int tailLength = Math.max(0, length - 4); + if (tokenValueRead) { + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextFieldNameUtf16Token", + TypeRef.of(boolean.class), + Expression.Literal.ofLong(utf16NameWord(name, 0, Math.min(length, 4))), + Expression.Literal.ofLong(utf16WordMask(Math.min(length, 4))), + Expression.Literal.ofLong(tailLength == 0 ? 0 : utf16NameWord(name, 4, tailLength)), + Expression.Literal.ofLong(tailLength == 0 ? 0 : utf16WordMask(tailLength)), + Expression.Literal.ofInt(length)) + .inline(); + } return new Expression.Invoke( readerRef(readerMode), "tryReadNextFieldNameUtf16", diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index d0dc65d778..8e22d35352 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -242,6 +242,10 @@ public boolean readNextBooleanValue() { return readBooleanValue(); } + public boolean readBooleanTokenValue() { + return readBooleanToken(); + } + private boolean readBooleanToken() { if (startsWithAscii("true")) { position += 4; @@ -265,6 +269,10 @@ public int readNextIntValue() { return readIntValue(); } + public int readIntTokenValue() { + return readIntToken(); + } + private int readIntToken() { int offset = position; int inputLength = length; @@ -378,6 +386,10 @@ public long readNextLongValue() { return readLongValue(); } + public long readLongTokenValue() { + return readLongToken(); + } + private long readLongToken() { int offset = position; int inputLength = length; @@ -636,6 +648,19 @@ public String readNextNullableString() { return readNullableString(); } + public String readNullableStringToken() { + if (position < length) { + char ch = charAtFast(position); + if (ch == '"') { + return readStringToken(); + } + if (ch == 'n' && tryReadNullLiteral()) { + return null; + } + } + return readStringToken(); + } + public LocalDate readIsoLocalDate() { skipWhitespaceFast(); int mark = position; @@ -1078,6 +1103,17 @@ public boolean tryReadNextFieldNameUtf16( secondMask); } + public boolean tryReadNextFieldNameUtf16Token( + long firstWord, long firstMask, long secondWord, long secondMask, int expectedLength) { + byte[] localBytes = bytes; + int mark = position; + if (localBytes == null || mark >= length || !utf16CharEquals(localBytes, mark << 1, '"')) { + return false; + } + return tryReadUtf16FieldNameTokenAt( + localBytes, mark, expectedLength, firstWord, firstMask, secondWord, secondMask); + } + private boolean tryReadFieldNameColonAt( int mark, long expectedHash, long expectedMask, int expectedLength) { byte[] localBytes = bytes; @@ -1160,6 +1196,36 @@ && utf16CharEquals(localBytes, quoteOffset << 1, '"')) { return false; } + private boolean tryReadUtf16FieldNameTokenAt( + byte[] localBytes, + int mark, + int expectedLength, + long firstWord, + long firstMask, + long secondWord, + long secondMask) { + int quoteOffset = mark + expectedLength + 1; + int colonOffset = quoteOffset + 1; + int nameOffset = (mark + 1) << 1; + if ((nameOffset + Long.BYTES > localBytes.length) + || (secondMask != 0 && nameOffset + (Long.BYTES << 1) > localBytes.length)) { + return false; + } + // Token-value generated reads do not skip whitespace, so this probe only accepts compact + // `"name":` spelling. The normal UTF16 field probe still owns whitespace-tolerant matching. + if (colonOffset < length + && (LittleEndian.getInt64(localBytes, nameOffset) & firstMask) == firstWord + && (secondMask == 0 + || (LittleEndian.getInt64(localBytes, nameOffset + Long.BYTES) & secondMask) + == secondWord) + && utf16CharEquals(localBytes, quoteOffset << 1, '"') + && utf16CharEquals(localBytes, colonOffset << 1, ':')) { + position = colonOffset + 1; + return true; + } + return false; + } + private static boolean isJsonPackedName(long packed, long mask) { long value = (packed & mask) | (~mask & CONTROL_LIMIT_BYTES); long control = (value - CONTROL_LIMIT_BYTES) & ~value & BYTE_HIGH_BITS; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index c35ce1eb80..aee7513d21 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -38,6 +38,7 @@ import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.reader.Utf16JsonReader; import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.serializer.StringSerializer; import org.testng.annotations.Test; @@ -102,6 +103,31 @@ public void readUtf16FieldNameProbe() { escaped.finish(); } + @Test + public void readUtf16FieldNameTokenProbe() { + String name = "duration"; + long firstWord = utf16NameWord(name, 0, 4); + long secondWord = utf16NameWord(name, 4, 4); + + Utf16JsonReader direct = utf16Reader("\"duration\":123"); + assertTrue(direct.tryReadNextFieldNameUtf16Token(firstWord, -1L, secondWord, -1L, 8)); + assertEquals(direct.readIntTokenValue(), 123); + direct.finish(); + + Utf16JsonReader nullable = utf16Reader("\"duration\":null"); + assertTrue(nullable.tryReadNextFieldNameUtf16Token(firstWord, -1L, secondWord, -1L, 8)); + assertEquals(nullable.readNullableStringToken(), null); + nullable.finish(); + + Utf16JsonReader spaced = utf16Reader("\"duration\" : 123"); + assertFalse(spaced.tryReadNextFieldNameUtf16Token(firstWord, -1L, secondWord, -1L, 8)); + assertTrue( + spaced.tryReadNextFieldNameUtf16( + JsonFieldNameHash.hash(name), packedNameMask(8), firstWord, -1L, secondWord, -1L, 8)); + assertEquals(spaced.readNextIntValue(), 123); + spaced.finish(); + } + @Test public void writeUtf16Char() { ForyJson json = ForyJson.builder().build(); @@ -363,4 +389,18 @@ private static Utf16JsonReader utf16Reader(String input) { private static long packedNameMask(int length) { return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L; } + + private static long utf16NameWord(String name, int start, int length) { + long value = 0; + for (int i = 0; i < length; i++) { + value |= (long) (name.charAt(start + i) & 0xFF) << (i << 3); + } + long word = spreadLatin1ToUtf16(value); + return NativeByteOrder.IS_LITTLE_ENDIAN ? word : word << 8; + } + + private static long spreadLatin1ToUtf16(long value) { + value = (value | (value << 16)) & 0x0000FFFF0000FFFFL; + return (value | (value << 8)) & 0x00FF00FF00FF00FFL; + } } From 6ff2ba7ac0306ecf03545db5eef81f9a9c057721 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 01:05:43 +0800 Subject: [PATCH 097/120] perf(java): skip UTF16 string quote reload --- .../org/apache/fory/json/reader/Utf16JsonReader.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index 8e22d35352..c6e1e691e0 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -636,7 +636,8 @@ public String readNextNullableString() { if (position < length) { char ch = charAtFast(position); if (ch == '"') { - return readStringToken(); + position++; + return readStringAfterQuote(); } if (ch == 'n' && tryReadNullLiteral()) { return null; @@ -652,7 +653,8 @@ public String readNullableStringToken() { if (position < length) { char ch = charAtFast(position); if (ch == '"') { - return readStringToken(); + position++; + return readStringAfterQuote(); } if (ch == 'n' && tryReadNullLiteral()) { return null; @@ -687,6 +689,10 @@ private String readStringToken() { if (position >= length || charAtFast(position++) != '"') { throw error("Expected string"); } + return readStringAfterQuote(); + } + + private String readStringAfterQuote() { int start = position; if (LITTLE_ENDIAN && bytes != null) { return readUtf16StringToken(start); From 1e2677046aa32d1a68f1c77c60d75c2777399f88 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 02:59:32 +0800 Subject: [PATCH 098/120] perf(java): split large JSON string writers --- .../fory/json/codegen/JsonWriterCodegen.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index 05b47c3d58..56b84c43e6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -47,10 +47,12 @@ import org.apache.fory.reflect.TypeRef; final class JsonWriterCodegen { - private static final int MIN_SPLIT_MEMBERS = 12; + private static final int MIN_STRING_SPLIT_MEMBERS = 10; + private static final int MIN_UTF8_SPLIT_MEMBERS = 12; // Keep generated member helpers below C2's big-method range without fragmenting them into // tiny calls. This mirrors Fory core's object-codec split strategy for large generated codecs. private static final int MEMBER_GROUP_SIZE = 8; + private static final int INLINE_TAIL_MEMBERS = 4; private final JsonCodegen codegen; private final boolean writeNullFields; @@ -248,7 +250,8 @@ private Expression writeExpression( expressions.add(index); } boolean commaKnown = objectStartFused; - boolean splitMembers = properties.length >= MIN_SPLIT_MEMBERS; + boolean splitMembers = + properties.length >= (utf8 ? MIN_UTF8_SPLIT_MEMBERS : MIN_STRING_SPLIT_MEMBERS); List memberGroup = splitMembers ? new ArrayList<>(MEMBER_GROUP_SIZE) : null; for (int i = 0; i < properties.length; i++) { Expression member; @@ -277,7 +280,7 @@ private Expression writeExpression( } } if (splitMembers) { - addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver, true); } expressions.add(new Expression.Invoke(writer, "writeObjectEnd")); return expressions; @@ -290,11 +293,24 @@ private static void addMemberGroup( Expression object, Reference writer, Reference typeResolver) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver, false); + } + + private static void addMemberGroup( + JsonGeneratedCodecBuilder builder, + Expression.ListExpression expressions, + List memberGroup, + Expression object, + Reference writer, + Reference typeResolver, + boolean inlineSmallTail) { if (memberGroup.isEmpty()) { return; } - if (memberGroup.size() == 1) { - expressions.add(memberGroup.get(0)); + if (memberGroup.size() == 1 || inlineSmallTail && memberGroup.size() <= INLINE_TAIL_MEMBERS) { + for (Expression member : memberGroup) { + expressions.add(member); + } memberGroup.clear(); return; } From bd62c0d4ca046c19d0c1282222b2a13928ccd6d3 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 03:27:43 +0800 Subject: [PATCH 099/120] perf(java): predict JSON string writer coder --- .../fory/json/writer/StringJsonWriter.java | 43 +++++++++++++++++-- .../org/apache/fory/json/JsonStringTest.java | 4 +- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index cd9d935097..2d165649d5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -84,6 +84,8 @@ public final class StringJsonWriter extends JsonWriter { private byte[] buffer; private byte[] scratch; private byte coder; + private byte nextCoder; + private boolean latin1Output; private int position; public StringJsonWriter(boolean writeNullFields) { @@ -103,15 +105,27 @@ public void reset() { if (scratch.length > RETAINED_CAPACITY) { scratch = new byte[RETAINED_CAPACITY]; } - coder = LATIN1; + coder = nextCoder; + latin1Output = coder == UTF16; position = 0; } public String toJson() { // The returned String may zero-copy this exact array, so pooled writer storage is never // exposed. - byte[] bytes = Arrays.copyOf(buffer, position); - return StringSerializer.newBytesStringZeroCopy(coder, bytes); + byte resultCoder = coder; + byte[] bytes; + if (coder == UTF16 && latin1Output) { + bytes = compressUtf16ToLatin1(buffer, position); + resultCoder = LATIN1; + } else { + bytes = Arrays.copyOf(buffer, position); + } + // Stable workloads often produce the same compact-string coder across operations. Remember the + // actual result coder so the next reset can avoid a LATIN1-to-UTF16 upgrade when UTF16 output + // repeats, while all-Latin1 outputs still materialize and continue as compact LATIN1 strings. + nextCoder = resultCoder; + return StringSerializer.newBytesStringZeroCopy(resultCoder, bytes); } @Override @@ -857,6 +871,9 @@ private void writeStringUtf16(String value) { for (int i = 0; i < length; i++) { char ch = value.charAt(i); if (isJsonUtf16(ch)) { + if (ch > 0xff) { + latin1Output = false; + } writeUtf16CharNoEnsure(ch); } else { writeStringUtf16Slow(value, i, length); @@ -867,6 +884,7 @@ private void writeStringUtf16(String value) { } private void writeUtf16StringBytes(String value, byte[] source) { + latin1Output = false; int length = value.length(); int byteLength = length << 1; ensure(byteLength + 4); @@ -965,6 +983,7 @@ private void writeStringUtf16Slow(String value, int index, int length) { if (!Character.isLowSurrogate(low)) { throw new ForyJsonException("Unpaired high surrogate in string"); } + latin1Output = false; ensure(4); writeUtf16CharNoEnsure(ch); writeUtf16CharNoEnsure(low); @@ -1110,6 +1129,9 @@ private void writeCharRaw(char value) { } private void writeCharRawUtf16(char value) { + if (value > 0xff) { + latin1Output = false; + } if (coder == LATIN1) { upgradeToUtf16((position << 1) + 2); } else { @@ -1224,6 +1246,21 @@ private static long spreadLatin1ToUtf16(long value) { return (value | (value << 8)) & UTF16_BYTE_MASK; } + private static byte[] compressUtf16ToLatin1(byte[] source, int length) { + int latin1Length = length >>> 1; + byte[] target = new byte[latin1Length]; + if (LITTLE_ENDIAN) { + for (int i = 0, j = 0; j < latin1Length; i += 2, j++) { + target[j] = source[i]; + } + } else { + for (int i = 1, j = 0; j < latin1Length; i += 2, j++) { + target[j] = source[i]; + } + } + return target; + } + private static void putLatin1WordAsUtf16(byte[] target, int offset, long word) { if (LITTLE_ENDIAN) { LittleEndian.putInt64(target, offset, spreadLatin1ToUtf16(word & 0xFFFFFFFFL)); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index aee7513d21..2d80fc48e3 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -170,10 +170,12 @@ public void stringWriterResetAfterMaterialize() { String utf16Json = writer.toJson(); writer.reset(); writer.writeString("ascii"); + String latin1Json = writer.toJson(); assertEquals(utf16Json, "\"你好,Fory\""); - assertEquals(writer.toJson(), "\"ascii\""); + assertEquals(latin1Json, "\"ascii\""); if (StringSerializer.isBytesBackedString()) { assertTrue(StringSerializer.isUtf16Coder(StringSerializer.getStringCoder(utf16Json))); + assertTrue(StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(latin1Json))); } } From 111b2a78923b3adcab93ef76bf1d78bf932a49d2 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 05:46:07 +0800 Subject: [PATCH 100/120] perf(java): speed up JSON UTF16 token reads --- .../fory/json/reader/Utf16JsonReader.java | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index c6e1e691e0..108432d965 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -101,7 +101,7 @@ public void clear() { public boolean consumeToken(char expected) { skipWhitespaceFast(); - if (position < length && charAtFast(position) == expected) { + if (position < length && asciiAtFast(position) == expected) { position++; return true; } @@ -109,7 +109,7 @@ public boolean consumeToken(char expected) { } public boolean consumeNextToken(char expected) { - if (position < length && charAtFast(position) == expected) { + if (position < length && asciiAtFast(position) == expected) { position++; return true; } @@ -123,7 +123,7 @@ public void expectToken(char expected) { } public void expectNextToken(char expected) { - if (position < length && charAtFast(position) == expected) { + if (position < length && asciiAtFast(position) == expected) { position++; return; } @@ -137,7 +137,7 @@ private void expectNextTokenSlow(char expected) { public boolean consumeNextCommaOrEndObject() { int inputLength = length; if (position < inputLength) { - char ch = charAtFast(position); + int ch = asciiAtFast(position); if (ch == ',') { position++; return true; @@ -156,7 +156,7 @@ public boolean consumeNextCommaOrEndObject() { private boolean consumeNextCommaOrEndObjectSlow(int inputLength) { skipWhitespaceFast(); if (position < inputLength) { - char ch = charAtFast(position); + int ch = asciiAtFast(position); if (ch == ',') { position++; return true; @@ -172,7 +172,7 @@ private boolean consumeNextCommaOrEndObjectSlow(int inputLength) { public boolean consumeNextCommaOrEndArray() { int inputLength = length; if (position < inputLength) { - char ch = charAtFast(position); + int ch = asciiAtFast(position); if (ch == ',') { position++; return true; @@ -191,7 +191,7 @@ public boolean consumeNextCommaOrEndArray() { private boolean consumeNextCommaOrEndArraySlow(int inputLength) { skipWhitespaceFast(); if (position < inputLength) { - char ch = charAtFast(position); + int ch = asciiAtFast(position); if (ch == ',') { position++; return true; @@ -211,7 +211,7 @@ public boolean tryReadNullToken() { public boolean tryReadNextNullToken() { if (position < length) { - char ch = charAtFast(position); + int ch = asciiAtFast(position); if (ch == 'n') { return tryReadNullLiteral(); } @@ -236,7 +236,7 @@ public boolean readBooleanValue() { } public boolean readNextBooleanValue() { - if (position < length && !isWhitespace(charAtFast(position))) { + if (position < length && !isWhitespace(asciiAtFast(position))) { return readBooleanToken(); } return readBooleanValue(); @@ -263,7 +263,7 @@ public int readIntValue() { } public int readNextIntValue() { - if (position < length && !isWhitespace(charAtFast(position))) { + if (position < length && !isWhitespace(asciiAtFast(position))) { return readIntToken(); } return readIntValue(); @@ -380,7 +380,7 @@ public long readLongValue() { } public long readNextLongValue() { - if (position < length && !isWhitespace(charAtFast(position))) { + if (position < length && !isWhitespace(asciiAtFast(position))) { return readLongToken(); } return readLongValue(); @@ -634,7 +634,7 @@ public String readNullableString() { public String readNextNullableString() { if (position < length) { - char ch = charAtFast(position); + int ch = asciiAtFast(position); if (ch == '"') { position++; return readStringAfterQuote(); @@ -651,7 +651,7 @@ public String readNextNullableString() { public String readNullableStringToken() { if (position < length) { - char ch = charAtFast(position); + int ch = asciiAtFast(position); if (ch == '"') { position++; return readStringAfterQuote(); @@ -1057,7 +1057,7 @@ public boolean tryReadNextFieldNameColon( long expectedHash, long expectedMask, int expectedLength) { int mark = position; if (mark < length) { - char ch = charAtFast(mark); + int ch = asciiAtFast(mark); if (ch == '"') { return tryReadFieldNameColonAt(mark, expectedHash, expectedMask, expectedLength); } @@ -1093,7 +1093,7 @@ public boolean tryReadNextFieldNameUtf16( secondWord, secondMask); } - if (!isWhitespace(charAtFast(mark))) { + if (!isWhitespace(asciiAtFast(mark))) { return false; } } @@ -1430,7 +1430,7 @@ protected String slice(int start, int end) { private void skipWhitespaceFast() { int inputLength = length; while (position < inputLength) { - char ch = charAtFast(position); + int ch = asciiAtFast(position); if (isWhitespace(ch)) { position++; } else { @@ -1439,7 +1439,7 @@ private void skipWhitespaceFast() { } } - private static boolean isWhitespace(char ch) { + private static boolean isWhitespace(int ch) { return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; } @@ -1518,4 +1518,17 @@ private char charAtFast(int index) { ? input.charAt(index) : StringSerializer.getBytesChar(localBytes, index << 1); } + + private int asciiAtFast(int index) { + byte[] localBytes = bytes; + if (localBytes == null) { + return input.charAt(index); + } + // JSON structural tokens are ASCII; non-ASCII code units cannot match them or whitespace. + int byteOffset = index << 1; + if (LITTLE_ENDIAN) { + return localBytes[byteOffset + 1] == 0 ? localBytes[byteOffset] & 0xff : 0x100; + } + return localBytes[byteOffset] == 0 ? localBytes[byteOffset + 1] & 0xff : 0x100; + } } From e35bf8ba030c1e34afa8a92dd08c3e45fb34a5cf Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 06:16:52 +0800 Subject: [PATCH 101/120] perf(java): cache JSON UTF16 digit quads --- .../java/org/apache/fory/json/writer/StringJsonWriter.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 2d165649d5..fc094739a8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -50,6 +50,7 @@ public final class StringJsonWriter extends JsonWriter { private static final int INT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C; private static final int[] DIGIT_TRIPLES = new int[1000]; private static final int[] DIGIT_QUADS = new int[10000]; + private static final long[] UTF16_DIGIT_QUADS = new long[10000]; private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL; private static final long UTF16_PAIR_MASK = 0x0000FFFF0000FFFFL; private static final long UTF16_ONES = 0x0001000100010001L; @@ -78,6 +79,8 @@ public final class StringJsonWriter extends JsonWriter { int c2 = '0' + low / 10; int c3 = '0' + low % 10; DIGIT_QUADS[i] = c0 | (c1 << 8) | (c2 << 16) | (c3 << 24); + long utf16 = spreadLatin1ToUtf16(DIGIT_QUADS[i] & 0xFFFFFFFFL); + UTF16_DIGIT_QUADS[i] = LITTLE_ENDIAN ? utf16 : utf16 << 8; } } @@ -1491,8 +1494,7 @@ private static int writeIntUpTo3Utf16(byte[] bytes, int pos, int value) { } private static int writePadded4Utf16(byte[] bytes, int pos, int value) { - long digits = spreadLatin1ToUtf16(DIGIT_QUADS[value] & 0xFFFFFFFFL); - LittleEndian.putInt64(bytes, pos, LITTLE_ENDIAN ? digits : digits << 8); + LittleEndian.putInt64(bytes, pos, UTF16_DIGIT_QUADS[value]); return pos + 8; } From 32c64c4edf785cd85fd483889ae265786b437a64 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 06:59:13 +0800 Subject: [PATCH 102/120] perf(java): avoid JSON string coder reloads --- .../fory/json/writer/StringJsonWriter.java | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index fc094739a8..6da4d9655a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -226,14 +226,13 @@ public void writeString(String value) { private void writeStringLatin1(String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - byte stringCoder = StringSerializer.getStringCoder(value); - if (StringSerializer.isLatin1Coder(stringCoder)) { + int length = value.length(); + if (isLatin1Bytes(bytes, length)) { ensure(bytes.length + 2); writeLatin1StringNoEnsure(bytes); return; } - if (LITTLE_ENDIAN && StringSerializer.isUtf16Coder(stringCoder)) { - int length = value.length(); + if (LITTLE_ENDIAN && isUtf16Bytes(bytes, length)) { upgradeToUtf16((position << 1) + ((length + 2) << 1)); writeUtf16StringBytes(value, bytes); return; @@ -514,15 +513,14 @@ private void writeStringFieldLatin1(byte[] prefix, String value) { private void writeStringFieldLatin1(byte[] prefix, byte[] utf16Prefix, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - byte stringCoder = StringSerializer.getStringCoder(value); - if (StringSerializer.isLatin1Coder(stringCoder)) { + int length = value.length(); + if (isLatin1Bytes(bytes, length)) { ensure(prefix.length + bytes.length + 2); writeRawLatin1NoEnsure(prefix); writeLatin1StringNoEnsure(bytes); return; } - if (LITTLE_ENDIAN && StringSerializer.isUtf16Coder(stringCoder)) { - int length = value.length(); + if (LITTLE_ENDIAN && isUtf16Bytes(bytes, length)) { int utf16PrefixLength = utf16Prefix == null ? prefix.length << 1 : utf16Prefix.length; // A compact UTF16 input cannot remain in a Latin1 JSON String. Upgrade before writing the // pending prefix so it is emitted once in the final coder instead of being written as @@ -586,8 +584,8 @@ private void writeNullStringElement(int comma) { private void writeStringElementLatin1(int comma, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - byte stringCoder = StringSerializer.getStringCoder(value); - if (StringSerializer.isLatin1Coder(stringCoder)) { + int length = value.length(); + if (isLatin1Bytes(bytes, length)) { ensure(comma + bytes.length + 2); if (comma != 0) { buffer[position++] = ','; @@ -595,8 +593,7 @@ private void writeStringElementLatin1(int comma, String value) { writeLatin1StringNoEnsure(bytes); return; } - if (LITTLE_ENDIAN && StringSerializer.isUtf16Coder(stringCoder)) { - int length = value.length(); + if (LITTLE_ENDIAN && isUtf16Bytes(bytes, length)) { upgradeToUtf16((position << 1) + ((comma + length + 2) << 1)); if (comma != 0) { writeUtf16ByteNoEnsure((byte) ','); @@ -858,12 +855,12 @@ private void writeEscapedChar(char ch) { private void writeStringUtf16(String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); - byte stringCoder = StringSerializer.getStringCoder(value); - if (StringSerializer.isLatin1Coder(stringCoder)) { + int length = value.length(); + if (isLatin1Bytes(bytes, length)) { writeLatin1StringUtf16(bytes); return; } - if (LITTLE_ENDIAN && StringSerializer.isUtf16Coder(stringCoder)) { + if (LITTLE_ENDIAN && isUtf16Bytes(bytes, length)) { writeUtf16StringBytes(value, bytes); return; } @@ -886,6 +883,16 @@ private void writeStringUtf16(String value) { writeByteRaw((byte) '"'); } + private static boolean isLatin1Bytes(byte[] bytes, int length) { + return bytes.length == length; + } + + private static boolean isUtf16Bytes(byte[] bytes, int length) { + // For byte-backed compact Strings, backing byte length identifies the coder without another + // String.coder field load. + return bytes.length != length && bytes.length == length + length; + } + private void writeUtf16StringBytes(String value, byte[] source) { latin1Output = false; int length = value.length(); From 26e9be0150e7b679d284e4f2b101925b7dd0b62d Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 10:50:29 +0800 Subject: [PATCH 103/120] perf(json): pack UTF16 generated prefixes Add packed UTF16 prefix overloads for Fory JSON String generated writers and emit them for small generated field prefixes.\n\nFocused EishayWriteString on JDK25: Fory JSON 6,660,535 ops/s vs retained focused baseline 6,247,353 ops/s (+6.61%). Full matrix WriteString: 6,543,238 ops/s vs fastjson2 5,520,224 ops/s (1.185x).\n\nValidation: mvn -pl fory-json spotless:check; ENABLE_FORY_DEBUG_OUTPUT=1 mvn -pl fory-json test. --- .../fory/json/codegen/JsonWriterCodegen.java | 129 ++++++++++-- .../fory/json/writer/StringJsonWriter.java | 192 ++++++++++++++++++ 2 files changed, 306 insertions(+), 15 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index 56b84c43e6..6a4decce32 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -115,6 +115,8 @@ String genWriterCode( ctx.addImports(StringJsonWriter.class, Utf8JsonWriter.class); ctx.implementsInterfaces(ctx.type(utf8 ? Utf8ObjectWriter.class : StringObjectWriter.class)); boolean objectStartFused = canFuseObjectStart(properties); + StringPrefixFields stringPrefixFields = + utf8 ? null : stringPrefixFields(properties, objectStartFused); for (int i = 0; i < properties.length; i++) { JsonFieldInfo property = properties[i]; if (usesWriteInfo(property)) { @@ -130,14 +132,18 @@ String genWriterCode( } else { ctx.addField(byte[].class, "s" + i); ctx.addField(byte[].class, "sc" + i); - ctx.addField(byte[].class, "s16" + i); - ctx.addField(byte[].class, "sc16" + i); + if (stringPrefixFields.name[i]) { + ctx.addField(byte[].class, "s16" + i); + } + if (stringPrefixFields.comma[i]) { + ctx.addField(byte[].class, "sc16" + i); + } } } } addGeneratedConstructor( ctx, - writerConstructorExpression(properties, utf8), + writerConstructorExpression(properties, utf8, stringPrefixFields), JsonFieldInfo[].class, "properties", JsonCodec[].class, @@ -157,6 +163,50 @@ String genWriterCode( return ctx.genCode(); } + private StringPrefixFields stringPrefixFields( + JsonFieldInfo[] properties, boolean objectStartFused) { + StringPrefixFields fields = new StringPrefixFields(properties.length); + boolean commaKnown = objectStartFused; + for (int i = 0; i < properties.length; i++) { + JsonFieldInfo property = properties[i]; + if (usesPrefix(property)) { + if (objectStartFused && i == 0) { + if (!canPackStringUtf16Prefix(property, false)) { + fields.name[i] = true; + } + } else { + markStringUtf16PrefixField(property, commaKnown, fields, i); + } + } + if (writeNullFields || property.writeRawType().isPrimitive()) { + commaKnown = true; + } + } + return fields; + } + + private static void markStringUtf16PrefixField( + JsonFieldInfo property, boolean commaKnown, StringPrefixFields fields, int id) { + if (!commaKnown) { + fields.name[id] = true; + fields.comma[id] = true; + return; + } + if (!canPackStringUtf16Prefix(property, true)) { + fields.comma[id] = true; + } + } + + private static final class StringPrefixFields { + private final boolean[] name; + private final boolean[] comma; + + private StringPrefixFields(int size) { + name = new boolean[size]; + comma = new boolean[size]; + } + } + private boolean usesPrefix(JsonFieldInfo property) { JsonFieldKind kind = property.writeKind(); return kind != JsonFieldKind.BOOLEAN && kind != JsonFieldKind.ENUM @@ -168,7 +218,8 @@ private static boolean usesWriteInfo(JsonFieldInfo property) { return kind == JsonFieldKind.BOOLEAN || kind == JsonFieldKind.ENUM; } - private Expression writerConstructorExpression(JsonFieldInfo[] properties, boolean utf8) { + private Expression writerConstructorExpression( + JsonFieldInfo[] properties, boolean utf8, StringPrefixFields stringPrefixFields) { Expression.ListExpression expressions = new Expression.ListExpression(); Reference propertiesRef = new Reference("properties", TypeRef.of(JsonFieldInfo[].class)); Reference codecsRef = new Reference("codecs", TypeRef.of(JsonCodec[].class)); @@ -211,17 +262,22 @@ private Expression writerConstructorExpression(JsonFieldInfo[] properties, boole new Reference("this.sc" + i, TypeRef.of(byte[].class)), new Expression.Invoke(property, "stringCommaNamePrefix", TypeRef.of(byte[].class)) .inline())); - expressions.add( - new Expression.Assign( - new Reference("this.s16" + i, TypeRef.of(byte[].class)), - new Expression.Invoke(property, "stringUtf16NamePrefix", TypeRef.of(byte[].class)) - .inline())); - expressions.add( - new Expression.Assign( - new Reference("this.sc16" + i, TypeRef.of(byte[].class)), - new Expression.Invoke( - property, "stringUtf16CommaNamePrefix", TypeRef.of(byte[].class)) - .inline())); + if (stringPrefixFields.name[i]) { + expressions.add( + new Expression.Assign( + new Reference("this.s16" + i, TypeRef.of(byte[].class)), + new Expression.Invoke( + property, "stringUtf16NamePrefix", TypeRef.of(byte[].class)) + .inline())); + } + if (stringPrefixFields.comma[i]) { + expressions.add( + new Expression.Assign( + new Reference("this.sc16" + i, TypeRef.of(byte[].class)), + new Expression.Invoke( + property, "stringUtf16CommaNamePrefix", TypeRef.of(byte[].class)) + .inline())); + } } } } @@ -354,6 +410,10 @@ private static Expression writeObjectStartPrimitive( writer, "writeObjectIntField", packedPrefixArgs(property, false, value)); } if (!utf8) { + if (canPackStringUtf16Prefix(property, false)) { + return new Expression.Invoke( + writer, "writeObjectIntField", stringPackedPrefixArgs(property, 0, false, value)); + } return new Expression.Invoke( writer, "writeObjectIntField", @@ -369,6 +429,10 @@ private static Expression writeObjectStartPrimitive( writer, "writeObjectLongField", packedPrefixArgs(property, false, value)); } if (!utf8) { + if (canPackStringUtf16Prefix(property, false)) { + return new Expression.Invoke( + writer, "writeObjectLongField", stringPackedPrefixArgs(property, 0, false, value)); + } return new Expression.Invoke( writer, "writeObjectLongField", @@ -471,6 +535,10 @@ private static Expression writeNumberField( return new Expression.Invoke(writer, writerMethod, packedPrefixArgs(property, true, value)); } if (!utf8) { + if (canPackStringUtf16Prefix(property, true)) { + return new Expression.Invoke( + writer, writerMethod, stringPackedPrefixArgs(property, id, true, value)); + } return new Expression.Invoke( writer, writerMethod, prefixRef(false, true, id), utf16PrefixRef(true, id), value); } @@ -519,6 +587,10 @@ private static Expression writeStringField( writer, "writeStringField", packedPrefixArgs(property, true, value)); } if (!utf8) { + if (canPackStringUtf16Prefix(property, true)) { + return new Expression.Invoke( + writer, "writeStringField", stringPackedPrefixArgs(property, id, true, value)); + } return new Expression.Invoke( writer, "writeStringField", @@ -570,6 +642,10 @@ private static Expression writeFieldName( return new Expression.Invoke(writer, "writeRawValue", packedPrefixArgs(property, true)); } if (!utf8 && commaKnown) { + if (canPackStringUtf16Prefix(property, true)) { + return new Expression.Invoke( + writer, "writeRawValue", stringPackedPrefixArgs(property, id, true)); + } return new Expression.Invoke( writer, "writeRawValue", prefixRef(false, true, id), utf16PrefixRef(true, id)); } @@ -712,6 +788,21 @@ private static Expression[] packedPrefixArgs( return args; } + private static Expression[] stringPackedPrefixArgs( + JsonFieldInfo property, int id, boolean comma, Expression... extraArgs) { + byte[] prefix = + comma ? property.stringUtf16CommaNamePrefix() : property.stringUtf16NamePrefix(); + Expression[] args = new Expression[6 + extraArgs.length]; + args[0] = prefixRef(false, comma, id); + args[1] = Expression.Literal.ofLong(packedPrefixWord(prefix, 0)); + args[2] = Expression.Literal.ofLong(packedPrefixWord(prefix, Long.BYTES)); + args[3] = Expression.Literal.ofLong(packedPrefixWord(prefix, Long.BYTES * 2)); + args[4] = Expression.Literal.ofLong(packedPrefixWord(prefix, Long.BYTES * 3)); + args[5] = Expression.Literal.ofInt(prefix.length); + System.arraycopy(extraArgs, 0, args, 6, extraArgs.length); + return args; + } + private static Expression[] packedDynamicPrefixArgs( JsonFieldInfo property, Expression index, Expression... extraArgs) { byte[] namePrefix = property.utf8NamePrefix(); @@ -731,6 +822,14 @@ private static boolean canPackPrefix(JsonFieldInfo property, boolean comma) { return length <= Long.BYTES * 2; } + private static boolean canPackStringUtf16Prefix(JsonFieldInfo property, boolean comma) { + int length = + comma + ? property.stringUtf16CommaNamePrefix().length + : property.stringUtf16NamePrefix().length; + return length <= Long.BYTES * 4; + } + private static boolean canPackSinglePrefix(JsonFieldInfo property, boolean comma) { int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length; return length <= Long.BYTES; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 6da4d9655a..640759f274 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -326,6 +326,22 @@ public void writeIntField(byte[] prefix, byte[] utf16Prefix, int value) { writeIntFieldUtf16Value(utf16Prefix, value); } + public void writeIntField( + byte[] prefix, + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + int value) { + if (coder == LATIN1) { + writeIntFieldLatin1(prefix, value); + return; + } + writeIntFieldUtf16Packed( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength, value); + } + private void writeIntFieldLatin1(byte[] prefix, int value) { ensure(prefix.length + 11); writeRawLatin1NoEnsure(prefix); @@ -344,6 +360,19 @@ private void writeIntFieldUtf16Value(byte[] utf16Prefix, int value) { writeIntUtf16NoEnsure(value); } + private void writeIntFieldUtf16Packed( + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + int value) { + ensurePackedUtf16Prefix(utf16PrefixLength, 22); + writePackedUtf16ValueNoEnsure( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength); + writeIntUtf16NoEnsure(value); + } + public void writeObjectIntField(byte[] namePrefix, int value) { if (coder == LATIN1) { writeObjectIntFieldLatin1(namePrefix, value); @@ -360,6 +389,22 @@ public void writeObjectIntField(byte[] namePrefix, byte[] utf16NamePrefix, int v writeObjectIntFieldUtf16Value(utf16NamePrefix, value); } + public void writeObjectIntField( + byte[] namePrefix, + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + int value) { + if (coder == LATIN1) { + writeObjectIntFieldLatin1(namePrefix, value); + return; + } + writeObjectIntFieldUtf16Packed( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength, value); + } + private void writeObjectIntFieldLatin1(byte[] namePrefix, int value) { ensure(namePrefix.length + 12); buffer[position++] = (byte) '{'; @@ -381,6 +426,20 @@ private void writeObjectIntFieldUtf16Value(byte[] utf16NamePrefix, int value) { writeIntUtf16NoEnsure(value); } + private void writeObjectIntFieldUtf16Packed( + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + int value) { + ensurePackedUtf16Prefix(utf16PrefixLength, 24); + writeUtf16ByteNoEnsure((byte) '{'); + writePackedUtf16ValueNoEnsure( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength); + writeIntUtf16NoEnsure(value); + } + public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, long value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeLongField(prefix, value); @@ -416,6 +475,22 @@ public void writeLongField(byte[] prefix, byte[] utf16Prefix, long value) { writeLongFieldUtf16Value(utf16Prefix, value); } + public void writeLongField( + byte[] prefix, + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + long value) { + if (coder == LATIN1) { + writeLongFieldLatin1(prefix, value); + return; + } + writeLongFieldUtf16Packed( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength, value); + } + private void writeLongFieldLatin1(byte[] prefix, long value) { ensure(prefix.length + 20); writeRawLatin1NoEnsure(prefix); @@ -434,6 +509,19 @@ private void writeLongFieldUtf16Value(byte[] utf16Prefix, long value) { writeLongUtf16NoEnsure(value); } + private void writeLongFieldUtf16Packed( + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + long value) { + ensurePackedUtf16Prefix(utf16PrefixLength, 40); + writePackedUtf16ValueNoEnsure( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength); + writeLongUtf16NoEnsure(value); + } + public void writeObjectLongField(byte[] namePrefix, long value) { if (coder == LATIN1) { writeObjectLongFieldLatin1(namePrefix, value); @@ -450,6 +538,22 @@ public void writeObjectLongField(byte[] namePrefix, byte[] utf16NamePrefix, long writeObjectLongFieldUtf16Value(utf16NamePrefix, value); } + public void writeObjectLongField( + byte[] namePrefix, + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + long value) { + if (coder == LATIN1) { + writeObjectLongFieldLatin1(namePrefix, value); + return; + } + writeObjectLongFieldUtf16Packed( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength, value); + } + private void writeObjectLongFieldLatin1(byte[] namePrefix, long value) { ensure(namePrefix.length + 21); buffer[position++] = (byte) '{'; @@ -471,6 +575,20 @@ private void writeObjectLongFieldUtf16Value(byte[] utf16NamePrefix, long value) writeLongUtf16NoEnsure(value); } + private void writeObjectLongFieldUtf16Packed( + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + long value) { + ensurePackedUtf16Prefix(utf16PrefixLength, 42); + writeUtf16ByteNoEnsure((byte) '{'); + writePackedUtf16ValueNoEnsure( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength); + writeLongUtf16NoEnsure(value); + } + public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeStringField(prefix, value); @@ -506,6 +624,22 @@ public void writeStringField(byte[] prefix, byte[] utf16Prefix, String value) { writeStringFieldUtf16Value(utf16Prefix, value); } + public void writeStringField( + byte[] prefix, + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + String value) { + if (coder == LATIN1) { + writeStringFieldLatin1(prefix, null, value); + return; + } + writeStringFieldUtf16Packed( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength, value); + } + private void writeStringFieldLatin1(byte[] prefix, String value) { writeStringFieldLatin1(prefix, null, value); } @@ -552,6 +686,19 @@ private void writeStringFieldUtf16Value(byte[] utf16Prefix, String value) { writeStringUtf16(value); } + private void writeStringFieldUtf16Packed( + long utf16Prefix0, + long utf16Prefix1, + long utf16Prefix2, + long utf16Prefix3, + int utf16PrefixLength, + String value) { + ensurePackedUtf16Prefix(utf16PrefixLength, 0); + writePackedUtf16ValueNoEnsure( + utf16Prefix0, utf16Prefix1, utf16Prefix2, utf16Prefix3, utf16PrefixLength); + writeStringUtf16(value); + } + public void writeStringElement(int index, String value) { int comma = index == 0 ? 0 : 1; if (value == null) { @@ -649,6 +796,20 @@ public void writeRawValue(byte[] value, byte[] utf16Value) { writeRawUtf16Value(utf16Value); } + public void writeRawValue( + byte[] value, + long utf16Value0, + long utf16Value1, + long utf16Value2, + long utf16Value3, + int utf16Length) { + if (coder == LATIN1) { + writeRaw(value); + return; + } + writePackedUtf16Value(utf16Value0, utf16Value1, utf16Value2, utf16Value3, utf16Length); + } + public void writeRawValue( byte[] namePrefix, byte[] commaNamePrefix, @@ -1096,11 +1257,34 @@ private void writeRawUtf16Value(byte[] bytes) { writeRawUtf16ValueNoEnsure(bytes); } + private void writePackedUtf16Value( + long value0, long value1, long value2, long value3, int length) { + ensure(packedUtf16PrefixSize(length)); + writePackedUtf16ValueNoEnsure(value0, value1, value2, value3, length); + } + private void writeRawUtf16ValueNoEnsure(byte[] bytes) { System.arraycopy(bytes, 0, buffer, position, bytes.length); position += bytes.length; } + private void writePackedUtf16ValueNoEnsure( + long value0, long value1, long value2, long value3, int length) { + byte[] target = buffer; + int pos = position; + LittleEndian.putInt64(target, pos, value0); + if (length > Long.BYTES) { + LittleEndian.putInt64(target, pos + Long.BYTES, value1); + if (length > Long.BYTES * 2) { + LittleEndian.putInt64(target, pos + Long.BYTES * 2, value2); + if (length > Long.BYTES * 3) { + LittleEndian.putInt64(target, pos + Long.BYTES * 3, value3); + } + } + } + position = pos + length; + } + private void writeRawUtf16NoEnsure(byte[] bytes) { byte[] target = buffer; int pos = position; @@ -1184,6 +1368,14 @@ private void ensure(int additional) { } } + private void ensurePackedUtf16Prefix(int prefixLength, int additionalAfterPrefix) { + ensure(Math.max(packedUtf16PrefixSize(prefixLength), prefixLength + additionalAfterPrefix)); + } + + private static int packedUtf16PrefixSize(int prefixLength) { + return (prefixLength + Long.BYTES - 1) & -Long.BYTES; + } + private void grow(int minCapacity) { buffer = Arrays.copyOf(buffer, growCapacity(buffer.length, minCapacity)); } From 2ffbd7134fc8533c4dea13bfa4e61e69c2d2e9f0 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 11:14:44 +0800 Subject: [PATCH 104/120] perf(json): pack UTF16 field tokens --- .../fory/json/codegen/JsonReaderCodegen.java | 53 +++++++++--- .../fory/json/reader/Utf16JsonReader.java | 80 ++++++++++--------- .../org/apache/fory/json/JsonStringTest.java | 48 ++++++++--- 3 files changed, 124 insertions(+), 57 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 3a42c1126a..4cc03fe04f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -622,7 +622,7 @@ private Expression fastReadField( Expression hashes, Expression[] skips, boolean record) { - if (isDirectName(readerMode, properties[index].name())) { + if (isDirectName(readerMode, properties[index].name(), usesTokenValueRead(readerMode))) { return statementIf( tryReadNextFieldNameColon(readerMode, properties[index], usesTokenValueRead(readerMode)), new Expression.ListExpression( @@ -688,7 +688,8 @@ private Expression nextDirectFallback( Expression[] skips, boolean record) { int nextIndex = index + 1; - if (nextIndex < groupEnd && isDirectName(readerMode, properties[nextIndex].name())) { + if (nextIndex < groupEnd + && isDirectName(readerMode, properties[nextIndex].name(), usesTokenValueRead(readerMode))) { return statementIf( tryReadNextFieldNameColon( readerMode, properties[nextIndex], usesTokenValueRead(readerMode)), @@ -848,13 +849,24 @@ private static boolean isPackedName(String name) { return isAsciiName(name); } - private static boolean isDirectName(int readerMode, String name) { + private static boolean isDirectName(int readerMode, String name, boolean tokenValueRead) { if (readerMode == LATIN1_READER || readerMode == UTF8_READER) { return JsonAsciiToken.isLongPackable(fieldNameToken(name)); } + if (readerMode == UTF16_READER && tokenValueRead) { + return isUtf16FieldNameToken(name); + } return isPackedName(name); } + private static boolean isUtf16FieldNameToken(String name) { + int length = name.length(); + if (length == 0 || length + 3 > 12) { + return false; + } + return isAsciiName(name); + } + private static boolean isAsciiName(String name) { int length = name.length(); for (int i = 0; i < length; i++) { @@ -1409,19 +1421,34 @@ private static Expression tryReadNextFieldNameColon( if (readerMode == UTF16_READER) { String name = property.name(); int length = name.length(); - int tailLength = Math.max(0, length - 4); if (tokenValueRead) { + String token = fieldNameToken(name); + int tokenLength = token.length(); + int tailLength = Math.max(0, tokenLength - 4); + if (tokenLength <= 8) { + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextFieldNameUtf16Token2", + TypeRef.of(boolean.class), + Expression.Literal.ofLong(utf16TokenWord(token, 0, Math.min(tokenLength, 4))), + Expression.Literal.ofLong(utf16WordMask(Math.min(tokenLength, 4))), + Expression.Literal.ofLong( + tailLength == 0 ? 0 : utf16TokenWord(token, 4, tailLength)), + Expression.Literal.ofLong(tailLength == 0 ? 0 : utf16WordMask(tailLength)), + Expression.Literal.ofInt(tokenLength)) + .inline(); + } return new Expression.Invoke( readerRef(readerMode), - "tryReadNextFieldNameUtf16Token", + "tryReadNextFieldNameUtf16Token3", TypeRef.of(boolean.class), - Expression.Literal.ofLong(utf16NameWord(name, 0, Math.min(length, 4))), - Expression.Literal.ofLong(utf16WordMask(Math.min(length, 4))), - Expression.Literal.ofLong(tailLength == 0 ? 0 : utf16NameWord(name, 4, tailLength)), - Expression.Literal.ofLong(tailLength == 0 ? 0 : utf16WordMask(tailLength)), - Expression.Literal.ofInt(length)) + Expression.Literal.ofLong(utf16TokenWord(token, 0, 4)), + Expression.Literal.ofLong(utf16TokenWord(token, 4, 4)), + Expression.Literal.ofLong(utf16TokenWord(token, 8, tokenLength - 8)), + Expression.Literal.ofInt(tokenLength)) .inline(); } + int tailLength = Math.max(0, length - 4); return new Expression.Invoke( readerRef(readerMode), "tryReadNextFieldNameUtf16", @@ -1446,9 +1473,13 @@ private static Expression tryReadNextFieldNameColon( } private static long utf16NameWord(String name, int start, int length) { + return utf16TokenWord(name, start, length); + } + + private static long utf16TokenWord(String token, int start, int length) { long value = 0; for (int i = 0; i < length; i++) { - value |= (long) (name.charAt(start + i) & 0xFF) << (i << 3); + value |= (long) (token.charAt(start + i) & 0xFF) << (i << 3); } long word = spreadLatin1ToUtf16(value); return LITTLE_ENDIAN ? word : word << 8; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index 108432d965..3f6a999f02 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -1109,15 +1109,53 @@ public boolean tryReadNextFieldNameUtf16( secondMask); } - public boolean tryReadNextFieldNameUtf16Token( - long firstWord, long firstMask, long secondWord, long secondMask, int expectedLength) { + public boolean tryReadNextFieldNameUtf16Token2( + long firstWord, long firstMask, long secondWord, long secondMask, int tokenLength) { byte[] localBytes = bytes; int mark = position; - if (localBytes == null || mark >= length || !utf16CharEquals(localBytes, mark << 1, '"')) { - return false; + int tokenEnd = mark + tokenLength; + if (localBytes != null && tokenEnd <= length) { + int byteOffset = mark << 1; + if ((LittleEndian.getInt64(localBytes, byteOffset) & firstMask) == firstWord + && (secondMask == 0 + || readUtf16TokenWord(localBytes, byteOffset + Long.BYTES, tokenLength - 4) + == secondWord)) { + position = tokenEnd; + return true; + } + } + return false; + } + + public boolean tryReadNextFieldNameUtf16Token3( + long firstWord, long secondWord, long thirdWord, int tokenLength) { + byte[] localBytes = bytes; + int mark = position; + int tokenEnd = mark + tokenLength; + if (localBytes != null && tokenEnd <= length) { + int byteOffset = mark << 1; + if (LittleEndian.getInt64(localBytes, byteOffset) == firstWord + && LittleEndian.getInt64(localBytes, byteOffset + Long.BYTES) == secondWord + && readUtf16TokenWord(localBytes, byteOffset + (Long.BYTES << 1), tokenLength - 8) + == thirdWord) { + position = tokenEnd; + return true; + } } - return tryReadUtf16FieldNameTokenAt( - localBytes, mark, expectedLength, firstWord, firstMask, secondWord, secondMask); + return false; + } + + private static long readUtf16TokenWord(byte[] localBytes, int byteOffset, int chars) { + if (chars == 4 || byteOffset + Long.BYTES <= localBytes.length) { + return LittleEndian.getInt64(localBytes, byteOffset) & utf16WordMask(chars); + } + long word = 0; + for (int i = 0; i < chars; i++) { + int offset = byteOffset + (i << 1); + long ch = (localBytes[offset] & 0xFFL) | ((localBytes[offset + 1] & 0xFFL) << 8); + word |= ch << (i << 4); + } + return word; } private boolean tryReadFieldNameColonAt( @@ -1202,36 +1240,6 @@ && utf16CharEquals(localBytes, quoteOffset << 1, '"')) { return false; } - private boolean tryReadUtf16FieldNameTokenAt( - byte[] localBytes, - int mark, - int expectedLength, - long firstWord, - long firstMask, - long secondWord, - long secondMask) { - int quoteOffset = mark + expectedLength + 1; - int colonOffset = quoteOffset + 1; - int nameOffset = (mark + 1) << 1; - if ((nameOffset + Long.BYTES > localBytes.length) - || (secondMask != 0 && nameOffset + (Long.BYTES << 1) > localBytes.length)) { - return false; - } - // Token-value generated reads do not skip whitespace, so this probe only accepts compact - // `"name":` spelling. The normal UTF16 field probe still owns whitespace-tolerant matching. - if (colonOffset < length - && (LittleEndian.getInt64(localBytes, nameOffset) & firstMask) == firstWord - && (secondMask == 0 - || (LittleEndian.getInt64(localBytes, nameOffset + Long.BYTES) & secondMask) - == secondWord) - && utf16CharEquals(localBytes, quoteOffset << 1, '"') - && utf16CharEquals(localBytes, colonOffset << 1, ':')) { - position = colonOffset + 1; - return true; - } - return false; - } - private static boolean isJsonPackedName(long packed, long mask) { long value = (packed & mask) | (~mask & CONTROL_LIMIT_BYTES); long control = (value - CONTROL_LIMIT_BYTES) & ~value & BYTE_HIGH_BITS; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index 2d80fc48e3..f113bba87e 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -105,25 +105,49 @@ public void readUtf16FieldNameProbe() { @Test public void readUtf16FieldNameTokenProbe() { + String shortToken = "\"size\":"; + int shortTail = shortToken.length() - 4; + Utf16JsonReader shortDirect = utf16Reader("\"size\":7"); + assertTrue( + shortDirect.tryReadNextFieldNameUtf16Token2( + utf16Word(shortToken, 0, 4), + -1L, + utf16Word(shortToken, 4, shortTail), + utf16WordMask(shortTail), + shortToken.length())); + assertEquals(shortDirect.readIntTokenValue(), 7); + shortDirect.finish(); + String name = "duration"; - long firstWord = utf16NameWord(name, 0, 4); - long secondWord = utf16NameWord(name, 4, 4); + String token = "\"" + name + "\":"; + long firstWord = utf16Word(token, 0, 4); + long secondWord = utf16Word(token, 4, 4); + long thirdWord = utf16Word(token, 8, token.length() - 8); Utf16JsonReader direct = utf16Reader("\"duration\":123"); - assertTrue(direct.tryReadNextFieldNameUtf16Token(firstWord, -1L, secondWord, -1L, 8)); + assertTrue( + direct.tryReadNextFieldNameUtf16Token3(firstWord, secondWord, thirdWord, token.length())); assertEquals(direct.readIntTokenValue(), 123); direct.finish(); Utf16JsonReader nullable = utf16Reader("\"duration\":null"); - assertTrue(nullable.tryReadNextFieldNameUtf16Token(firstWord, -1L, secondWord, -1L, 8)); + assertTrue( + nullable.tryReadNextFieldNameUtf16Token3(firstWord, secondWord, thirdWord, token.length())); assertEquals(nullable.readNullableStringToken(), null); nullable.finish(); Utf16JsonReader spaced = utf16Reader("\"duration\" : 123"); - assertFalse(spaced.tryReadNextFieldNameUtf16Token(firstWord, -1L, secondWord, -1L, 8)); + assertFalse( + spaced.tryReadNextFieldNameUtf16Token3(firstWord, secondWord, thirdWord, token.length())); assertTrue( spaced.tryReadNextFieldNameUtf16( - JsonFieldNameHash.hash(name), packedNameMask(8), firstWord, -1L, secondWord, -1L, 8)); + JsonFieldNameHash.hash(name), + packedNameMask(8), + utf16Word(name, 0, 4), + -1L, + utf16Word(name, 4, 4), + -1L, + 8)); assertEquals(spaced.readNextIntValue(), 123); spaced.finish(); } @@ -392,12 +416,16 @@ private static long packedNameMask(int length) { return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L; } - private static long utf16NameWord(String name, int start, int length) { - long value = 0; + private static long utf16WordMask(int length) { + return length == 4 ? -1L : (1L << (length << 4)) - 1L; + } + + private static long utf16Word(String value, int start, int length) { + long packed = 0; for (int i = 0; i < length; i++) { - value |= (long) (name.charAt(start + i) & 0xFF) << (i << 3); + packed |= (long) (value.charAt(start + i) & 0xFF) << (i << 3); } - long word = spreadLatin1ToUtf16(value); + long word = spreadLatin1ToUtf16(packed); return NativeByteOrder.IS_LITTLE_ENDIAN ? word : word << 8; } From c82f292116bf894c23c8605e1c3df054396ec413 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 11:29:12 +0800 Subject: [PATCH 105/120] perf(json): hoist UTF16 string collection writes --- .../fory/json/writer/StringJsonWriter.java | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 640759f274..6cac921a1f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -714,11 +714,7 @@ public void writeStringElement(int index, String value) { private void writeNullStringElement(int comma) { if (coder == UTF16) { - ensure((comma + 4) << 1); - if (comma != 0) { - writeUtf16ByteNoEnsure((byte) ','); - } - writeAsciiUtf16NoEnsure("null", 4); + writeNullStringElementUtf16(comma); return; } ensure(comma + 4); @@ -728,6 +724,14 @@ private void writeNullStringElement(int comma) { writeAsciiLatin1NoEnsure("null"); } + private void writeNullStringElementUtf16(int comma) { + ensure((comma + 4) << 1); + if (comma != 0) { + writeUtf16ByteNoEnsure((byte) ','); + } + writeAsciiUtf16NoEnsure("null", 4); + } + private void writeStringElementLatin1(int comma, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); @@ -764,8 +768,21 @@ private void writeStringElementUtf16(int comma, String value) { writeStringUtf16(value); } + private void writeStringElementUtf16Nullable(int comma, String value) { + if (value == null) { + writeNullStringElementUtf16(comma); + return; + } + writeStringElementUtf16(comma, value); + } + public void writeStringCollection(Collection values) { writeArrayStart(); + if (coder == UTF16) { + writeStringCollectionUtf16(values); + writeArrayEnd(); + return; + } if (values.getClass() == ArrayList.class) { ArrayList list = (ArrayList) values; int size = list.size(); @@ -784,6 +801,24 @@ public void writeStringCollection(Collection values) { writeArrayEnd(); } + private void writeStringCollectionUtf16(Collection values) { + if (values.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) values; + int size = list.size(); + if (size != 0) { + writeStringElementUtf16Nullable(0, list.get(0)); + for (int i = 1; i < size; i++) { + writeStringElementUtf16Nullable(1, list.get(i)); + } + } + } else { + int index = 0; + for (String value : values) { + writeStringElementUtf16Nullable(index++, value); + } + } + } + public void writeRawValue(byte[] value) { writeRaw(value); } From 88cd7e8352322ca7d9bd5709db2c0f7ac6539e24 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 12:26:51 +0800 Subject: [PATCH 106/120] perf(json): write small UTF16 ints exactly --- .../fory/json/writer/StringJsonWriter.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 6cac921a1f..31bffce5bf 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -1721,10 +1721,32 @@ private static int writeIntUpTo4Utf16(byte[] bytes, int pos, int value) { private static int writeIntUpTo3Utf16(byte[] bytes, int pos, int value) { int digits = DIGIT_TRIPLES[value]; int skip = digits & 0xFF; - long latin1 = (digits >>> ((skip + 1) << 3)) & 0xFFFFFFFFL; - long utf16 = spreadLatin1ToUtf16(latin1); - LittleEndian.putInt64(bytes, pos, LITTLE_ENDIAN ? utf16 : utf16 << 8); - return pos + ((3 - skip) << 1); + int count = 3 - skip; + int chars = digits >>> ((skip + 1) << 3); + if (LITTLE_ENDIAN) { + bytes[pos] = (byte) chars; + bytes[pos + 1] = 0; + if (count > 1) { + bytes[pos + 2] = (byte) (chars >>> 8); + bytes[pos + 3] = 0; + if (count > 2) { + bytes[pos + 4] = (byte) (chars >>> 16); + bytes[pos + 5] = 0; + } + } + } else { + bytes[pos] = 0; + bytes[pos + 1] = (byte) chars; + if (count > 1) { + bytes[pos + 2] = 0; + bytes[pos + 3] = (byte) (chars >>> 8); + if (count > 2) { + bytes[pos + 4] = 0; + bytes[pos + 5] = (byte) (chars >>> 16); + } + } + } + return pos + (count << 1); } private static int writePadded4Utf16(byte[] bytes, int pos, int value) { From 3907230664fee85115bb7ab0cdd815a8f09c575a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 12:53:47 +0800 Subject: [PATCH 107/120] perf(json): skip UTF16 surrogate scan for ASCII words --- .../fory/json/reader/Utf16JsonReader.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index 3f6a999f02..f08e391838 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -708,7 +708,8 @@ private String readUtf16StringToken(int start) { int doubleWordEnd = inputLength - 8; while (offset <= doubleWordEnd) { long word = LittleEndian.getInt64(localBytes, offset << 1); - long stopMask = utf16StringStopMask(word); + long nonLatinBytes = word & UTF16_NON_LATIN_BYTES; + long stopMask = utf16StringStopMask(word, nonLatinBytes); if (stopMask != 0) { int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 4); boolean currentNonLatin = hasNonLatinBeforeStop(nonLatin, word, stop - offset); @@ -724,10 +725,11 @@ private String readUtf16StringToken(int start) { offset = position; continue; } - nonLatin |= (word & UTF16_NON_LATIN_BYTES) != 0; + nonLatin |= nonLatinBytes != 0; int nextOffset = offset + 4; word = LittleEndian.getInt64(localBytes, nextOffset << 1); - stopMask = utf16StringStopMask(word); + nonLatinBytes = word & UTF16_NON_LATIN_BYTES; + stopMask = utf16StringStopMask(word, nonLatinBytes); if (stopMask != 0) { int stop = nextOffset + (Long.numberOfTrailingZeros(stopMask) >>> 4); boolean currentNonLatin = hasNonLatinBeforeStop(nonLatin, word, stop - nextOffset); @@ -743,15 +745,16 @@ private String readUtf16StringToken(int start) { offset = position; continue; } - nonLatin |= (word & UTF16_NON_LATIN_BYTES) != 0; + nonLatin |= nonLatinBytes != 0; offset = nextOffset + 4; } int wordEnd = inputLength - 4; while (offset <= wordEnd) { long word = LittleEndian.getInt64(localBytes, offset << 1); - long stopMask = utf16StringStopMask(word); + long nonLatinBytes = word & UTF16_NON_LATIN_BYTES; + long stopMask = utf16StringStopMask(word, nonLatinBytes); if (stopMask == 0) { - nonLatin |= (word & UTF16_NON_LATIN_BYTES) != 0; + nonLatin |= nonLatinBytes != 0; offset += 4; continue; } @@ -1252,13 +1255,16 @@ private static long byteMatchMask(long word, long repeatedByte) { return (match - BYTE_ONES) & ~match & BYTE_HIGH_BITS; } - private static long utf16StringStopMask(long word) { + private static long utf16StringStopMask(long word, long nonLatinBytes) { long syntaxStop = utf16CharMatchMask(word, UTF16_QUOTE_CHARS) | utf16CharMatchMask(word, UTF16_BACKSLASH_CHARS); long controlStop = (word - UTF16_CONTROL_LIMIT) & ~word & UTF16_HIGH_BITS; - long surrogateStop = utf16CharMatchMask(word & UTF16_SURROGATE_MASK, UTF16_SURROGATE_PREFIX); - return syntaxStop | controlStop | surrogateStop; + long stop = syntaxStop | controlStop; + if (nonLatinBytes != 0) { + stop |= utf16CharMatchMask(word & UTF16_SURROGATE_MASK, UTF16_SURROGATE_PREFIX); + } + return stop; } private static long utf16CharMatchMask(long word, long repeatedChar) { From 9f606c5a9eeed70deaaf2344642c156025067311 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 13:23:03 +0800 Subject: [PATCH 108/120] perf(json): skip nonlatin stop check for ASCII UTF16 --- .../apache/fory/json/reader/Utf16JsonReader.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index f08e391838..6220c91fff 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -712,7 +712,8 @@ private String readUtf16StringToken(int start) { long stopMask = utf16StringStopMask(word, nonLatinBytes); if (stopMask != 0) { int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 4); - boolean currentNonLatin = hasNonLatinBeforeStop(nonLatin, word, stop - offset); + boolean currentNonLatin = + nonLatin || nonLatinBytes != 0 && hasNonLatinBeforeStop(word, stop - offset); char ch = charAtFast(stop); if (Character.isHighSurrogate(ch)) { currentNonLatin = true; @@ -732,7 +733,8 @@ private String readUtf16StringToken(int start) { stopMask = utf16StringStopMask(word, nonLatinBytes); if (stopMask != 0) { int stop = nextOffset + (Long.numberOfTrailingZeros(stopMask) >>> 4); - boolean currentNonLatin = hasNonLatinBeforeStop(nonLatin, word, stop - nextOffset); + boolean currentNonLatin = + nonLatin || nonLatinBytes != 0 && hasNonLatinBeforeStop(word, stop - nextOffset); char ch = charAtFast(stop); if (Character.isHighSurrogate(ch)) { currentNonLatin = true; @@ -759,7 +761,8 @@ private String readUtf16StringToken(int start) { continue; } int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 4); - boolean currentNonLatin = hasNonLatinBeforeStop(nonLatin, word, stop - offset); + boolean currentNonLatin = + nonLatin || nonLatinBytes != 0 && hasNonLatinBeforeStop(word, stop - offset); char ch = charAtFast(stop); if (Character.isHighSurrogate(ch)) { currentNonLatin = true; @@ -807,10 +810,7 @@ private String readUtf16StringStop( throw error("Unpaired low surrogate in string"); } - private static boolean hasNonLatinBeforeStop(boolean previousNonLatin, long word, int chars) { - if (previousNonLatin) { - return true; - } + private static boolean hasNonLatinBeforeStop(long word, int chars) { long mask = chars == 4 ? -1L : (1L << (chars << 4)) - 1; return (word & mask & UTF16_NON_LATIN_BYTES) != 0; } From a3080faaea048476bc4103ff3ce4506233f60ee3 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 15:33:27 +0800 Subject: [PATCH 109/120] perf(json): speed up String JSON writing --- .../fory/json/writer/StringJsonWriter.java | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index 31bffce5bf..a726441e23 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -42,6 +42,8 @@ public final class StringJsonWriter extends JsonWriter { private static final int INT_HIGH_BITS = 0x80808080; private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; private static final int INT_ASCII_CONTROL_OFFSET = 0x60606060; + private static final long ASCII_GT_QUOTE_OFFSET = 0x5D5D5D5D5D5D5D5DL; + private static final int INT_ASCII_GT_QUOTE_OFFSET = 0x5D5D5D5D; private static final long ONE_BYTES = 0x0101010101010101L; private static final int INT_ONE_BYTES = 0x01010101; private static final long QUOTE_BYTES_COMPLEMENT = ~0x2222222222222222L; @@ -61,6 +63,8 @@ public final class StringJsonWriter extends JsonWriter { private static final long UTF16_SURROGATE_MASK = 0xf800f800f800f800L; private static final long UTF16_SURROGATE_PREFIX = 0xd800d800d800d800L; private static final boolean STRING_BYTES_BACKED = StringSerializer.isBytesBackedString(); + // Compact byte-backed Strings use one byte per char for LATIN1 and two bytes per char for UTF16. + // Length checks keep hot JSON string writing from loading the separate String coder field. private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; static { @@ -227,12 +231,12 @@ private void writeStringLatin1(String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); int length = value.length(); - if (isLatin1Bytes(bytes, length)) { + if (bytes.length == length) { ensure(bytes.length + 2); writeLatin1StringNoEnsure(bytes); return; } - if (LITTLE_ENDIAN && isUtf16Bytes(bytes, length)) { + if (LITTLE_ENDIAN) { upgradeToUtf16((position << 1) + ((length + 2) << 1)); writeUtf16StringBytes(value, bytes); return; @@ -648,13 +652,13 @@ private void writeStringFieldLatin1(byte[] prefix, byte[] utf16Prefix, String va if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); int length = value.length(); - if (isLatin1Bytes(bytes, length)) { + if (bytes.length == length) { ensure(prefix.length + bytes.length + 2); writeRawLatin1NoEnsure(prefix); writeLatin1StringNoEnsure(bytes); return; } - if (LITTLE_ENDIAN && isUtf16Bytes(bytes, length)) { + if (LITTLE_ENDIAN) { int utf16PrefixLength = utf16Prefix == null ? prefix.length << 1 : utf16Prefix.length; // A compact UTF16 input cannot remain in a Latin1 JSON String. Upgrade before writing the // pending prefix so it is emitted once in the final coder instead of being written as @@ -736,7 +740,7 @@ private void writeStringElementLatin1(int comma, String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); int length = value.length(); - if (isLatin1Bytes(bytes, length)) { + if (bytes.length == length) { ensure(comma + bytes.length + 2); if (comma != 0) { buffer[position++] = ','; @@ -744,7 +748,7 @@ private void writeStringElementLatin1(int comma, String value) { writeLatin1StringNoEnsure(bytes); return; } - if (LITTLE_ENDIAN && isUtf16Bytes(bytes, length)) { + if (LITTLE_ENDIAN) { upgradeToUtf16((position << 1) + ((comma + length + 2) << 1)); if (comma != 0) { writeUtf16ByteNoEnsure((byte) ','); @@ -1052,15 +1056,19 @@ private void writeStringUtf16(String value) { if (STRING_BYTES_BACKED) { byte[] bytes = StringSerializer.getStringBytes(value); int length = value.length(); - if (isLatin1Bytes(bytes, length)) { + if (bytes.length == length) { writeLatin1StringUtf16(bytes); return; } - if (LITTLE_ENDIAN && isUtf16Bytes(bytes, length)) { + if (LITTLE_ENDIAN) { writeUtf16StringBytes(value, bytes); return; } } + writeStringUtf16Chars(value); + } + + private void writeStringUtf16Chars(String value) { int length = value.length(); ensure((length + 2) << 1); writeUtf16ByteNoEnsure((byte) '"'); @@ -1079,16 +1087,6 @@ private void writeStringUtf16(String value) { writeByteRaw((byte) '"'); } - private static boolean isLatin1Bytes(byte[] bytes, int length) { - return bytes.length == length; - } - - private static boolean isUtf16Bytes(byte[] bytes, int length) { - // For byte-backed compact Strings, backing byte length identifies the coder without another - // String.coder field load. - return bytes.length != length && bytes.length == length + length; - } - private void writeUtf16StringBytes(String value, byte[] source) { latin1Output = false; int length = value.length(); @@ -1527,17 +1525,23 @@ private static boolean isJsonLatin1Byte(byte value) { } private static boolean isJsonAsciiWord(long word) { - long control = ((word + ASCII_CONTROL_OFFSET) & ~word) & HIGH_BITS; - long quote = ((word ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS; - long backslash = ((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS; - return (control & quote & backslash) == HIGH_BITS; + long notBackslash = ((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS; + if ((notBackslash & (word + ASCII_GT_QUOTE_OFFSET)) == HIGH_BITS) { + return true; + } + return (((word + ASCII_CONTROL_OFFSET) & ~word) & HIGH_BITS) == HIGH_BITS + && (((word ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS + && notBackslash == HIGH_BITS; } private static boolean isJsonAsciiInt(int word) { + int notBackslash = ((word ^ INT_BACKSLASH_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS; + if ((notBackslash & (word + INT_ASCII_GT_QUOTE_OFFSET)) == INT_HIGH_BITS) { + return true; + } return (((word + INT_ASCII_CONTROL_OFFSET) & ~word) & INT_HIGH_BITS) == INT_HIGH_BITS && (((word ^ INT_QUOTE_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) == INT_HIGH_BITS - && (((word ^ INT_BACKSLASH_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) - == INT_HIGH_BITS; + && notBackslash == INT_HIGH_BITS; } private void writeIntNoEnsure(int value) { From d44a283c1049aa7a34f2a4a268b200f1fdf77de5 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 17:12:57 +0800 Subject: [PATCH 110/120] perf(json): speed up utf8 writer surrogate check --- .../main/java/org/apache/fory/json/writer/Utf8JsonWriter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 641fdbc0f3..a94f8d7849 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -917,7 +917,7 @@ private boolean writeUtf16StringNoEnsure(byte[] value) { } else if (ch < 0x800) { bytes[pos++] = (byte) (0xC0 | (ch >>> 6)); bytes[pos++] = (byte) (0x80 | (ch & 0x3F)); - } else if (Character.isSurrogate(ch)) { + } else if (ch >= Character.MIN_SURROGATE && ch <= Character.MAX_SURROGATE) { position = start; return false; } else { From 53af0517cddff7e8dc923c7fc66324b0ba0383f9 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 17:19:04 +0800 Subject: [PATCH 111/120] perf(json): speed up utf8 unicode string reads --- .../fory/json/reader/Utf8JsonReader.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 1a73cc6583..d95e681ac1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -23,11 +23,13 @@ import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.Arrays; import java.util.UUID; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; import org.apache.fory.memory.LittleEndian; +import org.apache.fory.serializer.StringEncodingUtils; import org.apache.fory.serializer.StringSerializer; public final class Utf8JsonReader extends JsonReader { @@ -1457,13 +1459,39 @@ private String readStringStop(int start, int stop, int b) { return readStringTail(builder); } if (b < 0) { - appendUtf8(builder, b & 0xFF); - return readStringTail(builder); + // Two-byte UTF-8 sequences may still decode to compact Latin1 strings; keep that path on the + // StringBuilder decoder so String coder selection stays owned by JDK String construction. + if ((b & 0xE0) == 0xC0) { + appendUtf8(builder, b & 0xFF); + return readStringTail(builder); + } + return readUtf8StringStop(start, stop); } appendUtf8(builder, b); return readStringTail(builder); } + private String readUtf8StringStop(int start, int firstUtf8Byte) { + byte[] bytes = input; + int offset = position; + int inputLength = bytes.length; + while (offset < inputLength) { + int b = bytes[offset++]; + if (b == '"') { + position = offset; + return newUtf8String(start, offset - 1); + } + if (b == '\\' || (b >= 0 && b < 0x20)) { + position = firstUtf8Byte + 1; + StringBuilder builder = newStringBuilder(start, firstUtf8Byte); + appendAscii(builder, start, firstUtf8Byte); + appendUtf8(builder, bytes[firstUtf8Byte] & 0xFF); + return readStringTail(builder); + } + } + throw error("Unterminated string"); + } + private StringBuilder newStringBuilder(int start, int stop) { return new StringBuilder(Math.max(16, stop - start + 16)); } @@ -1866,6 +1894,19 @@ private String newLatin1String(int start, int end) { return StringSerializer.newLatin1StringZeroCopy(bytes); } + private String newUtf8String(int start, int end) { + int length = end - start; + byte[] bytes = new byte[length << 1]; + int utf16Length = StringEncodingUtils.convertUTF8ToUTF16(input, start, length, bytes); + if (utf16Length < 0) { + throw error("Invalid UTF-8 sequence"); + } + if (utf16Length != bytes.length) { + bytes = Arrays.copyOf(bytes, utf16Length); + } + return StringSerializer.newUtf16StringZeroCopy(bytes); + } + private String readStringTail(StringBuilder builder) { while (position < input.length) { int b = input[position++] & 0xFF; From 4fe5843e3add747fa4ebd2f60ef0eab5019178c4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 17:36:02 +0800 Subject: [PATCH 112/120] perf(json): unroll utf16 three-byte writes Users 10KB Chinese serialization with JDK 25, -wi 5 -i 10 -w 3s -r 3s -f 1 -t 1: Fory 418035 ops/s, fastjson2 312106 ops/s, Wast 300582 ops/s. --- .../fory/json/writer/Utf8JsonWriter.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index a94f8d7849..ad8502e6cd 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -906,8 +906,34 @@ private boolean writeUtf16StringNoEnsure(byte[] value) { LittleEndian.putInt32(bytes, pos, packed); pos += 4; } - for (; i < length; i += 2) { + while (i < length) { + if (i + 8 <= length) { + char c0 = StringSerializer.getBytesChar(value, i); + char c1 = StringSerializer.getBytesChar(value, i + 2); + char c2 = StringSerializer.getBytesChar(value, i + 4); + char c3 = StringSerializer.getBytesChar(value, i + 6); + if (isUtf8ThreeByte(c0) + && isUtf8ThreeByte(c1) + && isUtf8ThreeByte(c2) + && isUtf8ThreeByte(c3)) { + bytes[pos++] = (byte) (0xE0 | (c0 >>> 12)); + bytes[pos++] = (byte) (0x80 | ((c0 >>> 6) & 0x3F)); + bytes[pos++] = (byte) (0x80 | (c0 & 0x3F)); + bytes[pos++] = (byte) (0xE0 | (c1 >>> 12)); + bytes[pos++] = (byte) (0x80 | ((c1 >>> 6) & 0x3F)); + bytes[pos++] = (byte) (0x80 | (c1 & 0x3F)); + bytes[pos++] = (byte) (0xE0 | (c2 >>> 12)); + bytes[pos++] = (byte) (0x80 | ((c2 >>> 6) & 0x3F)); + bytes[pos++] = (byte) (0x80 | (c2 & 0x3F)); + bytes[pos++] = (byte) (0xE0 | (c3 >>> 12)); + bytes[pos++] = (byte) (0x80 | ((c3 >>> 6) & 0x3F)); + bytes[pos++] = (byte) (0x80 | (c3 & 0x3F)); + i += 8; + continue; + } + } char ch = StringSerializer.getBytesChar(value, i); + i += 2; if (ch < 0x80) { if (!isJsonAscii(ch)) { position = start; @@ -931,6 +957,10 @@ private boolean writeUtf16StringNoEnsure(byte[] value) { return true; } + private static boolean isUtf8ThreeByte(char ch) { + return ch >= 0x800 && (ch < Character.MIN_SURROGATE || ch > Character.MAX_SURROGATE); + } + private void writeEscapedChar(char ch) { switch (ch) { case '"': From 5e5e36f4fb0d40266c6bddf40a32325a3637361a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 17:41:45 +0800 Subject: [PATCH 113/120] perf(json): avoid eager utf8 string builder Users 10KB Chinese deserialization with JDK 25, -wi 5 -i 10 -w 3s -r 3s -f 1 -t 1: Fory byte path 107832 ops/s after the change versus 104907 ops/s before. --- .../java/org/apache/fory/json/reader/Utf8JsonReader.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index d95e681ac1..d8fe0caa32 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -1452,9 +1452,9 @@ private String readStringStop(int start, int stop, int b) { if (b >= 0 && b < 0x20) { throw error("Control character in string"); } - StringBuilder builder = newStringBuilder(start, stop); - appendAscii(builder, start, stop); if (b == '\\') { + StringBuilder builder = newStringBuilder(start, stop); + appendAscii(builder, start, stop); appendEscape(builder); return readStringTail(builder); } @@ -1462,11 +1462,15 @@ private String readStringStop(int start, int stop, int b) { // Two-byte UTF-8 sequences may still decode to compact Latin1 strings; keep that path on the // StringBuilder decoder so String coder selection stays owned by JDK String construction. if ((b & 0xE0) == 0xC0) { + StringBuilder builder = newStringBuilder(start, stop); + appendAscii(builder, start, stop); appendUtf8(builder, b & 0xFF); return readStringTail(builder); } return readUtf8StringStop(start, stop); } + StringBuilder builder = newStringBuilder(start, stop); + appendAscii(builder, start, stop); appendUtf8(builder, b); return readStringTail(builder); } From a9d178279ac37c20efd3fa1b5bd089e66a9f06cb Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 18:01:33 +0800 Subject: [PATCH 114/120] perf(json): reuse utf8 string decode buffer --- .../fory/json/reader/Utf8JsonReader.java | 291 ++++++++++++------ .../org/apache/fory/json/JsonStringTest.java | 51 +++ 2 files changed, 246 insertions(+), 96 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index d8fe0caa32..e708432118 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -29,11 +29,14 @@ import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; import org.apache.fory.memory.LittleEndian; -import org.apache.fory.serializer.StringEncodingUtils; +import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.serializer.StringSerializer; public final class Utf8JsonReader extends JsonReader { private static final byte[] EMPTY_BYTES = new byte[0]; + private static final int INITIAL_STRING_DECODE_BUFFER_SIZE = 1024; + private static final int RETAINED_STRING_DECODE_BUFFER_SIZE = 8192; + private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; private static final long BYTE_ONES = 0x0101010101010101L; private static final int INT_BYTE_ONES = 0x01010101; private static final long BYTE_HIGH_BITS = 0x8080808080808080L; @@ -58,6 +61,7 @@ public final class Utf8JsonReader extends JsonReader { // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. // UTF-8 string decoding must keep unsigned byte conversion for non-ASCII content. private byte[] input; + private byte[] stringDecodeBuffer = new byte[INITIAL_STRING_DECODE_BUFFER_SIZE]; public Utf8JsonReader() { input = EMPTY_BYTES; @@ -76,6 +80,9 @@ public Utf8JsonReader reset(byte[] input) { public void clear() { input = EMPTY_BYTES; position = 0; + if (stringDecodeBuffer.length > RETAINED_STRING_DECODE_BUFFER_SIZE) { + stringDecodeBuffer = new byte[RETAINED_STRING_DECODE_BUFFER_SIZE]; + } } public boolean consumeToken(char expected) { @@ -1449,55 +1456,13 @@ private static boolean isDigit(byte b) { private String readStringStop(int start, int stop, int b) { position = stop + 1; - if (b >= 0 && b < 0x20) { - throw error("Control character in string"); - } - if (b == '\\') { - StringBuilder builder = newStringBuilder(start, stop); - appendAscii(builder, start, stop); - appendEscape(builder); - return readStringTail(builder); - } - if (b < 0) { - // Two-byte UTF-8 sequences may still decode to compact Latin1 strings; keep that path on the - // StringBuilder decoder so String coder selection stays owned by JDK String construction. - if ((b & 0xE0) == 0xC0) { - StringBuilder builder = newStringBuilder(start, stop); - appendAscii(builder, start, stop); - appendUtf8(builder, b & 0xFF); - return readStringTail(builder); - } - return readUtf8StringStop(start, stop); + int out = stop - start; + byte[] bytes = stringDecodeBuffer; + if (bytes.length < out) { + bytes = growStringDecodeBuffer(bytes, out); } - StringBuilder builder = newStringBuilder(start, stop); - appendAscii(builder, start, stop); - appendUtf8(builder, b); - return readStringTail(builder); - } - - private String readUtf8StringStop(int start, int firstUtf8Byte) { - byte[] bytes = input; - int offset = position; - int inputLength = bytes.length; - while (offset < inputLength) { - int b = bytes[offset++]; - if (b == '"') { - position = offset; - return newUtf8String(start, offset - 1); - } - if (b == '\\' || (b >= 0 && b < 0x20)) { - position = firstUtf8Byte + 1; - StringBuilder builder = newStringBuilder(start, firstUtf8Byte); - appendAscii(builder, start, firstUtf8Byte); - appendUtf8(builder, bytes[firstUtf8Byte] & 0xFF); - return readStringTail(builder); - } - } - throw error("Unterminated string"); - } - - private StringBuilder newStringBuilder(int start, int stop) { - return new StringBuilder(Math.max(16, stop - start + 16)); + System.arraycopy(input, start, bytes, 0, out); + return readStringLatin1Tail(bytes, out, b); } private static long stringStopMask(long word) { @@ -1898,53 +1863,6 @@ private String newLatin1String(int start, int end) { return StringSerializer.newLatin1StringZeroCopy(bytes); } - private String newUtf8String(int start, int end) { - int length = end - start; - byte[] bytes = new byte[length << 1]; - int utf16Length = StringEncodingUtils.convertUTF8ToUTF16(input, start, length, bytes); - if (utf16Length < 0) { - throw error("Invalid UTF-8 sequence"); - } - if (utf16Length != bytes.length) { - bytes = Arrays.copyOf(bytes, utf16Length); - } - return StringSerializer.newUtf16StringZeroCopy(bytes); - } - - private String readStringTail(StringBuilder builder) { - while (position < input.length) { - int b = input[position++] & 0xFF; - if (b == '"') { - return builder.toString(); - } else if (b == '\\') { - appendEscape(builder); - } else if (b < 0x20) { - throw error("Control character in string"); - } else if (b < 0x80) { - builder.append((char) b); - } else { - appendUtf8(builder, b); - } - } - throw error("Unterminated string"); - } - - private void appendAscii(StringBuilder builder, int start, int end) { - for (int i = start; i < end; i++) { - builder.append((char) (input[i] & 0xFF)); - } - } - - private void appendUtf8(StringBuilder builder, int first) { - int codePoint = readUtf8CodePoint(first); - if (codePoint <= 0xFFFF) { - builder.append((char) codePoint); - } else { - builder.append(Character.highSurrogate(codePoint)); - builder.append(Character.lowSurrogate(codePoint)); - } - } - private int readUtf8CodePoint(int first) { if ((first & 0xE0) == 0xC0) { int second = continuation(); @@ -1974,6 +1892,187 @@ private int readUtf8CodePoint(int first) { throw error("Invalid UTF-8 sequence"); } + private String readStringLatin1Tail(byte[] bytes, int out, int b) { + while (true) { + if (b == '"') { + return finishDecodedString(bytes, out, false); + } + if (b == '\\') { + char ch = readEscapedStringChar(); + if (Character.isHighSurrogate(ch)) { + char low = readLowSurrogateEscape(); + bytes = widenStringDecodeBuffer(bytes, out); + out <<= 1; + bytes = ensureStringDecodeCapacity(bytes, out + 4); + out = putUtf16Char(bytes, out, ch); + out = putUtf16Char(bytes, out, low); + return readStringUtf16Tail(bytes, out); + } + if (Character.isLowSurrogate(ch)) { + throw error("Unpaired low surrogate escape"); + } + if (ch <= 0xFF) { + bytes = ensureStringDecodeCapacity(bytes, out + 1); + bytes[out++] = (byte) ch; + } else { + bytes = widenStringDecodeBuffer(bytes, out); + out <<= 1; + bytes = ensureStringDecodeCapacity(bytes, out + 2); + out = putUtf16Char(bytes, out, ch); + return readStringUtf16Tail(bytes, out); + } + } else if (b >= 0 && b < 0x20) { + throw error("Control character in string"); + } else if (b >= 0 && b < 0x80) { + bytes = ensureStringDecodeCapacity(bytes, out + 1); + bytes[out++] = (byte) b; + } else { + int codePoint = readUtf8CodePoint(b & 0xFF); + if (codePoint <= 0xFF) { + bytes = ensureStringDecodeCapacity(bytes, out + 1); + bytes[out++] = (byte) codePoint; + } else { + bytes = widenStringDecodeBuffer(bytes, out); + out <<= 1; + if (codePoint <= 0xFFFF) { + bytes = ensureStringDecodeCapacity(bytes, out + 2); + out = putUtf16Char(bytes, out, (char) codePoint); + } else { + bytes = ensureStringDecodeCapacity(bytes, out + 4); + out = putUtf16Char(bytes, out, Character.highSurrogate(codePoint)); + out = putUtf16Char(bytes, out, Character.lowSurrogate(codePoint)); + } + return readStringUtf16Tail(bytes, out); + } + } + if (position >= input.length) { + throw error("Unterminated string"); + } + b = input[position++] & 0xFF; + } + } + + private String readStringUtf16Tail(byte[] bytes, int out) { + while (position < input.length) { + int b = input[position++] & 0xFF; + if (b == '"') { + return finishDecodedString(bytes, out, true); + } + if (b == '\\') { + char ch = readEscapedStringChar(); + if (Character.isHighSurrogate(ch)) { + char low = readLowSurrogateEscape(); + bytes = ensureStringDecodeCapacity(bytes, out + 4); + out = putUtf16Char(bytes, out, ch); + out = putUtf16Char(bytes, out, low); + } else if (Character.isLowSurrogate(ch)) { + throw error("Unpaired low surrogate escape"); + } else { + bytes = ensureStringDecodeCapacity(bytes, out + 2); + out = putUtf16Char(bytes, out, ch); + } + } else if (b < 0x20) { + throw error("Control character in string"); + } else if (b < 0x80) { + bytes = ensureStringDecodeCapacity(bytes, out + 2); + out = putUtf16Char(bytes, out, (char) b); + } else { + int codePoint = readUtf8CodePoint(b); + if (codePoint <= 0xFFFF) { + bytes = ensureStringDecodeCapacity(bytes, out + 2); + out = putUtf16Char(bytes, out, (char) codePoint); + } else { + bytes = ensureStringDecodeCapacity(bytes, out + 4); + out = putUtf16Char(bytes, out, Character.highSurrogate(codePoint)); + out = putUtf16Char(bytes, out, Character.lowSurrogate(codePoint)); + } + } + } + throw error("Unterminated string"); + } + + private char readEscapedStringChar() { + if (position >= input.length) { + throw error("Unterminated escape"); + } + char escaped = (char) (input[position++] & 0xFF); + switch (escaped) { + case '"': + case '\\': + case '/': + return escaped; + case 'b': + return '\b'; + case 'f': + return '\f'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case 'u': + return readUnicodeEscape(); + default: + throw error("Invalid escape"); + } + } + + private char readLowSurrogateEscape() { + if (position + 2 > input.length || input[position] != '\\' || input[position + 1] != 'u') { + throw error("Unpaired high surrogate escape"); + } + position += 2; + char low = readUnicodeEscape(); + if (!Character.isLowSurrogate(low)) { + throw error("Unpaired high surrogate escape"); + } + return low; + } + + private String finishDecodedString(byte[] bytes, int length, boolean utf16) { + // Strings must not share the reader-owned decode buffer; the buffer is reused by later reads. + byte[] result = new byte[length]; + System.arraycopy(bytes, 0, result, 0, length); + return utf16 + ? StringSerializer.newUtf16StringZeroCopy(result) + : StringSerializer.newLatin1StringZeroCopy(result); + } + + private byte[] ensureStringDecodeCapacity(byte[] bytes, int capacity) { + if (bytes.length < capacity) { + return growStringDecodeBuffer(bytes, capacity); + } + return bytes; + } + + private byte[] growStringDecodeBuffer(byte[] bytes, int capacity) { + int newCapacity = Math.max(capacity, bytes.length << 1); + byte[] grown = Arrays.copyOf(bytes, newCapacity); + stringDecodeBuffer = grown; + return grown; + } + + private byte[] widenStringDecodeBuffer(byte[] bytes, int length) { + int utf16Length = length << 1; + bytes = ensureStringDecodeCapacity(bytes, utf16Length); + for (int i = length - 1, pos = utf16Length - 2; i >= 0; i--, pos -= 2) { + putUtf16Char(bytes, pos, (char) (bytes[i] & 0xFF)); + } + return bytes; + } + + private static int putUtf16Char(byte[] bytes, int pos, char value) { + if (LITTLE_ENDIAN) { + bytes[pos] = (byte) value; + bytes[pos + 1] = (byte) (value >>> 8); + } else { + bytes[pos] = (byte) (value >>> 8); + bytes[pos + 1] = (byte) value; + } + return pos + 2; + } + private int continuation() { if (position >= input.length) { throw error("Short UTF-8 sequence"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index f113bba87e..9df6dadcf5 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -389,6 +389,41 @@ public void readStringScanBoundaries() { assertThrows(ForyJsonException.class, () -> json.fromJson(invalidUtf8, String.class)); } + @Test + public void readUtf8DecodedStrings() { + ForyJson json = ForyJson.builder().build(); + String ascii = readUtf8String(json, "\"plain ascii\""); + String latin1 = readUtf8String(json, "\"caf\u00e9\""); + String escaped = readUtf8String(json, "\"line\\n\\t\\\\\\\"\\/end\""); + String escapedLatin1 = readUtf8String(json, "\"caf\\u00e9\""); + String chinese = readUtf8String(json, "\"\u4e2d\u6587\""); + String escapedChinese = readUtf8String(json, "\"\\u4e2d\\u6587\""); + String supplementary = readUtf8String(json, "\"\\uD83D\\uDE00\""); + + assertEquals(ascii, "plain ascii"); + assertEquals(latin1, "caf\u00e9"); + assertEquals(escaped, "line\n\t\\\"/end"); + assertEquals(escapedLatin1, "caf\u00e9"); + assertEquals(chinese, "\u4e2d\u6587"); + assertEquals(escapedChinese, "\u4e2d\u6587"); + assertEquals(supplementary, "\uD83D\uDE00"); + assertLatin1String(ascii); + assertLatin1String(latin1); + assertLatin1String(escaped); + assertLatin1String(escapedLatin1); + assertUtf16String(chinese); + assertUtf16String(escapedChinese); + assertUtf16String(supplementary); + + byte[] rawControl = {'"', 'b', 'a', 'd', 0x1f, '"'}; + byte[] malformedUtf8 = {'"', (byte) 0xE4, (byte) 0xB8, '"'}; + assertThrows(ForyJsonException.class, () -> json.fromJson(rawControl, String.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson(malformedUtf8, String.class)); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("\"\\uD800\"".getBytes(StandardCharsets.UTF_8), String.class)); + } + @Test public void rejectInvalidSurrogates() { ForyJson json = ForyJson.builder().build(); @@ -406,6 +441,22 @@ private static int writerBufferLength(StringJsonWriter writer) throws Exception return ((byte[]) field.get(writer)).length; } + private static String readUtf8String(ForyJson json, String input) { + return json.fromJson(input.getBytes(StandardCharsets.UTF_8), String.class); + } + + private static void assertLatin1String(String value) { + if (StringSerializer.isBytesBackedString()) { + assertTrue(StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(value))); + } + } + + private static void assertUtf16String(String value) { + if (StringSerializer.isBytesBackedString()) { + assertTrue(StringSerializer.isUtf16Coder(StringSerializer.getStringCoder(value))); + } + } + private static Utf16JsonReader utf16Reader(String input) { byte[] bytes = new byte[input.length() << 1]; StringSerializer.copyStringCharsToBytes(input, bytes); From 9cd9a2e9f53dde67e1683f88661d3df9408ecb90 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 19:17:03 +0800 Subject: [PATCH 115/120] perf(json): speed up utf8 string decoding --- .../fory/json/reader/Utf8JsonReader.java | 151 ++++++++++++++++-- 1 file changed, 135 insertions(+), 16 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index e708432118..42fd45dcfa 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -1458,6 +1458,12 @@ private String readStringStop(int start, int stop, int b) { position = stop + 1; int out = stop - start; byte[] bytes = stringDecodeBuffer; + if (out == 0 && b < 0) { + int first = b & 0xFF; + if ((first & 0xF0) == 0xE0 || (first & 0xF8) == 0xF0) { + return readStringUtf16FromFirst(bytes, first); + } + } if (bytes.length < out) { bytes = growStringDecodeBuffer(bytes, out); } @@ -1953,44 +1959,157 @@ private String readStringLatin1Tail(byte[] bytes, int out, int b) { } private String readStringUtf16Tail(byte[] bytes, int out) { - while (position < input.length) { + byte[] input = this.input; + int position = this.position; + int inputLength = input.length; + int capacity = bytes.length; + while (position < inputLength) { int b = input[position++] & 0xFF; - if (b == '"') { + if ((b & 0xF0) == 0xE0) { + if (position >= inputLength) { + this.position = position; + throw error("Short UTF-8 sequence"); + } + int second = input[position++] & 0xFF; + if ((second & 0xC0) != 0x80) { + this.position = position; + throw error("Invalid UTF-8 continuation"); + } + if (position >= inputLength) { + this.position = position; + throw error("Short UTF-8 sequence"); + } + int third = input[position++] & 0xFF; + if ((third & 0xC0) != 0x80) { + this.position = position; + throw error("Invalid UTF-8 continuation"); + } + int codePoint = ((b & 0x0F) << 12) | ((second & 0x3F) << 6) | (third & 0x3F); + if (codePoint < 0x800 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) { + this.position = position; + throw error("Invalid UTF-8 sequence"); + } + if (out + 2 > capacity) { + bytes = growStringDecodeBuffer(bytes, out + 2); + capacity = bytes.length; + } + out = putUtf16Char(bytes, out, (char) codePoint); + } else if (b == '"') { + this.position = position; return finishDecodedString(bytes, out, true); - } - if (b == '\\') { + } else if (b == '\\') { + this.position = position; char ch = readEscapedStringChar(); + position = this.position; if (Character.isHighSurrogate(ch)) { char low = readLowSurrogateEscape(); - bytes = ensureStringDecodeCapacity(bytes, out + 4); + position = this.position; + if (out + 4 > capacity) { + bytes = growStringDecodeBuffer(bytes, out + 4); + capacity = bytes.length; + } out = putUtf16Char(bytes, out, ch); out = putUtf16Char(bytes, out, low); } else if (Character.isLowSurrogate(ch)) { throw error("Unpaired low surrogate escape"); } else { - bytes = ensureStringDecodeCapacity(bytes, out + 2); + if (out + 2 > capacity) { + bytes = growStringDecodeBuffer(bytes, out + 2); + capacity = bytes.length; + } out = putUtf16Char(bytes, out, ch); } } else if (b < 0x20) { + this.position = position; throw error("Control character in string"); } else if (b < 0x80) { - bytes = ensureStringDecodeCapacity(bytes, out + 2); + if (out + 2 > capacity) { + bytes = growStringDecodeBuffer(bytes, out + 2); + capacity = bytes.length; + } out = putUtf16Char(bytes, out, (char) b); - } else { - int codePoint = readUtf8CodePoint(b); - if (codePoint <= 0xFFFF) { - bytes = ensureStringDecodeCapacity(bytes, out + 2); - out = putUtf16Char(bytes, out, (char) codePoint); - } else { - bytes = ensureStringDecodeCapacity(bytes, out + 4); - out = putUtf16Char(bytes, out, Character.highSurrogate(codePoint)); - out = putUtf16Char(bytes, out, Character.lowSurrogate(codePoint)); + } else if ((b & 0xE0) == 0xC0) { + if (position >= inputLength) { + this.position = position; + throw error("Short UTF-8 sequence"); + } + int second = input[position++] & 0xFF; + if ((second & 0xC0) != 0x80) { + this.position = position; + throw error("Invalid UTF-8 continuation"); + } + int codePoint = ((b & 0x1F) << 6) | (second & 0x3F); + if (codePoint < 0x80) { + this.position = position; + throw error("Overlong UTF-8 sequence"); + } + if (out + 2 > capacity) { + bytes = growStringDecodeBuffer(bytes, out + 2); + capacity = bytes.length; + } + out = putUtf16Char(bytes, out, (char) codePoint); + } else if ((b & 0xF8) == 0xF0) { + if (position >= inputLength) { + this.position = position; + throw error("Short UTF-8 sequence"); + } + int second = input[position++] & 0xFF; + if ((second & 0xC0) != 0x80) { + this.position = position; + throw error("Invalid UTF-8 continuation"); + } + if (position >= inputLength) { + this.position = position; + throw error("Short UTF-8 sequence"); + } + int third = input[position++] & 0xFF; + if ((third & 0xC0) != 0x80) { + this.position = position; + throw error("Invalid UTF-8 continuation"); + } + if (position >= inputLength) { + this.position = position; + throw error("Short UTF-8 sequence"); } + int fourth = input[position++] & 0xFF; + if ((fourth & 0xC0) != 0x80) { + this.position = position; + throw error("Invalid UTF-8 continuation"); + } + int codePoint = + ((b & 0x07) << 18) | ((second & 0x3F) << 12) | ((third & 0x3F) << 6) | (fourth & 0x3F); + if (codePoint < 0x10000 || codePoint > 0x10FFFF) { + this.position = position; + throw error("Invalid UTF-8 sequence"); + } + if (out + 4 > capacity) { + bytes = growStringDecodeBuffer(bytes, out + 4); + capacity = bytes.length; + } + out = putUtf16Char(bytes, out, Character.highSurrogate(codePoint)); + out = putUtf16Char(bytes, out, Character.lowSurrogate(codePoint)); + } else { + this.position = position; + throw error("Invalid UTF-8 sequence"); } } throw error("Unterminated string"); } + private String readStringUtf16FromFirst(byte[] bytes, int first) { + int codePoint = readUtf8CodePoint(first); + int out; + if (codePoint <= 0xFFFF) { + bytes = ensureStringDecodeCapacity(bytes, 2); + out = putUtf16Char(bytes, 0, (char) codePoint); + } else { + bytes = ensureStringDecodeCapacity(bytes, 4); + out = putUtf16Char(bytes, 0, Character.highSurrogate(codePoint)); + out = putUtf16Char(bytes, out, Character.lowSurrogate(codePoint)); + } + return readStringUtf16Tail(bytes, out); + } + private char readEscapedStringChar() { if (position >= input.length) { throw error("Unterminated escape"); From fcf1aaa76adca48f354039b255b54145bd036caa Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 19:38:09 +0800 Subject: [PATCH 116/120] perf(json): inline leading utf8 three-byte decode --- .../fory/json/reader/Utf8JsonReader.java | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 42fd45dcfa..9c36dfa25a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -2097,7 +2097,38 @@ private String readStringUtf16Tail(byte[] bytes, int out) { } private String readStringUtf16FromFirst(byte[] bytes, int first) { - int codePoint = readUtf8CodePoint(first); + int codePoint; + if ((first & 0xF0) == 0xE0) { + byte[] input = this.input; + int position = this.position; + int inputLength = input.length; + if (position >= inputLength) { + this.position = position; + throw error("Short UTF-8 sequence"); + } + int second = input[position++] & 0xFF; + if ((second & 0xC0) != 0x80) { + this.position = position; + throw error("Invalid UTF-8 continuation"); + } + if (position >= inputLength) { + this.position = position; + throw error("Short UTF-8 sequence"); + } + int third = input[position++] & 0xFF; + if ((third & 0xC0) != 0x80) { + this.position = position; + throw error("Invalid UTF-8 continuation"); + } + codePoint = ((first & 0x0F) << 12) | ((second & 0x3F) << 6) | (third & 0x3F); + if (codePoint < 0x800 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) { + this.position = position; + throw error("Invalid UTF-8 sequence"); + } + this.position = position; + } else { + codePoint = readUtf8CodePoint(first); + } int out; if (codePoint <= 0xFFFF) { bytes = ensureStringDecodeCapacity(bytes, 2); From 767b7a05d81274170ecd9621ca8aa7f39485a9c3 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 21:10:58 +0800 Subject: [PATCH 117/120] fix(java): validate optimized utf8 string reads --- .../fory/serializer/StringSerializer.java | 63 +++++++++----- .../fory/serializer/StringSerializerTest.java | 82 +++++++++++++++++++ 2 files changed, 125 insertions(+), 20 deletions(-) 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 704b603fd2..537ef69876 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 @@ -653,22 +653,26 @@ public byte[] readBytesUTF8(MemoryBuffer buffer, int numBytes) { } private byte[] readBytesUTF8PerfOptimized(MemoryBuffer buffer, int numBytes) { - int udf8Bytes = buffer.readInt32(); - checkStringSize(udf8Bytes); + int utf8Bytes = buffer.readInt32(); + checkUtf8DecodedBytes(numBytes, utf8Bytes); // noinspection Duplicates - buffer.checkReadableBytes(udf8Bytes); + buffer.checkReadableBytes(utf8Bytes); byte[] bytes = new byte[numBytes]; byte[] srcArray = buffer.getHeapMemory(); if (srcArray != null) { int srcIndex = buffer._unsafeHeapReaderIndex(); - int readLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, udf8Bytes, bytes); - assert readLen == numBytes : "Decode UTF8 to UTF16 failed"; - buffer._increaseReaderIndexUnsafe(udf8Bytes); + int readLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, utf8Bytes, bytes); + if (readLen != numBytes) { + throwUtf8DecodedSizeMismatch(numBytes, readLen); + } + buffer._increaseReaderIndexUnsafe(utf8Bytes); } else { - byte[] tmpArray = getByteArray(udf8Bytes); - buffer.readBytes(tmpArray, 0, udf8Bytes); - int readLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, udf8Bytes, bytes); - assert readLen == numBytes : "Decode UTF8 to UTF16 failed"; + byte[] tmpArray = getByteArray(utf8Bytes); + buffer.readBytes(tmpArray, 0, utf8Bytes); + int readLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, utf8Bytes, bytes); + if (readLen != numBytes) { + throwUtf8DecodedSizeMismatch(numBytes, readLen); + } } return bytes; } @@ -720,23 +724,27 @@ public String readCharsUTF8(MemoryBuffer buffer, int numBytes) { public String readCharsUTF8PerfOptimized(MemoryBuffer buffer, int numBytes) { checkStringSize(numBytes); + int utf8Bytes = buffer.readInt32(); + checkUtf8DecodedBytes(numBytes, utf8Bytes); int udf16Chars = numBytes >> 1; - int udf8Bytes = buffer.readInt32(); - checkStringSize(udf8Bytes); // noinspection Duplicates - buffer.checkReadableBytes(udf8Bytes); + buffer.checkReadableBytes(utf8Bytes); char[] chars = new char[udf16Chars]; byte[] srcArray = buffer.getHeapMemory(); if (srcArray != null) { int srcIndex = buffer._unsafeHeapReaderIndex(); - int readLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, udf8Bytes, chars); - assert readLen == udf16Chars : "Decode UTF8 to UTF16 failed"; - buffer._increaseReaderIndexUnsafe(udf8Bytes); + int readLen = StringEncodingUtils.convertUTF8ToUTF16(srcArray, srcIndex, utf8Bytes, chars); + if (readLen != udf16Chars) { + throwUtf8DecodedSizeMismatch(udf16Chars, readLen); + } + buffer._increaseReaderIndexUnsafe(utf8Bytes); } else { - byte[] tmpArray = getByteArray(udf8Bytes); - buffer.readBytes(tmpArray, 0, udf8Bytes); - int readLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, udf8Bytes, chars); - assert readLen == udf16Chars : "Decode UTF8 to UTF16 failed"; + byte[] tmpArray = getByteArray(utf8Bytes); + buffer.readBytes(tmpArray, 0, utf8Bytes); + int readLen = StringEncodingUtils.convertUTF8ToUTF16(tmpArray, 0, utf8Bytes, chars); + if (readLen != udf16Chars) { + throwUtf8DecodedSizeMismatch(udf16Chars, readLen); + } } return newCharsStringZeroCopy(chars); } @@ -756,6 +764,16 @@ private static void checkUtf16Bytes(int numBytes) { } } + // The optimized UTF-8 wire path stores encoded bytes plus the decoded UTF-16 byte count. + // Validate the proportional bound before allocating from the decoded count. + private void checkUtf8DecodedBytes(int numBytes, int utf8Bytes) { + checkStringSize(utf8Bytes); + if ((numBytes & 1) != 0 || numBytes > ((long) utf8Bytes << 1)) { + throw new DeserializationException( + "Invalid UTF-8 decoded byte size " + numBytes + " for encoded byte size " + utf8Bytes); + } + } + private void checkStringSize(int size) { if (size < 0) { throwStringSizeOutOfBounds(size); @@ -766,6 +784,11 @@ private void throwStringSizeOutOfBounds(long size) { throw new DeserializationException("Invalid string byte size " + size); } + private void throwUtf8DecodedSizeMismatch(int expected, int actual) { + throw new DeserializationException( + "UTF-8 decoded size mismatch: expected " + expected + " but got " + actual); + } + public void writeCharsLatin1(MemoryBuffer buffer, char[] chars, int numBytes) { int writerIndex = buffer.writerIndex(); long header = ((long) numBytes << 2) | LATIN1; 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 c910414959..bfa34aa59e 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 @@ -33,6 +33,7 @@ import org.apache.fory.Fory; import org.apache.fory.ForyTestBase; import org.apache.fory.collection.Tuple2; +import org.apache.fory.exception.DeserializationException; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; import org.apache.fory.platform.JdkVersion; @@ -456,6 +457,87 @@ public void testReadUtf8String(boolean writeNumUtf16BytesForUtf8Encoding) { } } + @Test + public void testRejectInvalidUtf8DecodedBytes() { + Fory fory = + Fory.builder() + .withXlang(false) + .withStringCompressed(true) + .withWriteNumUtf16BytesForUtf8Encoding(true) + .requireClassRegistration(false) + .withCompatible(false) + .build(); + StringSerializer serializer = new StringSerializer(fory.getConfig()); + byte utf8 = 2; + for (MemoryBuffer buffer : + new MemoryBuffer[] { + MemoryUtils.buffer(32), MemoryUtils.wrap(ByteBuffer.allocateDirect(32)) + }) { + buffer.writerIndex(0); + buffer.readerIndex(0); + buffer.writeVarUInt64(((long) (1 << 20) << 2) | utf8); + buffer.writeInt32(1); + buffer.writeByte((byte) 'a'); + buffer.readerIndex(0); + Assert.assertThrows( + DeserializationException.class, () -> serializer.readCompressedBytesString(buffer)); + } + } + + @Test + public void testRejectMismatchedUtf8DecodedBytes() { + Fory fory = + Fory.builder() + .withXlang(false) + .withStringCompressed(true) + .withWriteNumUtf16BytesForUtf8Encoding(true) + .requireClassRegistration(false) + .withCompatible(false) + .build(); + StringSerializer serializer = new StringSerializer(fory.getConfig()); + byte[] bytes = "éé".getBytes(StandardCharsets.UTF_8); + byte utf8 = 2; + for (MemoryBuffer buffer : + new MemoryBuffer[] { + MemoryUtils.buffer(32), MemoryUtils.wrap(ByteBuffer.allocateDirect(32)) + }) { + buffer.writerIndex(0); + buffer.readerIndex(0); + buffer.writeVarUInt64((6L << 2) | utf8); + buffer.writeInt32(bytes.length); + buffer.writeBytes(bytes); + buffer.readerIndex(0); + Assert.assertThrows( + DeserializationException.class, () -> serializer.readCompressedBytesString(buffer)); + } + } + + @Test + public void testRejectMismatchedUtf8DecodedChars() { + Fory fory = + Fory.builder() + .withXlang(false) + .withStringCompressed(true) + .withWriteNumUtf16BytesForUtf8Encoding(true) + .requireClassRegistration(false) + .withCompatible(false) + .build(); + StringSerializer serializer = new StringSerializer(fory.getConfig()); + byte[] bytes = "éé".getBytes(StandardCharsets.UTF_8); + for (MemoryBuffer buffer : + new MemoryBuffer[] { + MemoryUtils.buffer(32), MemoryUtils.wrap(ByteBuffer.allocateDirect(32)) + }) { + buffer.writerIndex(0); + buffer.readerIndex(0); + buffer.writeInt32(bytes.length); + buffer.writeBytes(bytes); + buffer.readerIndex(0); + Assert.assertThrows( + DeserializationException.class, () -> serializer.readCharsUTF8PerfOptimized(buffer, 6)); + } + } + /** * Comprehensive tests for readBytesUTF8ForXlang method. Tests the optimized single-pass UTF-8 to * Latin1/UTF-16 conversion. From 107fedefa84d5cd5de73fbeeab5d5104d4946952 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 21:30:05 +0800 Subject: [PATCH 118/120] perf(json): improve UTF-8 string token parsing --- .../fory/json/reader/Utf8JsonReader.java | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java index 9c36dfa25a..d85a2abbfa 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -1191,7 +1191,42 @@ private String readStringToken() { } int start = position; int offset = start; - // The unroll only reduces scanner loop backedges; stop bytes still use the shared slow path. + if (offset + Long.BYTES <= inputLength) { + long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + return readStringWordStop(start, offset, stopMask); + } + offset += Long.BYTES; + if (offset + Long.BYTES <= inputLength) { + stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + return readStringWordStop(start, offset, stopMask); + } + offset += Long.BYTES; + if (offset + Long.BYTES <= inputLength) { + stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask != 0) { + return readStringWordStop(start, offset, stopMask); + } + offset += Long.BYTES; + } + } + } + return readStringTokenLongTail(start, offset, inputLength); + } + + private String readStringWordStop(int start, int offset, long stopMask) { + int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); + int b = input[stop]; + if (b == '"') { + position = stop + 1; + return newLatin1String(start, stop); + } + return readStringStop(start, stop, b); + } + + private String readStringTokenLongTail(int start, int offset, int inputLength) { + byte[] bytes = input; int doubleWordEnd = inputLength - (Long.BYTES << 1); while (offset <= doubleWordEnd) { long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); From af2b3af1865ffd7ac3b821fd485b0c930b7d71c4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 21:50:03 +0800 Subject: [PATCH 119/120] perf(json): remove string escape builder fallback --- .../apache/fory/json/reader/JsonReader.java | 54 ---- .../fory/json/reader/Latin1JsonReader.java | 199 +++++++++++-- .../fory/json/reader/Utf16JsonReader.java | 261 ++++++++++++++++-- .../org/apache/fory/json/JsonStringTest.java | 63 +++++ 4 files changed, 475 insertions(+), 102 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java index 81a4d7981f..75e1bc838a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java @@ -444,40 +444,6 @@ protected final ForyJsonException error(String message) { return new ForyJsonException(message + " at JSON position " + position); } - protected final void appendEscape(StringBuilder builder) { - if (position >= length()) { - throw error("Unterminated escape"); - } - char escaped = charAt(position++); - switch (escaped) { - case '"': - case '\\': - case '/': - builder.append(escaped); - return; - case 'b': - builder.append('\b'); - return; - case 'f': - builder.append('\f'); - return; - case 'n': - builder.append('\n'); - return; - case 'r': - builder.append('\r'); - return; - case 't': - builder.append('\t'); - return; - case 'u': - appendUnicodeEscape(builder); - return; - default: - throw error("Invalid escape"); - } - } - private void skipObject() { enterDepth(); expect('{'); @@ -583,26 +549,6 @@ private void rejectFractionOrExponent() { } } - private void appendUnicodeEscape(StringBuilder builder) { - char ch = readUnicodeEscape(); - if (Character.isHighSurrogate(ch)) { - if (position + 2 > length() || charAt(position) != '\\' || charAt(position + 1) != 'u') { - throw error("Unpaired high surrogate escape"); - } - position += 2; - char low = readUnicodeEscape(); - if (!Character.isLowSurrogate(low)) { - throw error("Unpaired high surrogate escape"); - } - builder.append(ch); - builder.append(low); - } else if (Character.isLowSurrogate(ch)) { - throw error("Unpaired low surrogate escape"); - } else { - builder.append(ch); - } - } - protected final char readEscapedFieldNameChar() { if (position >= length()) { throw error("Unterminated escape"); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java index af4182572a..4eba668bd3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -23,15 +23,20 @@ import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.Arrays; import java.util.UUID; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; import org.apache.fory.memory.LittleEndian; +import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.serializer.StringSerializer; public final class Latin1JsonReader extends JsonReader { private static final byte[] EMPTY_BYTES = new byte[0]; + private static final int INITIAL_STRING_DECODE_BUFFER_SIZE = 1024; + private static final int RETAINED_STRING_DECODE_BUFFER_SIZE = 8192; + private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; private static final long BYTE_ONES = 0x0101010101010101L; private static final int INT_BYTE_ONES = 0x01010101; private static final long BYTE_HIGH_BITS = 0x8080808080808080L; @@ -56,6 +61,7 @@ public final class Latin1JsonReader extends JsonReader { // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. // Latin1 string content and field-name hashing must keep unsigned byte conversion. private byte[] input; + private byte[] stringDecodeBuffer = new byte[INITIAL_STRING_DECODE_BUFFER_SIZE]; public Latin1JsonReader() { input = EMPTY_BYTES; @@ -91,6 +97,9 @@ public Latin1JsonReader reset(String input) { public void clear() { input = EMPTY_BYTES; position = 0; + if (stringDecodeBuffer.length > RETAINED_STRING_DECODE_BUFFER_SIZE) { + stringDecodeBuffer = new byte[RETAINED_STRING_DECODE_BUFFER_SIZE]; + } } public boolean consumeToken(char expected) { @@ -1426,18 +1435,17 @@ private String readStringStop(int start, int stop, int ch) { } position = stop + 1; if (ch == '\\') { - StringBuilder builder = newStringBuilder(start, stop); - appendLatin1(builder, start, stop); - appendEscape(builder); - return readStringTail(builder); + int out = stop - start; + byte[] bytes = stringDecodeBuffer; + if (bytes.length < out) { + bytes = growStringDecodeBuffer(bytes, out); + } + System.arraycopy(input, start, bytes, 0, out); + return readStringLatin1Tail(bytes, out, ch); } throw error("Control character in string"); } - private StringBuilder newStringBuilder(int start, int stop) { - return new StringBuilder(Math.max(16, stop - start + 16)); - } - private static long asciiStringStopMask(long word) { // ASCII segments can use the cheaper UTF-8-style syntax scan. High-bit bytes stop this fast // path and continue through the precise Latin1 scanner, where non-ASCII Latin1 remains valid @@ -1822,28 +1830,6 @@ private String newLatin1String(int start, int end) { return StringSerializer.newLatin1StringZeroCopy(bytes); } - private String readStringTail(StringBuilder builder) { - while (position < input.length) { - int ch = input[position++] & 0xFF; - if (ch == '"') { - return builder.toString(); - } else if (ch == '\\') { - appendEscape(builder); - } else if (ch < 0x20) { - throw error("Control character in string"); - } else { - builder.append((char) ch); - } - } - throw error("Unterminated string"); - } - - private void appendLatin1(StringBuilder builder, int start, int end) { - for (int i = start; i < end; i++) { - builder.append((char) (input[i] & 0xFF)); - } - } - private void skipWhitespaceFast() { while (position < input.length) { int ch = input[position]; @@ -1872,6 +1858,159 @@ private boolean startsWithAscii(String value) { return true; } + private String readStringLatin1Tail(byte[] bytes, int out, int ch) { + while (true) { + if (ch == '"') { + return finishDecodedString(bytes, out, false); + } + if (ch == '\\') { + char escaped = readEscapedStringChar(); + if (Character.isHighSurrogate(escaped)) { + char low = readLowSurrogateEscape(); + bytes = widenStringDecodeBuffer(bytes, out); + out <<= 1; + bytes = ensureStringDecodeCapacity(bytes, out + 4); + out = putUtf16Char(bytes, out, escaped); + out = putUtf16Char(bytes, out, low); + return readStringUtf16Tail(bytes, out); + } + if (Character.isLowSurrogate(escaped)) { + throw error("Unpaired low surrogate escape"); + } + if (escaped <= 0xFF) { + bytes = ensureStringDecodeCapacity(bytes, out + 1); + bytes[out++] = (byte) escaped; + } else { + bytes = widenStringDecodeBuffer(bytes, out); + out <<= 1; + bytes = ensureStringDecodeCapacity(bytes, out + 2); + out = putUtf16Char(bytes, out, escaped); + return readStringUtf16Tail(bytes, out); + } + } else if (ch < 0x20) { + throw error("Control character in string"); + } else { + bytes = ensureStringDecodeCapacity(bytes, out + 1); + bytes[out++] = (byte) ch; + } + if (position >= input.length) { + throw error("Unterminated string"); + } + ch = input[position++] & 0xFF; + } + } + + private String readStringUtf16Tail(byte[] bytes, int out) { + while (position < input.length) { + int ch = input[position++] & 0xFF; + if (ch == '"') { + return finishDecodedString(bytes, out, true); + } + if (ch == '\\') { + char escaped = readEscapedStringChar(); + if (Character.isHighSurrogate(escaped)) { + char low = readLowSurrogateEscape(); + bytes = ensureStringDecodeCapacity(bytes, out + 4); + out = putUtf16Char(bytes, out, escaped); + out = putUtf16Char(bytes, out, low); + } else if (Character.isLowSurrogate(escaped)) { + throw error("Unpaired low surrogate escape"); + } else { + bytes = ensureStringDecodeCapacity(bytes, out + 2); + out = putUtf16Char(bytes, out, escaped); + } + } else if (ch < 0x20) { + throw error("Control character in string"); + } else { + bytes = ensureStringDecodeCapacity(bytes, out + 2); + out = putUtf16Char(bytes, out, (char) ch); + } + } + throw error("Unterminated string"); + } + + private char readEscapedStringChar() { + if (position >= input.length) { + throw error("Unterminated escape"); + } + char escaped = (char) (input[position++] & 0xFF); + switch (escaped) { + case '"': + case '\\': + case '/': + return escaped; + case 'b': + return '\b'; + case 'f': + return '\f'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case 'u': + return readUnicodeEscape(); + default: + throw error("Invalid escape"); + } + } + + private char readLowSurrogateEscape() { + if (position + 2 > input.length || input[position] != '\\' || input[position + 1] != 'u') { + throw error("Unpaired high surrogate escape"); + } + position += 2; + char low = readUnicodeEscape(); + if (!Character.isLowSurrogate(low)) { + throw error("Unpaired high surrogate escape"); + } + return low; + } + + private String finishDecodedString(byte[] bytes, int length, boolean utf16) { + // The decode buffer is reader-owned reusable storage; returned Strings must own exact bytes. + byte[] result = new byte[length]; + System.arraycopy(bytes, 0, result, 0, length); + return utf16 + ? StringSerializer.newUtf16StringZeroCopy(result) + : StringSerializer.newLatin1StringZeroCopy(result); + } + + private byte[] ensureStringDecodeCapacity(byte[] bytes, int capacity) { + if (bytes.length < capacity) { + return growStringDecodeBuffer(bytes, capacity); + } + return bytes; + } + + private byte[] growStringDecodeBuffer(byte[] bytes, int capacity) { + int newCapacity = Math.max(capacity, bytes.length << 1); + byte[] grown = Arrays.copyOf(bytes, newCapacity); + stringDecodeBuffer = grown; + return grown; + } + + private byte[] widenStringDecodeBuffer(byte[] bytes, int length) { + int utf16Length = length << 1; + bytes = ensureStringDecodeCapacity(bytes, utf16Length); + for (int i = length - 1, pos = utf16Length - 2; i >= 0; i--, pos -= 2) { + putUtf16Char(bytes, pos, (char) (bytes[i] & 0xFF)); + } + return bytes; + } + + private static int putUtf16Char(byte[] bytes, int pos, char value) { + if (LITTLE_ENDIAN) { + bytes[pos] = (byte) value; + bytes[pos + 1] = (byte) (value >>> 8); + } else { + bytes[pos] = (byte) (value >>> 8); + bytes[pos + 1] = (byte) value; + } + return pos + 2; + } + private void rejectLeadingDigitFast() { if (position < input.length) { int ch = input[position]; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java index 6220c91fff..6cd1abdd6c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -22,6 +22,7 @@ import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.Arrays; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; @@ -30,6 +31,8 @@ import org.apache.fory.serializer.StringSerializer; public final class Utf16JsonReader extends JsonReader { + private static final int INITIAL_STRING_DECODE_BUFFER_SIZE = 1024; + private static final int RETAINED_STRING_DECODE_BUFFER_SIZE = 8192; private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; private static final long BYTE_ONES = 0x0101010101010101L; private static final long BYTE_HIGH_BITS = 0x8080808080808080L; @@ -49,6 +52,7 @@ public final class Utf16JsonReader extends JsonReader { private String input; private byte[] bytes; private int length; + private byte[] stringDecodeBuffer = new byte[INITIAL_STRING_DECODE_BUFFER_SIZE]; public Utf16JsonReader() { input = ""; @@ -97,6 +101,9 @@ public void clear() { bytes = null; length = 0; position = 0; + if (stringDecodeBuffer.length > RETAINED_STRING_DECODE_BUFFER_SIZE) { + stringDecodeBuffer = new byte[RETAINED_STRING_DECODE_BUFFER_SIZE]; + } } public boolean consumeToken(char expected) { @@ -697,7 +704,7 @@ private String readStringAfterQuote() { if (LITTLE_ENDIAN && bytes != null) { return readUtf16StringToken(start); } - return readStringLoop(start, null); + return readStringLoop(start, false); } private String readUtf16StringToken(int start) { @@ -775,7 +782,7 @@ private String readUtf16StringToken(int start) { offset = position; } position = offset; - return readStringLoop(start, null); + return readStringLoop(start, nonLatin); } private String readUtf16StringStop( @@ -787,11 +794,8 @@ private String readUtf16StringStop( return nonLatin ? newUtf16String(localBytes, start, stop) : input.substring(start, stop); } if (ch == '\\') { - StringBuilder builder = new StringBuilder(Math.max(16, stop - start + 16)); - builder.append(input, start, stop); position = stop + 1; - appendEscape(builder); - return readStringLoop(position, builder); + return readEscapedStringTail(start, stop, nonLatin); } if (ch < 0x20) { position = stop; @@ -821,22 +825,240 @@ private static String newUtf16String(byte[] localBytes, int start, int stop) { return StringSerializer.newUtf16StringZeroCopy(valueBytes); } - private String readStringLoop(int start, StringBuilder builder) { + private String readEscapedStringTail(int start, int stop, boolean nonLatin) { + if (nonLatin) { + int out = (stop - start) << 1; + byte[] outBytes = stringDecodeBuffer; + if (outBytes.length < out) { + outBytes = growStringDecodeBuffer(outBytes, out); + } + copyPrefixUtf16(start, stop, outBytes); + return readStringUtf16Tail(outBytes, out, '\\'); + } + int out = stop - start; + byte[] outBytes = stringDecodeBuffer; + if (outBytes.length < out) { + outBytes = growStringDecodeBuffer(outBytes, out); + } + copyPrefixLatin1(start, stop, outBytes); + return readStringLatin1Tail(outBytes, out, '\\'); + } + + private void copyPrefixLatin1(int start, int stop, byte[] outBytes) { + for (int i = start, out = 0; i < stop; i++) { + outBytes[out++] = (byte) charAtFast(i); + } + } + + private void copyPrefixUtf16(int start, int stop, byte[] outBytes) { + byte[] localBytes = bytes; + int length = (stop - start) << 1; + if (localBytes != null) { + System.arraycopy(localBytes, start << 1, outBytes, 0, length); + return; + } + for (int i = start, out = 0; i < stop; i++) { + out = putUtf16Char(outBytes, out, charAtFast(i)); + } + } + + private String readStringLatin1Tail(byte[] outBytes, int out, int ch) { + while (true) { + if (ch == '"') { + return finishDecodedString(outBytes, out, false); + } + if (ch == '\\') { + char escaped = readEscapedStringChar(); + if (Character.isHighSurrogate(escaped)) { + char low = readLowSurrogateEscape(); + outBytes = widenStringDecodeBuffer(outBytes, out); + out <<= 1; + outBytes = ensureStringDecodeCapacity(outBytes, out + 4); + out = putUtf16Char(outBytes, out, escaped); + out = putUtf16Char(outBytes, out, low); + return readStringUtf16Tail(outBytes, out, nextStringChar()); + } + if (Character.isLowSurrogate(escaped)) { + throw error("Unpaired low surrogate escape"); + } + if (escaped <= 0xFF) { + outBytes = ensureStringDecodeCapacity(outBytes, out + 1); + outBytes[out++] = (byte) escaped; + } else { + outBytes = widenStringDecodeBuffer(outBytes, out); + out <<= 1; + outBytes = ensureStringDecodeCapacity(outBytes, out + 2); + out = putUtf16Char(outBytes, out, escaped); + return readStringUtf16Tail(outBytes, out, nextStringChar()); + } + } else if (ch < 0x20) { + throw error("Control character in string"); + } else if (Character.isHighSurrogate((char) ch)) { + char low = readRawLowSurrogate(); + outBytes = widenStringDecodeBuffer(outBytes, out); + out <<= 1; + outBytes = ensureStringDecodeCapacity(outBytes, out + 4); + out = putUtf16Char(outBytes, out, (char) ch); + out = putUtf16Char(outBytes, out, low); + return readStringUtf16Tail(outBytes, out, nextStringChar()); + } else if (Character.isLowSurrogate((char) ch)) { + throw error("Unpaired low surrogate in string"); + } else if (ch <= 0xFF) { + outBytes = ensureStringDecodeCapacity(outBytes, out + 1); + outBytes[out++] = (byte) ch; + } else { + outBytes = widenStringDecodeBuffer(outBytes, out); + out <<= 1; + outBytes = ensureStringDecodeCapacity(outBytes, out + 2); + out = putUtf16Char(outBytes, out, (char) ch); + return readStringUtf16Tail(outBytes, out, nextStringChar()); + } + ch = nextStringChar(); + } + } + + private String readStringUtf16Tail(byte[] outBytes, int out, int ch) { + while (true) { + if (ch == '"') { + return finishDecodedString(outBytes, out, true); + } + if (ch == '\\') { + char escaped = readEscapedStringChar(); + if (Character.isHighSurrogate(escaped)) { + char low = readLowSurrogateEscape(); + outBytes = ensureStringDecodeCapacity(outBytes, out + 4); + out = putUtf16Char(outBytes, out, escaped); + out = putUtf16Char(outBytes, out, low); + } else if (Character.isLowSurrogate(escaped)) { + throw error("Unpaired low surrogate escape"); + } else { + outBytes = ensureStringDecodeCapacity(outBytes, out + 2); + out = putUtf16Char(outBytes, out, escaped); + } + } else if (ch < 0x20) { + throw error("Control character in string"); + } else if (Character.isHighSurrogate((char) ch)) { + char low = readRawLowSurrogate(); + outBytes = ensureStringDecodeCapacity(outBytes, out + 4); + out = putUtf16Char(outBytes, out, (char) ch); + out = putUtf16Char(outBytes, out, low); + } else if (Character.isLowSurrogate((char) ch)) { + throw error("Unpaired low surrogate in string"); + } else { + outBytes = ensureStringDecodeCapacity(outBytes, out + 2); + out = putUtf16Char(outBytes, out, (char) ch); + } + ch = nextStringChar(); + } + } + + private int nextStringChar() { + if (position >= length) { + throw error("Unterminated string"); + } + return charAtFast(position++); + } + + private char readRawLowSurrogate() { + if (position >= length) { + throw error("Unpaired high surrogate in string"); + } + char low = charAtFast(position++); + if (!Character.isLowSurrogate(low)) { + throw error("Unpaired high surrogate in string"); + } + return low; + } + + private char readEscapedStringChar() { + if (position >= length) { + throw error("Unterminated escape"); + } + char escaped = charAtFast(position++); + switch (escaped) { + case '"': + case '\\': + case '/': + return escaped; + case 'b': + return '\b'; + case 'f': + return '\f'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case 'u': + return readUnicodeEscape(); + default: + throw error("Invalid escape"); + } + } + + private char readLowSurrogateEscape() { + if (position + 2 > length || charAtFast(position) != '\\' || charAtFast(position + 1) != 'u') { + throw error("Unpaired high surrogate escape"); + } + position += 2; + char low = readUnicodeEscape(); + if (!Character.isLowSurrogate(low)) { + throw error("Unpaired high surrogate escape"); + } + return low; + } + + private String finishDecodedString(byte[] outBytes, int length, boolean utf16) { + // The decode buffer is reader-owned reusable storage; returned Strings must own exact bytes. + byte[] result = new byte[length]; + System.arraycopy(outBytes, 0, result, 0, length); + return utf16 + ? StringSerializer.newUtf16StringZeroCopy(result) + : StringSerializer.newLatin1StringZeroCopy(result); + } + + private byte[] ensureStringDecodeCapacity(byte[] outBytes, int capacity) { + if (outBytes.length < capacity) { + return growStringDecodeBuffer(outBytes, capacity); + } + return outBytes; + } + + private byte[] growStringDecodeBuffer(byte[] outBytes, int capacity) { + int newCapacity = Math.max(capacity, outBytes.length << 1); + byte[] grown = Arrays.copyOf(outBytes, newCapacity); + stringDecodeBuffer = grown; + return grown; + } + + private byte[] widenStringDecodeBuffer(byte[] outBytes, int length) { + int utf16Length = length << 1; + outBytes = ensureStringDecodeCapacity(outBytes, utf16Length); + for (int i = length - 1, pos = utf16Length - 2; i >= 0; i--, pos -= 2) { + putUtf16Char(outBytes, pos, (char) (outBytes[i] & 0xFF)); + } + return outBytes; + } + + private static int putUtf16Char(byte[] outBytes, int pos, char value) { + if (LITTLE_ENDIAN) { + outBytes[pos] = (byte) value; + outBytes[pos + 1] = (byte) (value >>> 8); + } else { + outBytes[pos] = (byte) (value >>> 8); + outBytes[pos + 1] = (byte) value; + } + return pos + 2; + } + + private String readStringLoop(int start, boolean nonLatin) { while (position < length) { char ch = charAtFast(position++); if (ch == '"') { - if (builder == null) { - return input.substring(start, position - 1); - } - builder.append(input, start, position - 1); - return builder.toString(); + return input.substring(start, position - 1); } else if (ch == '\\') { - if (builder == null) { - builder = new StringBuilder(); - } - builder.append(input, start, position - 1); - appendEscape(builder); - start = position; + return readEscapedStringTail(start, position - 1, nonLatin); } else if (ch < 0x20) { throw error("Control character in string"); } else if (Character.isHighSurrogate(ch)) { @@ -844,8 +1066,11 @@ private String readStringLoop(int start, StringBuilder builder) { throw error("Unpaired high surrogate in string"); } position++; + nonLatin = true; } else if (Character.isLowSurrogate(ch)) { throw error("Unpaired low surrogate in string"); + } else if (ch > 0xFF) { + nonLatin = true; } } throw error("Unterminated string"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index 9df6dadcf5..3caf82909b 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -36,6 +36,7 @@ import org.apache.fory.json.data.UnicodeMatrix; import org.apache.fory.json.data.UnicodeValues; import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf16JsonReader; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.memory.NativeByteOrder; @@ -253,6 +254,62 @@ public void readStringInputLayouts() { assertEquals(json.fromJson("\"你好,Fory\"", String.class), ZH_TEXT); } + @Test + public void readStringInputEscapes() { + ForyJson json = ForyJson.builder().build(); + + String asciiEscaped = json.fromJson("\"line\\n\\r\\t\\b\\f\\\\\\\"\\/end\"", String.class); + String latin1Escaped = json.fromJson("\"caf\\u00E9\"", String.class); + String chineseEscaped = json.fromJson("\"\\u4E2D\\u6587\"", String.class); + String supplementary = json.fromJson("\"\\uD83D\\uDE00\"", String.class); + + assertEquals(asciiEscaped, "line\n\r\t\b\f\\\"/end"); + assertEquals(latin1Escaped, "café"); + assertEquals(chineseEscaped, "中文"); + assertEquals(supplementary, "\uD83D\uDE00"); + assertLatin1String(asciiEscaped); + assertLatin1String(latin1Escaped); + assertUtf16String(chineseEscaped); + assertUtf16String(supplementary); + + String utf16Escaped = "\"中文\\n\\u00E9\\uD83D\\uDE00\""; + if (StringSerializer.isBytesBackedString()) { + assertTrue(StringSerializer.isUtf16Coder(StringSerializer.getStringCoder(utf16Escaped))); + } + assertEquals(json.fromJson(utf16Escaped, String.class), "中文\né\uD83D\uDE00"); + + String utf16Object = "{\"active\":true,\"id\":7,\"name\":\"line\\n\",\"ignored\":\"中文\"}"; + PublicFields fields = json.fromJson(utf16Object, PublicFields.class); + assertEquals(fields.name, "line\n"); + assertLatin1String(fields.name); + + assertThrows(ForyJsonException.class, () -> json.fromJson("\"bad\\x\"", String.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("\"中文\\x\"", String.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("\"bad\u001f\"", String.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("\"中文\u001f\"", String.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("\"\\uD83D\"", String.class)); + } + + @Test + public void readerDecodeBufferShrinks() throws Exception { + String latin1Input = "\"" + repeat('a', 9000) + "\\n\""; + if (StringSerializer.isBytesBackedString() + && StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(latin1Input))) { + Latin1JsonReader latin1Reader = new Latin1JsonReader(latin1Input); + assertEquals(latin1Reader.readString(), repeat('a', 9000) + "\n"); + assertTrue(readerBufferLength(latin1Reader) > 8192); + latin1Reader.clear(); + assertEquals(readerBufferLength(latin1Reader), 8192); + } + + String utf16Input = "\"中文" + repeat('b', 9000) + "\\n\""; + Utf16JsonReader utf16Reader = new Utf16JsonReader(utf16Input); + assertEquals(utf16Reader.readString(), "中文" + repeat('b', 9000) + "\n"); + assertTrue(readerBufferLength(utf16Reader) > 8192); + utf16Reader.clear(); + assertEquals(readerBufferLength(utf16Reader), 8192); + } + @Test public void readUnicodeFieldNames() { ForyJson json = ForyJson.builder().build(); @@ -441,6 +498,12 @@ private static int writerBufferLength(StringJsonWriter writer) throws Exception return ((byte[]) field.get(writer)).length; } + private static int readerBufferLength(Object reader) throws Exception { + Field field = reader.getClass().getDeclaredField("stringDecodeBuffer"); + field.setAccessible(true); + return ((byte[]) field.get(reader)).length; + } + private static String readUtf8String(ForyJson json, String input) { return json.fromJson(input.getBytes(StandardCharsets.UTF_8), String.class); } From eedc4628bf83977946d95a097295bf2f1f16e830 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Tue, 7 Jul 2026 21:56:00 +0800 Subject: [PATCH 120/120] refactor(json): use field mode builder option --- .../org/apache/fory/json/ForyJsonBuilder.java | 10 +++-- .../apache/fory/json/codegen/JsonCodegen.java | 42 ++++++++++++++---- .../fory/json/meta/JsonFieldAccessor.java | 44 ++++++++++++++++--- .../org/apache/fory/json/JsonObjectTest.java | 2 +- .../apache/fory/json/JsonPropertyTest.java | 2 +- 5 files changed, 79 insertions(+), 21 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java index 873ccb81b7..148c24edde 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java @@ -44,9 +44,13 @@ public ForyJsonBuilder withCodegen(boolean codegenEnabled) { return this; } - /** Enables JavaBean getter/setter JSON property discovery. */ - public ForyJsonBuilder withPropertyDiscovery(boolean propertyDiscoveryEnabled) { - this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; + /** + * Enables field mode, where JSON object members are discovered from Java fields only. When + * disabled, Fory JSON uses the default JavaBean property model: public getters, public setters, + * and eligible fields are merged as JSON object members. + */ + public ForyJsonBuilder withFieldMode(boolean fieldMode) { + this.propertyDiscoveryEnabled = !fieldMode; return this; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index 95fbb65755..5480f5cdf8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -19,6 +19,8 @@ package org.apache.fory.json.codegen; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -41,6 +43,8 @@ import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.json.writer.StringObjectWriter; import org.apache.fory.json.writer.Utf8ObjectWriter; +import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.util.record.RecordUtils; public final class JsonCodegen { @@ -144,10 +148,18 @@ private Object compileWriter( .genCode(); try { Class writerClass = compileClass(generatedPackage, className, code); - Constructor constructor = - writerClass.getDeclaredConstructor(JsonFieldInfo[].class, JsonCodec[].class); - constructor.setAccessible(true); - return constructor.newInstance(properties, nestedCodecs); + if (AndroidSupport.IS_ANDROID) { + Constructor constructor = + writerClass.getDeclaredConstructor(JsonFieldInfo[].class, JsonCodec[].class); + constructor.setAccessible(true); + return constructor.newInstance(properties, nestedCodecs); + } + MethodHandle constructor = + _JDKAccess._trustedLookup(writerClass) + .findConstructor( + writerClass, + MethodType.methodType(void.class, JsonFieldInfo[].class, JsonCodec[].class)); + return constructor.invoke(properties, nestedCodecs); } catch (Throwable e) { throw new ForyJsonException("Cannot compile generated JSON writer " + className, e); } @@ -167,11 +179,23 @@ private Object compileReader( .genCode(); try { Class readerClass = compileClass(generatedPackage, className, code); - Constructor constructor = - readerClass.getDeclaredConstructor( - JsonFieldInfo[].class, JsonCodec[].class, BaseObjectCodec[].class); - constructor.setAccessible(true); - return constructor.newInstance(properties, readCodecs, nestedCodecs); + if (AndroidSupport.IS_ANDROID) { + Constructor constructor = + readerClass.getDeclaredConstructor( + JsonFieldInfo[].class, JsonCodec[].class, BaseObjectCodec[].class); + constructor.setAccessible(true); + return constructor.newInstance(properties, readCodecs, nestedCodecs); + } + MethodHandle constructor = + _JDKAccess._trustedLookup(readerClass) + .findConstructor( + readerClass, + MethodType.methodType( + void.class, + JsonFieldInfo[].class, + JsonCodec[].class, + BaseObjectCodec[].class)); + return constructor.invoke(properties, readCodecs, nestedCodecs); } catch (Throwable e) { throw new ForyJsonException("Cannot compile generated JSON reader " + className, e); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java index 76503a7759..712d58d233 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java @@ -19,10 +19,13 @@ package org.apache.fory.json.meta; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.FieldAccessor; public abstract class JsonFieldAccessor { @@ -236,10 +239,16 @@ public void putChar(Object target, char value) { private static final class GetterJsonAccessor extends JsonFieldAccessor { private final Method getter; + private final MethodHandle getterHandle; private GetterJsonAccessor(Method getter) { this.getter = getter; - getter.setAccessible(true); + if (AndroidSupport.IS_ANDROID) { + getter.setAccessible(true); + getterHandle = null; + } else { + getterHandle = methodHandle(getter); + } } @Override @@ -250,8 +259,11 @@ public Method getter() { @Override public Object getObject(Object target) { try { - return getter.invoke(target); - } catch (IllegalAccessException | InvocationTargetException e) { + if (AndroidSupport.IS_ANDROID) { + return getter.invoke(target); + } + return getterHandle.invoke(target); + } catch (Throwable e) { throw accessException(getter, e); } } @@ -259,10 +271,16 @@ public Object getObject(Object target) { private static final class SetterJsonAccessor extends JsonFieldAccessor { private final Method setter; + private final MethodHandle setterHandle; private SetterJsonAccessor(Method setter) { this.setter = setter; - setter.setAccessible(true); + if (AndroidSupport.IS_ANDROID) { + setter.setAccessible(true); + setterHandle = null; + } else { + setterHandle = methodHandle(setter); + } } @Override @@ -273,14 +291,26 @@ public Method setter() { @Override public void putObject(Object target, Object value) { try { - setter.invoke(target, value); - } catch (IllegalAccessException | InvocationTargetException e) { + if (AndroidSupport.IS_ANDROID) { + setter.invoke(target, value); + } else { + setterHandle.invoke(target, value); + } + } catch (Throwable e) { throw accessException(setter, e); } } } - private static ForyJsonException accessException(Method method, ReflectiveOperationException e) { + private static MethodHandle methodHandle(Method method) { + try { + return _JDKAccess._trustedLookup(method.getDeclaringClass()).unreflect(method); + } catch (IllegalAccessException e) { + throw accessException(method, e); + } + } + + private static ForyJsonException accessException(Method method, Throwable e) { Throwable cause = e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e; return new ForyJsonException("Cannot access JSON property method " + method, cause); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java index 1fc6ba3bca..3ed61ac7e4 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java @@ -163,7 +163,7 @@ public void writeNullFields() { @Test public void fieldOnlyModeIgnoresMethods() { - ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); + ForyJson json = ForyJson.builder().withFieldMode(true).build(); assertEquals( json.toJson(new MethodsIgnored()), "{\"setterCalls\":0,\"value\":\"field\",\"hidden\":\"hidden\"}"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java index 904f0bb4e3..ca3991dcfe 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java @@ -88,7 +88,7 @@ public void setterOnlyReads() { @Test public void fieldOnlyMode() { - ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); + ForyJson json = ForyJson.builder().withFieldMode(true).build(); assertEquals(json.toJson(new GetterBean()), "{\"id\":7,\"name\":\"field\"}"); assertEquals(json.toJson(new GetterOnlyBean()), "{}"); SetterOnlyBean value = json.fromJson("{\"secret\":\"alpha\"}", SetterOnlyBean.class);