Skip to content

Group generated renderers into hash-assigned source files for incremental compilation - #59

Merged
gregjotau merged 7 commits into
mainfrom
experiment/per-template-sources
Sep 19, 2026
Merged

gregjotau merged 7 commits into
mainfrom
experiment/per-template-sources

Conversation

@gregjotau

@gregjotau gregjotau commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Renderers are generated into 32 source files chosen by a stable hash of the page-model name (<Registry>Part<n>), each with a holder class that owns the file's static resource. Renderers nest in their holder and reference only its STATIC; the registry is the only file that references every renderer. An unchanged file regenerates byte-identical output, so Gradle's incremental Java compilation recompiles one file plus the registry, and Kotlin's incremental compilation sees one changed Java source. KSP dependencies stay aggregating.

Measured on ReAI (344 renderers, warm daemon, --no-build-cache):

Task, one-template edit 0.11.2 This PR
:web-app:kspKotlin 3.0–4.0 s 3.6 s
:web-app:compileKotlin 1.0–1.2 s 0.09 s
:web-app:compileJava 6.1–6.3 s 0.3 s
Total build 10.4–12.1 s 4.3–4.4 s

Full :web-app:compileJava: 6.3 s → 2.8 s. Static resources: 1.6 MB → 2.8 MB (deduplicated per file instead of per module).

Correctness

  • GoldenRenderTest: twelve renders (four example pages, three locales, form errors, 64-byte output buffer) recorded with the 0.11.2 compiler and compared byte for byte.
  • All 105 tests pass. Consumer preflight: ReAI web-app and bedri (the latter compiles generated Java with -Werror).
  • Upgrade transition from 0.11.2 with the build cache disabled: full build → this layout (incremental) → edit → retry → second edit → revert, all green.

What was tried and rejected

  • One file per template: compileJava 0.27 s, but KSP grew 2.9 s → 4.5 s because it registers every generated Java file (Thim's own generation only grew by 0.23 s; Dependencies.ALL_FILES changed nothing), and resources grew to 6.5 MB.
  • Package-private top-level renderer classes: javac's auxiliary-class lint rejects references from another file, fatal under -Werror.
  • Keeping the ThimRenderer suffix: Gradle's previous-compilation data kept listing every renderer under ThimTemplates.java, so the first post-upgrade edit deleted all class files and retrying did not recover.

Details in PERFORMANCE_AUDIT.md.

🤖 Generated with Claude Code

gregjotau and others added 7 commits September 18, 2026 17:17
Each renderer now carries its own STATIC bytes and references neither the
registry nor other renderers, so an unchanged template regenerates
byte-identical files and Gradle's incremental Java compilation recompiles only
the renderer whose template changed plus the registry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mental javac

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Per-template files made KSP about 1.1 s slower in ReAI because KSP registers
every generated Java file; Thim's own generation only grew by 0.2 s. Renderers
are now grouped into 32 files by a stable hash of the page-model name, each
with its own static resource, so an edit recompiles one file of roughly ten
renderers and KSP sees 32 files. Golden renders recorded with the 0.11.2
compiler prove the output is byte-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
javac's auxiliary-class lint, fatal under -Werror in one ReAI module, rejects
package-private top-level classes referenced from another source file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gregjotau

Copy link
Copy Markdown
Contributor Author

Full edit loop on ReAI (:web-app:classes after touching one template, warm daemon, --no-build-cache, three repetitions each):

Task 0.11.2 single source This branch
:web-app:kspKotlin 3.0–4.0 s 4.5–4.9 s
:web-app:compileKotlin 1.0–1.2 s 0.06–0.09 s
:web-app:compileJava 6.1–6.3 s 0.26–0.28 s
Total 10.4–12.1 s 5.2–5.6 s

Kotlin's incremental compilation also benefits, since only one Java file on its source path changes. KSP is now the dominant cost of a template edit, so the next step for the edit loop is inside the processor (property-lookup caching, or skipping regeneration for unchanged templates), not javac.

Upgrade transition verified with --no-build-cache: 0.11.2 full build → this branch (incremental, succeeds) → edit (succeeds, 2 real recompilations) → retry → second edit → revert, all green.

@gregjotau gregjotau changed the title Experiment: one generated source and static resource per template Group generated renderers into hash-assigned source files for incremental compilation Sep 18, 2026
@gregjotau
gregjotau merged commit a7e7e43 into main Sep 19, 2026
1 check passed
@gregjotau
gregjotau deleted the experiment/per-template-sources branch September 19, 2026 05:27
@gregjotau gregjotau mentioned this pull request Sep 19, 2026
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