From df254baffd8341bb3d00ada7020397ce66061063 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:06:05 -0600 Subject: [PATCH] Cheaper direct calls and real heap figures from Runtime. --- src/jit.ts | 23 +++-------------------- src/natives/lang.ts | 9 ++++++--- src/natives/management.ts | 23 +++++++++++++++-------- src/thread.ts | 16 ++++++++++++++-- test/java/RuntimeMemory.java | 16 ++++++++++++++++ 5 files changed, 54 insertions(+), 33 deletions(-) create mode 100644 test/java/RuntimeMemory.java diff --git a/src/jit.ts b/src/jit.ts index 455e154..769c29c 100644 --- a/src/jit.ts +++ b/src/jit.ts @@ -29,8 +29,6 @@ export type Compiled = (t: JThread, f: Frame) => number; const THRESHOLD = 500; const BACKEDGE_THRESHOLD = 2000; const MAX_CODE = 6000; -// compiled frames nested on one JS stack before calls go through the run loop -const MAX_DIRECT = 120; const MAX_FALLBACKS = 5000; // callees inlined as regions: bytecode outside throw-only blocks (HotSpot's // MaxInlineSize), nesting, and bytecode inlined into one compiled method @@ -191,20 +189,6 @@ const runtime = { f.sp = sp; return f; }, - // the pushed frame of a direct call to compiled m, or null when the call - // goes through the run loop: the stack is too deep or the monitor is taken - enter(t: JThread, m: JMethod, recv: JObject | null): Frame | null { - if (t.frames.length - t.base >= MAX_DIRECT || t.frames.length >= t.stackLimit) return null; - let lock: JObject | null = null; - if (m.isSynchronized) { - lock = m.isStatic ? t.vm.mirror(m.cls) : recv!; - if (!t.vm.tryMonitorEnter(t, lock)) return null; - } - const f = t.directFrame(m); - f.lock = lock; - t.frames.push(f); - return f; - }, // A call that is not direct: a native's result, PUSHED once a native that // must wait is on the stack, or INTERPRET for the run loop to make the call. slowCall(t: JThread, m: JMethod, args: any[]): any { @@ -295,7 +279,6 @@ const runtime = { }, // raises exc from the run loop, once every compiled caller has spilled *rethrow(exc: JObject): Generator { throw new JavaThrow(exc); }, - MAX_DIRECT, }; // ---- analysis ---------------------------------------------------------------- @@ -1187,7 +1170,7 @@ function genInvoke( const res = (v: string) => (ret === "V" ? "" : `s${base}=${v};`); // compiled callee: direct call const recv = target.isStatic ? "null" : args[0]; - parts.push(`if(tm.jit!==null&&(cf=rt.enter(t,tm,${recv}))!==null){`); + parts.push(`if(tm.jit!==null&&(cf=t.enter(tm,${recv}))!==null){`); parts.push(`r=tm.jit(t,cf${args.map((x) => "," + x).join("")});if(r!==0){t.unpool(cf);${exit(pc, base, EXIT_SUSPEND)}}`); parts.push(`${res("t.ret")}}`); // a native callee runs here; anything else goes through the interpreter @@ -1233,14 +1216,14 @@ function genPolymorphic(g: Gen, pc: number, next: number, d: number, target: JMe const cell = K([null, null]); const push = target.ret === "V" ? "" : `s${base}=t.ret;`; return `if(${args[0]}!==null&&${args[0]}===${cell}[0]){f.pc=${pc};tm=${cell}[1];` + - `if(tm.jit!==null&&tm.cls.state===${INITIALIZED}&&(cf=rt.enter(t,tm,null))!==null){` + + `if(tm.jit!==null&&tm.cls.state===${INITIALIZED}&&(cf=t.enter(tm,null))!==null){` + `r=tm.jit(t,cf${args.map((x) => "," + x).join("")});if(r!==0){t.unpool(cf);${exit(pc, base, EXIT_SUSPEND)}}${push}}` + `else{${call(pc, d)}}}else{rt.vhTarget(t,${cell},${args[0]},${K(link.appendix)});${code}}`; } } const push = target.ret === "V" ? "" : `s${base}=t.ret;`; return `f.pc=${pc};${select}` + - `if(tm.jit!==null&&tm.cls.state===${INITIALIZED}&&(cf=rt.enter(t,tm,${callArgs[0] ?? "null"}))!==null){` + + `if(tm.jit!==null&&tm.cls.state===${INITIALIZED}&&(cf=t.enter(tm,${callArgs[0] ?? "null"}))!==null){` + `r=tm.jit(t,cf${callArgs.map((x) => "," + x).join("")});if(r!==0){t.unpool(cf);${exit(pc, base, EXIT_SUSPEND)}}` + `${push}}else{${call(pc, d)}}`; } diff --git a/src/natives/lang.ts b/src/natives/lang.ts index d47c054..64928f5 100644 --- a/src/natives/lang.ts +++ b/src/natives/lang.ts @@ -1,7 +1,7 @@ // java.lang natives: Object, System, Thread, Throwable, String, Runtime, ... import { isHidden } from "./stackwalk.ts"; -import { gcStats } from "./management.ts"; +import { gcStats, javaHeap } from "./management.ts"; import { assertionsEnabled } from "../assertions.ts"; import { normalize } from "../fs.ts"; import { ACC_INTERFACE, JArray, JClass, JObject, MAX_HEAP, newArrayData } from "../runtime.ts"; @@ -316,8 +316,11 @@ export const natives: Natives = { // Runtime "java/lang/Runtime.availableProcessors()I": () => 1, // free <= total <= max, as callers that compute available memory expect - "java/lang/Runtime.freeMemory()J": () => BigInt(Math.min(512 << 20, MAX_HEAP) / 2), - "java/lang/Runtime.totalMemory()J": () => BigInt(Math.min(512 << 20, MAX_HEAP)), + "java/lang/Runtime.freeMemory()J": () => { + const h = javaHeap(); + return BigInt(h ? h.committed - h.used : Math.min(512 << 20, MAX_HEAP) / 2); + }, + "java/lang/Runtime.totalMemory()J": () => BigInt(javaHeap()?.committed ?? Math.min(512 << 20, MAX_HEAP)), "java/lang/Runtime.maxMemory()J": () => BigInt(MAX_HEAP), // Collects where the engine allows it (node --expose-gc), in a new JS job: // a WeakRef read earlier in a job keeps its target alive until the job ends. diff --git a/src/natives/management.ts b/src/natives/management.ts index 3f4acfc..89af721 100644 --- a/src/natives/management.ts +++ b/src/natives/management.ts @@ -49,12 +49,21 @@ export function gcStats(vm: JVM): GcStats { return s; } -function jsHeap(): { used: number; committed: number } { +// The JS heap as Java sees it, with used <= committed <= -Xmx; primitive +// arrays are typed arrays, outside the JS heap. Null where the engine cannot tell. +export function javaHeap(): { used: number; committed: number } | null { + let used: number, committed: number; const node = (globalThis as any).process?.memoryUsage?.(); - if (node) return { used: node.heapUsed, committed: node.heapTotal }; const chrome = (globalThis as any).performance?.memory; - if (chrome) return { used: chrome.usedJSHeapSize, committed: chrome.totalJSHeapSize }; - return { used: 0, committed: 0 }; + if (node) { + used = node.heapUsed + node.arrayBuffers; + committed = node.heapTotal + node.arrayBuffers; + } else if (chrome) { + used = chrome.usedJSHeapSize; + committed = chrome.totalJSHeapSize; + } else return null; + committed = Math.min(Math.max(used, committed), MAX_HEAP); + return { used: Math.min(used, committed), committed }; } function* beans(t: JThread): NativeGen { @@ -94,11 +103,9 @@ function* poolUsage(t: JThread, self: JObject): NativeGen { return yield* heapUsage(t); } -// MemoryUsage needs used <= committed <= max; node's heap does not know -Xmx function* heapUsage(t: JThread): NativeGen { - const h = jsHeap(); - const committed = Math.min(Math.max(h.used, h.committed), MAX_HEAP); - return yield* usage(t, 0, Math.min(h.used, committed), committed, MAX_HEAP); + const h = javaHeap() ?? { used: 0, committed: 0 }; + return yield* usage(t, 0, h.used, h.committed, MAX_HEAP); } function liveThreads(vm: JVM): JThread[] { diff --git a/src/thread.ts b/src/thread.ts index 54c27f1..80227d3 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -101,6 +101,9 @@ export const STATUS_NEW = 0, STATUS_RUNNABLE = 5, STATUS_TERMINATED = 2; export const STATUS_SLEEPING = 0x00e1, STATUS_WAITING = 0x0191, STATUS_TIMED_WAITING = 0x01a1; export const STATUS_PARKED = 0x0291, STATUS_PARKED_TIMED = 0x02a1, STATUS_BLOCKED = 0x0401; +// compiled frames nested on one JS stack before calls go through the run loop +const MAX_DIRECT = 120; + let nextId = 1; export class JThread { @@ -139,8 +142,16 @@ export class JThread { // it spilled. private pool: (Frame | null)[] = []; - directFrame(m: JMethod): Frame { + // The pushed frame of a direct call to compiled m, or null when the call + // goes through the run loop: the stack is too deep or the monitor is taken. + enter(m: JMethod, recv: JObject | null): Frame | null { const d = this.frames.length; + if (d - this.base >= MAX_DIRECT || d >= this.stackLimit) return null; + let lock: JObject | null = null; + if (m.isSynchronized) { + lock = m.isStatic ? this.vm.mirror(m.cls) : recv!; + if (!this.vm.tryMonitorEnter(this, lock)) return null; + } // filled without holes, which would make V8 store it as a dictionary while (this.pool.length <= d) this.pool.push(this.pooled(m, this.pool.length)); const f = this.pool[d] ?? (this.pool[d] = this.pooled(m, d)); @@ -150,9 +161,10 @@ export class JThread { f.sp = 0; f.locals = null as any; f.stack = null as any; - f.lock = null; + f.lock = lock; f.needLock = false; f.direct = true; + this.frames.push(f); return f; } diff --git a/test/java/RuntimeMemory.java b/test/java/RuntimeMemory.java new file mode 100644 index 0000000..c95476d --- /dev/null +++ b/test/java/RuntimeMemory.java @@ -0,0 +1,16 @@ +// Runtime's memory figures follow what the program holds. +public class RuntimeMemory { + static long used(Runtime rt) { + return rt.totalMemory() - rt.freeMemory(); + } + + public static void main(String[] args) { + Runtime rt = Runtime.getRuntime(); + long before = used(rt); + long[] held = new long[8 << 20]; + long after = used(rt); + System.out.println("used grows by 32 MB holding 64 MB " + (after - before >= 32 << 20)); + System.out.println("free <= total <= max " + (rt.freeMemory() <= rt.totalMemory() && rt.totalMemory() <= rt.maxMemory())); + System.out.println(held.length); + } +}