Add aot-cache module: the JDK 25 AOT cache on Spring Boot 4.1 vs AppCDS, Spring AOT and GraalVM native
A Spring Boot 4.1.1 order service started 12 ways (plain and extracted jar, Spring AOT output, AppCDS, AOT cache trained with and without traffic, JDK 27, native image), ten interleaved rounds each, with first-request latency; 24 cache-mismatch cases; Docker layer arithmetic. Transcripts are in output/ (no docs/ folder). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
@@ -37,8 +37,9 @@ files.
|
||||
| [`core-events/`](core-events) | [Spring Application Events: @EventListener, @TransactionalEventListener and Async Events](https://ankurm.com/spring-application-events-eventlistener-transactionaleventlistener-async/) | publishing and consuming events, listener ordering, conditional listeners, `AFTER_COMMIT` behaviour proven with a rollback, and async listeners |
|
||||
| [`multi-datasource/`](multi-datasource) | [Multiple DataSources in Spring Boot 4 with Spring Data JPA](https://ankurm.com/multiple-datasources-spring-boot-4-spring-data-jpa/) | two H2 databases each with its own `DataSource`, Flyway, `EntityManagerFactory` and transaction manager, six ways of getting the wiring wrong, plain vs named `@Transactional`, and `ChainedTransactionManager` under a failing commit |
|
||||
| [`i18n/`](i18n) | [Internationalization (i18n) in Spring Boot 4: MessageSource, LocaleResolver and Localized ProblemDetail](https://ankurm.com/spring-boot-4-internationalization-messagesource-localeresolver-problemdetail/) | English, Marathi and Hindi message bundles, `Accept-Language` vs a cookie resolver, apostrophes and Devanagari digits in `MessageFormat`, the JVM default locale as a hidden fallback, localized validation messages and `ProblemDetail` titles |
|
||||
| [`aot-cache/`](aot-cache) | [Faster Spring Boot Startup with the JDK AOT Cache (JEP 514/515): Benchmarks vs CDS and Native](https://ankurm.com/spring-boot-jdk-aot-cache-jep-514-515-startup-benchmarks/) | the JDK 25 AOT cache trained two ways, against a plain JVM, AppCDS, Spring's own AOT output and a GraalVM native image (ten interleaved rounds each), first-request latency, 24 cases where the cache silently stops applying (jar, timestamp, JVM build, ZGC, compact headers), `-XX:AOTMode=on`, Docker layer arithmetic |
|
||||
|
||||
`core-di`, `core-beans`, `custom-starter`, `core-events`, `multi-datasource` and `i18n` are the exception: they have **no `docs/` folder**. Their deeper material lives in
|
||||
`core-di`, `core-beans`, `custom-starter`, `core-events`, `multi-datasource`, `i18n` and `aot-cache` are the exception: they have **no `docs/` folder**. Their deeper material lives in
|
||||
collapsible sections inside the articles themselves, and their captured output sits in a top-level `output/`
|
||||
directory instead of `docs/output/`.
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
target/
|
||||
*.aot
|
||||
*.aotconf
|
||||
*.jsa
|
||||
work/
|
||||
work-run.log
|
||||
@@ -0,0 +1,70 @@
|
||||
# aot-cache: the JDK 25 AOT cache on a Spring Boot 4.1 service
|
||||
|
||||
Companion project for **[Faster Spring Boot Startup with the JDK AOT Cache (JEP 514/515): Benchmarks vs CDS and Native](https://ankurm.com/spring-boot-jdk-aot-cache-jep-514-515-startup-benchmarks/)**
|
||||
on [ankurm.com](https://ankurm.com). Everything the article quotes is a file in [`output/`](output), regenerated by
|
||||
[`run.sh`](run.sh). The explanation lives in the article; this directory holds the code and the raw transcripts.
|
||||
There is no `docs/` folder on purpose: the deeper material sits in collapsible sections of the article itself.
|
||||
|
||||
## The service
|
||||
|
||||
A small Spring Boot 4.1.1 web application, built to have the ingredients of a typical service and no more:
|
||||
Spring MVC and Jackson (`POST /orders` with a JSON body), Bean Validation (a record with `@Email`, `@Pattern`,
|
||||
`@Min`/`@Max`), the actuator health endpoint, and an in-memory store. No database. Sources are under
|
||||
[`src/main/java/com/ankurm/aotcache/`](src/main/java/com/ankurm/aotcache).
|
||||
|
||||
## Versions
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Spring Boot | 4.1.1 (Spring Framework 7.0.9, Tomcat 11.0.24, Jackson 3.1.5, Hibernate Validator 9.1.3) |
|
||||
| JDK for the article | Temurin 25.0.4.1 (LTS) |
|
||||
| JDK for the comparison | Temurin 27+35 |
|
||||
| GraalVM (native image) | Community Edition 25.0.2+10.1 |
|
||||
| Machine | 2 vCPU, 8 GB, Linux |
|
||||
|
||||
## What is here
|
||||
|
||||
| Script | What it does | Output |
|
||||
|---|---|---|
|
||||
| [`scripts/prepare.sh`](scripts/prepare.sh) | Builds the plain jar, a jar with Spring's own AOT output, a jar after a one-line change; extracts them | [`00-prepare.txt`](output/00-prepare.txt), [`00-environment.txt`](output/00-environment.txt) |
|
||||
| [`scripts/native.sh`](scripts/native.sh) | Compiles the GraalVM native image | [`03-native-build.txt`](output/03-native-build.txt) |
|
||||
| [`scripts/train.sh`](scripts/train.sh) | The training runs: AppCDS, AOT cache (context-only and with traffic, with and without Spring AOT, on JDK 27, under ZGC) | [`01-training.txt`](output/01-training.txt) |
|
||||
| [`scripts/startup.sh`](scripts/startup.sh) | 12 configurations x 10 interleaved rounds: time to ready, Boot's own figure, first request, resident memory | [`02-startup.txt`](output/02-startup.txt) |
|
||||
| [`scripts/mismatch.sh`](scripts/mismatch.sh) | 24 ways the cache and the run can disagree, and what the JVM says and does | [`04-mismatch.txt`](output/04-mismatch.txt) |
|
||||
| [`scripts/layers.sh`](scripts/layers.sh) | Docker layer arithmetic without Docker: which layers change on a one-line edit, sizes, gzipped sizes | [`05-layers.txt`](output/05-layers.txt) |
|
||||
| [`scripts/sample.sh`](scripts/sample.sh) | A transcript of the service's endpoints | [`06-sample-endpoints.txt`](output/06-sample-endpoints.txt) |
|
||||
| [`docker/Dockerfile`](docker/Dockerfile) | A layered image with the cache trained in the final stage (**not built here**: no Docker daemon) | |
|
||||
|
||||
## Headline result
|
||||
|
||||
Median of ten runs, milliseconds from starting the process to the first `200` from `GET /ping`
|
||||
([`output/02-startup.txt`](output/02-startup.txt)):
|
||||
|
||||
| Configuration | Ready | First `POST /orders` |
|
||||
|---|---|---|
|
||||
| Plain JVM, extracted jar | 3,920 | 173 |
|
||||
| AppCDS | 2,583 | 164 |
|
||||
| AOT cache, context-only training | 2,022 | 143 |
|
||||
| AOT cache, training with traffic | 1,949 | 68 |
|
||||
| AOT cache + Spring AOT, training with traffic | 1,436 | 89 |
|
||||
| GraalVM native image | 133 | 5.3 |
|
||||
|
||||
## Running it
|
||||
|
||||
Needs JDK 25, JDK 27, a GraalVM for JDK 25 and Maven; `scripts/env.sh` shows where they are looked up and each can be
|
||||
overridden with `JDK25`, `JDK27` and `GRAALVM`. The measurement scripts assume Linux and `curl`, and need the machine to
|
||||
themselves.
|
||||
|
||||
```console
|
||||
cd aot-cache
|
||||
./run.sh # about 35 minutes; the native build is 5 of them
|
||||
ROUNDS=3 ./run.sh # a quick pass
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
* One small service on one 2-CPU virtual machine. The ratios are what to take away, not the milliseconds.
|
||||
* No database and no JPA, so the service is smaller than most real ones; a larger classpath usually gains more from the cache.
|
||||
* JEP 515 (method profiles) is part of the cache on JDK 25 and cannot be switched off separately, so its own contribution was not isolated.
|
||||
* The Docker image was not built; the layer figures come from extracting the jar's layers and comparing files.
|
||||
* `openjdk.org/jeps` returned HTTP 403 to the tooling used, so JEP claims come from running the JDKs and from secondary sources named in the article.
|
||||
@@ -0,0 +1,21 @@
|
||||
# NOT BUILT OR RUN while writing the article: the sandbox has no Docker daemon. The commands inside it are the ones
|
||||
# scripts/train.sh and scripts/mismatch.sh ran (extract, AOTCacheOutput, AOTCache); the layer arithmetic is in
|
||||
# output/05-layers.txt. Treat the file as a starting point and measure it in your own registry.
|
||||
#
|
||||
# Build: mvn -DskipTests package && docker build -f docker/Dockerfile -t aot-cache .
|
||||
|
||||
FROM eclipse-temurin:25-jdk AS extract
|
||||
WORKDIR /build
|
||||
COPY target/app.jar app.jar
|
||||
RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted
|
||||
|
||||
# The final image is also where the cache is trained: the cache records the exact JVM build and the exact
|
||||
# jar files (size and modification time), so it must be created by the JVM that will use it, after the last COPY.
|
||||
FROM eclipse-temurin:25-jre
|
||||
WORKDIR /application
|
||||
COPY --from=extract /build/extracted/dependencies/ ./
|
||||
COPY --from=extract /build/extracted/spring-boot-loader/ ./
|
||||
COPY --from=extract /build/extracted/snapshot-dependencies/ ./
|
||||
COPY --from=extract /build/extracted/application/ ./
|
||||
RUN java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar app.jar
|
||||
ENTRYPOINT ["java", "-XX:AOTCache=app.aot", "-jar", "app.jar"]
|
||||
@@ -0,0 +1,24 @@
|
||||
# machine
|
||||
cpus: 2 memory: 8032 MB kernel: 6.18.44-fc-v37
|
||||
|
||||
# JDK 25 (the JVM the article is about)
|
||||
openjdk version "25.0.4.1" 2026-08-18 LTS
|
||||
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
|
||||
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
|
||||
|
||||
# JDK 27 (the comparison)
|
||||
openjdk version "27" 2026-09-15
|
||||
OpenJDK Runtime Environment Temurin-27+35 (build 27+35)
|
||||
OpenJDK 64-Bit Server VM Temurin-27+35 (build 27+35, mixed mode, sharing)
|
||||
|
||||
# GraalVM (native image)
|
||||
native-image 25.0.2 2026-01-20
|
||||
GraalVM Runtime Environment GraalVM CE 25.0.2+10.1 (build 25.0.2+10-jvmci-b01)
|
||||
|
||||
# libraries
|
||||
hibernate-validator-9.1.3.Final
|
||||
jackson-databind-3.1.5
|
||||
spring-boot-4.1.1
|
||||
spring-core-7.0.9
|
||||
spring-webmvc-7.0.9
|
||||
tomcat-embed-core-11.0.24
|
||||
@@ -0,0 +1,14 @@
|
||||
== build: plain jar (mvn package)
|
||||
== build: jar with Spring AOT output (mvn -Pnative package; this does NOT compile a native image)
|
||||
initializers in plain jar: 0
|
||||
initializers in Spring AOT jar: 1
|
||||
== extract both jars (java -Djarmode=tools -jar app.jar extract)
|
||||
/home/claude/spring-boot-demo/aot-cache/work/ext:
|
||||
app.jar
|
||||
lib
|
||||
|
||||
/home/claude/spring-boot-demo/aot-cache/work/ext-springaot:
|
||||
app.jar
|
||||
lib
|
||||
== build: the same service after a one-line change (the unit price in OrderService), for the mismatch and layer checks
|
||||
edited: 20: BigDecimal unit = BigDecimal.valueOf(5_99, 2);
|
||||
@@ -0,0 +1,17 @@
|
||||
# training runs (JDK 25.0.4.1 unless noted); wall time includes JVM start, context start and cache creation
|
||||
AppCDS (ArchiveClassesAtExit) 4741 ms appcds.jsa (38 MB)
|
||||
AOT cache, fat jar, context-only 8219 ms fat.aot (56 MB)
|
||||
AOT cache, context-only training 7916 ms refresh.aot (59 MB)
|
||||
AOT cache, traffic training 10157 ms traffic.aot (66 MB)
|
||||
AOT cache + Spring AOT, context-only 7164 ms springaot.aot (57 MB)
|
||||
AOT cache + Spring AOT, traffic 8733 ms springaot-traffic.aot (64 MB)
|
||||
AOT cache on JDK 27, context-only 9292 ms refresh27.aot (62 MB)
|
||||
AOT cache on JDK 27, trained under ZGC 9426 ms zgc27.aot (62 MB)
|
||||
|
||||
# what the JVM said at warning level while writing the context-only AOT cache (count, then the distinct kinds)
|
||||
warning lines: 122
|
||||
96 [warning][aot] Skipping <class>: Unsupported location
|
||||
4 [warning][aot] Skipping <class>: Unlinked class not supported by AOTClassLinking
|
||||
4 [warning][aot] Skipping <class>: Failed verification
|
||||
3 [warning][aot] Skipping <class>: JFR event class
|
||||
2 [warning][aot] Skipping <class>: Not in loaded state
|
||||
@@ -0,0 +1,138 @@
|
||||
# 12 configurations x 10 rounds; one unrecorded warm-up round first (so cache files are in the page cache)
|
||||
|
||||
== medians of 10 runs (min..max in brackets), milliseconds; rss in MB
|
||||
configuration ready (exec->200) Boot 'Started in' 1st POST /orders next 50 (median) RSS
|
||||
1 jvm-fat-jar 4630.5 [4281..4931] 3683.5 [3455..3954] 204.0 [166.6..230.7] 4.5 194.0
|
||||
2 jvm-extracted 3919.5 [3486..4018] 3242.0 [2890..3400] 173.3 [146.6..191.7] 4.8 189.0
|
||||
3 jvm+spring-aot 3162.0 [2991..3500] 2552.0 [2419..2907] 201.8 [165.3..254.3] 4.5 183.0
|
||||
4 appcds 2582.5 [2436..2730] 2181.0 [2022..2330] 164.3 [133.9..170.2] 4.8 195.0
|
||||
5 aot-cache fat-jar 3247.0 [3096..3507] 2553.5 [2432..2787] 156.6 [144.9..186.6] 4.7 235.0
|
||||
6 aot-cache context-only 2021.5 [1948..2253] 1568.0 [1495..1785] 142.8 [116.1..160.0] 4.8 218.0
|
||||
7 aot-cache traffic 1948.5 [1777..2142] 1623.5 [1406..1775] 67.7 [47.5..101.8] 4.5 198.5
|
||||
8 aot-cache+spring-aot 1588.5 [1482..1674] 1120.5 [1030..1218] 146.6 [131.4..163.9] 4.5 188.0
|
||||
9 aot-cache+spring-aot traffic 1436.0 [1372..1651] 1069.0 [1015..1280] 89.3 [65.9..113.0] 4.2 184.0
|
||||
10 jdk27 plain 3779.5 [3595..4205] 3122.5 [2919..3408] 164.8 [139.9..176.8] 4.6 234.0
|
||||
11 jdk27 aot-cache 1983.0 [1840..2181] 1571.5 [1444..1763] 138.2 [117.9..191.0] 4.2 195.5
|
||||
12 native image 133.0 [127..171] 101.5 [90..133] 5.3 [4.6..6.0] 1.0 120.0
|
||||
|
||||
== every run
|
||||
1 jvm-fat-jar 1 ready=4407 started=3460 first=201.9 second=6.4 next50=3.2 rss=186
|
||||
2 jvm-extracted 1 ready=3486 started=2890 first=187.6 second=7.4 next50=4.0 rss=189
|
||||
3 jvm+spring-aot 1 ready=3111 started=2495 first=203.0 second=8.9 next50=4.5 rss=189
|
||||
4 appcds 1 ready=2535 started=2133 first=166.6 second=12.8 next50=4.8 rss=190
|
||||
5 aot-cache fat-jar 1 ready=3096 started=2432 first=157.2 second=11.4 next50=4.3 rss=234
|
||||
6 aot-cache context-only 1 ready=1948 started=1555 first=133.0 second=6.0 next50=3.4 rss=211
|
||||
7 aot-cache traffic 1 ready=1984 started=1642 first=47.5 second=5.5 next50=4.5 rss=200
|
||||
8 aot-cache+spring-aot 1 ready=1482 started=1030 first=153.1 second=7.2 next50=4.4 rss=189
|
||||
9 aot-cache+spring-aot traffic 1 ready=1372 started=1057 first=73.2 second=11.0 next50=3.5 rss=184
|
||||
10 jdk27 plain 1 ready=3595 started=2919 first=165.6 second=13.0 next50=4.5 rss=224
|
||||
11 jdk27 aot-cache 1 ready=1890 started=1517 first=135.2 second=7.1 next50=3.4 rss=195
|
||||
12 native image 1 ready=127 started=90 first=4.6 second=0.9 next50=0.7 rss=120
|
||||
1 jvm-fat-jar 2 ready=4495 started=3692 first=166.6 second=20.1 next50=4.2 rss=195
|
||||
2 jvm-extracted 2 ready=3725 started=3117 first=173.6 second=7.5 next50=4.1 rss=191
|
||||
3 jvm+spring-aot 2 ready=3121 started=2522 first=199.1 second=10.0 next50=4.1 rss=188
|
||||
4 appcds 2 ready=2496 started=2104 first=166.9 second=8.3 next50=4.2 rss=200
|
||||
5 aot-cache fat-jar 2 ready=3250 started=2579 first=186.6 second=17.7 next50=4.6 rss=239
|
||||
6 aot-cache context-only 2 ready=2029 started=1568 first=140.9 second=16.2 next50=3.5 rss=218
|
||||
7 aot-cache traffic 2 ready=1840 started=1506 first=76.5 second=9.3 next50=3.4 rss=200
|
||||
8 aot-cache+spring-aot 2 ready=1550 started=1070 first=131.4 second=14.7 next50=5.0 rss=188
|
||||
9 aot-cache+spring-aot traffic 2 ready=1444 started=1070 first=78.5 second=5.8 next50=4.2 rss=185
|
||||
10 jdk27 plain 2 ready=3767 started=3121 first=164.1 second=8.4 next50=3.9 rss=219
|
||||
11 jdk27 aot-cache 2 ready=1844 started=1444 first=124.8 second=6.0 next50=4.2 rss=196
|
||||
12 native image 2 ready=171 started=133 first=5.6 second=1.4 next50=1.0 rss=120
|
||||
1 jvm-fat-jar 3 ready=4281 started=3455 first=188.9 second=7.0 next50=4.1 rss=187
|
||||
2 jvm-extracted 3 ready=3944 started=3315 first=163.8 second=8.1 next50=4.0 rss=191
|
||||
3 jvm+spring-aot 3 ready=2991 started=2419 first=254.3 second=9.6 next50=4.4 rss=177
|
||||
4 appcds 3 ready=2436 started=2049 first=170.2 second=7.7 next50=4.5 rss=198
|
||||
5 aot-cache fat-jar 3 ready=3127 started=2478 first=156.1 second=8.6 next50=4.4 rss=231
|
||||
6 aot-cache context-only 3 ready=2020 started=1601 first=149.9 second=12.2 next50=4.7 rss=219
|
||||
7 aot-cache traffic 3 ready=1777 started=1406 first=65.2 second=6.2 next50=4.2 rss=200
|
||||
8 aot-cache+spring-aot 3 ready=1547 started=1066 first=134.8 second=6.7 next50=5.5 rss=190
|
||||
9 aot-cache+spring-aot traffic 3 ready=1383 started=1015 first=84.9 second=6.3 next50=4.7 rss=184
|
||||
10 jdk27 plain 3 ready=3704 started=3067 first=139.9 second=7.2 next50=4.4 rss=237
|
||||
11 jdk27 aot-cache 3 ready=1840 started=1494 first=125.0 second=9.8 next50=4.1 rss=196
|
||||
12 native image 3 ready=157 started=131 first=5.6 second=1.2 next50=0.9 rss=120
|
||||
1 jvm-fat-jar 4 ready=4665 started=3675 first=190.9 second=7.0 next50=4.6 rss=196
|
||||
2 jvm-extracted 4 ready=3837 started=3188 first=191.7 second=7.7 next50=5.2 rss=190
|
||||
3 jvm+spring-aot 4 ready=3205 started=2568 first=165.3 second=12.6 next50=4.0 rss=177
|
||||
4 appcds 4 ready=2450 started=2022 first=168.7 second=11.6 next50=3.7 rss=193
|
||||
5 aot-cache fat-jar 4 ready=3244 started=2644 first=151.2 second=7.2 next50=4.9 rss=229
|
||||
6 aot-cache context-only 4 ready=2012 started=1495 first=126.3 second=11.3 next50=4.0 rss=205
|
||||
7 aot-cache traffic 4 ready=1899 started=1537 first=70.1 second=12.4 next50=5.5 rss=186
|
||||
8 aot-cache+spring-aot 4 ready=1644 started=1207 first=163.0 second=13.2 next50=4.5 rss=188
|
||||
9 aot-cache+spring-aot traffic 4 ready=1472 started=1144 first=65.9 second=10.6 next50=4.2 rss=184
|
||||
10 jdk27 plain 4 ready=3712 started=3060 first=176.8 second=7.3 next50=4.7 rss=236
|
||||
11 jdk27 aot-cache 4 ready=1988 started=1608 first=147.4 second=10.8 next50=4.0 rss=195
|
||||
12 native image 4 ready=127 started=94 first=5.0 second=1.1 next50=1.0 rss=120
|
||||
1 jvm-fat-jar 5 ready=4668 started=3654 first=217.1 second=7.7 next50=4.5 rss=203
|
||||
2 jvm-extracted 5 ready=4018 started=3400 first=180.4 second=10.5 next50=5.2 rss=196
|
||||
3 jvm+spring-aot 5 ready=3500 started=2907 first=177.4 second=12.6 next50=4.5 rss=186
|
||||
4 appcds 5 ready=2690 started=2267 first=133.9 second=9.2 next50=4.5 rss=197
|
||||
5 aot-cache fat-jar 5 ready=3169 started=2456 first=174.6 second=8.8 next50=5.1 rss=234
|
||||
6 aot-cache context-only 5 ready=2253 started=1785 first=144.7 second=9.3 next50=5.8 rss=218
|
||||
7 aot-cache traffic 5 ready=1939 started=1622 first=101.8 second=7.7 next50=4.5 rss=194
|
||||
8 aot-cache+spring-aot 5 ready=1591 started=1143 first=142.9 second=10.1 next50=4.3 rss=188
|
||||
9 aot-cache+spring-aot traffic 5 ready=1461 started=1126 first=96.8 second=6.1 next50=4.0 rss=184
|
||||
10 jdk27 plain 5 ready=3709 started=3007 first=174.4 second=8.6 next50=5.0 rss=230
|
||||
11 jdk27 aot-cache 5 ready=1987 started=1589 first=191.0 second=6.7 next50=4.5 rss=195
|
||||
12 native image 5 ready=136 started=96 first=6.0 second=1.0 next50=1.0 rss=119
|
||||
1 jvm-fat-jar 6 ready=4524 started=3575 first=196.3 second=8.5 next50=4.4 rss=195
|
||||
2 jvm-extracted 6 ready=3986 started=3300 first=150.2 second=10.1 next50=4.8 rss=176
|
||||
3 jvm+spring-aot 6 ready=3108 started=2560 first=217.2 second=13.3 next50=4.3 rss=177
|
||||
4 appcds 6 ready=2730 started=2330 first=152.3 second=7.5 next50=5.0 rss=197
|
||||
5 aot-cache fat-jar 6 ready=3399 started=2698 first=154.3 second=9.4 next50=5.2 rss=238
|
||||
6 aot-cache context-only 6 ready=2211 started=1697 first=151.7 second=14.4 next50=5.7 rss=218
|
||||
7 aot-cache traffic 6 ready=1872 started=1542 first=61.0 second=8.4 next50=5.2 rss=198
|
||||
8 aot-cache+spring-aot 6 ready=1624 started=1150 first=135.1 second=12.4 next50=5.7 rss=188
|
||||
9 aot-cache+spring-aot traffic 6 ready=1380 started=1051 first=107.5 second=11.7 next50=4.8 rss=185
|
||||
10 jdk27 plain 6 ready=4103 started=3408 first=168.7 second=7.7 next50=5.1 rss=249
|
||||
11 jdk27 aot-cache 6 ready=1961 started=1570 first=141.3 second=13.2 next50=5.0 rss=197
|
||||
12 native image 6 ready=131 started=105 first=5.0 second=2.7 next50=1.2 rss=119
|
||||
1 jvm-fat-jar 7 ready=4787 started=3913 first=224.4 second=10.0 next50=4.7 rss=197
|
||||
2 jvm-extracted 7 ready=3899 started=3237 first=165.2 second=8.8 next50=5.4 rss=189
|
||||
3 jvm+spring-aot 7 ready=3294 started=2623 first=172.8 second=8.7 next50=4.8 rss=184
|
||||
4 appcds 7 ready=2592 started=2172 first=162.1 second=9.6 next50=4.8 rss=205
|
||||
5 aot-cache fat-jar 7 ready=3347 started=2528 first=176.4 second=7.8 next50=4.8 rss=236
|
||||
6 aot-cache context-only 7 ready=1989 started=1567 first=116.1 second=7.3 next50=5.5 rss=218
|
||||
7 aot-cache traffic 7 ready=2111 started=1771 first=100.5 second=13.5 next50=4.5 rss=200
|
||||
8 aot-cache+spring-aot 7 ready=1597 started=1147 first=152.7 second=10.4 next50=3.9 rss=188
|
||||
9 aot-cache+spring-aot traffic 7 ready=1651 started=1280 first=85.4 second=9.1 next50=5.2 rss=185
|
||||
10 jdk27 plain 7 ready=3825 started=3124 first=166.7 second=8.9 next50=4.5 rss=240
|
||||
11 jdk27 aot-cache 7 ready=2033 started=1690 first=153.9 second=7.3 next50=5.5 rss=195
|
||||
12 native image 7 ready=132 started=100 first=4.9 second=1.3 next50=1.2 rss=120
|
||||
1 jvm-fat-jar 8 ready=4608 started=3729 first=211.0 second=8.0 next50=5.0 rss=192
|
||||
2 jvm-extracted 8 ready=4008 started=3345 first=180.4 second=8.2 next50=4.5 rss=181
|
||||
3 jvm+spring-aot 8 ready=3095 started=2479 first=214.1 second=8.6 next50=5.0 rss=177
|
||||
4 appcds 8 ready=2573 started=2199 first=161.4 second=8.6 next50=5.0 rss=192
|
||||
5 aot-cache fat-jar 8 ready=3177 started=2462 first=152.9 second=7.7 next50=4.6 rss=230
|
||||
6 aot-cache context-only 8 ready=1981 started=1528 first=160.0 second=37.6 next50=5.0 rss=218
|
||||
7 aot-cache traffic 8 ready=2142 started=1775 first=62.3 second=8.7 next50=4.1 rss=187
|
||||
8 aot-cache+spring-aot 8 ready=1586 started=1098 first=163.9 second=12.0 next50=5.3 rss=188
|
||||
9 aot-cache+spring-aot traffic 8 ready=1412 started=1047 first=93.3 second=9.0 next50=5.0 rss=184
|
||||
10 jdk27 plain 8 ready=3792 started=3125 first=152.4 second=11.5 next50=5.1 rss=235
|
||||
11 jdk27 aot-cache 8 ready=1979 started=1529 first=122.5 second=6.7 next50=3.8 rss=195
|
||||
12 native image 8 ready=166 started=117 first=5.3 second=1.5 next50=1.1 rss=119
|
||||
1 jvm-fat-jar 9 ready=4653 started=3760 first=230.7 second=10.4 next50=4.2 rss=193
|
||||
2 jvm-extracted 9 ready=3728 started=3060 first=173.0 second=8.9 next50=4.8 rss=182
|
||||
3 jvm+spring-aot 9 ready=3266 started=2544 first=200.6 second=10.7 next50=5.0 rss=182
|
||||
4 appcds 9 ready=2596 started=2244 first=136.5 second=7.9 next50=5.2 rss=192
|
||||
5 aot-cache fat-jar 9 ready=3507 started=2787 first=162.2 second=8.0 next50=4.8 rss=242
|
||||
6 aot-cache context-only 9 ready=2152 started=1720 first=155.7 second=12.1 next50=4.8 rss=219
|
||||
7 aot-cache traffic 9 ready=2088 started=1731 first=58.5 second=11.4 next50=4.0 rss=198
|
||||
8 aot-cache+spring-aot 9 ready=1674 started=1218 first=137.0 second=11.3 next50=4.2 rss=188
|
||||
9 aot-cache+spring-aot traffic 9 ready=1428 started=1068 first=113.0 second=10.8 next50=4.1 rss=184
|
||||
10 jdk27 plain 9 ready=4205 started=3396 first=157.9 second=10.0 next50=4.7 rss=224
|
||||
11 jdk27 aot-cache 9 ready=2181 started=1763 first=117.9 second=6.7 next50=4.0 rss=196
|
||||
12 native image 9 ready=133 started=98 first=5.4 second=1.4 next50=1.1 rss=120
|
||||
1 jvm-fat-jar 10 ready=4931 started=3954 first=206.1 second=8.8 next50=4.8 rss=193
|
||||
2 jvm-extracted 10 ready=3940 started=3247 first=146.6 second=12.8 next50=4.8 rss=185
|
||||
3 jvm+spring-aot 10 ready=3203 started=2587 first=206.7 second=8.9 next50=4.6 rss=187
|
||||
4 appcds 10 ready=2607 started=2190 first=169.4 second=7.8 next50=5.2 rss=193
|
||||
5 aot-cache fat-jar 10 ready=3495 started=2699 first=144.9 second=8.1 next50=3.7 rss=241
|
||||
6 aot-cache context-only 10 ready=2023 started=1568 first=134.5 second=32.5 next50=4.5 rss=218
|
||||
7 aot-cache traffic 10 ready=1958 started=1625 first=74.3 second=6.1 next50=5.1 rss=199
|
||||
8 aot-cache+spring-aot 10 ready=1556 started=1095 first=150.2 second=9.2 next50=4.3 rss=188
|
||||
9 aot-cache+spring-aot traffic 10 ready=1575 started=1213 first=107.0 second=6.5 next50=4.0 rss=184
|
||||
10 jdk27 plain 10 ready=4010 started=3356 first=156.0 second=7.2 next50=4.4 rss=233
|
||||
11 jdk27 aot-cache 10 ready=2016 started=1573 first=171.6 second=7.8 next50=4.5 rss=196
|
||||
12 native image 10 ready=133 started=103 first=5.4 second=1.2 next50=1.0 rss=120
|
||||
@@ -0,0 +1,9 @@
|
||||
# GraalVM: native-image 25.0.2 2026-01-20
|
||||
21,929 types, 31,331 fields, and 99,355 methods found reachable
|
||||
36.0s (11.7% of total time) in 2209 GCs | Peak RSS: 4.70GB | CPU load: 1.86
|
||||
Finished generating 'aot-cache' in 5m 6s.
|
||||
BUILD SUCCESS
|
||||
Total time: 05:18 min
|
||||
/home/claude/spring-boot-demo/aot-cache/work/aot-cache-native 99 MB
|
||||
/home/claude/spring-boot-demo/aot-cache/work/app.jar 22 MB
|
||||
build wall time: 320 s
|
||||
@@ -0,0 +1,152 @@
|
||||
# cache: /home/claude/spring-boot-demo/aot-cache/work/refresh.aot, trained on JDK 25.0.4.1, extracted layout, default GC (G1), 2 CPUs
|
||||
|
||||
== control: the same JVM, the same jar, the same path
|
||||
result: started, ready after 1987 ms (Started AotCacheApplication in 1.538 seconds)
|
||||
log: (nothing at warning or error level)
|
||||
|
||||
== the application jar was rebuilt after a one-line code change, default AOTMode=auto
|
||||
result: started, ready after 4124 ms (Started AotCacheApplication in 3.453 seconds)
|
||||
log: [warning][aot] This file is not the one used while building the AOT cache: 'app.jar', timestamp has changed, size has changed
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||
log: [error ][aot] Unable to map shared spaces
|
||||
|
||||
== the same, with -XX:AOTMode=on
|
||||
result: did not start (exit code 1)
|
||||
log: [warning][aot] This file is not the one used while building the AOT cache: 'app.jar', timestamp has changed, size has changed
|
||||
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] unrecoverable error
|
||||
|
||||
== a dependency jar has a different modification time (same bytes)
|
||||
result: started, ready after 3865 ms (Started AotCacheApplication in 3.201 seconds)
|
||||
log: [warning][aot] This file is not the one used while building the AOT cache: 'lib/spring-core-7.0.9.jar', timestamp has changed
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||
log: [error ][aot] Unable to map shared spaces
|
||||
|
||||
== the same files copied with plain cp (bytes and path layout identical, timestamps new)
|
||||
result: started, ready after 4013 ms (Started AotCacheApplication in 3.361 seconds)
|
||||
log: [warning][aot] This file is not the one used while building the AOT cache: 'app.jar', timestamp has changed
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||
log: [error ][aot] Unable to map shared spaces
|
||||
|
||||
== the application directory was moved after training (same bytes, different path)
|
||||
result: started, ready after 2005 ms (Started AotCacheApplication in 1.542 seconds)
|
||||
log: (nothing at warning or error level)
|
||||
|
||||
== cache trained on the fat jar, run from the extracted layout
|
||||
result: started, ready after 3900 ms (Started AotCacheApplication in 3.262 seconds)
|
||||
log: [warning][aot] This file is not the one used while building the AOT cache: 'app.jar', size has changed
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||
log: [error ][aot] Unable to map shared spaces
|
||||
|
||||
== JDK 25's cache used by JDK 27
|
||||
result: started, ready after 4221 ms (Started AotCacheApplication in 3.537 seconds)
|
||||
log: [warning][aot] The AOT cache version 0x13 does not match the required version 0x14.
|
||||
log: [warning][aot] The AOT cache has the wrong version.
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] Loading static archive failed.
|
||||
log: [warning][aot] The AOT cache version 0x13 does not match the required version 0x14.
|
||||
|
||||
== the same, with -XX:AOTMode=on
|
||||
result: did not start (exit code 1)
|
||||
log: [warning][aot] The AOT cache version 0x13 does not match the required version 0x14.
|
||||
log: [warning][aot] The AOT cache has the wrong version.
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] Loading static archive failed.
|
||||
log: [warning][aot] The AOT cache version 0x13 does not match the required version 0x14.
|
||||
|
||||
== -XX:+UseCompactObjectHeaders added at run time only (JDK 25)
|
||||
result: started, ready after 4138 ms (Started AotCacheApplication in 3.446 seconds)
|
||||
log: [warning][aot] Unable to use AOT cache.
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] Loading static archive failed.
|
||||
log: [error ][aot] Unable to map shared spaces
|
||||
|
||||
== a different collector at run time: -XX:+UseSerialGC
|
||||
result: started, ready after 1916 ms (Started AotCacheApplication in 1.516 seconds)
|
||||
log: (nothing at warning or error level)
|
||||
|
||||
== a different collector at run time: -XX:+UseParallelGC
|
||||
result: started, ready after 1957 ms (Started AotCacheApplication in 1.473 seconds)
|
||||
log: (nothing at warning or error level)
|
||||
|
||||
== a different collector at run time: -XX:+UseZGC
|
||||
result: started, ready after 3981 ms (Started AotCacheApplication in 3.347 seconds)
|
||||
log: [warning][aot] Unable to use AOT cache.
|
||||
[ ] The saved state of UseCompressedOops and UseCompressedClassPointers is different from runtime, CDS will be disabled.
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] Loading static archive failed.
|
||||
log: [error ][aot] Unable to map shared spaces
|
||||
|
||||
== a different heap size at run time: -Xmx256m
|
||||
result: started, ready after 2058 ms (Started AotCacheApplication in 1.602 seconds)
|
||||
log: (nothing at warning or error level)
|
||||
|
||||
== a cache file that does not exist
|
||||
result: started, ready after 4113 ms (Started AotCacheApplication in 3.451 seconds)
|
||||
log: [error][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error][aot] Loading static archive failed.
|
||||
log: [error][aot] Unable to map shared spaces
|
||||
|
||||
== a cache file that does not exist, with -XX:AOTMode=on
|
||||
result: did not start (exit code 1)
|
||||
log: [error][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error][aot] Loading static archive failed.
|
||||
log: [error][aot] Unable to map shared spaces
|
||||
|
||||
# and on JDK 27, whose cache was trained under the default collector (G1):
|
||||
|
||||
== JDK 27, cache from JDK 27 (control)
|
||||
result: started, ready after 1955 ms (Started AotCacheApplication in 1.547 seconds)
|
||||
log: (nothing at warning or error level)
|
||||
|
||||
== JDK 27, -XX:+UseZGC at run time
|
||||
result: started, ready after 4158 ms (Started AotCacheApplication in 3.502 seconds)
|
||||
log: [warning][aot] Unable to use AOT cache.
|
||||
[ ] The saved state of UseCompressedOops (1) is different from runtime (0), CDS will be disabled.
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] Loading static archive failed.
|
||||
log: [error ][aot] Unable to map shared spaces
|
||||
|
||||
== JDK 27, -XX:+UseSerialGC at run time
|
||||
result: started, ready after 2103 ms (Started AotCacheApplication in 1.672 seconds)
|
||||
log: (nothing at warning or error level)
|
||||
|
||||
== JDK 27, -XX:-UseCompactObjectHeaders at run time
|
||||
result: started, ready after 3906 ms (Started AotCacheApplication in 3.163 seconds)
|
||||
log: [warning][aot] Unable to use AOT cache.
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] Loading static archive failed.
|
||||
log: [warning][aot] Unable to use AOT cache.
|
||||
log: [error ][aot] Loading static archive failed.
|
||||
|
||||
# why ZGC is different: ask the JVM (-Xlog:aot=info) why it refused the G1-trained cache
|
||||
|
||||
== JDK 27, -XX:+UseZGC with the G1-trained cache, -Xlog:aot=info
|
||||
result: started, ready after 3885 ms (Started AotCacheApplication in 3.258 seconds)
|
||||
log: [info][aot] The AOT cache was created with UseCompressedOops = 1, UseCompactObjectHeaders = 1
|
||||
log: [warning][aot] Unable to use AOT cache.
|
||||
[ ] The saved state of UseCompressedOops (1) is different from runtime (0), CDS will be disabled.
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] Loading static archive failed.
|
||||
|
||||
== JDK 27, cache trained under ZGC, run under ZGC
|
||||
result: started, ready after 1988 ms (Started AotCacheApplication in 1.554 seconds)
|
||||
log: (nothing at warning or error level)
|
||||
|
||||
== JDK 27, cache trained under ZGC, run under G1 (compressed oops on again)
|
||||
result: started, ready after 3869 ms (Started AotCacheApplication in 3.199 seconds)
|
||||
log: [warning][aot] Unable to use AOT cache.
|
||||
[ ] The saved state of UseCompressedOops (0) is different from runtime (1), CDS will be disabled.
|
||||
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||
log: [error ][aot] Loading static archive failed.
|
||||
log: [warning][aot] Unable to use AOT cache.
|
||||
|
||||
== JDK 27, cache trained under ZGC, run under G1 with -XX:-UseCompressedOops
|
||||
result: started, ready after 2018 ms (Started AotCacheApplication in 1.534 seconds)
|
||||
log: (nothing at warning or error level)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# java -Djarmode=tools -jar app.jar extract --layers (the layer order Spring Boot's Dockerfile support uses)
|
||||
layer KB gzip KB files
|
||||
dependencies 22878 20490 47
|
||||
spring-boot-loader 0 0 0
|
||||
snapshot-dependencies 0 0 0
|
||||
application 8 7 1
|
||||
|
||||
# the AOT cache from the training run (context-only), for scale
|
||||
refresh.aot 61016 15362
|
||||
|
||||
# change one line of application code (the unit price in OrderService, built by prepare.sh), extract again
|
||||
layer before after verdict
|
||||
dependencies 81a2cb4f9f5c 81a2cb4f9f5c identical: reused from cache/registry
|
||||
spring-boot-loader abcfa6a9d4df abcfa6a9d4df identical: reused from cache/registry
|
||||
snapshot-dependencies abcfa6a9d4df abcfa6a9d4df identical: reused from cache/registry
|
||||
application 4ecdea8230c6 19563974ba7b CHANGED: rebuilt and pushed
|
||||
AOT cache: trained against the previous jar, so it is stale for the new one (see output/04-mismatch.txt); it must be regenerated after the application layer
|
||||
@@ -0,0 +1,12 @@
|
||||
$ curl localhost:18110/ping
|
||||
ok [HTTP 200]
|
||||
$ curl -H 'Content-Type: application/json' -d '{"customer":"[email protected]","sku":"ABC-1234","quantity":3}' localhost:18110/orders
|
||||
{"id":1,"customer":"[email protected]","sku":"ABC-1234","quantity":3,"total":14.97,"createdAt":"2026-09-24T16:35:45.259486118Z"} [HTTP 201]
|
||||
$ curl localhost:18110/orders/1
|
||||
{"id":1,"customer":"[email protected]","sku":"ABC-1234","quantity":3,"total":14.97,"createdAt":"2026-09-24T16:35:45.259486118Z"} [HTTP 200]
|
||||
$ curl -H 'Content-Type: application/json' -d '{"customer":"bad","sku":"x","quantity":0}' localhost:18110/orders
|
||||
{"timestamp":"2026-09-24T16:35:45.351Z","status":400,"error":"Bad Request","path":"/orders"} [HTTP 400]
|
||||
$ curl localhost:18110/orders/99
|
||||
[HTTP 404]
|
||||
$ curl localhost:18110/actuator/health
|
||||
{"groups":["liveness","readiness"],"status":"UP"} [HTTP 200]
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>aot-cache</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>aot-cache</name>
|
||||
<description>The JDK 25 AOT cache (JEP 514/515) on a Spring Boot 4.1 service, measured against a plain JVM, AppCDS and a GraalVM native image</description>
|
||||
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>app</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates everything under output/. Takes about 30 minutes; the native build alone is about 5.
|
||||
# Needs JDK 25, JDK 27, a GraalVM for JDK 25 and Maven (paths: scripts/env.sh). Use the machine for nothing else while it runs.
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
source scripts/env.sh
|
||||
ROUNDS=${ROUNDS:-10}
|
||||
mkdir -p output
|
||||
bash scripts/prepare.sh > output/00-prepare.txt 2>&1
|
||||
bash scripts/environment.sh > output/00-environment.txt 2>&1
|
||||
bash scripts/native.sh > output/03-native-build.txt 2>&1
|
||||
bash scripts/train.sh > output/01-training.txt 2>&1
|
||||
bash scripts/startup.sh "$ROUNDS" > output/02-startup.txt 2> "$WORK/startup.err"
|
||||
bash scripts/mismatch.sh > output/04-mismatch.txt 2>&1
|
||||
bash scripts/layers.sh > output/05-layers.txt 2>&1
|
||||
bash scripts/sample.sh > output/06-sample-endpoints.txt 2>&1
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Where the JDKs and GraalVM live. Override with environment variables; nothing else is hard-coded.
|
||||
: "${JDK25:=/tmp/tools/j25/jdk-25.0.4.1+1}"
|
||||
: "${JDK27:=/tmp/tools/j27/jdk-27+35}"
|
||||
: "${GRAALVM:=$(ls -d /tmp/tools/graal/graalvm-community* 2>/dev/null | head -1)}"
|
||||
export JDK25 JDK27 GRAALVM
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORK="$ROOT/work" # generated jars, caches and logs (git-ignored)
|
||||
OUT="$ROOT/output" # transcripts that the article quotes (committed)
|
||||
export ROOT WORK OUT
|
||||
# Maven may need proxy/trust-store options that must not leak into the JVM runs being measured.
|
||||
MVN_JTO="${JAVA_TOOL_OPTIONS:-}"
|
||||
unset JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS
|
||||
mvn_run() { JAVA_TOOL_OPTIONS="$MVN_JTO" "$@"; }
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/env.sh"
|
||||
echo "# machine"; echo "cpus: $(nproc) memory: $(free -m | awk '/Mem:/ {print $2}') MB kernel: $(uname -r)"
|
||||
echo; echo "# JDK 25 (the JVM the article is about)"; "$JDK25/bin/java" -version 2>&1
|
||||
echo; echo "# JDK 27 (the comparison)"; "$JDK27/bin/java" -version 2>&1
|
||||
echo; echo "# GraalVM (native image)"; "$GRAALVM/bin/native-image" --version 2>&1 | head -2
|
||||
echo; echo "# libraries"; ls "$WORK/ext/lib" | grep -E '^(spring-boot|spring-core|spring-webmvc|tomcat-embed-core|jackson-databind|hibernate-validator)-[0-9]' | sed 's/\.jar$//'
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Docker layer arithmetic without a Docker daemon: which layers of the extracted application change when one line of
|
||||
# code changes, how big each layer is (uncompressed, and gzipped as a registry would store it), and where the AOT
|
||||
# cache falls. A layer is "a directory tree"; two builds share a layer when the tree is byte-identical.
|
||||
source "$(dirname "$0")/env.sh"
|
||||
cd "$ROOT"
|
||||
|
||||
layer_sum() { # dir -> one hash over every file's path and contents
|
||||
( cd "$1" && find . -type f | LC_ALL=C sort | xargs sha256sum | sha256sum | cut -c1-12 )
|
||||
}
|
||||
gz_kb() { tar -C "$1" -cf - . 2>/dev/null | gzip -6 | wc -c | awk '{printf "%d", $1/1024}'; }
|
||||
kb() { find "$1" -type f -printf '%s\n' | awk '{s+=$1} END{printf "%d", s/1024}'; }
|
||||
|
||||
echo "# java -Djarmode=tools -jar app.jar extract --layers (the layer order Spring Boot's Dockerfile support uses)"
|
||||
rm -rf "$WORK/layers-a" "$WORK/layers-b"
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app.jar" extract --layers --destination "$WORK/layers-a" >/dev/null
|
||||
printf '%-22s %10s %10s %s\n' layer "KB" "gzip KB" "files"
|
||||
for d in dependencies spring-boot-loader snapshot-dependencies application; do
|
||||
printf '%-22s %10s %10s %s\n' "$d" "$(kb "$WORK/layers-a/$d")" "$(gz_kb "$WORK/layers-a/$d")" "$(find "$WORK/layers-a/$d" -type f | wc -l)"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "# the AOT cache from the training run (context-only), for scale"
|
||||
printf '%-22s %10s %10s\n' "refresh.aot" "$(( $(stat -c %s "$WORK/refresh.aot") / 1024 ))" "$(gzip -6 -c "$WORK/refresh.aot" | wc -c | awk '{printf "%d", $1/1024}')"
|
||||
|
||||
echo
|
||||
echo "# change one line of application code (the unit price in OrderService, built by prepare.sh), extract again"
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-changed.jar" extract --layers --destination "$WORK/layers-b" >/dev/null
|
||||
printf '%-22s %-14s %-14s %s\n' layer "before" "after" "verdict"
|
||||
for d in dependencies spring-boot-loader snapshot-dependencies application; do
|
||||
a=$(layer_sum "$WORK/layers-a/$d"); b=$(layer_sum "$WORK/layers-b/$d")
|
||||
printf '%-22s %-14s %-14s %s\n' "$d" "$a" "$b" "$([ "$a" = "$b" ] && echo "identical: reused from cache/registry" || echo "CHANGED: rebuilt and pushed")"
|
||||
done
|
||||
echo "AOT cache: trained against the previous jar, so it is stale for the new one (see output/04-mismatch.txt); it must be regenerated after the application layer"
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start / probe / stop helpers shared by the measurement scripts.
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/env.sh"
|
||||
|
||||
PORT=18110
|
||||
BODY='{"customer":"[email protected]","sku":"ABC-1234","quantity":3}'
|
||||
|
||||
now_ms() { echo $(( $(date +%s%N) / 1000000 )); }
|
||||
|
||||
# start_app WORKDIR LOGFILE cmd args... -> sets APP_PID (the java/native process itself, thanks to exec)
|
||||
start_app() {
|
||||
local wd=$1 log=$2; shift 2
|
||||
( cd "$wd" && exec "$@" ) > "$log" 2>&1 &
|
||||
APP_PID=$!
|
||||
}
|
||||
|
||||
# wait_ready -> 0 when GET /ping answers 200 within 60 s
|
||||
wait_ready() {
|
||||
local i
|
||||
for i in $(seq 1 3000); do
|
||||
curl -sf -o /dev/null --max-time 2 "localhost:$PORT/ping" && return 0
|
||||
kill -0 "$APP_PID" 2>/dev/null || return 1
|
||||
sleep 0.02
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
stop_app() {
|
||||
kill "$APP_PID" 2>/dev/null
|
||||
wait "$APP_PID" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
# ms_of SECONDS -> milliseconds with one decimal
|
||||
ms_of() { awk -v s="$1" 'BEGIN{printf "%.1f", s*1000}'; }
|
||||
|
||||
# post_order -> prints curl's time_total in ms
|
||||
post_order() {
|
||||
ms_of "$(curl -s -o /dev/null -w '%{time_total}' -H 'Content-Type: application/json' -d "$BODY" "localhost:$PORT/orders")"
|
||||
}
|
||||
get_order() { ms_of "$(curl -s -o /dev/null -w '%{time_total}' "localhost:$PORT/orders/1")"; }
|
||||
|
||||
rss_mb() { awk '/VmRSS/ {printf "%d", $2/1024}' "/proc/$APP_PID/status"; }
|
||||
|
||||
median() { sort -n | awk '{a[NR]=$1} END{ if (NR==0) {print "n/a"; exit} if (NR%2) printf "%s", a[(NR+1)/2]; else printf "%.1f", (a[NR/2]+a[NR/2+1])/2 }'; }
|
||||
minmax() { sort -n | awk 'NR==1{lo=$1} {hi=$1} END{printf "%s..%s", lo, hi}'; }
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# What happens when the cache does not match the run. Each case starts the service with a cache that was made
|
||||
# under different conditions, and prints (1) what the JVM logged about the cache, (2) how long start-up took.
|
||||
# -XX:AOTMode=auto is the default: use the cache if it is valid, otherwise carry on without it.
|
||||
# -XX:AOTMode=on : refuse to start if the cache cannot be used.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
J25=$JDK25/bin/java; J27=$JDK27/bin/java
|
||||
|
||||
run_case() { # title, workdir, cmd... (runs once with -Xlog:aot=warning so the cache's complaints show up)
|
||||
local title=$1 wd=$2; shift 2
|
||||
local t0 log="$WORK/mismatch.log"
|
||||
echo "== $title"
|
||||
t0=$(now_ms)
|
||||
start_app "$wd" "$log" "$@" --server.port=$PORT
|
||||
if wait_ready; then
|
||||
echo " result: started, ready after $(( $(now_ms) - t0 )) ms ($(grep -o 'Started [A-Za-z]* in [0-9.]* seconds' "$log"))"
|
||||
stop_app
|
||||
else
|
||||
wait "$APP_PID" 2>/dev/null
|
||||
echo " result: did not start (exit code $?)"
|
||||
fi
|
||||
grep -E '\[(warning|error)[ ]*\]\[(aot|cds)|saved state of|created with' "$log" | grep -v 'Skipping' | head -5 | sed -E 's/^\[[0-9.]+s\]/ log: /' | cut -c1-170
|
||||
[ -z "$(grep -E '\[(warning|error)[ ]*\]\[(aot|cds)' "$log" | grep -v Skipping)" ] && echo " log: (nothing at warning or error level)"
|
||||
echo
|
||||
}
|
||||
|
||||
echo "# cache: $WORK/refresh.aot, trained on JDK 25.0.4.1, extracted layout, default GC (G1), 2 CPUs"
|
||||
echo
|
||||
run_case "control: the same JVM, the same jar, the same path" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 1. the jar changed after training
|
||||
# (cp -a everywhere below: plain cp gives every file a new modification time, which the cache checks - see the timestamp case)
|
||||
rm -rf "$WORK/ext-changed"; cp -a "$WORK/ext" "$WORK/ext-changed"
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-changed.jar" extract --destination "$WORK/changed-tmp" >/dev/null
|
||||
cp "$WORK/changed-tmp/app-changed.jar" "$WORK/ext-changed/app.jar"; rm -rf "$WORK/changed-tmp" # only the application jar differs
|
||||
run_case "the application jar was rebuilt after a one-line code change, default AOTMode=auto" "$WORK/ext-changed" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "the same, with -XX:AOTMode=on" "$WORK/ext-changed" "$J25" -XX:AOTMode=on -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 2. a dependency jar changed
|
||||
rm -rf "$WORK/ext-dep"; cp -a "$WORK/ext" "$WORK/ext-dep"
|
||||
( cd "$WORK/ext-dep/lib" && f=$(ls spring-core-*.jar) && touch -d '2001-01-01' "$f" )
|
||||
run_case "a dependency jar has a different modification time (same bytes)" "$WORK/ext-dep" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 2b. only a copy, with fresh timestamps (what `cp -r` or a re-extract does)
|
||||
rm -rf "$WORK/ext-copy"; cp -r "$WORK/ext" "$WORK/ext-copy"
|
||||
run_case "the same files copied with plain cp (bytes and path layout identical, timestamps new)" "$WORK/ext-copy" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 3. moved to another directory
|
||||
rm -rf "$WORK/moved"; mkdir -p "$WORK/moved"; cp -a "$WORK/ext" "$WORK/moved/app-dir"
|
||||
run_case "the application directory was moved after training (same bytes, different path)" "$WORK/moved/app-dir" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 4. fat jar cache used with the extracted layout
|
||||
run_case "cache trained on the fat jar, run from the extracted layout" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/fat.aot" -jar app.jar
|
||||
|
||||
# 5. different JVM
|
||||
run_case "JDK 25's cache used by JDK 27" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "the same, with -XX:AOTMode=on" "$WORK/ext" "$J27" -XX:AOTMode=on -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 6. flags that change what the cache holds
|
||||
run_case "-XX:+UseCompactObjectHeaders added at run time only (JDK 25)" "$WORK/ext" "$J25" -XX:+UseCompactObjectHeaders -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a different collector at run time: -XX:+UseSerialGC" "$WORK/ext" "$J25" -XX:+UseSerialGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a different collector at run time: -XX:+UseParallelGC" "$WORK/ext" "$J25" -XX:+UseParallelGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a different collector at run time: -XX:+UseZGC" "$WORK/ext" "$J25" -XX:+UseZGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a different heap size at run time: -Xmx256m" "$WORK/ext" "$J25" -Xmx256m -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a cache file that does not exist" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/nonexistent.aot" -jar app.jar
|
||||
run_case "a cache file that does not exist, with -XX:AOTMode=on" "$WORK/ext" "$J25" -XX:AOTMode=on -XX:AOTCache="$WORK/nonexistent.aot" -jar app.jar
|
||||
|
||||
echo "# and on JDK 27, whose cache was trained under the default collector (G1):"
|
||||
echo
|
||||
run_case "JDK 27, cache from JDK 27 (control)" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
run_case "JDK 27, -XX:+UseZGC at run time" "$WORK/ext" "$J27" -XX:+UseZGC -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
run_case "JDK 27, -XX:+UseSerialGC at run time" "$WORK/ext" "$J27" -XX:+UseSerialGC -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
run_case "JDK 27, -XX:-UseCompactObjectHeaders at run time" "$WORK/ext" "$J27" -XX:-UseCompactObjectHeaders -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
|
||||
echo "# why ZGC is different: ask the JVM (-Xlog:aot=info) why it refused the G1-trained cache"
|
||||
echo
|
||||
run_case "JDK 27, -XX:+UseZGC with the G1-trained cache, -Xlog:aot=info" "$WORK/ext" "$J27" -XX:+UseZGC -Xlog:aot=info -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
run_case "JDK 27, cache trained under ZGC, run under ZGC" "$WORK/ext" "$J27" -XX:+UseZGC -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
|
||||
run_case "JDK 27, cache trained under ZGC, run under G1 (compressed oops on again)" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
|
||||
run_case "JDK 27, cache trained under ZGC, run under G1 with -XX:-UseCompressedOops" "$WORK/ext" "$J27" -XX:-UseCompressedOops -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the GraalVM native image of the same service (no reflection tricks: the app has none). Takes minutes.
|
||||
source "$(dirname "$0")/env.sh"
|
||||
cd "$ROOT"; mkdir -p "$WORK"
|
||||
t0=$(date +%s)
|
||||
JAVA_HOME=$GRAALVM mvn_run mvn -B -DskipTests -Pnative native:compile > "$WORK/native-build.log" 2>&1
|
||||
rc=$?
|
||||
echo "# GraalVM: $("$GRAALVM/bin/native-image" --version 2>&1 | head -1)"
|
||||
grep -E "types, .* fields, and .* methods found reachable|Peak RSS|Finished generating|BUILD (SUCCESS|FAILURE)|Total time" "$WORK/native-build.log" | sed -E 's/^\[INFO\] //; s/^ +//'
|
||||
[ $rc -eq 0 ] && cp "target/aot-cache" "$WORK/aot-cache-native" && ls -l "$WORK/aot-cache-native" "$WORK/app.jar" | awk '{printf "%-30s %d MB\n", $NF, $5/1048576}'
|
||||
echo "build wall time: $(( $(date +%s) - t0 )) s"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds everything the measurements need under work/: the plain jar, a jar with Spring's own AOT output,
|
||||
# the extracted layout, an AppCDS archive and the AOT caches. Prints what it built and how big each artifact is.
|
||||
source "$(dirname "$0")/env.sh"
|
||||
set -e
|
||||
cd "$ROOT"
|
||||
rm -rf "$WORK"; mkdir -p "$WORK"
|
||||
|
||||
echo "== build: plain jar (mvn package)"
|
||||
rm -rf "$ROOT/target" # a stale target/ would carry Spring AOT output from an earlier -Pnative build into the plain jar
|
||||
JAVA_HOME=$JDK25 mvn_run mvn -q -B -DskipTests package > "$WORK/build-plain.log" 2>&1
|
||||
cp target/app.jar "$WORK/app.jar"
|
||||
rm -rf "$ROOT/target"
|
||||
echo "== build: jar with Spring AOT output (mvn -Pnative package; this does NOT compile a native image)"
|
||||
JAVA_HOME=$JDK25 mvn_run mvn -q -B -DskipTests -Pnative package > "$WORK/build-springaot.log" 2>&1
|
||||
cp target/app.jar "$WORK/app-springaot.jar"
|
||||
echo "initializers in plain jar: $(unzip -l "$WORK/app.jar" | grep -c '__ApplicationContextInitializer')"
|
||||
echo "initializers in Spring AOT jar: $(unzip -l "$WORK/app-springaot.jar" | grep -c '__ApplicationContextInitializer')"
|
||||
|
||||
echo "== extract both jars (java -Djarmode=tools -jar app.jar extract)"
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app.jar" extract --destination "$WORK/ext" >/dev/null
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-springaot.jar" extract --destination "$WORK/ext-springaot" >/dev/null
|
||||
mv "$WORK/ext-springaot/app-springaot.jar" "$WORK/ext-springaot/app.jar" # the launcher jar keeps its lib/ Class-Path
|
||||
ls "$WORK/ext" "$WORK/ext-springaot"
|
||||
|
||||
echo "== build: the same service after a one-line change (the unit price in OrderService), for the mismatch and layer checks"
|
||||
mkdir -p "$WORK/changed-src"
|
||||
cp -r pom.xml src "$WORK/changed-src/"
|
||||
sed -i 's/BigDecimal.valueOf(4_99, 2)/BigDecimal.valueOf(5_99, 2)/' "$WORK/changed-src/src/main/java/com/ankurm/aotcache/OrderService.java"
|
||||
grep -n 'valueOf(5_99' "$WORK/changed-src/src/main/java/com/ankurm/aotcache/OrderService.java" | sed 's/^/edited: /'
|
||||
( cd "$WORK/changed-src" && JAVA_HOME=$JDK25 mvn_run mvn -q -B -DskipTests package > "$WORK/build-changed.log" 2>&1 )
|
||||
cp "$WORK/changed-src/target/app.jar" "$WORK/app-changed.jar"
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# A transcript of the service's own endpoints, so the reader knows what the training run's traffic is.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
start_app "$WORK/ext" "$WORK/sample.log" "$JDK25/bin/java" -jar app.jar --server.port=$PORT
|
||||
wait_ready || { echo "did not start"; exit 1; }
|
||||
show() { # print the command the way a shell would need it typed, then run it
|
||||
printf '$'; for a in "$@"; do case $a in *[' {}"']*) printf " '%s'" "$a";; *) printf ' %s' "$a";; esac; done; echo
|
||||
"$@" -s -w ' [HTTP %{http_code}]\n'
|
||||
}
|
||||
show curl "localhost:$PORT/ping"
|
||||
show curl -H 'Content-Type: application/json' -d "$BODY" "localhost:$PORT/orders"
|
||||
show curl "localhost:$PORT/orders/1"
|
||||
show curl -H 'Content-Type: application/json' -d '{"customer":"bad","sku":"x","quantity":0}' "localhost:$PORT/orders"
|
||||
show curl "localhost:$PORT/orders/99"
|
||||
show curl "localhost:$PORT/actuator/health"
|
||||
stop_app
|
||||
exit 0
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Startup and first-request latency for every configuration. Configurations are run in interleaved rounds
|
||||
# (round 1 runs all of them once, then round 2, ...) so slow drift on a shared machine hits all of them alike.
|
||||
# scripts/startup.sh [ROUNDS] default 10
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
ROUNDS=${1:-10}
|
||||
LOG="$WORK/startup-runs.txt"; : > "$LOG"
|
||||
|
||||
declare -A WD CMD
|
||||
labels=()
|
||||
add_cfg() { local l=$1 wd=$2; shift 2; labels+=("$l"); WD[$l]=$wd; CMD[$l]="$*"; }
|
||||
|
||||
J25=$JDK25/bin/java; J27=$JDK27/bin/java
|
||||
add_cfg "1 jvm-fat-jar" "$WORK" "$J25 -jar app.jar"
|
||||
add_cfg "2 jvm-extracted" "$WORK/ext" "$J25 -jar app.jar"
|
||||
add_cfg "3 jvm+spring-aot" "$WORK/ext-springaot" "$J25 -Dspring.aot.enabled=true -jar app.jar"
|
||||
add_cfg "4 appcds" "$WORK/ext" "$J25 -XX:SharedArchiveFile=$WORK/appcds.jsa -jar app.jar"
|
||||
add_cfg "5 aot-cache fat-jar" "$WORK" "$J25 -XX:AOTCache=$WORK/fat.aot -jar app.jar"
|
||||
add_cfg "6 aot-cache context-only" "$WORK/ext" "$J25 -XX:AOTCache=$WORK/refresh.aot -jar app.jar"
|
||||
add_cfg "7 aot-cache traffic" "$WORK/ext" "$J25 -XX:AOTCache=$WORK/traffic.aot -jar app.jar"
|
||||
add_cfg "8 aot-cache+spring-aot" "$WORK/ext-springaot" "$J25 -Dspring.aot.enabled=true -XX:AOTCache=$WORK/springaot.aot -jar app.jar"
|
||||
add_cfg "9 aot-cache+spring-aot traffic" "$WORK/ext-springaot" "$J25 -Dspring.aot.enabled=true -XX:AOTCache=$WORK/springaot-traffic.aot -jar app.jar"
|
||||
add_cfg "10 jdk27 plain" "$WORK/ext" "$J27 -jar app.jar"
|
||||
add_cfg "11 jdk27 aot-cache" "$WORK/ext" "$J27 -XX:AOTCache=$WORK/refresh27.aot -jar app.jar"
|
||||
[ -x "$WORK/aot-cache-native" ] && add_cfg "12 native image" "$WORK" "$WORK/aot-cache-native"
|
||||
|
||||
one_run() { # label round
|
||||
local l=$1 r=$2 t0 ready started first second med50 rss
|
||||
t0=$(now_ms)
|
||||
start_app "${WD[$l]}" "$WORK/run.log" ${CMD[$l]} --server.port=$PORT
|
||||
if ! wait_ready; then echo "RUN|$l|$r|FAILED" >> "$LOG"; stop_app; return; fi
|
||||
ready=$(( $(now_ms) - t0 ))
|
||||
started=$(grep -o 'Started [A-Za-z]* in [0-9.]* seconds' "$WORK/run.log" | awk '{print $4*1000}')
|
||||
first=$(post_order); second=$(get_order)
|
||||
med50=$(for i in $(seq 1 50); do post_order; echo; done | grep . | median)
|
||||
rss=$(rss_mb)
|
||||
echo "RUN|$l|$r|ready=$ready|started=${started:-n/a}|first=$first|second=$second|next50=$med50|rss=$rss" >> "$LOG"
|
||||
stop_app; sleep 0.5
|
||||
}
|
||||
|
||||
echo "# ${#labels[@]} configurations x $ROUNDS rounds; one unrecorded warm-up round first (so cache files are in the page cache)"
|
||||
for l in "${labels[@]}"; do one_run "$l" 0; done; : > "$LOG"
|
||||
for r in $(seq 1 $ROUNDS); do
|
||||
for l in "${labels[@]}"; do one_run "$l" "$r"; done
|
||||
echo "round $r done" >&2
|
||||
done
|
||||
|
||||
field() { grep -F "RUN|$1|" "$LOG" | grep -o "$2=[0-9.]*" | cut -d= -f2; }
|
||||
echo
|
||||
echo "== medians of $ROUNDS runs (min..max in brackets), milliseconds; rss in MB"
|
||||
printf '%-32s %-20s %-20s %-20s %-18s %s\n' "configuration" "ready (exec->200)" "Boot 'Started in'" "1st POST /orders" "next 50 (median)" "RSS"
|
||||
for l in "${labels[@]}"; do
|
||||
f=$(grep -cF "RUN|$l|" "$LOG"); ok=$(grep -F "RUN|$l|" "$LOG" | grep -vc FAILED)
|
||||
cell() { local v; v=$(field "$l" "$1"); printf '%-20s ' "$(echo "$v" | median) [$(echo "$v" | minmax)]"; }
|
||||
printf '%-32s ' "$l"; cell ready; cell started; cell first; printf '%-18s ' "$(field "$l" next50 | median)"; printf '%s' "$(field "$l" rss | median)"
|
||||
[ "$ok" != "$f" ] && printf ' (%s of %s runs failed)' $((f-ok)) $f
|
||||
echo
|
||||
done
|
||||
echo
|
||||
echo "== every run"
|
||||
sed 's/^RUN|//; s/|/ /g' "$LOG"
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# The training runs: one per cache flavour. Prints how long each took, how big the artifact is, and what the
|
||||
# JVM logged at warning level while writing it. Needs scripts/prepare.sh to have run.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
exercise() { # the traffic a "real" training run sends: success, validation failure, 404, actuator
|
||||
for i in 1 2 3 4 5; do post_order >/dev/null; done
|
||||
get_order >/dev/null
|
||||
curl -s -o /dev/null -H 'Content-Type: application/json' -d '{"customer":"bad","sku":"x","quantity":0}' "localhost:$PORT/orders"
|
||||
curl -s -o /dev/null "localhost:$PORT/orders/99"
|
||||
curl -s -o /dev/null "localhost:$PORT/actuator/health"
|
||||
}
|
||||
|
||||
report() { # label file t0
|
||||
printf '%-34s %6d ms %s (%s MB)\n' "$1" $(( $(now_ms) - $3 )) "$(basename "$2")" "$(( $(stat -c %s "$2") / 1048576 ))"
|
||||
}
|
||||
|
||||
echo "# training runs (JDK 25.0.4.1 unless noted); wall time includes JVM start, context start and cache creation"
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext" && "$JDK25/bin/java" -XX:ArchiveClassesAtExit="$WORK/appcds.jsa" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-appcds.log" 2>&1
|
||||
report "AppCDS (ArchiveClassesAtExit)" "$WORK/appcds.jsa" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
"$JDK25/bin/java" -XX:AOTCacheOutput="$WORK/fat.aot" -Dspring.context.exit=onRefresh -jar "$WORK/app.jar" > "$WORK/train-fat.log" 2>&1
|
||||
report "AOT cache, fat jar, context-only" "$WORK/fat.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext" && "$JDK25/bin/java" -XX:AOTCacheOutput="$WORK/refresh.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-refresh.log" 2>&1
|
||||
report "AOT cache, context-only training" "$WORK/refresh.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
start_app "$WORK/ext" "$WORK/train-traffic.log" "$JDK25/bin/java" -XX:AOTCacheOutput="$WORK/traffic.aot" -jar app.jar --server.port=$PORT
|
||||
wait_ready && exercise
|
||||
kill -TERM "$APP_PID"; wait "$APP_PID" 2>/dev/null
|
||||
report "AOT cache, traffic training" "$WORK/traffic.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext-springaot" && "$JDK25/bin/java" -Dspring.aot.enabled=true -XX:AOTCacheOutput="$WORK/springaot.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-springaot.log" 2>&1
|
||||
report "AOT cache + Spring AOT, context-only" "$WORK/springaot.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
start_app "$WORK/ext-springaot" "$WORK/train-springaot-traffic.log" "$JDK25/bin/java" -Dspring.aot.enabled=true -XX:AOTCacheOutput="$WORK/springaot-traffic.aot" -jar app.jar --server.port=$PORT
|
||||
wait_ready && exercise
|
||||
kill -TERM "$APP_PID"; wait "$APP_PID" 2>/dev/null
|
||||
report "AOT cache + Spring AOT, traffic" "$WORK/springaot-traffic.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext" && "$JDK27/bin/java" -XX:AOTCacheOutput="$WORK/refresh27.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-refresh27.log" 2>&1
|
||||
report "AOT cache on JDK 27, context-only" "$WORK/refresh27.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext" && "$JDK27/bin/java" -XX:+UseZGC -XX:AOTCacheOutput="$WORK/zgc27.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-zgc27.log" 2>&1
|
||||
report "AOT cache on JDK 27, trained under ZGC" "$WORK/zgc27.aot" $t0
|
||||
|
||||
echo
|
||||
echo "# what the JVM said at warning level while writing the context-only AOT cache (count, then the distinct kinds)"
|
||||
grep -c '\[warning\]\[aot\]' "$WORK/train-refresh.log" | sed 's/^/warning lines: /'
|
||||
grep '\[warning\]\[aot\]' "$WORK/train-refresh.log" | sed -E 's/^\[[0-9.]+s\]//; s/(Skipping) [^ ]+:/\1 <class>:/' | sort | uniq -c | sort -rn | head -5
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ankurm.aotcache;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class AotCacheApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AotCacheApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ankurm.aotcache;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
/** The request body of POST /orders. Bean Validation and Jackson both have to work on it. */
|
||||
public record NewOrder(
|
||||
@NotBlank @Email String customer,
|
||||
@NotBlank @Pattern(regexp = "[A-Z]{3}-[0-9]{4}") String sku,
|
||||
@Min(1) @Max(1000) int quantity) {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ankurm.aotcache;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
|
||||
/** A stored order. */
|
||||
public record Order(long id, String customer, String sku, int quantity, BigDecimal total, Instant createdAt) {
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.ankurm.aotcache;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class OrderController {
|
||||
|
||||
private final OrderService service;
|
||||
|
||||
public OrderController(OrderService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@GetMapping("/ping")
|
||||
public String ping() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@PostMapping("/orders")
|
||||
public ResponseEntity<Order> create(@Valid @RequestBody NewOrder body) {
|
||||
Order o = service.create(body);
|
||||
return ResponseEntity.created(URI.create("/orders/" + o.id())).body(o);
|
||||
}
|
||||
|
||||
@GetMapping("/orders/{id}")
|
||||
public ResponseEntity<Order> get(@PathVariable long id) {
|
||||
return service.find(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ankurm.aotcache;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class OrderService {
|
||||
|
||||
private final AtomicLong ids = new AtomicLong();
|
||||
private final Map<Long, Order> orders = new ConcurrentHashMap<>();
|
||||
|
||||
public Order create(NewOrder in) {
|
||||
long id = ids.incrementAndGet();
|
||||
BigDecimal unit = BigDecimal.valueOf(4_99, 2);
|
||||
Order o = new Order(id, in.customer(), in.sku(), in.quantity(), unit.multiply(BigDecimal.valueOf(in.quantity())), Instant.now());
|
||||
orders.put(id, o);
|
||||
return o;
|
||||
}
|
||||
|
||||
public Optional<Order> find(long id) {
|
||||
return Optional.ofNullable(orders.get(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
spring.application.name=aot-cache
|
||||
management.endpoints.web.exposure.include=health
|
||||
Reference in New Issue
Block a user