Perf/stack zone#193
Open
agustincico wants to merge 36 commits into
Open
Conversation
Design for Cog-style stack zone (flat frames + lazy context reification) written against SqueakJS semantics (send/doReturn/transferTo/closures). Synthetic fib-by-sends benchmark isolates heap contexts vs flat frames and JS vs WASM: flat frames alone give 1.38x in pure JS (incremental path inside jit.js); frames + per-method WASM compilation give 2.7x (phase 2 target). A WASM bytecode interpreter alone is break-even. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runs ws/client/cuis.image headless with a fully virtualized environment (virtual Date/Date.now/performance.now, seeded Math.random, inert WebSocket) and accumulates an FNV-1a hash of the execution trace (sendCount/pc/sp/method oop per slice). Two runs of the same VM produce identical hashes (validated 6/6 over the full 3.6M-send Cuis startup), so any semantic divergence introduced by VM changes is caught by comparing against a recorded golden. --bench mode measures wall time on the same workload with the real clock: baseline 2.87M sends/s. golden.json is machine-specific (timezone offset enters the trace), so it is gitignored; each machine records its own with --golden. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The image creates its .changes file and debug logs next to itself, so a fresh clone's first run saw different filesystem state than subsequent runs. Delete those artifacts before every run (and gitignore them) so all runs start from the canonical state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sp is an index into context.pointers today but a zone index with the stack zone, so it (and scheduling-level slices/virtualMs) must not be part of the cross-representation oracle. Hash sendCount/method/pc only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanical refactor, groundwork for the stack zone: vm.stack is THE operand array (today activeContext.pointers, kept in sync at every context switch; a zone page once flat frames land). All stack helpers, arg copies, perform's stack slides, and jit-generated code now index vm.stack instead of reaching through activeContext. become refreshes vm.stack in case the active context's pointers array was swapped. No behavior change: differential trace hash is identical to the pre-refactor golden; bench unchanged (2.90M vs 2.87M sends/s, noise). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on-write) Mapping vm.primitives.js showed unwind prims 195-199 return false, so all exception handling walks contexts from Smalltalk code via sends — which means every image-level access to context fields goes through either a send with the context as receiver or a reflective primitive. That bounds the read-through surface to two chokepoints and allows a proxy-free v1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second mechanical groundwork refactor: interpretOne and jit-generated code now access temps as temps[tempOffset+n] instead of reaching through homeContext.pointers[6+n]. In context mode tempOffset stays 6 and vm.temps === homeContext.pointers (synced at the same points as vm.stack), so generated code is equivalent; in stack-zone mode temps will live in the frame at a per-activation base. Trace hash identical to golden; bench unchanged (2.80M sends/s, noise). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…validated New vm.stackzone.js implements the design in perf/stack-zone-design.md: sends push flat frames into growable zone pages; heap Contexts exist only as married snapshots (created on thisContext/closure creation/ process switch), synced when a married context is a send receiver or reflective-primitive target, widowed on frame return, and flushed wholesale on writes/become/snapshot. makeBaseFrame inflates dormant contexts; the interpreter never executes on heap contexts. Enabled per instance via options.stackZone; context mode is untouched (golden trace identical). Supporting changes: - interpretOne receiver inst-var stores routed through storeInstVar (incl. the doubleExtended 0x84 cases Cuis uses for Context>>sender/ terminate — missing those made unwind chain surgery invisible to live frames) - blockReturn bytecodes routed through doBlockReturn - context identity hashes drawn from a separate stream so ordinary objects get allocation-order-independent hashes across representations - tryPrimitive activation-change check uses page/fp in frames mode (an instance-level activeContext accessor was a 10x+ slowdown) - harness: --frames/--nojit flags, cadence-independent oracle sampling at fixed sendCount checkpoints, per-send fine logging, debug hooks Validation: full Cuis startup (3,398,634 sends, boot through WS timeout to quit) produces the identical trace hash as context mode without jit, zero divergent checkpoints. Bench (20M sends): frames-nojit 2.62M sends/s vs contexts-nojit 2.46M (+6.5%) vs contexts+jit 2.84M — the jit frames templates are the next milestone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated code in stack-zone mode detects activation changes by (fp, zonePage) instead of context identity, routes receiver inst-var stores through the married-context write-through (with a fresh vm.pc and a resume label, since a flush rebuilds the frame), and handles the at:put: quick prim's flush hazard by re-executing the bytecode idempotently. blockReturn and closure creation now use the mode-neutral doBlockReturn/activeContextObj in both modes (identical semantics in context mode). Frames mode compiles methods again (executeNewMethodZ and full-closure activation call compileIfPossible). Validation: frames+jit produces the IDENTICAL trace to the context-mode jit golden over the full Cuis startup (3.6M sends to quit) — the jit's trace-visible at:-cache fast path behaves the same in both modes. Context mode codegen is byte-identical to before (golden still passes). Bench (20M sends): frames+jit 2.99-3.06M sends/s vs contexts+jit 2.74-2.81M — ~9% faster on a primitive-heavy workload; send-heavy code should gain more (spike: 1.38x on pure send/return). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ates - squeak.js / squeak_headless.js import vm.stackzone.js; the run/ launcher's generic option parsing already forwards #stackZone from the URL fragment to the interpreter. squeak_node.js gets -stackZone. - syncMarriedContext replaces whole-page sync at the hot call sites (thisContext, married send receivers, reflective reads): syncing one frame is O(frame) for the page top and marries the caller shallowly instead of cascading down the chain. Cuis 6's ensure:-heavy code made the cascade pathological: 68% of JS time in syncPage, 5x slowdown. - married-context probes are gated by a class-identity compare before touching .frame, keeping megamorphic receivers off that load. - makeBaseFrame refuses Context subclasses (the gates compare against MethodContext identity; a married subclass would evade them). - zone stats + marriage-source counters reported by the harness bench. Validated on both image formats: V3 (cuis.image) golden identical in both modes; Spur 32 (Cuis6.2-32) context and frames traces identical. Bench: V3 frames+jit still +9%; Spur frames+jit 1.68M vs 2.05M sends/s (-18%, down from -83% before the sync fix) — remaining gap tracks the 154k closure-creation marriages per 10M sends and their GC pressure; known issue, next optimization target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New Squeak.Compiler2 compiles V3 methods to JS where operand-stack slots live in JS locals tracked at compile time, spilled to the zone only at send sites and reloaded on trampoline re-entry (a local `entering` flag distinguishes re-entry from internal jumps; reload depth is the label's static stack depth, which also covers mid-method interpreter-to-compiled transitions at loop heads). Monomorphic inline caches per send site bypass findSelectorInClass on class hit. Unsupported bytecodes bail the whole method to jit1. Bugs already caught by the differential oracle and fixed: resume labels past the last jump target were not being generated; closure creation marries the active frame so it must spill first (stale vm.sp corrupted married snapshots and GC roots); the IC send path skipped sendZ's married-context sync gate. One known semantic divergence remains (V3 block temp pattern in WorldState>>runStepMethods, first differs at send ~717679 of the Cuis startup), so jit2 is opt-in: options.jit2 / --jit2 in the harness. Frames mode without jit2 remains golden-identical. Perf preview (20M sends, primitive-heavy workload): frames+jit2 3.16M sends/s vs frames+jit1 2.85M vs contexts+jit1 2.77M (+14% over today's VM) — with 75% method coverage, no direct calls yet, on the least favorable workload. Send-heavy code should gain much more. Also: Dialogo.32bits.image (Cuis 4507, V3 6521) validated in the oracle: frames and contexts traces identical; bench at parity with a known flushAll storm (23k full flushes / 10M sends from married- context stores) to optimize. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the remaining divergence: in V3 blocks, temps beyond numArgs+numCopied are allocated as operand-stack slots (the pushNil prologue), so a block's storeTemp writes the same physical slot that jit2 maps to a JS local — the local kept the stale value and the next spill clobbered the stored one. Found by per-method bisection (JIT2SEL=div,rem) down to SharedQueue>>nextOrNil, whose dequeued item was overwritten with nil, making the World's event queue look empty. v1 fix: methods whose blocks access temps at or beyond their args+copied ceiling bail to jit1 (correct, loses ~125 methods; a pinned-zone-slot refinement can recover them later). Validation: frames+jit2 now produces the IDENTICAL trace to the context-mode golden over the full Cuis startup, and identical hashes to context mode on Dialogo.32bits.image. Bench (20M sends): contexts 2.78M / frames+jit1 2.87M / frames+jit2 3.16M sends/s (+14%), with 1370 methods compiled and correctness fully intact. Also adds JIT2SEL bisection switch and marriage-source counters to the harness debug tooling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
executeNewMethodZ and the closure activations now invoke the callee's compiled function as a plain JS call instead of returning to the trampoline. The JS stack never holds suspended state: every level returns as soon as its Smalltalk continuation leaves its frame (the existing activation checks), so a process switch unwinds the whole chain with one compare+return per level — no exceptions needed. Depth-capped at 512 for the JS stack limit. Because this lives in the activation path, interpreter, jit1 and jit2 senders all get it with zero template changes. Validation: golden-identical on the full Cuis startup, hashes identical on Dialogo. Perf: neutral on real workloads (startup wall 2.39s vs 2.39-2.50s; the trampoline hop was not the dominant send cost — frame materialization is). Kept because it is the structural prerequisite for frameless leaf calls (args as JS arguments, Cog-style frameless methods), which is where the send-heavy 2.5x actually lives. Also: -jit2 flag for squeak_node.js; the run/ launcher already forwards #jit2 from the URL fragment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Leaf-eligible methods (prim-less; pushes, temp/inst stores, smallint arithmetic/comparisons, ==/class, forward jumps, returns; no sends, no closures, no loops, no allocations — arithmetic overflow deopts instead of allocating so a deopt-restart never double-allocates; inst stores bail if any later deopt point exists) get a second compiled form that takes (vm, rcvr, args...) as plain JS arguments and returns the result or a deopt sentinel. jit2 send sites with an IC hit call it with NO spill, NO frame, NO zone traffic. Trace parity vs the golden is exact: sendCount++ on the leaf path (-- on deopt; the framed retry re-increments); leaves only run when interruptCheckCounter > 0 and decrement it, so check cadence matches the framed path; a harness hook samples successful leaf sends with the same (sendCount, method, pc) tuples as the executeNewMethod wrapper. 228 leaf forms out of 1370 jit2 methods on the Cuis startup; full-run trace identical to the context-mode golden. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every named-primitive call (prim 117) was rebuilding two JS strings from Smalltalk bytes (module + function name), looking the module up in a dictionary and doing a string-keyed function lookup. tryPrimitiveZ now resolves once and caches a bound wrapper on the CompiledMethod (method.primFn), preserving the namedPrimitive protocol exactly (success flag reset, interpreterProxy.argCount/primitiveName, primMethod, boolean-vs-success return, unbalance warning). Missing modules/functions keep the slow path so warnings behave identically. FloatArrayPlugin is denylisted: cached, it diverges the trace when combined with any other cached plugin (undiagnosed global-state interaction, possibly the at-cache); its slow path is correct. Bisection tooling: PRIMFN_SKIP=mod1,mod2 or * disables caching. Golden-identical on Cuis startup and Dialogo. Bench (20M sends): 3.22M sends/s vs 2.75M contexts baseline (+17%, best so far). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
loadModule checks interpreterProxy.failed() after initialiseModule, which reads the AMBIENT primHandler.success flag. The original 117 path always runs after doPrimitive resets success=true on entry, but resolveNamedPrim resolves before that point, so a stale success=false from a previously failed primitive made the module load report "initialization failed", get cached as null, and every named prim of that module fail forever (Smalltalk fallbacks ran instead — correct but trace-divergent, and slower). This also explains the phantom "interaction between cached plugins": which module hit the stale flag depended on which loads shifted to resolve time. Fix: reset success before loadModule in resolveNamedPrim. The FloatArrayPlugin denylist is removed — it was just the first victim. Golden-identical with all plugins cached; Dialogo identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… quick prims Coverage jumps from 1370/485 to 1779/76 (96%) on the Cuis startup: - super sends (0x85, 0x84-op1) with a statically-filled IC (the lookup class is fixed per site: method class's superclass), plus 0x86 second extended sends. Gotcha fixed: verifyAtClass for super sends must be the lookup class, not the receiver class — using rc poisoned the at:-cache cacheability test (makeAtCacheInfo) for super at: sends - closure indirection temps (0x8A) built from mapped locals - remote temp vector push/store/storePop (0x8C-0x8E) - generic quick prims next/atEnd/new/x/y/nextPut:/new: as direct full sends (quickSendOther always fails for these) - JIT2NO=feature,... bisection gates for future debugging Golden-identical; Dialogo identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generated per-class constructors now predeclare oop/hash/dirty/ mark/nextObject, so lazily-added properties (dirty on first store, mark/nextObject during GC) no longer fork hidden classes per object. Covers freshly instantiated objects and image-loaded ones (rebuilt via renameFromImage's `new instProto`). Small win (~1%); also records the negative result of the flat-single-shape experiment (~7% slower). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dialogo's UI loop does ~23k sender-stores into married contexts per 10M sends (terminate-style chain surgery); each one flushed EVERY live page. storeToMarriedContext now flushes only the owning page. That exposed a page leak the full flush had been masking: pages of terminated processes stay live-but-unreachable (termination cuts the chains at the context level, never touching the page), growing the zone to 23k pages and making allocPage scans O(n). Fix: when no dead page exists and the zone holds 32+ pages, allocPage evicts a suspended page by flushing it (always correct: resuming re-inflates via makeBaseFrame) and reuses it. Zone now capped at 32 pages on Dialogo. Golden and Dialogo traces identical. Dialogo bench 3.05M sends/s (2.99M before; 2.16M with the leak). PAGEDBG/SMCDBG harness counters. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The run/ launcher rewrites fragment options as key= (empty value), which parsed as falsy and silently disabled the flags in the browser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rn headless Wraps all Node FilePlugin primitives in vm.freeze + setImmediate unfreeze, mimicking the browser's async IndexedDB file access. Main loop is now async and yields to the event loop while frozen (interpret gets a noop thenDo, required by freeze). SSDBG counts enableSingleStepping calls. Reproduces the accumulative frames+jit2 slowdown seen in the browser (semantics identical: ctx and frames hashes match under FREEZE_SIM). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wing fingerprint The old oracle could only run boot-to-quit: headless reported no screen (primitiveScreenSize=false, image quits — hence -ignoreQuit) and delivered no input events. So it never exercised the interactive code paths where the browser actually crashed (resume:through:/terminateTo: under the async file freeze + reentrant event delivery). --ui installs a measuring headless display: reports a real (offscreen) screen size, accepts the Display form, drains an event queue. Morphic's World comes up and BitBlt draws into the Display Form in object memory (no canvas needed). A deterministic synthetic event script (mouse sweep, click, drag, keyboard), scheduled by sendCount, drives the interactive machinery; --events FILE.json replays a recorded browser trace for pixel-accurate scenarios. Adds a representation-independent drawing fingerprint (FNV hash of the Display Form bits) that also computes identically in the browser, so headless-vs-browser output is directly checkable — the signal the trace-hash oracle lacked. Verified on Dialogo.32bits.image: the World renders the full 1024x768 UI headless, and ctx vs frames+jit2 are byte-identical in BOTH the trace hash and the drawing hash, with and without events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…itch counts) Answers 'does the workload even reach the paths where the browser crashed?' Counts executeNewMethod activations for a selector watchlist (terminateTo:, resume:through:, aboutToReturn:through:, ...) plus process switches and married- context resumes. On the --ui Dialogo workload: terminateTo: fires 2813x and married contexts resume 20415x (both ctx==frames), but resume:through: never fires — the synthetic script doesn't navigate Dialogo's specific menus, so the exact crash needs a recorded --events trace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h sizing
#record installs an event recorder in the browser display: captures each mouse/
wheel/keyboard/drag event at the enqueue point together with the VM sendCount,
so the timing/interleaving can be reproduced headless. SqueakJS.downloadEvents()
dumps {width, height, events} (display resolution included so the headless replay
uses the same size and coordinates land on the same morphs).
difftrace --events FILE.json replays it: sizes the offscreen display from the
recording, rebases browser sendCounts to the replay (first event at --evstart,
inter-event gaps capped at --evgap to compress user pauses). Verified end-to-end
with a synthetic recording.
Workflow: record a session in plain ctx mode (no stackZone/jit2, so it completes
without the crash), then replay headless in BOTH modes to catch the divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…prof/run) Dialogo auto-loads its projects from the run directory at startup, so the boot with its file environment is a representative send-heavy workload with no user interaction needed. This script drives it headless without opening a browser: dialogo.sh compare [sends] ctx vs frames+jit2: wall, sends/s, correctness dialogo.sh prof [ctx|frames] [n] top JS hotspots (node --prof, auto-cleaned) dialogo.sh run [mode] [sends] single run, full output Prepares a stable run dir (/tmp/dialogo-run) from ~/Desktop/Dialogo/Archivos and stabilizes the recorded event trace there. compare verifies trace+display hashes match across modes and prints the perf delta. On the real auto-load workload: frames+jit2 is +7.5% vs ctx (send-heavy, unlike the synthetic pure-render workload which was BitBlt-bound). Dominant remaining cost in frames mode is the V8 LoadIC family (~23%) — megamorphic property access. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-33,20,34-37) SqueakJS never implemented the LargeInteger arithmetic primitives — add (21), subtract (22), multiply (29), divide (30), floored div/mod (32/31), quo/rem (33/20), and bit-and/or/xor/shift (34-37) all did warnOnce + return false, forcing every operation to fall back to its Smalltalk implementation (hundreds of sends each: digitAdd:, normalize, ...). Only the comparisons (23-28) existed. Implemented with JS BigInt (exact for any size). Operands may be SmallInteger or LargeInteger; results normalize back to SmallInteger when in range. Squeak semantics: // /\ are floored (sign of divisor), quo:/rem: truncated (sign of dividend), / (30) succeeds only when exact, div-by-zero and negative bit-op operands fall back. primHandler.largeIntPrims=false restores the old fallback. On the real Dialogo workload (its clock/Delay scheduling is heavy on large-int time arithmetic): LargeInteger receiver sends drop 6.9% -> 2.6%, and the same image-work takes 18% fewer sends / 18% less wall (1.22x). Verified: math matches BigInt + Squeak reference (//,\,quo,rem,invariants,byte round-trip); ctx==frames identical; display bit-identical with prims ON vs OFF at equal image-time; no regression on the Cuis boot trace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ARGEINT toggle HOTSEL=1 prints the top methods and receiver-classes by activation — how the real image actually spends its sends (surfaced that LargeInteger arithmetic + timer/Delay scheduling dominate, not interpreter overhead). --until-ms stops at a virtual-time target to measure 'time for the same work' (needed for opts that cut sends-per-op rather than cost-per-send). LARGEINT=0 disables the LargeInteger primitives for A/B. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the action' The harness warps the virtual clock when the image waits on a timer, so the Delay/stepping loop (rate-limited to ~60fps in the browser) runs at full speed headless — inflating the timer/scheduling machinery in fixed-send and fixed- virtual-time measurements. --until-stable stops when the Display bits stop changing for ~1M sends and events are exhausted: the real work (render the result) is done, excluding the over-represented idle-spin. This is the honest 'time to render the result' metric. Under it, both the LargeInteger primitives and the entire stack-zone+jit2 give ~0% on the felt Dialogo workload (both reach the final frame 8ff54efc at the same ~10M sends, same wall). The earlier +5-21% were artifacts of send-heavy microbenchmarks or the clock-warp-inflated timer loop. Felt-window profile: V8 LoadIC (megamorphic property access on Squeak-objects-as-JS-objects) ~26%, executeNewMethod ~12%, write barriers/stores ~9%, BitBlt ~2%, GC ~2.5%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(~7-8% felt) The per-class instance constructors (one new Function per Squeak class) gave V8 a distinct hidden class (map) per class, so the VM's hottest property loads (.sqClass, .pointers[i], .bytes on objects of any class) went megamorphic — the single dominant cost of the real workload (LoadIC family ~26% of felt-window ticks). classInstProto now returns ONE shared constructor for all classes, so V8 sees only ~4-5 maps (by storage format), turning those loads mono/low-poly. LoadIC disappears from the profile. Measured with the honest metric (wall to render the result, not a send-heavy microbench): ~7-8% faster on the real Dialogo workload (4363 -> 4022 ms), and byte-identical semantics — same trace hash, same drawing (8ff54efc), ctx==frames, on both the Dialogo V3 image and Cuis. General win: helps any Squeak image, not just Dialogo. This is the opposite result of the earlier 'flat shape' experiment (-7%), which put ALL storage fields in every object and was measured send-heavy; here the per-format fields are kept and only the constructor is shared. Opt out (per-class names in devtools) with Squeak.perClassShape = true. Note: frames+jit2 is still ~0% on this workload even with LoadIC gone — the stack-zone marriage/sync overhead cancels the jit2 gain on Dialogo's heavy process-switching. The win here is the representation change, independent of the stack zone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ibution was wrong) The ~6.6% win from the shared constructor is real and reproducible (best-of-5 interleaved: 4022 vs 4307 ms, mono faster in all 5). But the earlier claim that it 'eliminates LoadIC (~26%)' was wrong — that reading came from a noisy profile sample. A/B profiling shows the actual mechanism: monomorphization collapses the MEGAMORPHIC IC operations to monomorphic — StoreIC of initInstanceOf (156->27 ticks) and LoadIC_Megamorphic (258->32). The regular LoadIC (loads of .pointers/ .sqClass) remains as the top residual cost (~18%): the loads still happen, now cheap but not free. Fully removing them needs objects in linear memory (WASM), which stays the open lever — this change is a partial step (megamorphic->mono), not the full one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the WASM-linear bet Measures field access in the interpreter's hot pattern (context alloc + 9 field writes + inst-var reads per send/return) across four object representations, on the same V8 Node runs: A- megamorphic JS objects (300 shapes, = pre-fix per-class): 31373 ms A monomorphic JS objects (= current default post-fix): 63 ms B Int32Array heap from JS (linear memory, no WASM): 80 ms C linear memory from WASM (raw i32.load/store): 64 ms Conclusion: monomorphic JS objects are AS FAST as raw WASM linear memory (C/A = 1.01x). The megamorphism was the only real object-access cost (~500x on this microbench), and we already eliminated it in pure JS with the shared- constructor fix (+6.6% felt, banked). Moving objects to linear memory — the core of the WASM bet — gives ~0% over what we already have. Linear memory in JS (B) is even 27% slower (bounds checks). The earlier phase-0 2.7x was method compilation on fib (a different lever we already have via jit2, which is 0% felt here), not linear-memory objects. The WASM-linear-memory rewrite would give ~0% felt on Dialogo. This spike saved that months-long bet. We are at the JS ceiling for object representation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hod (ctx mode) doNamedPrimitive (primitive 117, the generic named-primitive path) called bytesAsString twice per invocation to decode the module and function name from the method's first literal — names that never change for a given method. A browser Performance profile of a real drawing/scripting session showed bytesAsString as the codefrau#1 self-time cost at ~10% (ctx mode; the stack-zone/frames path already avoids this via its primFn cache). Cache the decoded strings on the method (_primModName/_primFuncName) so bytesAsString runs once per method, not per call. Correctness preserved: ctx==frames trace and display byte-identical. Headless shows only ~0.6% because the synthetic auto-load+mouse workload barely exercises prim-117; the win lands in the browser's drawing/scripting workload (the 10%), to be confirmed by re-profiling there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Launches the actual run/ page in headless Chrome (puppeteer-core + the installed Chrome, no Chromium download), captures console + page errors + full JS stacks, confirms the VM mode, and dispatches real mouse events (which go through recordMouseEvent -> display.runNow -> interpret — the reentrancy the pure-node harness never modelled). Auto-dismisses the alert() that squeak.js raises after a crash (it blocks headless Chrome forever otherwise — that was the hang). Image is served over HTTP (image=/Dialogo.32bits.image from repo root, gitignored) and the project files are loaded on demand via SqueakJS templates + sqindex.json (a served dialogo-fs/ dir, gitignored), so a fresh headless Chrome reproduces the user's environment without 140MB of preloaded IndexedDB. With this, reproduced the stackZone+jit2 mouse crash headless for the first time: undefined.sqClass in primitiveBlockValue via jit2-compiled ContextPart>>resume:through:, cascading into 'invalid PC' in jit1-compiled ContextPart>>terminateTo:. Setup: node perf/harness/browser.js '<url>' --drive [--events f.json] [--secs N]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rement Adds --trace FILE to capture a Chrome Performance trace during the driven workload (same format as DevTools export), so freeze/hotspot analysis can be run headless via perf/harness/parseprof-style parsing without the user re-recording. Also gitignores the local Dialogo image + served dialogo-fs/ used by the loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ible freezes squeak.js's run() gives the interpreter a fixed 50ms wall-clock budget per slice before yielding to the browser (vm.interpret(50, ...)); the interrupt-check cadence is every ~1000 sends, so the interpreter COULD yield far more often — 50ms was just an arbitrary default, not an architectural limit. This is a scheduling-fairness lever, orthogonal to everything else tried in this project: it doesn't reduce total CPU-seconds, it reduces how long any single browser task blocks the main thread, which is what the spinner literally measures. Measured on a driven mouse-move/click Chrome session (browser.js --drive --trace), 5s window: baseline (50ms slices) had 210 tasks >=20ms and 171 >=50ms (max 215ms). #sliceMs=8: 10 tasks >=20ms, 5 >=50ms (max 134ms) but sendCount dropped 55.2M->32.7M in the window (-41%, likely setTimeout's ~4-6ms scheduling floor eating a bigger fraction of a smaller slice — possibly inflated by headless automation overhead vs a real session). #sliceMs=16 recovers most throughput (39.7M, -28%) while keeping nearly all the smoothness win (4 tasks >=50ms). Opt-in via URL (#sliceMs=16), default unchanged (50) — this trades throughput for perceived responsiveness, a product judgment call, not a pure win. Natural follow-up if pursued further: replace the setTimeout(fn,0) reschedule with a MessageChannel/postMessage-based scheduler (the classic 'setImmediate' shim trick) to avoid the timer floor and get smoothness without the throughput cost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.