bank-ro opened a new issue, #3999: URL: https://github.com/apache/fory/issues/3999
### Search before asking - [x] I had searched in the [issues](https://github.com/apache/fory/issues) and found no similar issues. ### Version 1.7.0 ### Component(s) Java ### Minimal reproduce step a ### What did you expect to see? a ### What did you see instead? a ### Anything Else? ## Summary Upgrading `fory-core` **1.6.1 → 1.7.0**, Java deserialization throughput in `CompatibleMode.COMPATIBLE` collapses by **~100–900×** for object graphs that reference more than one distinct registered struct type. The generated per-type deserializer codec appears to be **regenerated (re-JIT-compiled) on every `deserialize()` call** instead of being cached across calls on the same `Fory` instance. `SCHEMA_CONSISTENT` mode is **not** affected. This is a performance regression only — values still deserialize correctly, and the wire format is unchanged between 1.6.1 and 1.7.0. ## Environment - fory-core **1.6.1** (baseline) vs **1.7.0** (regressed) - JDK 26 (Temurin), Windows 11 - `Language.JAVA`, `requireClassRegistration(true)`, `withRefTracking(false)` - Reproduces with both `withAsyncCompilation(false)` and `withAsyncCompilation(true)` ## Reproduction Compile once (the class uses only Fory's stable public API, so the same compiled class runs under either jar); run under each version. Deps: `fory-core`, `janino`, `commons-compiler`, `slf4j-api`. ```java import org.apache.fory.Fory; import org.apache.fory.ThreadSafeFory; import org.apache.fory.config.CompatibleMode; import org.apache.fory.config.Language; public class ForyCompatibleRegression { public static class A { public long a; public int b; public String c; public A() {} } public static class B { public long a; public int b; public String c; public B() {} } public static class C { public long a; public int b; public String c; public C() {} } public static class D { public long a; public int b; public String c; public D() {} } public static class E { public long a; public int b; public String c; public E() {} } public static class F { public long a; public int b; public String c; public F() {} } public static class G { public long a; public int b; public String c; public G() {} } public static class H { public long a; public int b; public String c; public H() {} } public static class Container { public A a; public B b; public C c; public D d; public E e; public F f; public G g; public H h; public Container() {} } public static void main(String[] args) { CompatibleMode mode = CompatibleMode.valueOf(System.getProperty("fory.mode", "COMPATIBLE")); ThreadSafeFory fory = Fory.builder() .withLanguage(Language.JAVA) .withCompatibleMode(mode) .withRefTracking(false) .requireClassRegistration(true) .withAsyncCompilation(false) .buildThreadSafeFory(); fory.register(A.class, 201); fory.register(B.class, 202); fory.register(C.class, 203); fory.register(D.class, 204); fory.register(E.class, 205); fory.register(F.class, 206); fory.register(G.class, 207); fory.register(H.class, 208); fory.register(Container.class, 200); Container obj = new Container(); obj.a = new A(); obj.b = new B(); obj.c = new C(); obj.d = new D(); obj.e = new E(); obj.f = new F(); obj.g = new G(); obj.h = new H(); obj.a.c = "x"; byte[] payload = fory.serialize(obj); long warmEnd = System.nanoTime() + 2_000_000_000L; while (System.nanoTime() < warmEnd) sink += id(fory.deserialize(payload, Container.class)); long ops = 0, start = System.nanoTime(), end = start + 3_000_000_000L; while (System.nanoTime() < end) { sink += id(fory.deserialize(payload, Container.class)); ops++; } double opsPerSec = ops / ((System.nanoTime() - start) / 1e9); System.out.printf("fory-core %s mode=%s Container(8 distinct types)=%d bytes%n", version(), mode, payload.length); System.out.printf(" deserialize: %,.0f ops/s (%.2f us/op)%n", opsPerSec, 1e6 / opsPerSec); System.exit(0); } private static long sink; private static int id(Object o) { return o == null ? 0 : System.identityHashCode(o); } private static String version() { Package p = Fory.class.getPackage(); String v = p == null ? null : p.getImplementationVersion(); return v == null ? "<unknown>" : v; } } ``` ``` javac -cp fory-core-1.7.0.jar ForyCompatibleRegression.java java --add-opens=java.base/java.lang.invoke=ALL-UNNAMED -cp ".:fory-core-1.6.1.jar:janino-3.1.12.jar:commons-compiler-3.1.12.jar:slf4j-api-2.0.18.jar" ForyCompatibleRegression java --add-opens=java.base/java.lang.invoke=ALL-UNNAMED -cp ".:fory-core-1.7.0.jar:janino-3.1.12.jar:commons-compiler-3.1.12.jar:slf4j-api-2.0.18.jar" ForyCompatibleRegression # add -Dfory.mode=SCHEMA_CONSISTENT to confirm that mode is unaffected ``` ## Results Same compiled classes, same payload bytes, only the `fory-core` jar differs. Deserialize throughput: | mode | 1.6.1 | 1.7.0 | delta | |---|---|---|---| | COMPATIBLE | 5,779,968 ops/s (0.17 µs/op) | 19,978 ops/s (50.06 µs/op) | **~289× slower** | | SCHEMA_CONSISTENT | 7,860,811 ops/s (0.13 µs/op) | 7,823,780 ops/s (0.13 µs/op) | none | The slowdown scales with the number of **distinct registered types** in the graph (COMPATIBLE, deserialize ops/s): | distinct types in graph | 1.6.1 | 1.7.0 | delta | |---|---|---|---| | 1 | 13.6M | 9.6M | ~1.4× | | 4 | 7.1M | 41.4k | ~172× | | 8 | 3.7M | 20.6k | ~178× | | 20 | 1.5M | 7.8k | ~192× | | 40 | 0.78M | 3.2k | ~242× | The 1.7.0 cost grows ~6–8 µs **per distinct type per deserialize call**, which points at per-call codec resolution/regeneration. Running the same repro with `withAsyncCompilation(true)` makes the mechanism explicit — the JIT compile pool is flooded with per-call codec-generation tasks: ``` WARN CodeGenerator - Task org.apache.fory.builder.JITContext$$Lambda/... rejected from java.util.concurrent.ThreadPoolExecutor@...[Shutting down, pool size = 16, active threads = 16, queued tasks = 69648, completed tasks = 113347] ``` i.e. ~113k completed + ~70k queued `CodeGenerator` tasks accumulated during a ~2 s measurement window for a graph that has only 9 types — one codec (re)generation per deserialize call. ## Suspected cause Appears related to **#3963 "refactor java generated codec cache granularity."** The regression is COMPATIBLE-mode only (the meta-share path), is absent for a single-type graph (N=1), scales with distinct-type count, and affects both async settings — consistent with the generated deserializer codec no longer being cached (or being cached under a per-call key) on the meta-shared read path. ## Impact note Real-world impact depends on payload composition: records dominated by large primitive arrays hide it (bulk copy dominates, ~20–30% observed), while structure-heavy records with many nested types are hit hardest (100×+). Any COMPATIBLE-mode workload that deserializes such graphs repeatedly on a shared `Fory` instance is affected. ### Are you willing to submit a PR? - [ ] I'm willing to submit a PR! -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
