diff --git a/src/interp.ts b/src/interp.ts index d285417..5cd3dae 100644 --- a/src/interp.ts +++ b/src/interp.ts @@ -4,11 +4,11 @@ import { INITIALIZED, JArray, JClass, JMethod, JObject, isWide, longToFloat, newArrayData, } from "./runtime.ts"; import { - Frame, JThread, JavaThrow, NativeFrame, PendingThrow, RUNNABLE, TERMINATED, WAITING, fromNative, + Frame, JThread, JavaThrow, MAX_DIRECT, NativeFrame, PendingThrow, RUNNABLE, TERMINATED, WAITING, fromNative, } from "./thread.ts"; import type { NativeGen } from "./thread.ts"; import type { JVM } from "./jvm.ts"; -import { compileIfHot, hooks, noteBackedge } from "./jit.ts"; +import { INTERPRET, PUSHED, compileIfHot, hooks, noteBackedge } from "./jit.ts"; const MAX_FRAMES = 6000; @@ -86,6 +86,7 @@ Object.assign(hooks, { popFrame: (t: JThread, f: Frame) => popFrame(t, f), findHandler: findHandlerAt, checkStack: (t: JThread) => checkStack(t), + callNested, }); export function interpret(t: JThread, f: Frame, budget: number): number { @@ -290,16 +291,19 @@ export function interpret(t: JThread, f: Frame, budget: number): number { case 0xac: case 0xae: case 0xb0: v = stack[sp - 1]; popFrame(t, f); - deliver(t, v, m.ret, false); + if (f.direct) t.ret = v; + else deliver(t, v, m.ret, false); return budget; case 0xad: case 0xaf: v = stack[sp - 2]; popFrame(t, f); - deliver(t, v, m.ret, false); + if (f.direct) t.ret = v; + else deliver(t, v, m.ret, false); return budget; case 0xb1: popFrame(t, f); - deliver(t, undefined, "V", false); + if (f.direct) t.ret = undefined; + else deliver(t, undefined, "V", false); return budget; // fields case 0xb2: case 0xb3: { @@ -633,6 +637,8 @@ export function raise(t: JThread, exc: JObject) { return; } popFrame(t, f); + // the rest of the stack belongs to the compiled code that called f + if (f.direct) throw new JavaThrow(exc); } } @@ -697,6 +703,47 @@ export function handleError(t: JThread, e: unknown) { else t.vm.internalError(t, e); } +// Interprets a call from compiled code nested on the JS stack, as a direct +// call runs, so the caller need not spill. Returns m's result, PUSHED when +// the call must finish in the run loop (its frames stay and the caller +// spills), or INTERPRET when the run loop must make the call. +function callNested(t: JThread, m: JMethod, args: any[]): any { + if (m.isSynchronized || t.frames.length - t.base >= MAX_DIRECT || t.frames.length >= t.stackLimit) return INTERPRET; + if (m.jit === null && !m.noJit) compileIfHot(t, m); + const f = new Frame(m); + const locals = f.locals; + let slot = 0, i = 0; + if (!m.isStatic) locals[slot++] = args[i++]; + for (const p of m.params) { + locals[slot] = args[i++]; + slot += isWide(p) ? 2 : 1; + } + f.direct = true; + t.frames.push(f); + const depth = t.frames.length; + for (;;) { + if (t.frames.length < depth) return t.ret; + const top = t.top; + if (!f.direct || t.slice <= 0 || t.state !== RUNNABLE || top instanceof NativeFrame || top.needLock) { + f.direct = false; + return PUSHED; + } + const jit = top.m.jit; + if (jit === null) t.slice = interpret(t, top, t.slice); + else { + const n = t.frames.length; + try { + jit(t, top); + } catch (e) { + if (t.frames.length < depth) throw e; + handleError(t, e); + } + // a call that returned is cheap; one left part done used up its own budget + t.slice -= t.frames[n - 1] === top ? 4000 : 1; + } + } +} + // Feeds a native frame its pending input and acts on what it yields. function stepNative(t: JThread, f: NativeFrame) { let r: IteratorResult; @@ -759,7 +806,7 @@ function stepNative(t: JThread, f: NativeFrame) { const entryStats = typeof process !== "undefined" && process.env?.JW_JIT_STATS ? new Map() : null; if (entryStats) process.on("exit", () => { - console.error("-- run loop entries into compiled code"); + console.error("-- run loop entries into compiled code", [...entryStats.values()].reduce((a, b) => a + b, 0)); for (const [k, v] of [...entryStats].sort((a, b) => b[1] - a[1]).slice(0, 25)) console.error(String(v).padStart(8), k); }); @@ -794,12 +841,13 @@ export function runThread(t: JThread, budget: number) { if (jit !== null) { if (entryStats) countEntry(t, f); t.base = t.frames.length; + t.slice = budget; try { jit(t, f); } catch (e) { handleError(t, e); } - budget -= 4000; + budget = t.slice - 4000; } else budget = interpret(t, f, budget); } if (t.frames.length === 0 && t.state !== TERMINATED) terminate(t); diff --git a/src/jit.ts b/src/jit.ts index 769c29c..4c4dbb0 100644 --- a/src/jit.ts +++ b/src/jit.ts @@ -48,6 +48,7 @@ export const hooks = { popFrame: null as unknown as (t: JThread, f: Frame) => void, findHandler: null as unknown as (t: JThread, f: Frame, pc: number, exc: JObject) => number, checkStack: null as unknown as (t: JThread) => void, + callNested: null as unknown as (t: JThread, m: JMethod, args: any[]) => any, }; let disabled = typeof process !== "undefined" && !!process.env?.JW_NOJIT; @@ -129,7 +130,7 @@ function at(e: PendingThrow, pc: number | undefined): PendingThrow { } // results of runtime.slowCall -const PUSHED = Object.freeze({ pushed: true }), INTERPRET = Object.freeze({ interpret: true }); +export const PUSHED = Object.freeze({ pushed: true }), INTERPRET = Object.freeze({ interpret: true }); const STRING_BUILDER = "java/lang/StringBuilder"; @@ -189,10 +190,12 @@ const runtime = { f.sp = sp; 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. + // A call that is not direct: the result of a native or an interpreted + // method, PUSHED once the callee's frames must finish in the run loop, or + // INTERPRET for the run loop to make the call. slowCall(t: JThread, m: JMethod, args: any[]): any { - if (m.native === null || m.isAbstract) return INTERPRET; + if (m.isAbstract) return INTERPRET; + if (m.native === null) return m.code ? hooks.callNested(t, m, args) : INTERPRET; let r; try { r = m.native(t, args); @@ -580,7 +583,7 @@ function generate(t: JThread, m: JMethod): { src: string; consts: any[] } { const out: string[] = []; // direct calls pass arguments as parameters and a frame without arrays out.push(`return function ${fname}(t,f${params.map((p) => "," + p).join("")}){let L=f.locals,S=f.stack;const vm=t.vm,H=rt.hooks;`); - out.push(`let pc=f.pc,bud=4000,tm,cf,r,a,v,ic,ifs=0,dpc=0,dsp=0,x=0,xs=0,xe=null,${[...g.vars].join(",")};`); + out.push(`let pc=f.pc,tm,cf,r,a,v,ic,ifs=0,dpc=0,dsp=0,x=0,xs=0,xe=null,${[...g.vars].join(",")};`); out.push(`if(L===null){${params.map((p, i) => `${L(i)}=${p};`).join("")}}else{${Array.from({ length: nl }, (_, i) => `${L(i)}=L[${i}];`).join("")}${Array.from({ length: ns }, (_, i) => `${S(i)}=S[${i}];`).join("")}}`); out.push(`X:{for(;;){try{switch(pc){`); out.push(...top.out); @@ -688,7 +691,9 @@ function insn(g: Gen, b: Body, pc: number) { const top = (k = 0) => S(d - 1 - k); const jump = (target: number) => { if (region) return `{${region.pcVar}=${target};continue R${region.id};}`; - if (target <= pc) return `{if(--bud===0)${g.exit(target, b.a.depth.get(target)!, EXIT_SUSPEND)}pc=${target};continue;}`; + // loops spend the thread's slice wherever they are on the stack, so a loop + // calling others' loops still yields + if (target <= pc) return `{if(--t.slice<=0)${g.exit(target, b.a.depth.get(target)!, EXIT_SUSPEND)}pc=${target};continue;}`; return `{pc=${target};continue;}`; }; const idx2 = u16(c, pc + 1); diff --git a/src/thread.ts b/src/thread.ts index 80227d3..59e1cee 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -102,7 +102,7 @@ export const STATUS_SLEEPING = 0x00e1, STATUS_WAITING = 0x0191, STATUS_TIMED_WAI 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; +export const MAX_DIRECT = 120; let nextId = 1; @@ -129,6 +129,9 @@ export class JThread { // run loop last entered compiled code ret: any = undefined; base = 0; + // what is left of the run loop's budget while compiled code runs: its + // loops and the calls it interprets in place spend it + slice = 0; // wakes the thread early from sleep, park or wait, for interrupt/unpark wake: (() => void) | null = null; // set when the bottom frame finishes diff --git a/test/java/NestedCalls.java b/test/java/NestedCalls.java new file mode 100644 index 0000000..e105f6d --- /dev/null +++ b/test/java/NestedCalls.java @@ -0,0 +1,140 @@ +import java.util.Arrays; + +// Compiled code calling methods still too cold to compile: results of each +// type, exceptions caught on either side with their stack traces, monitors, +// recursion, stack overflow and a callee that waits on another thread. Each +// callee calls pad, so it is too big to be inlined. +public class NestedCalls { + static int counter, noise; + static final Object LOCK = new Object(); + + static void pad(int x) { + noise += x * 3 + (x >>> 2) - (x ^ 5) + (x | 9) + (x & 0x77) + x % 13 + x / 7 + (x << 3) - (x >> 1); + } + + static int i(int x) { pad(x); pad(x + 1); return x + 1; } + static long j(long x) { pad((int) x); pad(1); return x * 3; } + static double d(double x) { pad((int) x); pad(2); return x / 2; } + static boolean z(int x) { pad(x); pad(3); return (x & 1) == 0; } + static Object a(int x) { pad(x); pad(4); return x % 3 == 0 ? null : new int[] {x}; } + static void v() { pad(5); pad(6); counter++; } + + static int throwsOut(int x) { + pad(x); + pad(7); + if (x % 7 == 0) throw new IllegalStateException("x" + x); + return x; + } + + static int catchesInside(int x) { + try { + return throwsOut(x); + } catch (IllegalStateException e) { + return -1; + } + } + + static int npe(int[] a) { pad(8); pad(9); return a[0]; } + + // cold, called by compiled hotCaller, which is called by cold outerCatch: + // its exception passes compiled code to reach outerCatch's handler + static int coldThrow(int x) { + pad(x); + pad(10); + if (x < 0) throw new IllegalArgumentException("neg " + x); + return x; + } + + static int hotCaller(int x, boolean call) { + pad(x); + pad(11); + return call ? coldThrow(x) : x; + } + + static int outerCatch(int x) { + pad(x); + try { + return hotCaller(x, true); + } catch (IllegalArgumentException e) { + return e.getMessage().length() - 100; + } + } + + static int locked(int x) { + synchronized (LOCK) { + return x * 2; + } + } + + static synchronized int syncMethod(int x) { return x - 1; } + + static int depth(int n) { return n == 0 ? 0 : 1 + depth(n - 1); } + + static int forever(int n) { return forever(n + 1) + 1; } + + static long sum(int n) { + long s = 0; + for (int k = 0; k < n; k++) s += i(k) + depth(k & 15); + return s; + } + + static int joined(int x) throws InterruptedException { + Thread t = new Thread(() -> counter += 1000); + t.start(); + t.join(); + return x; + } + + static String where(Throwable e) { + StackTraceElement[] st = e.getStackTrace(); + return st[0].getMethodName() + ":" + st[0].getLineNumber() + " < " + st[1].getMethodName() + ":" + st[1].getLineNumber(); + } + + // compiled by its loop after the first pass resolves the calls and before + // the others start + static void run() throws InterruptedException { + long acc = 0; + double dacc = 0; + int thrown = 0, inside = 0, nulls = 0; + String firstThrow = null, firstNpe = null; + for (int k = 0; k < 3000; k++) { + acc += k & 3; + if (k != 0 && (k < 2000 || k >= 2300)) continue; + acc += i(k) + j(k) + (z(k) ? 1 : 0) + locked(k) + syncMethod(k) + depth(k & 31); + dacc += d(k); + v(); + Object o = a(k); + if (o == null) nulls++; + try { + acc += throwsOut(k); + } catch (IllegalStateException e) { + thrown++; + if (firstThrow == null) firstThrow = e.getMessage() + " at " + where(e); + } + if (catchesInside(k) < 0) inside++; + try { + acc += npe(k % 50 == 0 ? null : new int[] {k}); + } catch (NullPointerException e) { + if (firstNpe == null) firstNpe = where(e); + } + if (k % 100 == 0) acc += joined(k); + if (k % 10 == 0) acc += outerCatch(k % 20 == 0 ? -k : k); + } + System.out.println(acc + " " + dacc + " " + counter + " " + thrown + " " + inside + " " + nulls + " " + noise); + System.out.println(firstThrow); + System.out.println(firstNpe); + System.out.println(sum(5000)); + try { + forever(0); + } catch (StackOverflowError e) { + System.out.println("StackOverflowError"); + } + System.out.println(Arrays.toString((int[]) a(4))); + } + + public static void main(String[] args) throws InterruptedException { + hotCaller(1, true); + for (int k = 0; k < 600; k++) hotCaller(k, false); + run(); + } +}