AOT cache — le cold start quasi instantané de JDK 25
L'AOT cache de Project Leyden, généré automatiquement au build et embarqué dans le installateur. Un flag Gradle, ~1 s de cold boot avec un JIT pleinement tiered prêt à compiler.
Les apps desktop JVM payaient une taxe de cold start : des milliers de classes chargées, du bytecode vérifié, des chemins chauds JIT-compilés avant que l'UI ne réponde. C'est la raison originelle pour laquelle Nucleus existe. JDK 25 corrige ça avec l'AOT cache single-step de Project Leyden — classes, profils et code préchauffés hors ligne, rejoués au lancement. Nucleus génère ce cache pendant le build et l'embarque dans l'installateur.
TL;DR
- Un flag Gradle :
enableAotCache = true. - Le cold start tombe à ~1.2 s — HotSpot C2 reste le JIT, mais le warmup disparaît.
- Le cache est généré par un run de le app en mode training pendant le build, puis embarqué dans chaque distribution.
nucleus.aot-runtimepermet à le code de brancher selon training vs runtime.
Install
nucleus.aot-runtime est tiré transitivement par nucleus.nucleus-application. Ajoute-le explicitement si l'on n'utilises pas l'umbrella :
dependencies {
implementation("dev.nucleusframework:nucleus.aot-runtime:<version>")
}Quickstart
// build.gradle.kts
nucleus.application {
mainClass = "com.example.MainKt"
nativeDistributions {
enableAotCache = true
}
}JDK 25+ requis
La génération de l'AOT cache exige JDK 25 ou supérieur. Les toolchains plus anciennes échouent au build.
C'est tout côté build. Le plugin va :
- Lancer le app en mode training après
createDistributable. - Enregistrer le chargement de classes et la compilation JIT dans
app.aot(-XX:AOTCacheOutput). - Injecter
-XX:AOTCache=app.aotdans le launcher. - Embarquer le cache dans chaque installateur (
.dmg,.msi,.deb, …).
Mode training — le app doit s'auto-terminer
Pendant le training, la JVM observe un vrai run de le app. Le code doit sortir proprement pour que le build ne reste pas bloqué jusqu'au timeout de sécurité de 300 s. Avec nucleusApplication { … }, c'est une ligne :
import dev.nucleusframework.application.aotTraining
import dev.nucleusframework.application.nucleusApplication
import kotlin.time.Duration.Companion.seconds
fun main(args: Array<String>) = nucleusApplication(args) {
// No-op hors training. Appelle exitApplication() après 45 s.
aotTraining(duration = 45.seconds)
if (isAotTraining) {
// Plus le run touche d'écrans réels, meilleur le cache.
// Parcours le graphe de navigation, précharge polices et images.
preloadNavigationScreens()
preloadFontsAndImages()
}
DecoratedWindow(onCloseRequest = ::exitApplication, title = "MyApp") {
App()
}
}Si on appelle application { … } de Compose directement, gère le timer manuellement :
import dev.nucleusframework.aot.runtime.AotRuntime
fun main() {
if (AotRuntime.mode() == AotRuntimeMode.TRAINING) {
Thread({ Thread.sleep(45_000); System.exit(0) }, "aot-timer").start()
preloadNavigationScreens()
preloadFontsAndImages()
}
application { /* ... */ }
}Comment ça marche
Le plugin met la propriété système nucleus.aot.mode à training pendant la génération et à runtime quand le cache est chargé. AotRuntime.mode() lit cette propriété et renvoie AotRuntimeMode.TRAINING, RUNTIME ou NONE.
Le run de training doit :
- Sortir avec le code 0 (le DSL
aotTrainings'en charge). - Finir avant le timeout de sécurité de 300 s.
- Être représentatif d'une vraie session utilisateur — plus on touche d'écrans réels, plus de méthodes sont profilées, plus le cold start post-cache est rapide.
Sous Linux headless, le plugin démarre Xvfb automatiquement.
Verrouillage plateforme & JDK
L'AOT cache est spécifique à la plateforme et au JDK. Un cache produit sur macOS ARM64 avec JBR 25.0.2 ne fonctionne pas sous Linux x64 ni avec un autre build de JDK.
- Build sur chaque plateforme cible — une matrice CI est la mise en place la plus propre.
- Le plugin utilise toujours le JDK embarqué par
createDistributablepour le training, donc le JDK runtime correspond au JDK training octet par octet. setup-nucleusépingle la même version JBR sur tous les runners. Voir CI/CD.
Référence
DSL Gradle
| Propriété | Défaut | Notes |
|---|---|---|
nativeDistributions.enableAotCache | false | Un seul switch. Déclenche training + bundling. |
AotRuntime
| API | Renvoie |
|---|---|
AotRuntime.mode() | AotRuntimeMode.TRAINING, RUNTIME ou NONE |
Scope nucleusApplication
| Propriété | Équivalent |
|---|---|
aotMode | AotRuntime.mode() |
isAotTraining | aotMode == TRAINING |
isAotRuntime | aotMode == RUNTIME |
aotTraining(duration, onTimeout?) | Timer d'auto-sortie pour les runs training |
Méthodologie de mesure
Quand on compare des chiffres de cold start, mesure depuis le spawn de process OS (time ./MyApp sous Linux/macOS, Measure-Command sous Windows) jusqu'au premier frame visible. Le chiffre de 1.2 s du landing vient d'un démarrage JBR 25 standard avec un AOT cache pré-chauffé par un run de training de 45 s qui parcourt l'écran d'accueil et un onglet réglages.
Notes
- Le cache est par binaire — chaque release embarque un cache neuf. N'essaie pas de partager les caches entre versions.
- Préfère la voie GraalVM native image si l'on veut un cold start sous 100 ms ou une empreinte résidente minuscule. L'AOT cache gagne sur le débit ; GraalVM gagne sur le démarrage et la taille.
Tâches & CI
Tâches Gradle ajoutées par le plugin Nucleus, emplacements de sortie, et une matrice GitHub Actions qui cache le build native-image.
Accès natif — la metadata de réflexion résolue automatiquement
Nucleus injecte automatiquement les reflect-config, resource-config et metadata JNI pour chaque module Nucleus et la plupart des libs Kotlin idiomatiques — le build GraalVM marche sans écrire de JSON.