Skip to content

JVM: opt-in JDK 25 AOT cache (ARCADEDB_AOT=1 / start_jvm(aot=True)) (#19) - #31

Merged
tae898 merged 3 commits into
mainfrom
port/aot-cache
Oct 24, 2026
Merged

tae898 merged 3 commits into
mainfrom
port/aot-cache

Conversation

@tae898

@tae898 tae898 commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

What

Ports humemai/arcadedb-embedded-python#490 (merged as 43d723c9e1), part of #19. An opt-in Project Leyden AOT cache (JDK 25, JEP 483/514/515) for the JVM wheel. ARCADEDB_AOT=1 or start_jvm(aot=True) (also jvm_kwargs={"aot": True}) lets the JVM keep a per-user cache of the classes it loads and links at start. It is off by default, and nothing changes for anyone who does not set it. New module src/arcadedb_embedded/aot.py, a small hook in start_jvm (_aot_flags(aot, jvm_args, classpath) and the aot keyword) and tests in tests/test_aot.py.

  • Cache directory: $XDG_CACHE_HOME/arcadedb/aot or ~/.cache/arcadedb/aot (macOS ~/Library/Caches/arcadedb/aot, Windows %LOCALAPPDATA%\arcadedb\Cache\aot); ARCADEDB_AOT_CACHE=<file> overrides it. File name = hash of package version, bundled JRE version, jars (jar_fingerprint sha256 plus each jar's size and mtime), the exact class path (including JPype's org.jpype.jar, which the cache records too) and platform.
  • First process trains with -XX:AOTCacheOutput=<unique tmp> (the JVM writes it when the process exits normally); one trainer at a time via an exclusive lock file holding its pid, other processes start normally. The next start recovers a finished or dead trainer: the bundled JVM must accept the file (-XX:AOTMode=on -version), then an atomic rename and a sidecar with size and sha256. Use is -XX:AOTCache=<file> -XX:AOTMode=auto; a file whose size or sha256 differs from the sidecar is deleted and rebuilt, so a damaged cache is never mapped.
  • Every failure (unwritable or blocked directory, full disk, JRE below 25 or unreadable, missing JPype jar, lock error) is a normal start with a debug line on the arcadedb_embedded.aot logger; start_jvm never raises because of it. User-given -XX:AOT*, -XX:SharedArchiveFile or -Xshare flags win and the bindings add nothing.

Why (measured in #490, not re-measured here)

Measured on the laptop with ab.py (study jvm-aot-ab-laptop-20261022 in the benchmark notes, 12 clean pairs per figure): cold total -33 to -36 percent, JVM start -25 to -30 percent, database open -37 to -42 percent, and no detectable difference in steady-state inserts, lookups or scans. A tiny training run was as good as a broad one. A cache records absolute jar paths and jar mtime and size, so it cannot ship inside the wheel and has to be created on the user's machine.

Robustness matrix (from #490)

Probe output in humemai/arcadedb-embedded-python .notes/bench/repros/jvm-aot-robust-20261024. Every repetition is fresh processes and must end with a working database; "mapped" means the JVM's own -Xlog:aot log shows it opened and mapped the cache. The probes ran the #490 aot.py and jvm.py on top of the merged-main wheel 26.11.1.dev0 (Corretto 25.0.4.1 jre), cores 0-5. They were not rerun for this port; the module is a verbatim copy and the jvm.py hook is the same code.

probe variant repetitions passed
1-train-then-use - 10 10
2-race-8 (8 processes at once, no cache) - 20 20
3-corrupt truncate-half, zero-length, random-middle-4k, random-whole, header-zeroed, meta-missing, meta-garbage 3 each 21 of 21
4-stale jar-touched, jar-replaced, version-changed, jar-touched-fixed-path, jar-replaced-fixed-path 3 each 15 of 15
5-readonly-dir - 3 3
5-path-blocked-by-file - 3 3
5-full-disk-tmpfs-1m - 3 3
5-full-disk-tmpfs-20m - 3 3
6-jdk24-fake - 3 3
6-jre-release-unreadable - 3 3
6-jvm-rejects-aot-flag (documented limit) - 3 3
6-opt-in-off - 3 3
7-trainer-killed - 5 5

Total: 95 of 95 repetitions passed; SUMMARY runs=514 failed_probe_variants=0. A damaged cache is never mapped and is rebuilt by the third run; after the 8-process race the cache is mapped with exactly one cache file and no leftover temp or lock files.

Known limit (from #490): if the JVM itself rejects a flag at creation, the start fails with the usual ArcadeDBError and the lock is freed; there is no in-process retry because JPype cannot start a second JVM after a failed one. The flags are only added for a bundled JRE of version 25 or newer, so this is not reachable today. A process that never exits normally (os._exit, SIGKILL) trains nothing. Windows and macOS code paths (cache directory, pid check) are covered by unit tests with mocks only. Making it the default needs a new ab.py run on the release wheel.

Tests

tests/test_aot.py, 27 tests: key derivation, per-platform cache directory, JRE identity, promotion, damaged caches, stale locks, fallbacks (mocked), override path, off by default. Run on this branch's src overlaid on the unpacked linux/amd64 wheel 26.11.1.dev0 (the setup used for #27 and #29), together with the tests that exercise start_jvm(): tests/test_aot.py tests/test_jvm_args.py tests/test_log_dir.py gave 52 passed in 4.79 s. black 26.10.1, isort 9.0.2 (--profile black) and bandit (-c pyproject.toml --severity-level low --confidence-level low) are clean on the three files, with the # nosec B404/B603 comments kept.

Base

Based on #27 (port/runtime-fixes), not main: both change the start_jvm() signature, docstring and the jpype.startJVM call, and #27 serializes start_jvm(), so on main this hook would conflict. Merge #27 first; the diff then shows only the AOT change. #26 (Ctrl-C) also edits the jpype.startJVM region, so whichever of #26 and this lands second needs a small rebase.

Left out

The documentation part of #490 (a section in the JVM API page and a line in the architecture page): this repository has no counterpart page yet (the Antora component is #23). It can follow once #23 is merged. The code docstrings of start_jvm and aot.py describe the option.

🤖 Generated with Claude Code

)

Ports humemai/arcadedb-embedded-python#490. A per-user Project Leyden
cache of the classes the JVM loads and links at start; off by default,
every failure is a normal start. The docs part of #490 is left out until
the Antora component (#23) has a JVM API page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The only conflict was src/arcadedb_embedded/jvm.py, where this branch adds
_aot_flags() and runtime-fixes (through main's #26) adds
_restore_python_signal_handlers() after start_jvm(). Kept both functions; the
start path calls _aot_flags() before JPype starts and the signal restore after.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@tae898
tae898 changed the base branch from port/runtime-fixes to main October 24, 2026 20:15
@tae898
tae898 merged commit a2bc879 into main Oct 24, 2026
46 checks passed
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