Skip to content

Perf/stack zone - #193

Open
agustincico wants to merge 99 commits into
codefrau:mainfrom
agustincico:perf/stack-zone
Open

Perf/stack zone#193
agustincico wants to merge 99 commits into
codefrau:mainfrom
agustincico:perf/stack-zone

Conversation

@agustincico

Copy link
Copy Markdown

No description provided.

agustincico and others added 30 commits July 5, 2026 19:46
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>
agustincico and others added 30 commits July 25, 2026 12:33
…, skip bundle binaries

filePut needs the parent directory to exist (else it returns null without callback), so
deep zip paths (Dialogo/Archivos/projects/...) hung the loader forever. Now dirCreate the
hierarchy first, process files sequentially (hundreds of concurrent unzip+IndexedDB writes
never settled), and skip macOS metadata + desktop binaries (.exe/.sh/.dmg/... a Dialogo.zip
ships a 22MB .exe). Verified: Squeak/Cuis/Dialogo zips boot in the worker; the 32-bit image
is preferred when a bundle ships both.
… worker JIT fallback

Modern Cuis (7.x) images use 16-bit (DoubleByteArray, format 12-15) and 64-bit
(DoubleWordArray/float64, format 9) indexable objects, which the image reader rejected
("16 bit arrays not supported" / "Unknown object format: 9"). Read them into this.words16
(Uint16Array) and this.words64 (BigUint64Array), with matching size/clone/typed-array
helpers. This lets Cuis 7.x images load.

Once loaded, Cuis 7.x also tripped jit1 with "invalid PC" on some Sista bytecode. The
worker now restarts the image once with the JIT disabled when that happens — images that
work with the JIT (Squeak, Dialogo) keep it; those that don't fall back to the interpreter,
which runs every bytecode set correctly. Verified: Cuis 7.8 boots (auto no-JIT), Dialogo
still boots with the JIT.
…ploy con 404) + loader lineal moderno en vez de la ruedita
…ranca sin debugger)

Cuis 7.x llama primitiveGetWorkingDirectory al bootear (readAndApplyUserPrefs) y
SqueakJS no la tenía -> el primitivo fallaba y abría un debugger antes de levantar
el mundo. Se agregan ambas primitivas y se amplía la regex del módulo FilePlugin
(estaba anclada a File|Directory, no matcheaba Get/SetWorkingDirectory).
…WorkingDirectory/getenv/fileDescriptorType + SocketPlugin en worker

Progreso hacia correr Pharo en el worker: el mundo Morphic renderiza. Faltan
FileAttributesPlugin (stat/exists/dir), FFI (no viable en browser), MD5Plugin, fuentes.
…stem, mapeado a la FS virtual

Limpia toda la cascada de errores de FileSystem en el boot de Pharo (File primExists:/
modeOf:/etc). Hand-written como SocketPlugin. Restante: FFI (libgit2/Iceberg — techo), SDL2, fuentes.
…a que Pharo use sus fallbacks en vez de errorear
…oard_* 8/16/32/64; el evento del VM espera 1/2/4/8) + setear cmd+ctrl juntos + masking ctrl-letra, replicando squeak.js onkeydown

Sin esto los atajos de comando (do-it, select-all, etc.) no funcionaban en el worker para ninguna imagen (los modificadores se insertaban como letras). Faithful port del handler real.
… SubscriptOutOfBounds loop de Pharo 64-bit)

CompiledMethod>>at: direcciona los literales como slots de wordSize bytes (8 en 64-bit,
no 4 hardcodeado). Arregla indexableSize + objectAt/objectAtPut + guarda image.wordSize.
Esto elimina el loop infinito de SubscriptOutOfBounds al bootear Pharo 64-bit.
PENDIENTE: queda una sutileza del encoding del context/closure PC en 64-bit (los method
contexts decodifican con x4 pero los full-closure contexts necesitan x8) que impide el
boot completo — necesita más investigación del VM. 32-bit no afectado (wordSize=4, idéntico).
… + errores de primitiva tipados + fixes de startup

- hackImage: nueva entrada VirtualMachine>>wordSize (Pharo cachea WordSize=8 en class var;
  con la normalización x4 de fixPCs el código in-image debe computar pcs con 4). ESTE era
  el bug raíz del 64-bit: el hack de Squeak no matcheaba el cache de Pharo.
- revert del at:-indexing x wordSize (iba contra la normalización; x4 es correcto).
- fixPCs: no convertir a NaN los ips nil de contextos terminados.
- getErrorObjectFromPrimFailCode: una primitiva puede entregar su objeto de error
  (primFailErrorObject); FileAttributesPlugin falla con PrimitiveError errorCode -3
  (cantStatPath) para que Pharo levante FileDoesNotExistException (isDirectory: etc).
- primitiveInterpreterSourceVersion: formato '<version> Date: <iso>' — DiskStore
  checkVMVersion parsea la fecha y si falla aborta su startUp dejando
  FileSystem workingDirectory en nil (rompía los startup scripts).
- vm.plugins.file.node.js: paridad (fileDescriptorType, get/setWorkingDirectory).
- fileattributes: degradar limpio sin FS virtual (Node).
opendir responde {nombreCodificado. statAttributes. dirPointer} (shape 'new VM' de
FileAttributesPlugin >1.4, ver DiskStore>>directoryAt:nodesDo:) y readdir
{nombre. stat} o nil al final; stat[1] = symlink target (nil aca). Antes devolviamos
un String pelado -> el loop hacia 'at: 1' sobre el String -> Character -> DNU
#asByteArray, matando el listado de directorios de Pharo (y con el, el mecanismo
de startup scripts que carga startup.st).
… hasta el primer frame) + Cuis desde su repo oficial

- worker postea 'first-frame' en el primer draw real; hasta entonces el launcher muestra
  un overlay oscuro localizado (es/en) con etapas descargando/arrancando y un heartbeat
  de bytecodes — clave para Pharo que tarda 1-2 min. Sin rectángulo negro ni flash blanco.
- Cuis 7.8 carga directo del repo oficial (raw.githubusercontent tiene CORS); Squeak y
  Pharo siguen mirroreados porque files.squeak.org/files.pharo.org no mandan CORS (nota
  en la página lo explica).
Causa raíz del "muestra errores de FFI al abrir": al iniciar, Iceberg
llama a LGitLibrary class>>startUp: → initializeLibGit2 → libgit2_init,
un callout FFI que SqueakJS no tiene backend para resolver. El fallo se
re-lanza (ex pass) y Pharo abre un post-mortem debugger sobre un mundo
por lo demás sano — idéntico en 32-bit ("Cannot locate libgit2") y en
64-bit (FFICallout subclassResponsibility).

hackImage ahora neutraliza LGitLibrary class>>startUp: a ^self (nuevo op
`neuter`: sobrescribe el primer bytecode con return-receiver, Sista 0x58
/ V3 0x78, sólo si el método no tiene primitiva). No hay git en el
browser, así que Iceberg simplemente corre sin git, como en una máquina
sin libgit2. LGitLibrary sólo existe en Pharo → no-op en Cuis/Squeak.

Verificado: Pharo 10 32-bit y 64-bit arrancan a la ventana Welcome sin
debugger; Cuis 7.8 (repo oficial) sin regresión. La página aclara que el
32-bit es limpio e interactivo y el 64-bit sigue experimental (el input
de mouse aún no llega a Morphic en el build SDL2-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
demo-startup.st abre, al arrancar, una app Pharo real y liviana: cierra
la ventana Welcome, muestra una leyenda sobre el logo de Pharo y anima
14 bolitas Morphic rebotando (el clásico demo de "átomos"). Sólo Morphic
del núcleo: sin FFI ni red. Prueba que "una app hecha en Pharo corre bien
en el browser", con el IDE completo a un clic en la barra de menú.

La animación corre en un proceso a userSchedulingPriority-1 e invalida
cada bolita + el World por frame, para que el path de update diferido del
worker repinte (con prioridad de fondo no se programaba y no se veía).

Para publicarla: dialog.ar/Pharo-demo.zip = contenido de Pharo.zip +
este archivo renombrado a startup.st en la raíz. La página ya enlaza a
"live Morphic demo".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deja documentado en el workspace lo que hasta ahora vivía sólo en la
cabeza de la sesión:

- ESTADO.md: qué imágenes andan hoy y cuáles no, números de performance
  medidos, el runbook de deploy completo (son DOS repos y la copia al
  sitio es MANUAL — no hay webhook ni Action), la divergencia entre main
  y perf/stack-zone, los hallazgos técnicos de esta ronda (el neuter de
  LGitLibrary y por qué `primitive: 256` no sirve en Spur; el mouse
  muerto del 64-bit y hasta dónde llegó el diagnóstico), el tooling de
  prueba y la lista de pendientes priorizada.

- pharo/README.md: documenta demo-startup.st (con el gotcha de la
  prioridad del proceso de animación) y el estado real de 32 vs 64.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…xedDB

filePut escribía la entrada de directorio ANTES que el contenido, y en
dos medios distintos: la entrada va sincrónica a localStorage (minúscula,
nunca falla) y el contenido asincrónico a IndexedDB (enorme, sí falla
cuando se agota la cuota del origen — varios bundles de imágenes suman
cientos de MB, y Chrome recorta la cuota si queda poco disco).

Si esa escritura fallaba, el error sólo se logueaba (trans.onerror) o
caía a memoria volátil (trans.onabort → dbFake, que se pierde al
recargar). En ambos casos quedaba un archivo fantasma: listado en el
directorio con su tamaño, pero sin contenido. Al recargar, fileGet no lo
encontraba y la imagen reportaba mucho después un desconcertante "File
read failed" (visto en Cuis: UniFileStream>>error:), en un estado que ya
no se arregla solo — hay que borrar los datos del sitio a mano.

Ahora dbTransaction acepta un callback de error opcional (sin él, el
comportamiento es idéntico al anterior) y filePut lo usa para revertir la
entrada de directorio. El archivo queda simplemente ausente y se vuelve a
bajar en la próxima carga, en vez de romperse para siempre. El callback
de éxito se sigue disparando para no colgar a quien esté esperando la
escritura (el flujo de zip hace await sobre eso).

Verificado: escritura OK → el archivo queda listado; escritura fallida →
no queda entrada y el callback igual dispara. Sin regresión en Cuis 7.8
(dos corridas, la segunda desde el cache) ni en Pharo 10 32-bit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…andMorph)

Pharo 64 abría el mundo completo pero ignoraba todo click. La causa:
los métodos que el script inyecta en HandMorph usan variables del pool
EventSensorConstants (EventTypeMouse, EventTypeKeyboard, EventKeyDown,
...). El build 32-bit declara ese pool en HandMorph; el 64-bit lo quitó
junto con las clases clásicas de eventos, y el script lo declaraba sólo
en las clases que crea (InputEventHandler/Sensor/Fetcher), nunca en
HandMorph.

Sin el pool, esos nombres compilan como globales indefinidas —nil en
silencio— así que `type = EventTypeMouse` era siempre falso,
nextEventFrom: caía en `^ #invalid` y processEventsFromQueue: retornaba
antes de llamar a handleEvent:. El mundo renderizaba pero descartaba
todos los eventos.

Fix: agregar el pool a HandMorph ANTES de compilar sus métodos.

Diagnóstico (instrumentando el worker, click sobre el menú System):
la cadena estaba viva salvo el último paso — doOneCycleFor: +398,
processEvents +398, nextEvent +398, processEvent: +5, signalEvent: +5,
pero handleEvent: +0 y CERO redibujos, con los sends en tasa de reposo.
Con el fix: handleEvent: +8, +53 redibujos y +897k sends, los mismos
números que el 32-bit sano.

Verificado en Chrome real: se despliega el menú System, la selección en
lista cambia el panel y las ventanas se arrastran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…de archivos fantasma

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…carteles)

Los avisos semitransparentes de Pharo (el de FFI al arrancar, el del
atajo de teclado al abrir el browser) se desvanecen por pasos, y en
64-bit "titilaban" como si se reabrieran, mientras que en 32-bit funden
suave.

Causa: VMWorldRenderer entra a deferUpdatesDuring:, pero la primitiva
126 nunca se ejecutaba, porque el script no definía DisplayScreen>>
deferUpdates: y la versión nativa del build 64-bit (SDL2-only) no llega
a la primitiva. Sin diferido, CADA BitBlt se propaga sola a la pantalla:
el fundido de un cartel redibujaba ~300 veces por segundo en vez de una
vez por ciclo de Morphic.

Fix: portar deferUpdates: (instancia y class-side) del build 32-bit,
agregando antes su class var DeferringUpdates —si se compilara sin ella,
quedaría ligada a una global indefinida en nil, la misma trampa de scope
que el bug del pool en HandMorph.

Medido en el mismo arranque (Chrome real):
              deferUpdates(true)   draws   pico draws/seg
  32-bit              21             21          3
  64-bit antes         0           1268        323
  64-bit ahora        21             21          4

Es decir, 60x menos dibujados y exactamente el mismo perfil que 32-bit.
Interactividad sin regresión (selección en lista y arrastre de ventana).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…carteles)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ncher con main

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El sitio sirve el launcher en dialog.ar/SmalltalkJsVm/ (sin /run/) desde
una copia con las rutas reescritas. Se venía haciendo a mano y por eso
quedó una release atrás: la demo y la UX nueva estaban en run/index.html
pero la página que la gente abre seguía vieja.

mk-site-index.py hace la reescritura (../x -> x, squeakjs.* -> run/...)
salteando los bloques <pre>, cuyas rutas son ejemplos para el sitio del
lector y no deben tocarse. El runbook de ESTADO.md ahora exige
regenerarla cada vez que cambia run/index.html.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant