# zavarnik

> Trains a Leyden AOT cache through the application plugin's real start script, fails the build when the JVM would reject it or when the JDK would not even look, and ships it inside distTar and the container image.

Page: https://kotlin.website/zavarnik

## Why it exists

Project Leyden (JDK 25 and 26) makes a JVM service ready two to three times faster from an AOT
cache, with no change to the code. Spring Boot and Quarkus wrapped the workflow into their own
tooling; a Ktor server, an http4k service or a CLI on the plain `application` plugin has two
commands and a list of ways the cache stops working without telling anyone. The JVM rejects a
cache silently by default, exit code 0 and three lines on stderr. Four builds of JDK 25 and two of
JDK 26 do not compare the cache with the jars at all
([JDK-8377932](https://bugs.openjdk.org/browse/JDK-8377932)), so a rebuilt jar runs last week's
classes. A `-jre` image refuses a cache trained on the `-jdk` image of the same build. A zip loses
the timestamps the check depends on. Each of these was measured before the plugin was written;
the research document and the logs are in the repository.

## What you get

- **`aotTrain`** runs the installed distribution through its own start script with
  `-XX:AOTCacheOutput`, waits for a readiness URL, runs a workload of HTTP requests or commands,
  stops the JVM with `SIGTERM` and waits for the cache to be assembled. Jar timestamps are pinned
  to the constant Gradle's tar uses, so the cache survives `distTar` and Docker `COPY`.
- **`aotVerify`**, on `check`, is the point. It compares the jars with a SHA-256 manifest written
  at training time, starts the application under `-XX:AOTMode=on` where a rejected cache is fatal,
  and fails unless the configured share of the application's classes came from the cache. The
  message names which of the three failed and why.
- **`aotReport`** writes two tables: time to the first `200` cold and cached, medians over
  sorted runs, and what the JIT still compiles after the start — because the cache holds classes,
  heap objects and profiles, not compiled code, and a report that showed only the first table
  would promise a warm service the JVM does not deliver.
- **The start scripts pick the cache up** when it is there and run without it when it is not;
  `distTar` carries it, `distZip` says why it cannot.
- **A runner for the image.** Every distribution carries `lib/zavarnik-runner.jar`, the same
  training and verification on a bare JRE with no Gradle and no curl, so the `-jre` runtime stage
  of a Dockerfile trains and verifies the cache on the JVM that will use it.
- **Portable by default.** The adapter code the JVM caches for the training CPU is switched off,
  so a cache trained on a CI runner with AVX-512 does not `SIGILL` on a narrower machine; the cost
  was measured at one millisecond of readiness.

## Numbers

In a cluster — konekt, Ktor CIO with Exposed and Postgres under a one-core limit, the cache
trained in the release pipeline inside the image that ships, the readiness probe asking every
second — the pod is Ready 3 s after its container starts, against 11 s for the release before.
On its stand, `docker start` to `/health` went from 4.4 s to 2.0 s and the first request from
510 ms to 240 ms at the median of ten restarts; the next hundred requests did not move.


Time to the first `200` on `/health` of the Ktor sample, ten runs per variant, medians, Linux
x86_64, OpenJDK 25.0.4: 143 ms without the cache, 47 ms with it; on macOS arm64 with JetBrains
Runtime 25.0.4.1, 98 and 37 ms. The JIT is not warmed: C2 compiles the same thousand-odd methods
after the start either way, about ten per cent fewer at best. The cache for the sample is
31 MiB; the container image with a `-jre` runtime stage is 562 MB against 647 MB with two `-jdk`
stages.

## Requirements

No wildcard on the start script's classpath: the JVM expands `lib/*` in the filesystem's order,
which differs between container runtimes, and a cache trained under one is refused under another
with nothing on the build machine able to tell. The plugin refuses it at `installDist`.


JDK 25 or newer as the toolchain, preferably 25.0.4 or 26.0.2 — the plugin warns on the six
builds that skip the jar check and verifies the jars itself on all of them. The `application`
plugin. Linux or macOS for training; production on Windows works, training on it does not yet.
The same JDK build in production as in training, down to the image.

## Install


```kotlin
// build.gradle.kts
plugins {
    application
    id("io.github.youndie.zavarnik") version "0.1.0"
}

zavarnik {
    training {
        readyWhen.url("http://127.0.0.1:8080/health")
        workload { get("http://127.0.0.1:8080/api/items") }
    }
}
```

`0.1.0` is on the [Gradle Plugin Portal](https://plugins.gradle.org/plugin/io.github.youndie.zavarnik),
so the `plugins { }` block alone installs it — no repository to declare. Every push to `main` also
publishes a snapshot, `0.1.0.<run>`, to `https://reposilite.kotlin.website/snapshots`; the README
says how to take one. `./gradlew check` then trains and verifies; `./gradlew distTar` ships the cache;
the repository's `samples/ktor/Dockerfile` is the container recipe. What the plugin hides — the
training, the verification and the packaging, written out as five steps by hand — is
[the how-to](https://kotlin.website/blog/leyden-aot-cache-for-a-plain-kotlin-jvm-service); the JDK builds that accept a
stale cache have [their own post](https://kotlin.website/blog/stale-aot-cache-on-jdk25). A cache is not the only way to
start faster: the repository's third phase takes a **CRaC checkpoint** of the warmed-up process
instead, which restores in 131 ms against 2 317 with its JIT already hot — and what that costs in
open sockets is
[every socket that refuses the checkpoint](https://kotlin.website/blog/crac-on-a-ktor-service-with-hikari-and-exposed).

The repository also holds a second phase that was measured and not built: a Kotlin bytecode
optimiser, closed because user code owns 1–4 % of a Ktor service's CPU —
[the write-up](https://kotlin.website/blog/user-code-share-of-a-ktor-service).
