Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
63563cd
refactor(json): centralize canonical codec ownership
chaokunyang Jul 22, 2026
ca69c72
perf(json): build atomic UTF-8 reader graphs
chaokunyang Jul 22, 2026
1f0e17f
perf(json): bound positive int tail
chaokunyang Jul 22, 2026
30340e4
perf(json): accelerate decimal double parsing
chaokunyang Jul 22, 2026
b43a624
perf(json): combine UTF-8 string stop checks
chaokunyang Jul 22, 2026
51b5fe9
perf(json): accelerate short field names
chaokunyang Jul 22, 2026
8609512
perf(json): publish generated capability graphs atomically
chaokunyang Jul 23, 2026
1a9dc33
perf(json): generate UTF-8 string collection readers
chaokunyang Jul 23, 2026
b777929
perf(json): pack dynamic UTF-8 field prefixes
chaokunyang Jul 23, 2026
16bbea0
perf(json): use current-token checks in generated readers
chaokunyang Jul 23, 2026
84c74de
perf(json): stabilize generated UTF-8 writer ownership
chaokunyang Jul 23, 2026
8863b4d
refactor(json): restore natural writer leaf boundaries
chaokunyang Jul 23, 2026
25860e2
perf(json): stabilize UTF-8 temporal writer boundary
chaokunyang Jul 23, 2026
96a4442
perf(json): stabilize UTF-8 string writer boundary
chaokunyang Jul 23, 2026
d3e11b7
perf(json): stabilize UTF-8 long writer boundary
chaokunyang Jul 23, 2026
87acdaf
perf(json): isolate UTF-8 long field profiling
chaokunyang Jul 23, 2026
e5e9498
perf(json): stabilize generated UTF-8 writer boundaries
chaokunyang Jul 24, 2026
9b835ed
perf(json): isolate UTF-8 long array writing
chaokunyang Jul 24, 2026
aab7601
perf(json): keep shared UTF-8 dispatch generated-only
chaokunyang Jul 24, 2026
c04043b
docs(java): document JSON C2 ownership boundaries
chaokunyang Jul 24, 2026
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
24 changes: 24 additions & 0 deletions .agents/languages/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,30 @@ Load this file when changing anything under `java/` or when Java drives a cross-
- In `MemoryBuffer` and `MemoryOps` hot paths, duplicate small straight-line copy/read/write logic
when that keeps control flow direct. Do not add private helper indirection to hot paths just to
reduce local code duplication; keep helpers for slow, cold, or error paths.
- In JDK 25 Fory JSON C2-sensitive code, preserve measured, naturally large hot-method boundaries.
A method that exceeds HotSpot's 325-byte hot-inline limit through real representation, scalar,
array, collection, or generated-schema work is an independent subtree owner. Generated group
planning counts only its invocation bytecodes, not the callee's transitive body. Do not shrink
such a method into a wrapper, add its body back into the caller budget, or manufacture a boundary
with padding, `@DontInline`, `CompileCommand`, fake receivers, or JVM flags. Keep escape,
malformed-input, Unicode, arbitrary-length, and other cold fallback work in separate methods.
- `JsonTrampolineInvoke` is the generated UTF-8 writer body's single shared
`invokeinterface`-bytecode owner. Only fully constructed generated object bodies and member groups
held in final interface-typed fields may call it. Root facades, startup `ObjectCodec`s, handwritten
codecs, arrays, collections, maps, readers, and writers must not call it. A generated collection
loop calls the element codec's ordinary entry; a qualifying generated element entry reaches its
own object-body trampoline. The static helper remains tiny and may inline because the shared
`invokeinterface` profile, not helper size, creates the boundary. Do not create one trampoline per
generated class, pass concrete receivers, add dispatch/state/resolution work, or introduce
synthetic receivers; each of those breaks the shared real-receiver profile or its clean owner
model.
- Intentional hot-path source duplication is required when helper extraction loses local
buffer/cursor state or makes C2 layout depend on compilation order. In particular, Long read/write
paths may repeat Int parsing or formatting logic, and unrolled array lanes may repeat complete
signed/range dispatch. Do not deduplicate these blocks without generated-source, `javap`,
PrintInlining, LogCompilation, nmethod, allocation, intrinsic, and aggregate evidence that the
independent boundary and performance remain stable. Preserve the nearby source comments that
record the owner and failure mode.
- In `MemoryBuffer` small-varint read/write hot paths, once Android has exited through the single
`MemoryOps` call, keep JVM bulk loads/stores local with raw Unsafe operations instead of routing
through branchful `_unsafeGet*` or `_unsafePut*` helpers. Add or preserve source comments that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ public byte[] toJsonBytes(Object value) {
writer.writeNull();
} else {
JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass());
// Root startup receivers must bypass the generated-only shared trampoline. Calling it
// here lets ObjectCodec dominate that BCI before generated body/group publication.
typeInfo.utf8Writer().writeUtf8(writer, value);
}
} finally {
Expand Down Expand Up @@ -223,6 +225,8 @@ public void writeJsonTo(Object value, OutputStream output) {
writer.writeNull();
} else {
JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass());
// Keep root dispatch direct; startup ObjectCodec receivers must not enter the
// generated body/group profile owned by JsonTrampolineInvoke.
typeInfo.utf8Writer().writeUtf8(writer, value);
}
} finally {
Expand Down Expand Up @@ -288,6 +292,8 @@ private byte[] toJsonBytesDeclared(Object value, Type type, Class<?> fallback) {
try {
state.typeResolver.lockJIT();
try {
// Declared root dispatch is still startup/root work. It must remain outside the
// generated-only JsonTrampolineInvoke receiver profile.
state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value);
} finally {
state.typeResolver.unlockJIT();
Expand All @@ -310,6 +316,8 @@ private void writeJsonDeclared(Object value, Type type, Class<?> fallback, Outpu
try {
state.typeResolver.lockJIT();
try {
// Declared root dispatch is direct for the same reason as toJsonBytesDeclared: only
// completed generated bodies and groups may contribute to the shared trampoline BCI.
state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value);
} finally {
state.typeResolver.unlockJIT();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@
* Resolver-local closed subtype dispatcher whose branch slots follow child JsonTypeInfo updates.
*
* <p>Inline discriminator state belongs to this parent. Any-readable children use parent-local
* field tables and generated reader instances so one child can be shared by parents with different
* discriminator names without changing the child's canonical metadata. Nested values of the same
* child type still use its canonical reader; the derived skip table applies only to the outer
* inline object.
* field tables and complete generated-reader arrays so one child can be shared by parents with
* different discriminator names without changing canonical child metadata. The derived skip table
* applies only to the outer inline object.
*
* <p>Writing rejects fixed-schema discriminator collisions when branches are resolved, but never
* queries an Any Map. Runtime dynamic-key conflicts are owned by the application; probing here
Expand All @@ -56,9 +55,9 @@ public final class ClosedSubtypeCodec implements JsonValueCodec<Object> {
private final JsonTypeInfo[] children;
private final ObjectCodec<Object>[] objectCodecs;
private JsonFieldTable[] inlineReadTables;
private Latin1ReaderCodec<Object>[] inlineLatin1Readers;
private Utf16ReaderCodec<Object>[] inlineUtf16Readers;
private Utf8ReaderCodec<Object>[] inlineUtf8Readers;
private volatile Latin1ReaderCodec<Object>[] inlineLatin1Readers;
private volatile Utf16ReaderCodec<Object>[] inlineUtf16Readers;
private volatile Utf8ReaderCodec<Object>[] inlineUtf8Readers;

/** Creates an unresolved resolver-local dispatcher shell for a validated subtype definition. */
@Internal
Expand All @@ -85,7 +84,7 @@ public void resolve(JsonTypeResolver resolver) {
Class<?> subtype = definition.classes[i];
JsonTypeInfo child = resolver.getTypeInfo(subtype, subtype);
if (definition.inclusion == Inclusion.PROPERTY) {
if (!child.usesDefaultObjectCodec()) {
if (resolver.canonicalObjectCodec(child) == null) {
throw new ForyJsonException(
"Inline JSON subtype requires the default object representation: " + subtype);
}
Expand All @@ -96,16 +95,15 @@ public void resolve(JsonTypeResolver resolver) {
if (any != null && (any.readField() != null || any.readSetter() != null)) {
if (inlineReadTables == null) {
inlineReadTables = new JsonFieldTable[children.length];
inlineLatin1Readers = new Latin1ReaderCodec[children.length];
inlineUtf16Readers = new Utf16ReaderCodec[children.length];
inlineUtf8Readers = new Utf8ReaderCodec[children.length];
}
JsonFieldTable table =
objectCodec.readTable().withSkippedName(definition.scanInfo.property());
inlineReadTables[i] = table;
// The subtype scan restores the cursor, so the outer child rereads the discriminator and
// needs this parent-local skip table. Nested child values must use the canonical table.
resolver.resolveInlineAnyReaders(this, i, objectCodec, table);
// needs this parent-local skip table. The resolver constructs a complete immutable array
// of generated readers for each representation and publishes that array in one volatile
// write; never publish its elements independently. Nested child values keep the canonical
// table and canonical capability.
}
}
children[i] = child;
Expand Down Expand Up @@ -178,10 +176,11 @@ public Object readLatin1(Latin1JsonReader reader) {
int index = reader.scanObjectStringField(definition.scanInfo);
JsonFieldTable[] tables = inlineReadTables;
if (tables != null && tables[index] != null) {
Latin1ReaderCodec<Object> codec = inlineLatin1Readers[index];
return codec == null
? objectCodecs[index].readLatin1Object(reader, tables[index])
: codec.readLatin1(reader);
Latin1ReaderCodec<Object>[] readers = inlineLatin1Readers;
if (readers != null) {
return readers[index].readLatin1(reader);
}
return objectCodecs[index].readLatin1Object(reader, tables[index]);
}
return children[index].latin1Reader().readLatin1(reader);
}
Expand Down Expand Up @@ -213,10 +212,11 @@ public Object readUtf16(Utf16JsonReader reader) {
int index = reader.scanObjectStringField(definition.scanInfo);
JsonFieldTable[] tables = inlineReadTables;
if (tables != null && tables[index] != null) {
Utf16ReaderCodec<Object> codec = inlineUtf16Readers[index];
return codec == null
? objectCodecs[index].readUtf16Object(reader, tables[index])
: codec.readUtf16(reader);
Utf16ReaderCodec<Object>[] readers = inlineUtf16Readers;
if (readers != null) {
return readers[index].readUtf16(reader);
}
return objectCodecs[index].readUtf16Object(reader, tables[index]);
}
return children[index].utf16Reader().readUtf16(reader);
}
Expand Down Expand Up @@ -248,10 +248,11 @@ public Object readUtf8(Utf8JsonReader reader) {
int index = reader.scanObjectStringField(definition.scanInfo);
JsonFieldTable[] tables = inlineReadTables;
if (tables != null && tables[index] != null) {
Utf8ReaderCodec<Object> codec = inlineUtf8Readers[index];
return codec == null
? objectCodecs[index].readUtf8Object(reader, tables[index])
: codec.readUtf8(reader);
Utf8ReaderCodec<Object>[] readers = inlineUtf8Readers;
if (readers != null) {
return readers[index].readUtf8(reader);
}
return objectCodecs[index].readUtf8Object(reader, tables[index]);
}
return children[index].utf8Reader().readUtf8(reader);
}
Expand Down Expand Up @@ -284,18 +285,71 @@ private int requireSubtype(Class<?> runtimeType) {
}

@Internal
public void setInlineLatin1Reader(int index, Latin1ReaderCodec<Object> reader) {
inlineLatin1Readers[index] = reader;
public int childCount() {
return children.length;
}

@Internal
public JsonTypeInfo child(int index) {
return children[index];
}

@Internal
public JsonFieldTable inlineReadTable(int index) {
return inlineReadTables == null ? null : inlineReadTables[index];
}

@Internal
public Latin1ReaderCodec<Object>[] inlineLatin1Readers() {
return inlineLatin1Readers;
}

@Internal
public Utf16ReaderCodec<Object>[] inlineUtf16Readers() {
return inlineUtf16Readers;
}

@Internal
public void setInlineUtf16Reader(int index, Utf16ReaderCodec<Object> reader) {
inlineUtf16Readers[index] = reader;
public Utf8ReaderCodec<Object>[] inlineUtf8Readers() {
return inlineUtf8Readers;
}

@Internal
public void setInlineUtf8Reader(int index, Utf8ReaderCodec<Object> reader) {
inlineUtf8Readers[index] = reader;
public void installInlineLatin1Readers(Latin1ReaderCodec<Object>[] readers) {
validateInlineReaders(readers);
if (inlineLatin1Readers != null) {
throw new IllegalStateException("Inline Latin1 readers are already installed");
}
inlineLatin1Readers = readers;
}

@Internal
public void installInlineUtf16Readers(Utf16ReaderCodec<Object>[] readers) {
validateInlineReaders(readers);
if (inlineUtf16Readers != null) {
throw new IllegalStateException("Inline UTF16 readers are already installed");
}
inlineUtf16Readers = readers;
}

@Internal
public void installInlineUtf8Readers(Utf8ReaderCodec<Object>[] readers) {
validateInlineReaders(readers);
if (inlineUtf8Readers != null) {
throw new IllegalStateException("Inline UTF8 readers are already installed");
}
inlineUtf8Readers = readers;
}

private void validateInlineReaders(Object[] readers) {
if (inlineReadTables == null || readers == null || readers.length != children.length) {
throw new IllegalArgumentException("Inline reader array does not match subtype branches");
}
for (int i = 0; i < children.length; i++) {
if (inlineReadTables[i] != null && readers[i] == null) {
throw new IllegalArgumentException("Missing inline reader for subtype branch " + i);
}
}
}

private static void rejectDiscriminatorCollision(ObjectCodec<?> codec, String property) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,23 @@ public static CollectionCodec<?> create(
Class<?> elementRawType = CodecUtils.rawType(elementType, Object.class);
CollectionFactory factory = collectionFactory(rawType, elementRawType);
JsonTypeInfo elementTypeInfo = resolver.getTypeInfo(elementType, elementRawType);
return create(factory, elementTypeInfo);
return create(factory, elementTypeInfo, resolver.canonicalObjectCodec(elementTypeInfo) != null);
}

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

private static CollectionCodec<?> create(
CollectionFactory factory, JsonTypeInfo elementTypeInfo) {
CollectionFactory factory, JsonTypeInfo elementTypeInfo, boolean objectElement) {
Object elementCodec = elementTypeInfo.stringWriter();
if (elementCodec == ScalarCodecs.StringCodec.INSTANCE) {
return new StringCollectionCodec(factory);
Expand Down Expand Up @@ -131,7 +137,7 @@ private static CollectionCodec<?> create(
if (elementCodec == ScalarCodecs.BigDecimalCodec.INSTANCE) {
return new BigDecimalCollectionCodec(factory);
}
if (elementTypeInfo.usesDefaultObjectCodec()) {
if (objectElement) {
return new ObjectCollectionCodec(factory, elementTypeInfo);
}
return new GenericCollectionCodec(factory, elementTypeInfo);
Expand Down Expand Up @@ -195,7 +201,7 @@ final Collection<?> finishCollection(Collection<Object> collection) {
}

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

Expand Down Expand Up @@ -833,6 +839,11 @@ private ObjectCollectionCodec(CollectionFactory factory, JsonTypeInfo elementTyp
this.elementTypeInfo = elementTypeInfo;
}

@Internal
public JsonTypeInfo elementTypeInfo() {
return elementTypeInfo;
}

@Override
public void writeString(StringJsonWriter writer, Collection<?> value) {
if (value == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.fory.json.codec;

import org.apache.fory.annotation.Internal;
import org.apache.fory.json.writer.Utf8JsonWriter;

/**
* Owns the single interface-invocation bytecode used by large generated UTF-8 writer receivers.
*
* <p>This helper is deliberately not a general codec dispatcher. Generated object bodies and member
* groups store their completed receiver in a final interface-typed field and call this class.
* Consequently, every qualifying generated receiver contributes to the type profile of the same
* {@code invokeinterface} bytecode index instead of creating one profile per generated class. That
* real polymorphic profile prevents an outer generated collection or object from absorbing a
* receiver's complete transitive field closure before the receiver has formed its own level-4
* nmethod.
*
* <p>The static helper itself is intentionally tiny and may inline into generated callers. The
* boundary comes from the shared interface-call profile that remains after helper inlining, not
* from making this method large. Generated receivers own the real work and any naturally large
* method; this class owns only their common interface-call bytecode index.
*
* <p>Do not duplicate this helper per generated type, replace the interface field with a concrete
* receiver, or move root/handwritten codec calls through it. Root startup codecs execute before
* generated capabilities are published and would dominate the shared profile; concrete receiver
* types would let C2 recover and inline the exact implementation. Do not add type resolution,
* lifecycle state, allocation, callbacks, or fallback logic here. Those concerns belong to the
* resolver or the generated receiver, while this class owns only the shared invocation bytecode.
*/
@Internal
public final class JsonTrampolineInvoke {
private JsonTrampolineInvoke() {}

/**
* Invokes generated UTF-8 object bodies and field groups through one interface-call BCI.
*
* <p>The interface invocation below must remain the only operation and the only {@code
* invokeinterface} bytecode in this method. The static helper call may itself be inlined, but
* HotSpot still consumes the one method-data profile owned by this bytecode index. The receivers
* must be real, fully constructed generated capabilities published only after construction; never
* add synthetic receivers merely to force polymorphism.
*/
public static void writeUtf8(Utf8WriterCodec<Object> codec, Utf8JsonWriter writer, Object value) {
codec.writeUtf8(writer, value);
}
}
Loading
Loading