Transition microbenchmark (EMBED_BENCH) and a main-Worker-only profile

- T.CallBench, run by the embed program when EMBED_BENCH=N: ns per call to
  a tiny NoInlining callee in four arrangements:
  - same: caller and callee in the same tier;
  - to-refused: JIT code calling a method the wasm JIT refuses (it holds
    a calli that never runs);
  - from-refused: the reverse;
  - refused-refused.
  With GEXMONO_WASM_JIT=0, every leg is the interpreter's own call.
- browser-harness: GEX_NT_PROFILE_US sets the sampling interval, and
  GEX_NT_PROFILE_FIRST=N profiles only the first N matching Workers, with
  none attaching later. Sampling all thirty of Hollow Knight's Workers
  stalled its frames (hg10: 0 presents in the window).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_1896c5b302a54696aaa92c5f4047f20b
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-09 17:09:07 -04:00
1 parent 42f09b39a4
commit ce395febaf
3 files changed
+52 -2

No files matched your search

+12 -2
View File
@@ -323,13 +323,23 @@ export async function openPage(query, { width = 1280, height = 900, shotDir = nu
// Workers that start during the window (a game's threads) are profiled
// from when they attach.
const sids = [];
// GEX_NT_PROFILE_US: the sampling interval (default 250 us). GEX_NT_PROFILE_FIRST=N: only the
// first N matching Workers (a game's main Worker is the first), none that attach later --
// sampling thirty Workers at once stalls a game's frames.
const interval = Number(process.env.GEX_NT_PROFILE_US || 250);
const first = Number(process.env.GEX_NT_PROFILE_FIRST || 0);
const start = async () => {
for (const s of [...(withPage ? [page] : []), ...[...targets].filter(([, u]) => u.includes(match)).map(([x]) => x)]) {
let picked = [...targets].filter(([, u]) => u.includes(match)).map(([x]) => x);
if (first) {
if (sids.length) return;
picked = picked.slice(0, first);
}
for (const s of [...(withPage ? [page] : []), ...picked]) {
if (sids.includes(s)) continue;
sids.push(s);
try {
await browser.send('Profiler.enable', {}, s);
await browser.send('Profiler.setSamplingInterval', { interval: 250 }, s);
await browser.send('Profiler.setSamplingInterval', { interval }, s);
await browser.send('Profiler.start', {}, s);
} catch { /* gone already */ }
}
+8
View File
@@ -230,6 +230,14 @@ main (int argc, char **argv)
P clr = mono_runtime_invoke (clears, NULL, clargs, &exc);
check ("Clears", exc);
printf ("%s\n", mono_string_to_utf8 (clr));
if (getenv ("EMBED_BENCH")) { /* transition costs, for the bench box only */
P cb = mono_class_get_method_from_name (k, "CallBench", 1);
int cn = atoi (getenv ("EMBED_BENCH"));
void *cargs [1] = { &cn };
P cr = mono_runtime_invoke (cb, NULL, cargs, &exc);
check ("CallBench", exc);
fprintf (stderr, "%s\n", mono_string_to_utf8 (cr));
}
P dels = mono_class_get_method_from_name (k, "Delegates", 1);
int dn = 40;
void *dargs [1] = { &dn };
+32
View File
@@ -59,6 +59,38 @@ public static class T {
}
return "delegates " + sum + " " + tags;
}
// Call-transition costs (EMBED_BENCH): ns per call for a tiny callee, from a caller in the same
// tier, from JIT code into a method the wasm JIT refuses (it holds a calli, never taken), and from
// such a method into a JIT-compiled one. With GEXMONO_WASM_JIT=0 every leg is the interpreter's.
[MethodImpl(MethodImplOptions.NoInlining)] static int BInc(int x) { return x + 1; }
static unsafe delegate*<int, int> bnever = null;
[MethodImpl(MethodImplOptions.NoInlining)] static unsafe int BIncInterp(int x) { if (x == int.MinValue) return bnever(x); return x + 1; }
static int BLoopSame(int n) { int a = 0; for (int i = 0; i < n; i++) a = BInc(a); return a; }
static int BLoopToInterp(int n) { int a = 0; for (int i = 0; i < n; i++) a = BIncInterp(a); return a; }
static unsafe int BLoopFromInterp(int n) {
int a = 0;
if (n == int.MinValue) return bnever(n);
for (int i = 0; i < n; i++) a = BInc(a);
return a;
}
static unsafe int BLoopInterpInterp(int n) {
int a = 0;
if (n == int.MinValue) return bnever(n);
for (int i = 0; i < n; i++) a = BIncInterp(a);
return a;
}
public static string CallBench(int n) {
var sb = new System.Text.StringBuilder("callbench ns/call:");
foreach (var (name, f) in new (string, Func<int, int>)[] { ("same", BLoopSame), ("to-refused", BLoopToInterp), ("from-refused", BLoopFromInterp), ("refused-refused", BLoopInterpInterp) }) {
f(1000); f(1000); // past the JIT threshold, compiled
var sw = System.Diagnostics.Stopwatch.StartNew();
int r = f(n);
sw.Stop();
sb.Append($" {name} {sw.Elapsed.TotalMilliseconds * 1e6 / n:F1}");
if (r != n) sb.Append("(wrong)");
}
return sb.ToString();
}
public static object LvGraph(int n) {
var r = new LvRoot { arr = new object[n], pairs = new Pair[n], list = new List<Leaf>(), s = "not a leaf" };
for (int i = 0; i < n; i++) r.arr[i] = i % 2 == 0 ? new SubLeaf { id = i } : new Leaf { id = i };