Repository navigation
CrucibleVM 0.2.0
Pre-releaseCrucibleVM 0.2.0: open-source profile-guided optimization for GraalVM Community Edition native-image, on GraalVM CE 25.4.4.1.1 (JDK 25), Linux x86-64. The second downloadable build; the first was 0.1.0 on 2026-10-13.
Alpha. Measured, and checked for identical output, on a handful of benchmarks and two services; the builder's own unit tests now pass with a profile and in a recording image, but this is not yet the breadth of code a production compiler has to get right. Options and the profile format may change. Keep a build without a profile to compare against, and report anything that miscompiles.
Install
tar xzf cruciblevm-0.2.0-linux-x86_64.tar.gz
cruciblevm/crucible/pgo.sh --helpThe archive is a GraalVM JDK with native-image and the CrucibleVM scripts; it needs neither mx nor the source tree. native-image needs a C toolchain: gcc, glibc-devel and zlib-devel on Fedora or Rocky, gcc libc6-dev zlib1g-dev on Debian or Ubuntu; the profile scripts need python3. Built on Rocky Linux 10.
Build an optimized executable in one command
cruciblevm/crucible/pgo.sh --name app --run '$APP <your workload arguments>' -- -cp app.jar com.example.MainIt builds an image that records, runs your workload on it, builds an image that samples call stacks, runs the workload again, and builds ./app, the image to ship. The arguments after -- go to native-image as they are. For a service, --run is a script that starts the image, puts load on it and stops it with SIGTERM (build with --install-exit-handlers). --no-samples stops after the counted profile. The steps one at a time, merging the profiles of several runs, and converting an Oracle GraalVM .iprof are in the user guide.
New in 0.2.0
- Young collections on several threads, opt-in: build with
-H:+ParallelYoungCollection;-XX:ParallelYoungCollectionThreads=<n>sets the number at run time (default: the processors, up to 9). With a profile, akka-uct runs 18% faster, scala-stm-bench7 20%, reactors 12% and scrabble 8 to 11%, and the fourteen Renaissance benchmarks go from 0.86 to 0.83 of Oracle's profile-guided binary. It stays off by default: one akka-uct run hung on 2026-10-17 and the cause has not been found, though more than 2,000 runs since, on every benchmark and under a copy check, have not hung. Use it where a hang would be noticed and tell us if you see one. - Two run-time knobs for the memory the serial collector takes, both off by default.
-XX:SerialGCOldGrowthFactor=2collects completely once the old generation holds twice what the last complete collection kept; with-Xmsat the maximum heap the policy otherwise collects completely only when the heap is full, and Spring PetClinic under load then holds 311 MB; with the factor it holds 240 to 280 MB and answers 1% more requests (Oracle's binary holds 200).-XX:SerialGCMinDistance=50lets the young generation grow while young collections are up to 50 ms apart instead of 20, which suits a program that drops everything at intervals: rx-scrabble comes level with Oracle's binary, for more memory on some programs. Both are measured incrucible/README.md. - Correctness. GraalVM's own native-image unit-test gate passes with a profile and in a recording image, under the builder's assertions. That found five bugs in the instrumentation, all fixed: a probe placed where objects were being moved, counters in methods with their own calling convention clobbering argument registers, and three in how the recording nodes declare the memory they touch. The build is clean with javac warnings as errors, and a GitHub workflow builds, runs checkstyle and the sample's record-and-rebuild check on every push.
-XX:+VerboseGCprints which rule sized eden at each young collection.
How it compares
Same machine, each binary checked for identical output, time of the profile-guided binary from each compiler; lower is better. Oracle GraalVM 25.0.4, which has Oracle's PGO.
| workload | Oracle GraalVM | CrucibleVM | CrucibleVM, parallel young collection |
|---|---|---|---|
| Renaissance, fourteen benchmarks, geometric mean of the time | 1 | 0.86 | 0.83 |
| GameOfLife (Oracle's own PGO example) | 3.39 s | 2.91 s | |
| ArrayBench | 1.19 s | 0.31 s | |
| BranchBench | 0.55 s | 0.54 s | |
| JsonBench | 1.56 s | 1.39 s | |
| BenchPGO | 0.37 s | 0.32 s | |
Quarkus REST/JSON quickstart, requests a second on /fruits |
100,600 | 107,400 | |
Spring PetClinic, requests a second on /vets (JSON) |
36,800 | 47,200 |
Ahead on thirteen of the fourteen Renaissance benchmarks; behind on rx-scrabble by 2 to 3%, which is the collector's sizing under that harness rather than the compiler. Spring PetClinic, built by Spring's own native build and measured over 90 s of load, answers 31% more requests a second than Oracle's profile-guided binary, Quarkus 3% fewer; Spring's memory under load is above Oracle's (see the old-growth option above). Without a profile, at the default level, the fourteen run at 0.917 of Oracle's default binary. The measurements, the method and everything tried that did not work are in crucible/README.md, crucible/ROADMAP.md and docs/issues/.
Known limits
- Linux x86-64 only; nothing else has been built or tested.
- The parallel young collector is new, see above.
- The recording image is larger and slower than Oracle's: 100 to 140 MB where Oracle's is 50 to 65, and scrabble records at 3.2 s an iteration where Oracle's recording image takes 2.4.
- A type the workload used without locking and production locks heavily is locked slowly; record a workload that locks what production locks.
- akka-uct peaks in about 2 GB where Oracle's binary peaks in 0.9; give such a program an
-Xmxof about three times what it keeps alive.
Checksum
cf6821998e5f53e2dd85594a2875728ed2bc32873090a62553f38e011cea4351 cruciblevm-0.2.0-linux-x86_64.tar.gz