Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .agents/languages/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ Load this file when changing anything under `java/` or when Java drives a cross-
by generated serializers, do extra self-review: inspect the generated output impact, preserve
unsafe/codegen optimizations unless intentionally changing them, and run validation appropriate to
the regression risk.
- For HotSpot generated-code boundaries, use cold type-subtree cost to select only generated child
or member graphs that need isolation, then route their hot receiver implementations through one
shared ordinary-class trampoline `invokeinterface` bytecode. The polymorphic call site prevents C2
from recursively inlining several large field graphs into one parent, while each receiver remains
an independent compilation unit whose internal scalar operations can still inline. Do not apply
the trampoline blanketly to handwritten codecs, root facades, or small generated subtrees, and do
not replace it with per-generated-class call sites without `PrintInlining` proof.
- Android and JVM serializers must use a unified wire protocol: each side must be able to
deserialize data written by the other side. If implementation paths diverge, the writer must emit
enough metadata for either reader to identify and parse that path correctly; add both
Expand Down
5 changes: 5 additions & 0 deletions .agents/skills/fory-performance-optimization/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Deliver measurable performance improvements in Apache Fory without protocol drif
- Keep only measured wins or explicitly requested architecture cleanups.
- Revert speculative changes that do not pay off.
- Align with reference runtimes (usually C++ first, then Rust/Java) when behavior and ownership models differ.
- When containing HotSpot-generated code, select boundaries from cold type-subtree cost and route
only the large generated receiver implementations through one shared trampoline `invokeinterface`
bytecode. Do not blanket handwritten codecs, root facades, or small generated subtrees. Verify
with `PrintInlining` that C2 retains this outer polymorphic boundary while each independently
compiled receiver still inlines its internal scalar subtree.

## Enforce Hard Constraints

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ public void resolve(JsonTypeResolver resolver) {
Class<?> subtype = definition.classes[i];
JsonTypeInfo child = resolver.getTypeInfo(subtype, subtype);
if (definition.inclusion == Inclusion.PROPERTY) {
if (!child.usesDefaultObjectCodec()) {
ObjectCodec<?> objectCodec = resolver.canonicalObjectCodec(child);
if (objectCodec == null) {
throw new ForyJsonException(
"Inline JSON subtype requires the default object representation: " + subtype);
}
ObjectCodec<?> objectCodec = resolver.getObjectCodec(subtype);
rejectDiscriminatorCollision(objectCodec, definition.scanInfo.property());
objectCodecs[i] = (ObjectCodec<Object>) objectCodec;
ObjectCodec.AnyInfo any = objectCodec.anyInfo();
Expand All @@ -103,15 +103,29 @@ public void resolve(JsonTypeResolver resolver) {
JsonFieldTable table =
objectCodec.readTable().withSkippedName(definition.scanInfo.property());
inlineReadTables[i] = table;
// The subtype scan restores the cursor, so the outer child rereads the discriminator and
// needs this parent-local skip table. Nested child values must use the canonical table.
resolver.resolveInlineAnyReaders(this, i, objectCodec, table);
}
}
children[i] = child;
}
}

/** Registers inline readers after the complete subtype graph has resolved successfully. */
@Internal
public void resolveInlineReaders(JsonTypeResolver resolver) {
JsonFieldTable[] tables = inlineReadTables;
if (tables == null) {
return;
}
for (int i = 0; i < tables.length; i++) {
JsonFieldTable table = tables[i];
if (table != null) {
// The subtype scan restores the cursor, so the outer child rereads the discriminator and
// needs this parent-local skip table. Nested child values must use the canonical table.
resolver.resolveInlineAnyReaders(this, i, objectCodecs[i], table);
}
}
}

@Override
public void writeString(StringJsonWriter writer, Object value) {
if (value == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,20 @@ public static CollectionCodec<?> create(
Class<?> elementRawType = CodecUtils.rawType(elementType, Object.class);
CollectionFactory factory = collectionFactory(rawType, elementRawType);
JsonTypeInfo elementTypeInfo = resolver.getTypeInfo(elementType, elementRawType);
return create(factory, elementTypeInfo);
return create(factory, elementTypeInfo, resolver);
}

@Internal
public static CollectionCodec<?> create(
Class<?> rawType, Class<?> elementRawType, JsonTypeInfo elementTypeInfo) {
return create(collectionFactory(rawType, elementRawType), elementTypeInfo);
Class<?> rawType,
Class<?> elementRawType,
JsonTypeInfo elementTypeInfo,
JsonTypeResolver resolver) {
return create(collectionFactory(rawType, elementRawType), elementTypeInfo, resolver);
}

private static CollectionCodec<?> create(
CollectionFactory factory, JsonTypeInfo elementTypeInfo) {
CollectionFactory factory, JsonTypeInfo elementTypeInfo, JsonTypeResolver resolver) {
Object elementCodec = elementTypeInfo.stringWriter();
if (elementCodec == ScalarCodecs.StringCodec.INSTANCE) {
return new StringCollectionCodec(factory);
Expand Down Expand Up @@ -131,7 +134,7 @@ private static CollectionCodec<?> create(
if (elementCodec == ScalarCodecs.BigDecimalCodec.INSTANCE) {
return new BigDecimalCollectionCodec(factory);
}
if (elementTypeInfo.usesDefaultObjectCodec()) {
if (resolver.canonicalObjectCodec(elementTypeInfo) != null) {
return new ObjectCollectionCodec(factory, elementTypeInfo);
}
return new GenericCollectionCodec(factory, elementTypeInfo);
Expand Down Expand Up @@ -183,19 +186,19 @@ static Collection<Object> readUntyped(Utf8JsonReader reader) {
}

@Internal
final Collection<Object> newCollection() {
public final Collection<Object> newCollection() {
// JSON arrays do not carry a trusted size. Avoid speculative backing-array preallocation in
// parser hot paths; it can waste memory for small arrays and amplify untrusted input.
return factory.newCollection();
}

@Internal
final Collection<?> finishCollection(Collection<Object> collection) {
public final Collection<?> finishCollection(Collection<Object> collection) {
return factory.finish(collection);
}

@Internal
final boolean createsArrayList() {
public final boolean createsArrayList() {
return createsArrayList;
}

Expand Down Expand Up @@ -607,6 +610,9 @@ private ArrayList<Object> readUtf16ArrayListLongTail(
}

private ArrayList<Object> readUtf8ArrayList(Utf8JsonReader reader) {
// The complete short-list path is one stable collection owner. Splitting the first elements
// into a smaller helper lets a generated schema group absorb the loop according to C2
// compile order, while splitting e4/e5 into a tail creates the same race one level lower.
reader.enterDepth();
reader.expectNextToken('[');
if (reader.consumeNextToken(']')) {
Expand Down Expand Up @@ -647,11 +653,6 @@ private ArrayList<Object> readUtf8ArrayList(Utf8JsonReader reader) {
list.add(e3);
return list;
}
return readUtf8ArrayListTail(reader, e0, e1, e2, e3);
}

private ArrayList<Object> readUtf8ArrayListTail(
Utf8JsonReader reader, Object e0, Object e1, Object e2, Object e3) {
Object e4 = readUtf8Element(reader);
if (!reader.consumeNextCommaOrEndArray()) {
reader.exitDepth();
Expand Down Expand Up @@ -707,6 +708,21 @@ private ArrayList<Object> readUtf8ArrayListLongTail(
list.add(e7);
return list;
}
Object e8 = readUtf8Element(reader);
if (!reader.consumeNextCommaOrEndArray()) {
reader.exitDepth();
ArrayList<Object> 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);
list.add(e8);
return list;
}
ArrayList<Object> list = new ArrayList<>(9);
list.add(e0);
list.add(e1);
Expand All @@ -716,6 +732,7 @@ private ArrayList<Object> readUtf8ArrayListLongTail(
list.add(e5);
list.add(e6);
list.add(e7);
list.add(e8);
do {
list.add(readUtf8Element(reader));
} while (reader.consumeNextCommaOrEndArray());
Expand Down Expand Up @@ -833,6 +850,12 @@ private ObjectCollectionCodec(CollectionFactory factory, JsonTypeInfo elementTyp
this.elementTypeInfo = elementTypeInfo;
}

/** Returns the resolved element binding used by exact generated collection capabilities. */
@Internal
public JsonTypeInfo elementTypeInfo() {
return elementTypeInfo;
}

@Override
public void writeString(StringJsonWriter writer, Collection<?> value) {
if (value == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,15 +492,84 @@ private T readUtf8FixedObject(Utf8JsonReader reader) {
reader.exitDepth();
return object;
}
do {
JsonFieldInfo field = reader.readField(readTable);
// Keep the shared fixed-object fallback as one complete compilation owner. Root call sites can
// retain both this fallback and a generated reader in their type profile; parsing several
// fields per loop prevents C2 from recursively absorbing the fallback before it parses the
// generated receiver. Every block below is the same general field operation, including
// unknown-field handling, so objects of every field count retain identical semantics.
JsonFieldInfo field;
while (true) {
field = reader.readField(readTable);
reader.expect(':');
if (field == null) {
reader.skipValue();
} else {
field.readUtf8(reader, object);
}
} while (reader.consume(','));
if (!reader.consume(',')) {
break;
}
field = reader.readField(readTable);
reader.expect(':');
if (field == null) {
reader.skipValue();
} else {
field.readUtf8(reader, object);
}
if (!reader.consume(',')) {
break;
}
field = reader.readField(readTable);
reader.expect(':');
if (field == null) {
reader.skipValue();
} else {
field.readUtf8(reader, object);
}
if (!reader.consume(',')) {
break;
}
field = reader.readField(readTable);
reader.expect(':');
if (field == null) {
reader.skipValue();
} else {
field.readUtf8(reader, object);
}
if (!reader.consume(',')) {
break;
}
field = reader.readField(readTable);
reader.expect(':');
if (field == null) {
reader.skipValue();
} else {
field.readUtf8(reader, object);
}
if (!reader.consume(',')) {
break;
}
field = reader.readField(readTable);
reader.expect(':');
if (field == null) {
reader.skipValue();
} else {
field.readUtf8(reader, object);
}
if (!reader.consume(',')) {
break;
}
field = reader.readField(readTable);
reader.expect(':');
if (field == null) {
reader.skipValue();
} else {
field.readUtf8(reader, object);
}
if (!reader.consume(',')) {
break;
}
}
reader.expect('}');
reader.exitDepth();
return object;
Expand Down
Loading
Loading