Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 55 additions & 7 deletions src/interp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
}
}

Expand Down Expand Up @@ -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<any, any>;
Expand Down Expand Up @@ -759,7 +806,7 @@ function stepNative(t: JThread, f: NativeFrame) {

const entryStats = typeof process !== "undefined" && process.env?.JW_JIT_STATS ? new Map<string, number>() : 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);
});

Expand Down Expand Up @@ -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);
Expand Down
17 changes: 11 additions & 6 deletions src/jit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion src/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down
140 changes: 140 additions & 0 deletions test/java/NestedCalls.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading