Skip to content

Keep generated registry dispatch JIT-compilable and remove dead code (0.13.0) - #64

Merged
gregjotau merged 1 commit into
mainfrom
perf/bounded-registry-dispatch
Sep 23, 2026
Merged

gregjotau merged 1 commit into
mainfrom
perf/bounded-registry-dispatch

Conversation

@gregjotau

Copy link
Copy Markdown
Contributor

Why

The generated TemplateSet.render kept the class-index switch and the subclass instanceof fallback chain in one method. That costs about 37 bytes of bytecode per template. From roughly 215 templates the method exceeds HotSpot's 8000-byte HugeMethodLimit, and with the default -XX:+DontCompileHugeMethods it is never JIT-compiled.

ReAI's web-app registry has 353 templates and a 13,137-byte render. ReAI's Dockerfile doesn't override the flag, so every production render currently runs through an interpreted dispatch.

JMH replica of the generated shape at 353 templates (M5 Max, JDK 27, 3 forks). The host was under unrelated load, which widens the interpreted error:

Registry shape ns/op
0.12.2 (switch and fallback chain in render) 195.1 ± 142.9
0.12.2 with -XX:-DontCompileHugeMethods (control) 11.9 ± 0.6
this PR 10.2 ± 0.2

The control run shows the whole difference comes from the method being interpreted. ReAI pages render in about 6–40 µs, so this removes roughly 0.5–3% per request, with no trade-off.

What

  • Registry dispatch (moved into a new RegistryGenerator, same as MessageGenerator and RouteGenerator):
    • render is now a small dispatcher that looks up the index and switches on index / 256 into private chunk methods of at most about 4 KB each.
    • The unchanged instanceof chain moves to renderSubtype.
    • The isAssignableFrom chain moves to isSupertypeOfModel.
    • Behavior is identical. The fallback deliberately keeps every model: KSP's isOpen() can't see all-open compiler plugins or Java library classes, so dropping "final" models would not be provably safe.
  • Dead code:
    • HtmlOutput.url(TrustedUrl) was never called by generated code or by any consumer, and calling it directly skipped context-path resolution.
    • RouteCatalog.isEmpty().
    • A test-only SpringRequestDataValues constructor; the test now uses create(...).
    • An unused TaskProvider import.
  • Small compiler win: the naming regexes are compiled once instead of once per catalog key, template or route. ReAI has about 19k message-name splits.
  • Stale docs:
    • README said 0.11.2; docs site said 0.11.1 and JDK 26.
    • The PERFORMANCE_AUDIT "Further opportunities" table listed shipped or rejected items as open, and said "per-template sources" when 32 hash-assigned files shipped.
    • ISOLATED_PROJECTS described Gradle #39057 and KSP #3189 as pending; both were closed without merging.
  • The version goes to 0.13.0 because a public method is removed. Publishing still only happens on tag push.

Validation

  • ./gradlew build passes, including the example golden renders.
  • New RegistryGeneratorTest compiles a 600-template registry (3 chunks) plus an open model with a subclass. It renders every model, checks supports, supportsReturnType and usesRequestDataValues plus the unknown-model error, and asserts every per-request method is under 8000 bytes. The test fails when the chunking is disabled.
  • ReAI web-app generated output compared with 0.12.2: only ThimTemplates.java differs. All 32 ThimTemplatesPart*.java files, their .bin resources and the message usage manifest are byte-identical. With the candidate, the largest ReAI registry methods are renderSubtype at 6,727 bytes and render0 at 4,127 bytes; render is 108 bytes.
  • ReAI (web-app, bedri), Utin, Eteo and Ecomtools compile against this branch via -PthimBuild.

Considered and left out

  • The runtime config getRuntimeClasses() in settings-plugin looks unused but is a public task property, so it's not provably unused.
  • The GitHub Packages publishing repository may still be used manually.
  • Dependency patch bumps (KSP 2.3.12, Spring 7.0.9 / Boot 4.1.1, JUnit 6.1.3) are better as their own PR.

🤖 Generated with Claude Code

The generated TemplateSet kept the index switch and the subclass instanceof chain in one
render method. From about 215 templates that method exceeds HotSpot's 8000-byte huge-method
limit and is never JIT-compiled; ReAI's web-app registry (353 templates, 13,137 bytes)
paid about 190 ns per render instead of about 10 ns. Dispatch now goes through a small
render method, 256-template chunk methods and a separate subclass fallback.

Also removes the unused HtmlOutput.url, RouteCatalog.isEmpty, a test-only constructor and
an unused import, compiles naming regexes once, and corrects stale versions and statuses
in the docs. Releases as 0.13.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gregjotau
gregjotau merged commit 8bbac97 into main Sep 23, 2026
1 check passed
@gregjotau
gregjotau deleted the perf/bounded-registry-dispatch branch September 23, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant