Repository navigation
Save the JIT's generated code in warm snapshots #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bb0aced
9ae0824
181c32e
a57ab23
566be54
1f9db0a
a20037f
c3b360d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ import { hostNative, jniNames } from "./hostnatives.ts"; | |
| import { Library } from "./library.ts"; | ||
| import { dumpVM, restoreVM, snapshotKey } from "./snapshot.ts"; | ||
| import type { SnapshotImage, WarmSnapshotImage } from "./snapshot.ts"; | ||
| import type { SavedJit } from "./jit.ts"; | ||
| import type { LibraryRequest } from "./library.ts"; | ||
| import type { HostNative } from "./hostnatives.ts"; | ||
| import { PROXY_HOST, PROXY_PORT } from "./http-proxy.ts"; | ||
|
|
@@ -143,6 +144,10 @@ export class JVM { | |
| classCount = 0; | ||
| // every class in creation order, until the snapshot after boot is taken | ||
| created: JClass[] | null; | ||
| // Generated JS of compiled methods: kept until the warm snapshot is saved, | ||
| // then (after a warm restore) compiled on each method's first call. | ||
| keepJit: boolean; | ||
| jitCode = new Map<JMethod, SavedJit>(); | ||
| threadsStarted = 0; | ||
| peakThreads = 0; | ||
| exitCode: number | null = null; | ||
|
|
@@ -159,6 +164,7 @@ export class JVM { | |
|
|
||
| constructor(opts: JvmOptions) { | ||
| this.created = opts.snapshots ? [] : null; | ||
| this.keepJit = !!(opts.warmSnapshot && opts.warmSnapshots && opts.snapshots); | ||
| this.opts = opts; | ||
| this.fs = opts.fs; | ||
| const extra = ipv4Literal(opts.hostName); | ||
|
|
@@ -1139,10 +1145,17 @@ export class JVM { | |
| this.warmTraceActive = this.fs.beginWarmTrace(); | ||
| this.warmTraceClasses = this.classCount; | ||
| } | ||
| // no warm snapshot will be taken (a restored VM keeps its saved code) | ||
| if (!this.warmTraceActive && this.keepJit) this.dropJit(); | ||
| started(); | ||
| yield* this.library.loop(t); | ||
| } | ||
|
|
||
| private dropJit() { | ||
| this.keepJit = false; | ||
| this.jitCode.clear(); | ||
| } | ||
|
|
||
| libraryIdle(t: JThread) { | ||
| const cache = this.opts.warmSnapshots; | ||
| if (!this.opts.warmSnapshot || !this.warmTraceActive || this.warmSaved || !cache) return; | ||
|
|
@@ -1156,6 +1169,7 @@ export class JVM { | |
| if (!trace) { | ||
| this.fs.endWarmTrace(); | ||
| this.warmTraceActive = false; | ||
| this.dropJit(); | ||
| this.debug("warm snapshot busy: access record too large"); | ||
| return; | ||
| } | ||
|
|
@@ -1167,6 +1181,7 @@ export class JVM { | |
| this.fs.endWarmTrace(); | ||
| this.warmTraceActive = false; | ||
| this.warmSaved = true; | ||
| this.dropJit(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| for (const c of this.created ?? []) c.bytes = undefined; | ||
| this.created = null; | ||
| this.debug("warm snapshot saved"); | ||
|
|
@@ -1262,6 +1277,7 @@ export class JVM { | |
| return false; | ||
| } | ||
| try { | ||
| this.dropJit(); | ||
| restoreVM(this, image, t); | ||
| this.warmRestored = true; | ||
| this.warmSaved = true; | ||
|
|
@@ -1326,6 +1342,7 @@ export class JVM { | |
| if (!this.opts.warmSnapshot || !this.libraryStarting) { | ||
| for (const c of this.created ?? []) c.bytes = undefined; | ||
| this.created = null; | ||
| this.dropJit(); | ||
| } | ||
| if (cmd !== undefined) { | ||
| this.props.set("sun.java.command", cmd); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { execFileSync } from "node:child_process"; | ||
| import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { test } from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { startLibrary } from "../src/node.ts"; | ||
| import { hotspot } from "../scripts/hotspot.ts"; | ||
|
|
||
| // Own file: a warm image restores reliably only in a process that has run no other VM. | ||
| const root = join(import.meta.dirname, ".."); | ||
| const classes = join(root, "build", "test-warm-snapshot"); | ||
| mkdirSync(classes, { recursive: true }); | ||
| execFileSync(hotspot("javac"), ["-d", classes, join(root, "test", "warm-snapshot", "WarmJavac.java")]); | ||
|
|
||
| async function compile(cache: string, args: string[]) { | ||
| const vm = await startLibrary({ classpath: [classes], snapshotDir: cache, warmSnapshot: true }); | ||
| try { | ||
| const before = vm.jitCode.size; | ||
| const result = await vm.callStatic("WarmJavac", "compile", args); | ||
| return { result, before, after: vm.jitCode.size }; | ||
| } finally { | ||
| vm.halt(0); | ||
| } | ||
| } | ||
|
|
||
| test("a warm restore compiles methods from their saved code on first call", async () => { | ||
| const dir = mkdtempSync(join(tmpdir(), "java-web-warm-jit-")); | ||
| try { | ||
| const src = join(dir, "Result.java"); | ||
| writeFileSync(src, "public class Result { public static int value() { return 7; } }\n"); | ||
| const reference = join(dir, "reference"); | ||
| mkdirSync(reference); | ||
| execFileSync(hotspot("javac"), ["-g:none", "-d", reference, src]); | ||
| const expected = readFileSync(join(reference, "Result.class")).toString("base64"); | ||
| const args = [src, join(dir, "not-created", "Absent.class"), join(dir, "out")]; | ||
|
|
||
| const first = await compile(dir, args); | ||
| assert.equal(first.result, expected); | ||
| assert.ok(first.before < 100, "the first run has no saved code"); | ||
|
|
||
| const restored = await compile(dir, args); | ||
| assert.equal(restored.result, expected); | ||
| assert.ok(restored.before > 100, `saved code for ${restored.before} methods`); | ||
| // compile removes a method's saved code only when it compiles from it | ||
| assert.ok(restored.before - restored.after >= 50, `${restored.before - restored.after} methods compiled from saved code`); | ||
| } finally { | ||
| rmSync(dir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.