Skip to content
Draft
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
423 changes: 423 additions & 0 deletions docs/plans/2026-07-02-fork-instrument-size-overhead-eval.md

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions test-runs/kd-ok55/build-tool.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
=== build restart at Thu Jul 2 05:19:24 EDT 2026 with DEVELOPER_DIR=/Library/Developer/CommandLineTools ===
==> Building wasm-fork-instrument for aarch64-apple-darwin...
Compiling proc-macro2 v1.0.106
Compiling serde_core v1.0.228
Compiling quote v1.0.45
Compiling serde v1.0.228
Compiling anyhow v1.0.102
Compiling clap_builder v4.6.0
Compiling syn v2.0.117
Compiling indexmap v2.14.0
Compiling hashbrown v0.16.1
Compiling gimli v0.32.3
Compiling wasmparser v0.245.1
Compiling clap_derive v4.6.1
Compiling walrus-macro v0.26.0
Compiling clap v4.6.1
Compiling walrus v0.26.1
Compiling fork-instrument v0.1.0 (/Users/brandon/src/kandelo-gascity/worktrees/kandelo/kd-ok55-evaluate-fork-instrumentation-size-overhead-42-of-perl.w/crates/fork-instrument)
Finished `release` profile [optimized] target(s) in 13.63s
==> Installed /Users/brandon/src/kandelo-gascity/worktrees/kandelo/kd-ok55-evaluate-fork-instrumentation-size-overhead-42-of-perl.w/tools/bin/wasm-fork-instrument
=== build done at Thu Jul 2 05:19:37 EDT 2026 ===
169 changes: 169 additions & 0 deletions test-runs/kd-ok55/measure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Fork-instrumentation size-overhead measurement harness (kd-ok55).

Compiles raw wasm inputs (hand-authored .wat test fixtures + synthetic
scaling modules), runs `wasm-fork-instrument` on each, and records:
raw_bytes, instrumented_bytes, delta, %-of-instrumented,
fork-path function count (--discover-only), and Code-section size
before/after (wasm-objdump -h).

Everything runs on host; no package build required. Emits a TSV to
stdout and a formatted table to stderr.
"""
import os, re, subprocess, sys, tempfile, json

REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
TOOL = os.path.join(REPO, "tools", "bin", "wasm-fork-instrument")
FIXDIR = os.path.join(REPO, "crates", "fork-instrument", "tests", "fixtures")
OUT = os.path.join(REPO, "test-runs", "kd-ok55")
WORK = os.path.join(OUT, "work")
os.makedirs(WORK, exist_ok=True)

ENV = dict(os.environ, DEVELOPER_DIR="/Library/Developer/CommandLineTools")

def sh(cmd, **kw):
return subprocess.run(cmd, capture_output=True, text=True, env=ENV, **kw)

def wat2wasm(wat_path, wasm_path):
r = sh(["wat2wasm", wat_path, "-o", wasm_path])
if r.returncode != 0:
r = sh(["wat2wasm", "--enable-all", wat_path, "-o", wasm_path])
if r.returncode != 0:
raise RuntimeError(f"wat2wasm failed for {wat_path}: {r.stderr.strip()}")

def instrument(raw, inst):
r = sh([TOOL, raw, "-o", inst])
if r.returncode != 0:
raise RuntimeError(f"instrument failed for {raw}: {r.stderr.strip()}")

def fork_path_count(raw):
r = sh([TOOL, raw, "--discover-only"])
if r.returncode != 0:
return None
try:
return json.loads(r.stdout)["count"]
except Exception:
return None

SEC_RE = re.compile(r"^\s*(\w+)\s+start=0x[0-9a-f]+\s+end=0x[0-9a-f]+\s+\(size=0x([0-9a-f]+)\)(?:\s+count:\s*(\d+))?")

def sections(wasm):
"""Return {section_name: (size_bytes, count_or_None)} from wasm-objdump -h.
'Custom' rows are keyed by their quoted name so they don't collide."""
r = sh(["wasm-objdump", "-h", wasm])
out = {}
for line in r.stdout.splitlines():
m = SEC_RE.match(line)
if not m:
continue
name = m.group(1)
if name == "Custom":
q = re.search(r'"([^"]+)"', line)
name = f"Custom:{q.group(1)}" if q else "Custom"
size = int(m.group(2), 16)
cnt = int(m.group(3)) if m.group(3) else None
out[name] = (size, cnt)
return out

# ---- synthetic generators (mirror crates/.../tests/large_dispatcher.rs) ----
def wat_direct_dispatcher(n):
body = "".join(" call $fork\n" + (" drop\n" if i + 1 < n else "")
for i in range(n))
return f'''(module
(import "kernel" "kernel_fork" (func $fork (result i32)))
(func $dispatcher (export "dispatcher") (result i32)
{body} )
(memory 1))
'''

def wat_many_functions(m):
fns = "".join(f' (func $f{i} (export "f{i}") (result i32) call $fork)\n'
for i in range(m))
return f'''(module
(import "kernel" "kernel_fork" (func $fork (result i32)))
{fns} (memory 1))
'''

def measure(label, raw_wasm):
inst = os.path.join(WORK, os.path.basename(raw_wasm).replace(".wasm", ".inst.wasm"))
instrument(raw_wasm, inst)
raw_b = os.path.getsize(raw_wasm)
inst_b = os.path.getsize(inst)
delta = inst_b - raw_b
pct_inst = 100.0 * delta / inst_b if inst_b else 0.0
pct_raw = 100.0 * delta / raw_b if raw_b else 0.0
fpc = fork_path_count(raw_wasm)
sraw = sections(raw_wasm)
sinst = sections(inst)
code_raw = sraw.get("Code", (0, None))
code_inst = sinst.get("Code", (0, None))
return dict(label=label, raw=raw_b, inst=inst_b, delta=delta,
pct_inst=pct_inst, pct_raw=pct_raw, fpc=fpc,
code_raw=code_raw[0], code_inst=code_inst[0],
nfuncs_raw=(sraw.get("Function", (0, None))[1] or 0)
+ (sraw.get("Import", (0, None))[1] or 0),
nfuncs_inst=(sinst.get("Function", (0, None))[1] or 0),
sraw=sraw, sinst=sinst)

rows = []

# Group A: real hand-authored raw fixtures (import kernel_fork, no wpk_* yet)
fixtures = []
for root, _, files in os.walk(FIXDIR):
for fn in sorted(files):
if not fn.endswith(".wat"):
continue
p = os.path.join(root, fn)
txt = open(p).read()
if "kernel_fork" in txt and "wpk_fork_" not in txt:
fixtures.append(p)
for p in sorted(fixtures):
name = os.path.relpath(p, FIXDIR)
w = os.path.join(WORK, name.replace("/", "__").replace(".wat", ".wasm"))
wat2wasm(p, w)
rows.append(("fixture", measure(name, w)))

# Group B: synthetic direct-dispatcher (single fn, N fork call-sites)
for n in (1, 10, 100, 1000):
wat = os.path.join(WORK, f"dispatcher_n{n}.wat")
open(wat, "w").write(wat_direct_dispatcher(n))
w = wat.replace(".wat", ".wasm"); wat2wasm(wat, w)
rows.append(("dispatcher", measure(f"dispatcher_n{n} (1 fn, {n} fork-calls)", w)))

# Group C: synthetic many-functions (M fns each 1 fork call)
for m in (1, 10, 100, 1000):
wat = os.path.join(WORK, f"manyfn_m{m}.wat")
open(wat, "w").write(wat_many_functions(m))
w = wat.replace(".wat", ".wasm"); wat2wasm(wat, w)
rows.append(("manyfn", measure(f"manyfn_m{m} ({m} fork-path fns)", w)))

# ---- output TSV ----
tsv = os.path.join(OUT, "measurements.tsv")
with open(tsv, "w") as f:
f.write("group\tlabel\traw_bytes\tinst_bytes\tdelta_bytes\tpct_of_inst\tpct_of_raw\tforkpath_fns\tnfuncs_raw\tnfuncs_inst\tcode_raw\tcode_inst\tcode_delta\n")
for grp, r in rows:
f.write(f"{grp}\t{r['label']}\t{r['raw']}\t{r['inst']}\t{r['delta']}\t"
f"{r['pct_inst']:.1f}\t{r['pct_raw']:.1f}\t{r['fpc']}\t"
f"{r['nfuncs_raw']}\t{r['nfuncs_inst']}\t{r['code_raw']}\t"
f"{r['code_inst']}\t{r['code_inst']-r['code_raw']}\n")

# ---- pretty table to stderr ----
def p(*a): print(*a, file=sys.stderr)
p(f"\n{'label':<42} {'raw':>8} {'inst':>8} {'delta':>8} {'%inst':>6} {'fp':>4} {'code_raw':>8} {'code_inst':>9} {'code_Δ':>8}")
p("-" * 110)
for grp, r in rows:
p(f"{r['label']:<42} {r['raw']:>8} {r['inst']:>8} {r['delta']:>8} "
f"{r['pct_inst']:>5.1f}% {str(r['fpc']):>4} {r['code_raw']:>8} "
f"{r['code_inst']:>9} {r['code_inst']-r['code_raw']:>8}")

# section breakdown for one representative + largest synthetic
p("\n=== Section breakdown (bytes): dispatcher_n1000 raw vs instrumented ===")
for grp, r in rows:
if r['label'].startswith("dispatcher_n1000"):
keys = sorted(set(r['sraw']) | set(r['sinst']))
p(f"{'section':<20} {'raw':>10} {'inst':>10} {'delta':>10}")
for k in keys:
a = r['sraw'].get(k, (0, None))[0]
b = r['sinst'].get(k, (0, None))[0]
p(f"{k:<20} {a:>10} {b:>10} {b-a:>10}")
print(f"TSV written: {tsv}")
21 changes: 21 additions & 0 deletions test-runs/kd-ok55/measurements.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
group label raw_bytes inst_bytes delta_bytes pct_of_inst pct_of_raw forkpath_fns nfuncs_raw nfuncs_inst code_raw code_inst code_delta
fixture switch_dispatch/carryover_at_subregion.wat 93 625 532 85.1 572.0 2 2 6 22 227 205
fixture switch_dispatch/direct_call_carryover_in_block.wat 113 819 706 86.2 624.8 3 3 7 35 414 379
fixture switch_dispatch/guard_dispatch_non_fork_call.wat 118 647 529 81.8 448.3 2 3 6 24 226 202
fixture switch_dispatch/nested_fork_call.wat 120 672 552 82.1 460.0 2 3 6 26 251 225
fixture switch_dispatch/posix_spawn_class.wat 160 696 536 77.0 335.0 2 3 6 43 255 212
fixture switch_dispatch/top_level_carryover.wat 114 838 724 86.4 635.1 3 3 7 36 433 397
fixture switch_dispatch/waitpid_class.wat 113 618 505 81.7 446.9 2 3 6 19 197 178
fixture trampoline/legacy_try_fork.wat 102 630 528 83.8 517.6 2 2 6 22 227 205
fixture trampoline/nested_call_indirect.wat 128 789 661 83.8 516.4 3 3 7 41 375 334
fixture trampoline/nested_carryover_in_loop.wat 135 907 772 85.1 571.9 3 3 7 57 502 445
fixture trampoline/nested_multivalue_params.wat 98 662 564 85.2 575.5 2 2 6 21 258 237
fixture trampoline/top_level_carryover.wat 114 838 724 86.4 635.1 3 3 7 36 433 397
dispatcher dispatcher_n1 (1 fn, 1 fork-calls) 72 555 483 87.0 670.8 2 2 6 6 163 157
dispatcher dispatcher_n10 (1 fn, 10 fork-calls) 99 808 709 87.7 716.2 2 2 6 33 416 383
dispatcher dispatcher_n100 (1 fn, 100 fork-calls) 371 3486 3115 89.4 839.6 2 2 6 304 3094 2790
dispatcher dispatcher_n1000 (1 fn, 1000 fork-calls) 3071 30456 27385 89.9 891.7 2 2 6 3004 30063 27059
manyfn manyfn_m1 (1 fork-path fns) 64 547 483 88.3 754.7 2 2 6 6 163 157
manyfn manyfn_m10 (10 fork-path fns) 163 1628 1465 90.0 898.8 11 11 15 51 1189 1138
manyfn manyfn_m100 (100 fork-path fns) 1245 12554 11309 90.1 908.4 101 101 105 501 11485 10984
manyfn manyfn_m1000 (1000 fork-path fns) 13822 124142 110320 88.9 798.1 1001 1001 1005 5002 114986 109984
25 changes: 25 additions & 0 deletions test-runs/kd-ok55/policy-evidence.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
== disabled-policy packages ==
packages/registry/node-vfs/package.toml:27:fork_instrumentation = "disabled"
packages/registry/node/package.toml:21:fork_instrumentation = "disabled"
packages/registry/spidermonkey/package.toml:63:fork_instrumentation = "disabled"
packages/registry/spidermonkey-node/package.toml:21:fork_instrumentation = "disabled"

== direct $FORK_INSTRUMENT / run-wasm-fork-instrument.sh callers in build scripts ==
packages/registry/coreutils/build-coreutils.sh:255: FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/bash/build-bash.sh:406:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/dash/build-dash.sh:162:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/git/build-git.sh:99:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/dinit/build-dinit.sh:203:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/lsof/build-lsof.sh:43:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/modeset/build-modeset.sh:36:"$REPO_ROOT/scripts/run-wasm-fork-instrument.sh" \
packages/registry/nginx/build-nginx-local.sh:405:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/netcat/build-netcat.sh:110:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/php/build-php.sh:331:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/ruby/build-ruby.sh:1129:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/vim/build-vim.sh:53:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/sqlite/build-testfixture.sh:280:FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"
packages/registry/tcl/build-tcl.sh:168: FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh"

== perl fork config + install (auto-policy path) ==
307: -Dd_fork=define \
531:install_local_binary perl "$SCRIPT_DIR/bin/perl.wasm"
39 changes: 39 additions & 0 deletions test-runs/kd-ok55/real-runtime-findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Real-runtime fork-instrumentation measurements (kd-ok55)

Tool: tools/bin/wasm-fork-instrument (built from convoy base, aarch64-apple-darwin).
Method: `--discover-only` for fork-path count; `wasm-objdump -h` for section/function counts.
Inputs: shipped instrumented binaries in /Users/brandon/src/kandelo/packages/registry/*/bin/
(read-only, primary_checkout_exception=read-only recorded on bead); ruby raw/stripped from
sibling worktree kd-drt.9 (read-only).

## Selection ratio (fraction of ALL functions instrumented) — shipped binaries
| runtime | defined funcs | instrumented F | F/defined |
|---|--:|--:|--:|
| bash | 1800 | 1445 | 80.3% |
| coreutils | 3351 | 2313 | 69.0% |
| vim | 4423 | 3489 | 78.9% |
| git | 5071 | 4417 | 87.1% |
| ruby | 7183 | 6238 | 86.8% |
| php | 23475 | 18228 | 77.6% |
| php-fpm | 23699 | 18401 | 77.6% |

=> The fork-path reverse-reachability closure captures ~69-87% of every program.
This, not per-function stub inefficiency, is the primary size driver.

## Clean single-step CODE-section delta on real interpreter (ruby, stripped `roots` build)
raw code = 5,946,789 B (data 3,671,022 B, 9299 funcs)
instrumented = 15,858,681 B (data 3,671,022 B unchanged, 9304 funcs, 5 wpk exports)
CODE delta = +9,911,892 B = +166.7% of raw code
instrumented F = 7,681 => ~1,290 B per instrumented function (no-cflags build; upper bound)
Data unchanged => overhead is ~100% code.

## Reference (kd-lfas, shipped optimized perl)
raw 4,232,959 B -> instrumented 7,291,919 B (+3,058,960 B, +72% of raw, 42% of shipped)
instrumented code section = 5,477,124 B (=> raw code ~2.42 MB, +127% code growth).

## Cost model (triangulated: analytical + synthetic + real ruby)
bytes(f) ~= A + B*callsites + C*scalar_locals + C_ref*ref_locals + Sarg
A ~= 110 B (fixed preamble/postamble/dispatch boilerplate) -- ~9% of real per-fn cost
B ~= 26-27 B per fork-path call site
C ~= 20 B per scalar local <-- DOMINANT term for real local-heavy C functions
Module floor ~= 483 B (5 wpk_fork_* funcs + 2 globals + exports).
8 changes: 8 additions & 0 deletions test-runs/kd-ok55/real-selection.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
binary size_B defined_funcs forkpath_count instrumented(F) F/defined_%
bash 2650187 1800 1446 1445 80.3
coreutils 3948279 3351 2314 2313 69.0
vim 6994969 4423 3490 3489 78.9
git 8206340 5071 4418 4417 87.1
ruby 16879420 7183 6239 6238 86.8
php 36890652 23475 18229 18228 77.6
php-fpm 37123997 23699 18402 18401 77.6
57 changes: 57 additions & 0 deletions test-runs/kd-ok55/section-dumps.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
### nested_fork_call.wat (fixture) RAW

switch_dispatch__nested_fork_call.wasm: file format wasm 0x1

Sections:

Type start=0x0000000a end=0x00000015 (size=0x0000000b) count: 2
Import start=0x00000017 end=0x0000003e (size=0x00000027) count: 2
Function start=0x00000040 end=0x00000042 (size=0x00000002) count: 1
Memory start=0x00000044 end=0x00000047 (size=0x00000003) count: 1
Export start=0x00000049 end=0x0000005c (size=0x00000013) count: 2
Code start=0x0000005e end=0x00000078 (size=0x0000001a) count: 1

### nested_fork_call INSTRUMENTED

switch_dispatch__nested_fork_call.inst.wasm: file format wasm 0x1

Sections:

Type start=0x0000000a end=0x0000001c (size=0x00000012) count: 4
Import start=0x0000001e end=0x00000045 (size=0x00000027) count: 2
Function start=0x00000047 end=0x0000004e (size=0x00000007) count: 6
Memory start=0x00000050 end=0x00000053 (size=0x00000003) count: 1
Global start=0x00000055 end=0x00000060 (size=0x0000000b) count: 2
Export start=0x00000063 end=0x000000e3 (size=0x00000080) count: 7
Code start=0x000000e6 end=0x000001e1 (size=0x000000fb) count: 6
Custom start=0x000001e4 end=0x00000277 (size=0x00000093) "name"
Custom start=0x00000279 end=0x000002a0 (size=0x00000027) "producers"

### manyfn_m1000 RAW

manyfn_m1000.wasm: file format wasm 0x1

Sections:

Type start=0x0000000a end=0x0000000f (size=0x00000005) count: 1
Import start=0x00000011 end=0x00000027 (size=0x00000016) count: 1
Function start=0x0000002a end=0x00000414 (size=0x000003ea) count: 1000
Memory start=0x00000416 end=0x00000419 (size=0x00000003) count: 1
Export start=0x0000041c end=0x00002271 (size=0x00001e55) count: 1000
Code start=0x00002274 end=0x000035fe (size=0x0000138a) count: 1000

### manyfn_m1000 INSTRUMENTED

manyfn_m1000.inst.wasm: file format wasm 0x1

Sections:

Type start=0x0000000a end=0x00000016 (size=0x0000000c) count: 3
Import start=0x00000018 end=0x0000002e (size=0x00000016) count: 1
Function start=0x00000031 end=0x00000420 (size=0x000003ef) count: 1005
Memory start=0x00000422 end=0x00000425 (size=0x00000003) count: 1
Global start=0x00000427 end=0x00000432 (size=0x0000000b) count: 2
Export start=0x00000435 end=0x000022fc (size=0x00001ec7) count: 1005
Code start=0x00002300 end=0x0001e42a (size=0x0001c12a) count: 1005
Custom start=0x0001e42d end=0x0001e4c5 (size=0x00000098) "name"
Custom start=0x0001e4c7 end=0x0001e4ee (size=0x00000027) "producers"
35 changes: 35 additions & 0 deletions test-runs/kd-ok55/table.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

label raw inst delta %inst fp code_raw code_inst code_Δ
--------------------------------------------------------------------------------------------------------------
switch_dispatch/carryover_at_subregion.wat 93 625 532 85.1% 2 22 227 205
switch_dispatch/direct_call_carryover_in_block.wat 113 819 706 86.2% 3 35 414 379
switch_dispatch/guard_dispatch_non_fork_call.wat 118 647 529 81.8% 2 24 226 202
switch_dispatch/nested_fork_call.wat 120 672 552 82.1% 2 26 251 225
switch_dispatch/posix_spawn_class.wat 160 696 536 77.0% 2 43 255 212
switch_dispatch/top_level_carryover.wat 114 838 724 86.4% 3 36 433 397
switch_dispatch/waitpid_class.wat 113 618 505 81.7% 2 19 197 178
trampoline/legacy_try_fork.wat 102 630 528 83.8% 2 22 227 205
trampoline/nested_call_indirect.wat 128 789 661 83.8% 3 41 375 334
trampoline/nested_carryover_in_loop.wat 135 907 772 85.1% 3 57 502 445
trampoline/nested_multivalue_params.wat 98 662 564 85.2% 2 21 258 237
trampoline/top_level_carryover.wat 114 838 724 86.4% 3 36 433 397
dispatcher_n1 (1 fn, 1 fork-calls) 72 555 483 87.0% 2 6 163 157
dispatcher_n10 (1 fn, 10 fork-calls) 99 808 709 87.7% 2 33 416 383
dispatcher_n100 (1 fn, 100 fork-calls) 371 3486 3115 89.4% 2 304 3094 2790
dispatcher_n1000 (1 fn, 1000 fork-calls) 3071 30456 27385 89.9% 2 3004 30063 27059
manyfn_m1 (1 fork-path fns) 64 547 483 88.3% 2 6 163 157
manyfn_m10 (10 fork-path fns) 163 1628 1465 90.0% 11 51 1189 1138
manyfn_m100 (100 fork-path fns) 1245 12554 11309 90.1% 101 501 11485 10984
manyfn_m1000 (1000 fork-path fns) 13822 124142 110320 88.9% 1001 5002 114986 109984

=== Section breakdown (bytes): dispatcher_n1000 raw vs instrumented ===
section raw inst delta
Code 3004 30063 27059
Custom:name 0 147 147
Custom:producers 0 39 39
Export 14 123 109
Function 2 7 5
Global 0 11 11
Import 22 22 0
Memory 3 3 0
Type 5 12 7
Loading
Loading