Skip to content

DO NOT MERGE: #34154: test(java25): what an AOT cache actually costs dotCMS - #37425

Draft
fabrizzio-dotCMS wants to merge 1 commit into
mainfrom
issue-34154-java25-aot-study
Draft

DO NOT MERGE: #34154: test(java25): what an AOT cache actually costs dotCMS#37425
fabrizzio-dotCMS wants to merge 1 commit into
mainfrom
issue-34154-java25-aot-study

Conversation

@fabrizzio-dotCMS

Copy link
Copy Markdown
Member

Why this exists

Session 2 of the Lunch & Learn (#34154) budgets 3 minutes of concept and 2.5 of dotCMS
application
to ahead-of-time computation. It is the only one of the six features in that
half where our adoption is zero and we had no evidence of our own — every other beat is
backed by real code.

This closes that gap by measuring instead of speculating. It is study material for the talk,
not a proposal: DO NOT MERGE.

What it does

AotCacheDemo re-launches itself in child JVMs, because -XX:AOTMode and -XX:AOTCache are
read once at startup and no single-JVM test can compare them — the same reason
CompactObjectHeadersDemo spawns children. Every phase runs with
-XX:+UseCompactObjectHeaders, because that is how dotCMS actually runs.

java dotCMS/src/test/java/com/dotcms/jdk/AotCacheDemo.java

No Maven, no dotCMS classpath, no network. JDK 24+ for AOTMode; verified on 25.0.2.

What it found

1 — The cache is bound to a flag we already set for an unrelated reason.

exit status              : 0
[warning][aot] Unable to use AOT cache.
[         ] The AOT cache's UseCompactObjectHeaders setting (enabled) does not
            equal the current UseCompactObjectHeaders setting (disabled).
[error  ][aot] Loading static archive failed.
workload still completed : true

We enable UseCompactObjectHeaders in container/tomcat9/bin/setenv.sh and in the test JVMs
in parent/pom.xml. Any cache we build has to be built with it. On a mismatch the JVM does
not fail
— it reports it and starts anyway, without the cache. Flip that flag in either
direction, in any environment, and the server keeps booting while quietly paying full startup
cost. The only signal is a log line nobody is watching for.

2 — An exploded WAR ends the training run.

-cp <directory>   exit=1
    [error][aot] Error: non-empty directory '.../classes'
    Cannot have non-empty directory in paths
-cp <jar>         exit=0
    AOTConfiguration recorded: .../jar.aotconf

Our own classes live in WEB-INF/classes. This is a blocker to clear before any measurement,
not a limitation to design around. (Recording against the current directory, -cp ., behaves
differently again: the class is merely skipped as an Unsupported location and the dump
succeeds. Worth knowing before someone reports the two as the same thing.)

3 — The workflow itself is three commands and it works. On the demo's own toy workload the
cached start is roughly 45% faster. Treat that as an order of magnitude and nothing more — it
loads a few hundred classes where dotCMS loads tens of thousands.

The cheapest real first step is not this

dotcms.startup.ms is already publishedServletContainerHealthCheck at lines 141, 152
and 472. Before any AOT work, plotting the distribution of a number we already collect gives
the baseline that AOT would have to beat. That costs nothing and needs no new code.

Not in scope

  • No production or container change: nothing here alters how dotCMS starts.
  • No dotcms.startup.ms dashboard — that needs data from real environments, not a laptop.
  • The System.getProperty("dotcms.startup.ms") reads in ServletContainerHealthCheck violate
    the Config-only Critical Rule in CLAUDE.md. Noticed, deliberately not fixed here.

Refs #34154

AOT adoption in core is zero and the talk had no evidence for it. This adds a
self-relaunching demo that drives the record/create/use workflow in child JVMs
and reproduces the two constraints that decide whether it is usable here.

Findings, all reproduced by the demo rather than asserted:

- The cache is bound to the UseCompactObjectHeaders setting it was built with.
  dotCMS enables that flag in the shipped container and in the test JVMs. On a
  mismatch the JVM logs the error and KEEPS RUNNING without the cache, exit 0 --
  startup silently returns to full cost.

- A directory on the class path ends the training run outright: "Cannot have
  non-empty directory in paths", non-zero exit. A JAR records normally. dotCMS
  serves an exploded WAR with its classes in WEB-INF/classes, so this blocks any
  measurement rather than degrading it.

Verified on JDK 25.0.2. Needs no Maven, no dotCMS classpath and no network;
test-compile -pl :dotcms-core --am passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @fabrizzio-dotCMS's task in 1m 6s —— View job


Code Review — AotCacheDemo.java

This is a standalone, single-file study demo (DO NOT MERGE), so the usual production rules are applied with that in mind. System.out / System.getProperty here are correct — this runs via the source launcher with no dotCMS classpath, Config/Logger aren't available, and the file mirrors the sibling CompactObjectHeadersDemo pattern. Not flagged.

Two real findings, both non-blocking:

New Issues

  • 🟡 Medium: dotCMS/src/test/java/com/dotcms/jdk/AotCacheDemo.java:22,51,286 — The {@link CompactObjectHeadersDemo} and @see CompactObjectHeadersDemo references point to a class that does not exist anywhere in the repo (grep finds CompactObjectHeadersDemo only inside this file). The javadoc and the PR body both lean on it ("the same reason CompactObjectHeadersDemo spawns children"), but there's nothing for a reader to follow, and -Xdoclint javadoc generation would reject the dangling @link/@see. Either add that sibling demo, or drop the references and inline the explanation. Fix this →

  • 🟡 Medium: dotCMS/src/test/java/com/dotcms/jdk/AotCacheDemo.java:227-230, 273-276run() and child() drain the child process sequentially: getInputStream().readAllBytes() fully, then getErrorStream().readAllBytes(). This is the classic ProcessBuilder deadlock — if a child fills its ~64 KB stderr pipe buffer while the parent is still blocked reading stdout to EOF, the child blocks on the stderr write, stdout never closes, and both hang forever. In practice the -Xlog:aot=warning filter keeps output small so it likely won't trigger, but run() in locationExperiment (lines 208–218) uses no -Xlog filter and AOTMode=record can emit many Skipping ... Unsupported location lines. Safer to read the two streams concurrently, or use ProcessBuilder.redirectErrorStream(true) / redirectOutput. Assumption: default OS pipe buffer ~64 KB. What to verify: run locationExperiment against a larger classpath and confirm it doesn't hang.

Notes (not blocking, already acknowledged in the PR)

  • The System.getProperty("dotcms.startup.ms") Config-rule violation called out in the PR body is in ServletContainerHealthCheck, not in this diff — nothing to fix here.
  • median() runs RUNS + 1 times and discards the first (line 310) — correct and matches the doc.

Overall: clean, well-documented study material. Nothing here alters how dotCMS starts, consistent with the DO NOT MERGE intent.
· issue-34154-java25-aot-study

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant