diff --git a/.gitignore b/.gitignore index 245b454e..7bed5c07 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ node_modules/ gh-pages dist/*.min.js* *.code-workspace +perf/harness/golden.json +ws/client/cuis.changes +ws/client/CuisDebug-*.log +/Dialogo.32bits.image +/dialogo-fs/ diff --git a/ESTADO.md b/ESTADO.md new file mode 100644 index 00000000..5a0b665c --- /dev/null +++ b/ESTADO.md @@ -0,0 +1,243 @@ +# Estado del proyecto — Smalltalk en el browser + +> Documento de trabajo de este fork (`agustincico/SqueakJS`). No es del proyecto upstream. +> Última actualización: **2026-07-27**. + +Sitio en vivo: **https://dialog.ar/SmalltalkJsVm/run/** + +--- + +## 1. Qué está vivo hoy + +| Imagen | Estado | Notas | +|---|---|---| +| **Squeak 6.0** | ✅ anda | `#zip=https://dialog.ar/Squeak.zip` | +| **Cuis 7.8** | ✅ anda | Se baja de su **repo oficial de GitHub** (manda CORS). Nota: esa imagen dispara un "JIT fault" que SqueakJS resuelve solo reiniciando sin JIT — preexistente, no afecta el uso | +| **Pharo 10 (32-bit)** | ✅ **anda bien** | Arranca **limpio** (sin debugger de FFI/git) y es **plenamente interactivo**. Es el que hay que mostrar | +| **Pharo 10 (64-bit)** | ✅ **anda** | Arranca limpio y responde (menús, listas, arrastre de ventanas). Requiere el `startup.st` de compatibilidad dentro del zip | +| **Dialogo** | ✅ anda | App de dibujo para chicos, `dialog.ar/Dialogo.zip` | +| **Demo Morphic** | ✅ anda | `dialog.ar/Pharo-demo.zip` — Pharo 32 abriendo una app propia al arrancar | + +**Performance de Pharo** (medida con `sendCount` por tick, Chrome real): +- Arranque: **32-bit ≈ 5 s**, **64-bit ≈ 11 s** (el 64 pesa más por 8 bytes/oop). +- En reposo: **~23k sends/s en ambos** → la VM está casi dormida. **Pharo no es "pesado" en idle**; + no hay ningún loop desbocado. El costo real es el arranque. + +--- + +## 2. Deploy — ⚠️ NO hay auto-deploy + +El sitio **no se sirve desde este repo**. Son dos repos y la copia es **manual**: + +``` +agustincico/SqueakJS ProfeFuturo/profefuturo.github.io + (el código) ──────▶ └── SmalltalkJsVm/ ──────▶ dialog.ar + copia + MANUAL +``` + +No existe webhook ni GitHub Action (verificado: no hay `.github/workflows/` ni script de deploy). +**Pushear a `SqueakJS/main` no deploya nada por sí solo.** + +### Runbook (2 pasos) + +**Paso 1 — código a GitHub.** Ojo: la rama local `main` trackea `upstream` (codefrau), así que hay +que pushear explícito a `origin`: + +```bash +git push origin main +``` + +**Paso 2 — copiar al repo del sitio.** El clone tiene que ser *disk-safe*: el historial pesa +~400 MB (binarios de Dialogo) y llena el disco. + +```bash +git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/ProfeFuturo/profefuturo.github.io site +cd site && git sparse-checkout set SmalltalkJsVm # aun así ocupa ~350 MB + +# antes de copiar, confirmar que la copia del sitio == la versión previa de main, +# así el diff introduce SOLO el cambio buscado: +cmp <(git -C /ruta/SqueakJS show :vm.interpreter.js) SmalltalkJsVm/vm.interpreter.js + +cp /ruta/SqueakJS/ SmalltalkJsVm/ + +# ⚠️ SI CAMBIÓ run/index.html: el sitio sirve el launcher en la RAÍZ del path +# (dialog.ar/SmalltalkJsVm/, sin /run/) desde una copia aparte con las rutas +# reescritas. Es la página que ve la gente. Regenerarla SIEMPRE, o queda una +# release atrás (ya pasó: la demo no aparecía porque sólo se actualizó run/): +python3 /ruta/SqueakJS/utils/mk-site-index.py /ruta/SqueakJS/run/index.html \ + > SmalltalkJsVm/index.html + +git add SmalltalkJsVm/ && git commit && git push origin HEAD +cd .. && rm -rf site # ¡borrar el clone al terminar! +``` + +**Paso 3 — verificar.** Pages reconstruye en ~1–2 min y hay CDN (`cache-control: max-age=600`), +así que hay que romper la caché: + +```bash +curl -s "https://dialog.ar/SmalltalkJsVm/vm.interpreter.js?cb=$(date +%s)" | grep -c 'LGitLibrary' +``` + +Y después probar la imagen de verdad en Chrome (ver §5). + +--- + +## 3. Ramas + +| Rama | Trackea | Estado | +|---|---|---| +| `main` | `upstream` (codefrau) — pushear a `origin` explícito | Lo curado y deployable. Sincronizada con `origin/main` | +| `perf/stack-zone` | `origin` | **La rama de trabajo. 55 commits sin pushear a `origin`** | + +**Los archivos del launcher (`run/index.html`, `run/squeakjs.css`, `squeak_worker.js`) ya están +sincronizados entre las dos ramas** — la UX de carga, el `first-frame` y los links nuevos se +portaron a `main` y se deployaron. `perf/stack-zone` conserva además el trabajo de perf +(stackzone/jit2) que no va a `main`. + +> ⚠️ Al portar el worker a `main`, verificar siempre que sus `import` existan ahí: en su momento +> `vm.stackzone.js` / `jit2.js` (perf-only) rompieron el deploy con 404. + +--- + +## 4. Hallazgos técnicos (ronda 9) + +### 4.1 Debugger de FFI/git eliminado ✅ deployado + +**Causa:** al arrancar, Iceberg llama `LGitLibrary class>>startUp:` → `initializeLibGit2` → +`libgit2_init`, un callout FFI que SqueakJS no puede resolver. `initializeLibGit2` captura el +error pero hace `ex pass` → **re-lanza** → Pharo abre un debugger post-mortem sobre un mundo +por lo demás sano. **Idéntico en 32-bit** (`Cannot locate libgit2`) **y 64-bit** +(`FFICallout subclassResponsibility`) — el 32 solo parecía sano porque el debugger se cierra. + +**Fix** (`vm.interpreter.js`, commit `4565ea9` en main + `af3a99c` en el sitio): nueva operación +**`neuter`** en `hackImage`, que vuelve un método `^self` sobrescribiendo su primer bytecode con +*return-receiver* (Sista `0x58` / V3 `0x78`), solo si `methodPrimitiveIndex() === 0`: + +```js +{method: "LGitLibrary class>>startUp:", neuter: true, enabled: true}, +``` + +Pharo entonces corre **sin git**, igual que en una máquina sin libgit2. `LGitLibrary` solo existe +en Pharo → es no-op en Cuis y Squeak. + +> 🔑 **Por qué no alcanzaba el mecanismo viejo:** `primitive: returnSelf (256)` hace +> `m.pointers[0] |= prim`, que funciona en imágenes V3 pero **no en Spur**, donde la primitiva se +> decodifica del primer bytecode (`callPrimitive`), no del header. De ahí el sobrescribir bytecode. + +Quedó solo un aviso "VM does not support TFFI Callbacks" que se auto-cierra. + +### 4.2 Pharo 64-bit ignoraba todo click ✅ resuelto + +**Causa:** los métodos que `startup-compat64.st` 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**, nunca en `HandMorph`. Sin el pool esos nombres +compilan como **globales indefinidas — `nil` en silencio** (el compilador no falla), así que +`type = EventTypeMouse` era siempre falso, `nextEventFrom:` caía en `^ #invalid` y +`processEventsFromQueue:` retornaba **antes** de llamar a `handleEvent:`. + +**Fix:** agregar el pool a `HandMorph` *antes* de compilar sus métodos. + +**Cómo se localizó** (instrumentando el worker y clickeando el menú System): toda la cadena estaba +viva salvo el último eslabón. + +| | antes | después | +|---|---|---| +| `doOneCycleFor:` / `processEvents` / `nextEvent` | +398 | +293 | +| `InputEventSensor>>processEvent:` | +5 | +5 | +| **`HandMorph>>handleEvent:`** | **0** ✗ | **+8** ✔ | +| redibujos | 0 | +53 | +| sends | +94k (tasa de reposo) | +897k | + +> 💡 **Lección para futuros parches por script:** inyectar métodos en una clase **existente** no le +> agrega los pools/globals que esos métodos necesitan, y el compilador de Pharo **no falla** — crea +> la variable indefinida en `nil`. El síntoma aparece lejísimos del origen. Ante "compila pero se +> comporta como si todo fuera nil", sospechar del scope antes que de la lógica. + +### 4.3 Carteles de Pharo 64 que "titilaban" ✅ resuelto + +Los avisos semitransparentes (el de FFI al arrancar, el del atajo al abrir el browser) se +desvanecen por pasos y en 64-bit parecían reabrirse una y otra vez, mientras que en 32-bit funden +suave. **Causa:** `VMWorldRenderer` sí 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 solo a la +pantalla**: el fundido 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` — misma trampa de scope que §4.2. + +| mismo arranque | `deferUpdates: true` | draws | pico draws/seg | +|---|---|---|---| +| 32-bit | 21 | 21 | 3 | +| 64-bit antes | **0** | **1268** | **323** | +| 64-bit ahora | 21 | 21 | 4 | + +> 🔍 **Trampa de medición que costó dos vueltas:** instrumentar contadores *después* del arranque +> (a los 40 s) mostró "0 llamadas" para métodos que **sí** se ejecutaban — los carteles ya habían +> terminado. Lo mismo con `vm.inputEventSemaIndex`, que en realidad vive en `primHandler`. Ante un +> contador en 0, verificar primero la ventana temporal y el objeto medido. Lo que destrabó el +> diagnóstico fue capturar `vm.printStack()` **dentro** del `putImageData`, que mostró la pila real. + +### 4.4 Archivos fantasma en el cache del browser ✅ resuelto + +`filePut` escribía la entrada de directorio (localStorage, sincrónica, nunca falla) **antes** que +el contenido (IndexedDB, asincrónico, sí falla al agotarse la cuota). Si la segunda fallaba, el +error sólo se logueaba y quedaba un **archivo fantasma**: listado con su tamaño pero ilegible para +siempre, que la imagen reportaba mucho después como `File read failed` (visto en Cuis, vía +`UniFileStream>>error:`) y que no se arreglaba solo — había que borrar los datos del sitio a mano. +Ahora `dbTransaction` acepta un callback de error opcional y `filePut` revierte la entrada, de modo +que el archivo simplemente falta y se vuelve a bajar. Además el callback de éxito ahora se dispara +igual en el camino de error: antes, `trans.onerror` no lo llamaba nunca y el `await` del flujo de +zip quedaba colgado para siempre (posible causa de cargas que "se congelaban"). + +### 4.5 Demo Morphic + +`pharo/demo-startup.st` — ver `pharo/README.md`. + +--- + +## 5. Cómo probar (tooling) + +No hay `gh` CLI ni `puppeteer` instalados; sí **`puppeteer-core`** en `node_modules` y Chrome del +sistema. Los scripts hay que correrlos **desde la raíz del repo** (si no, no resuelve el módulo): + +```js +const puppeteer = (await import("puppeteer-core")).default; // ESM: import dinámico, no require +await puppeteer.launch({ + headless: "new", + executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + args: ["--no-sandbox", "--use-gl=swiftshader"], +}); +``` + +- Servidor local: `npx http-server -p 8091 -c-1` +- Probar una imagen: `http://localhost:8091/run/index.html#zip=` +- Los zips de prueba (Pharo 32/64) **no** van en el repo — bajarlos de `dialog.ar/Pharo.zip` y + `dialog.ar/Pharo64.zip`. +- Señal de arranque: en `perf/stack-zone` escuchar el postMessage `first-frame`; en `main`, esperar + por tiempo. +- Para medir performance, interceptar los mensajes `tick` del worker y leer `sends`. + +--- + +## 6. TODO + +### Media +- [ ] **Diagnosticar Pharo 11 / 12 / 13** — nunca se hizo. Son 64-bit only, así que probablemente + arrastren el mismo problema de input. + +### Baja +- [ ] Rearmar `Pharo-demo.zip` si se cambia `pharo/demo-startup.st` (el zip publicado lleva una + copia como `startup.st`; no se actualiza solo). +- [ ] Bajar el tiempo de arranque del 64-bit (~11 s). +- [ ] `Character>>asByteArray` (FontSubstitutionDuringLoading) — sin diagnosticar, secundario. + +--- + +## 7. Techo conocido: FFI + +Pharo depende fuerte de FFI (`primitiveCalloutWithArgs`, `primitiveLoadSymbolFromModule`, +callbacks) para integrarse con el SO. **El FFI real no es viable en el sandbox del browser.** La +mayoría de los usos no son fatales (Pharo loguea y sigue); el que sí abría un debugger era el de +git/Iceberg, ya neutralizado. diff --git a/jit.js b/jit.js index 6129a2b1..47cd9f3f 100644 --- a/jit.js +++ b/jit.js @@ -200,6 +200,18 @@ to single-step. // if a compiler does not support single-stepping, return false return true; }, + activationCheck: function() { + // expression that is true when the send/jump above switched activations + return this.vm.useStackZone + ? "vm.fp !== fp || vm.zonePage !== page" + : "context !== vm.activeContext"; + }, + tempIndex: function(n) { + // index expression for temp n inside this.source: + // context mode: constant (temps at homeContext.pointers[6+n]); + // stack-zone mode: frame-relative (temps at vm.temps[tempBase+n], tempBase per activation) + return this.vm.useStackZone ? "tB + " + n : Squeak.Context_tempFrameStart + n; + }, functionNameFor: function(cls, sel) { if (cls === undefined || cls === '?') { var isMethod = this.method.sqClass === this.vm.specialObjects[Squeak.splOb_ClassCompiledMethod]; @@ -234,11 +246,15 @@ to single-step. this.source.push("// ", optClass, ">>", optSel, "\n"); this.instVarNames = optInstVarNames; this.allVars = ['context', 'stack', 'rcvr', 'inst[', 'temp[', 'lit[']; - this.sourcePos['context'] = this.source.length; this.source.push("var context = vm.activeContext;\n"); - this.sourcePos['stack'] = this.source.length; this.source.push("var stack = context.pointers;\n"); + this.sourcePos['context'] = this.source.length; this.source.push(this.vm.useStackZone + ? "var fp = vm.fp, page = vm.zonePage;\n" + : "var context = vm.activeContext;\n"); + this.sourcePos['stack'] = this.source.length; this.source.push("var stack = vm.stack;\n"); this.sourcePos['rcvr'] = this.source.length; this.source.push("var rcvr = vm.receiver;\n"); this.sourcePos['inst['] = this.source.length; this.source.push("var inst = rcvr.pointers;\n"); - this.sourcePos['temp['] = this.source.length; this.source.push("var temp = vm.homeContext.pointers;\n"); + this.sourcePos['temp['] = this.source.length; this.source.push(this.vm.useStackZone + ? "var temp = vm.temps, tB = vm.tempOffset;\n" + : "var temp = vm.temps;\n"); this.sourcePos['lit['] = this.source.length; this.source.push("var lit = vm.method.pointers;\n"); this.sourcePos['loop-start'] = this.source.length; this.source.push("while (true) switch (vm.pc) {\ncase 0:\n"); if (this.sista) this.generateSista(method); @@ -267,7 +283,7 @@ to single-step. break; // load temp var case 0x10: case 0x18: - this.generatePush("temp[", 6 + (byte & 0xF), "]"); + this.generatePush("temp[", this.tempIndex((byte & 0xF)), "]"); break; // loadLiteral case 0x20: case 0x28: case 0x30: case 0x38: @@ -283,7 +299,7 @@ to single-step. break; // storeAndPop temp var case 0x68: - this.generatePopInto("temp[", 6 + (byte & 0x07), "]"); + this.generatePopInto("temp[", this.tempIndex((byte & 0x07)), "]"); break; // Quick push case 0x70: @@ -361,7 +377,7 @@ to single-step. byte2 = this.method.bytes[this.pc++]; switch (byte2 >> 6) { case 0: this.generatePush("inst[", byte2 & 0x3F, "]"); return; - case 1: this.generatePush("temp[", 6 + (byte2 & 0x3F), "]"); return; + case 1: this.generatePush("temp[", this.tempIndex((byte2 & 0x3F)), "]"); return; case 2: this.generatePush("lit[", 1 + (byte2 & 0x3F), "]"); return; case 3: this.generatePush("lit[", 1 + (byte2 & 0x3F), "].pointers[1]"); return; } @@ -370,7 +386,7 @@ to single-step. byte2 = this.method.bytes[this.pc++]; switch (byte2 >> 6) { case 0: this.generateStoreInto("inst[", byte2 & 0x3F, "]"); return; - case 1: this.generateStoreInto("temp[", 6 + (byte2 & 0x3F), "]"); return; + case 1: this.generateStoreInto("temp[", this.tempIndex((byte2 & 0x3F)), "]"); return; case 2: throw Error("illegal store into literal"); case 3: this.generateStoreInto("lit[", 1 + (byte2 & 0x3F), "].pointers[1]"); return; } @@ -380,7 +396,7 @@ to single-step. byte2 = this.method.bytes[this.pc++]; switch (byte2 >> 6) { case 0: this.generatePopInto("inst[", byte2 & 0x3F, "]"); return; - case 1: this.generatePopInto("temp[", 6 + (byte2 & 0x3F), "]"); return; + case 1: this.generatePopInto("temp[", this.tempIndex((byte2 & 0x3F)), "]"); return; case 2: throw Error("illegal pop into literal"); case 3: this.generatePopInto("lit[", 1 + (byte2 & 0x3F), "].pointers[1]"); return; } @@ -444,19 +460,19 @@ to single-step. case 0x8C: byte2 = this.method.bytes[this.pc++]; byte3 = this.method.bytes[this.pc++]; - this.generatePush("temp[", 6 + byte3, "].pointers[", byte2, "]"); + this.generatePush("temp[", this.tempIndex(byte3), "].pointers[", byte2, "]"); return; // remote store into temp vector case 0x8D: byte2 = this.method.bytes[this.pc++]; byte3 = this.method.bytes[this.pc++]; - this.generateStoreInto("temp[", 6 + byte3, "].pointers[", byte2, "]"); + this.generateStoreInto("temp[", this.tempIndex(byte3), "].pointers[", byte2, "]"); return; // remote store and pop into temp vector case 0x8E: byte2 = this.method.bytes[this.pc++]; byte3 = this.method.bytes[this.pc++]; - this.generatePopInto("temp[", 6 + byte3, "].pointers[", byte2, "]"); + this.generatePopInto("temp[", this.tempIndex(byte3), "].pointers[", byte2, "]"); return; // pushClosureCopy case 0x8F: @@ -502,10 +518,10 @@ to single-step. break; // load temporary variable case 0x40: case 0x41: case 0x42: case 0x43: case 0x44: case 0x45: case 0x46: case 0x47: - this.generatePush("temp[", 6 + (b & 0x07), "]"); + this.generatePush("temp[", this.tempIndex((b & 0x07)), "]"); break; case 0x48: case 0x49: case 0x4A: case 0x4B: - this.generatePush("temp[", 6 + (b & 0x03) + 8, "]"); + this.generatePush("temp[", this.tempIndex((b & 0x03) + 8), "]"); break; case 0x4C: this.generatePush("rcvr"); break; @@ -577,7 +593,7 @@ to single-step. this.generatePopInto("inst[", b & 0x07, "]"); break; case 0xD0: case 0xD1: case 0xD2: case 0xD3: case 0xD4: case 0xD5: case 0xD6: case 0xD7: - this.generatePopInto("temp[", 6 + (b & 0x07), "]"); + this.generatePopInto("temp[", this.tempIndex((b & 0x07)), "]"); break; case 0xD8: this.generateInstruction("pop", "vm.sp--"); break; @@ -609,7 +625,7 @@ to single-step. break; case 0xE5: b2 = bytes[this.pc++]; - this.generatePush("temp[", 6 + b2, "]"); + this.generatePush("temp[", this.tempIndex(b2), "]"); break; case 0xE6: throw Error("unusedBytecode 0xE6"); @@ -662,7 +678,7 @@ to single-step. break; case 0xF2: b2 = bytes[this.pc++]; - this.generatePopInto("temp[", 6 + b2, "]"); + this.generatePopInto("temp[", this.tempIndex(b2), "]"); break; case 0xF3: b2 = bytes[this.pc++]; @@ -674,7 +690,7 @@ to single-step. break; case 0xF5: b2 = bytes[this.pc++]; - this.generateStoreInto("temp[", 6 + b2, "]"); + this.generateStoreInto("temp[", this.tempIndex(b2), "]"); break; case 0xF6: case 0xF7: throw Error("unusedBytecode " + b); @@ -702,17 +718,17 @@ to single-step. case 0xFB: b2 = bytes[this.pc++]; b3 = bytes[this.pc++]; - this.generatePush("temp[", 6 + b3, "].pointers[", b2, "]"); + this.generatePush("temp[", this.tempIndex(b3), "].pointers[", b2, "]"); break; case 0xFC: b2 = bytes[this.pc++]; b3 = bytes[this.pc++]; - this.generateStoreInto("temp[", 6 + b3, "].pointers[", b2, "]"); + this.generateStoreInto("temp[", this.tempIndex(b3), "].pointers[", b2, "]"); break; case 0xFD: b2 = bytes[this.pc++]; b3 = bytes[this.pc++]; - this.generatePopInto("temp[", 6 + b3, "].pointers[", b2, "]"); + this.generatePopInto("temp[", this.tempIndex(b3), "].pointers[", b2, "]"); break; case 0xFE: case 0xFF: throw Error("unusedBytecode " + b); @@ -742,6 +758,16 @@ to single-step. this.generateLabel(); this.needsVar[target] = true; this.needsVar['stack'] = true; + if (this.vm.useStackZone && target === "inst[") { + // un store a un context casado-vivo debe pasar por write-through + // (flush + escritura sobre el context real); pc fresco para el resume + this.needsVar['context'] = true; // fp/page + this.source.push( + "if (rcvr.sqClass !== vm.contextClass_ || rcvr.frame == null) { inst[", arg1, "] = stack[vm.sp]; rcvr.dirty = true; }\n", + "else { vm.pc = ", this.pc, "; vm.storeToMarriedContext(rcvr, ", arg1, ", stack[vm.sp]); if (vm.fp !== fp || vm.zonePage !== page) return; }\n"); + this.needsLabel[this.pc] = true; + return; + } this.source.push(target); if (arg1 !== undefined) { this.source.push(arg1, suffix1); @@ -757,6 +783,15 @@ to single-step. this.generateLabel(); this.needsVar[target] = true; this.needsVar['stack'] = true; + if (this.vm.useStackZone && target === "inst[") { + this.needsVar['context'] = true; // fp/page + this.source.push( + "var iv = stack[vm.sp--];\n", + "if (rcvr.sqClass !== vm.contextClass_ || rcvr.frame == null) { inst[", arg1, "] = iv; rcvr.dirty = true; }\n", + "else { vm.pc = ", this.pc, "; vm.storeToMarriedContext(rcvr, ", arg1, ", iv); if (vm.fp !== fp || vm.zonePage !== page) return; }\n"); + this.needsLabel[this.pc] = true; + return; + } this.source.push(target); if (arg1 !== undefined) { this.source.push(arg1, suffix1); @@ -786,7 +821,7 @@ to single-step. // actually stack === context.pointers but that would look weird this.needsVar['context'] = true; this.source.push( - "vm.pc = ", this.pc, "; vm.doReturn(", retVal, ", context.pointers[0]); return;\n"); + "vm.pc = ", this.pc, "; vm.doBlockReturn(", retVal, "); return;\n"); this.needsBreak = false; // returning anyway this.done = this.pc > this.endPC; }, @@ -799,7 +834,7 @@ to single-step. if (distance < 0) this.source.push( "\nif (vm.interruptCheckCounter-- <= 0) {\n", " vm.checkForInterrupts();\n", - " if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return;\n", + " if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return;\n", "}\n"); if (this.singleStep) this.source.push("\nif (vm.breakOutOfInterpreter) return;\n"); this.source.push("continue;\n"); @@ -831,16 +866,28 @@ to single-step. "var a, b; if ((a=stack[vm.sp-1]).sqClass === vm.specialObjects[7] && a.pointers && typeof (b=stack[vm.sp]) === 'number' && b>0 && b<=a.pointers.length) {\n", " stack[--vm.sp] = a.pointers[b-1];", "} else { var c = vm.primHandler.objectAt(true,true,false); if (vm.primHandler.success) stack[--vm.sp] = c; else {\n", - " vm.pc = ", this.pc, "; vm.sendSpecial(16); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return; }}\n"); + " vm.pc = ", this.pc, "; vm.sendSpecial(16); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return; }}\n"); this.needsLabel[this.pc] = true; return; case 0x1: // at:put: this.needsVar['stack'] = true; + if (this.vm.useStackZone) { + this.needsVar['context'] = true; // fp/page + this.source.push( + "var a, b; if ((a=stack[vm.sp-2]).sqClass === vm.specialObjects[7] && a.pointers && typeof (b=stack[vm.sp-1]) === 'number' && b>0 && b<=a.pointers.length) {\n", + " var c = stack[vm.sp]; stack[vm.sp-=2] = a.pointers[b-1] = c; a.dirty = true;", + "} else { var c = stack[vm.sp]; vm.pc = ", this.prevPC, "; vm.primHandler.objectAtPut(true,true,false); if (vm.fp !== fp || vm.zonePage !== page) return;\n", + " if (vm.primHandler.success) stack[vm.sp-=2] = c; else {\n", + " vm.pc = ", this.pc, "; vm.sendSpecial(17); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return; }}\n"); + this.needsLabel[this.prevPC] = true; + this.needsLabel[this.pc] = true; + return; + } this.source.push( "var a, b; if ((a=stack[vm.sp-2]).sqClass === vm.specialObjects[7] && a.pointers && typeof (b=stack[vm.sp-1]) === 'number' && b>0 && b<=a.pointers.length) {\n", " var c = stack[vm.sp]; stack[vm.sp-=2] = a.pointers[b-1] = c; a.dirty = true;", "} else { vm.primHandler.objectAtPut(true,true,false); if (vm.primHandler.success) stack[vm.sp-=2] = c; else {\n", - " vm.pc = ", this.pc, "; vm.sendSpecial(17); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return; }}\n"); + " vm.pc = ", this.pc, "; vm.sendSpecial(17); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return; }}\n"); this.needsLabel[this.pc] = true; return; case 0x2: // size @@ -848,7 +895,7 @@ to single-step. this.source.push( "if (stack[vm.sp].sqClass === vm.specialObjects[7]) stack[vm.sp] = stack[vm.sp].pointersSize();\n", // Array "else if (stack[vm.sp].sqClass === vm.specialObjects[6]) stack[vm.sp] = stack[vm.sp].bytesSize();\n", // ByteString - "else { vm.pc = ", this.pc, "; vm.sendSpecial(18); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return; }\n"); + "else { vm.pc = ", this.pc, "; vm.sendSpecial(18); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return; }\n"); this.needsLabel[this.pc] = true; return; //case 0x3: return false; // next @@ -889,7 +936,7 @@ to single-step. this.source.push( "vm.pc = ", this.pc, "; if (!vm.primHandler.quickSendOther(rcvr, ", (byte & 0x0F), "))", " vm.sendSpecial(", ((byte & 0x0F) + 16), ");\n", - "if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return;\n"); + "if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return;\n"); this.needsBreak = false; // already checked // if falling back to a full send we need a label for coming back this.needsLabel[this.pc] = true; @@ -906,42 +953,42 @@ to single-step. this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = vm.primHandler.signed32BitIntegerFor(a + b);\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(0); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(0); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x1: // MINUS - this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = vm.primHandler.signed32BitIntegerFor(a - b);\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(1); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(1); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x2: // LESS < this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = a < b ? vm.trueObj : vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(2); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(2); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x3: // GRTR > this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = a > b ? vm.trueObj : vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(3); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(3); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x4: // LEQ <= this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = a <= b ? vm.trueObj : vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(4); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(4); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x5: // GEQ >= this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = a >= b ? vm.trueObj : vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(5); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(5); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x6: // EQU = this.needsVar['stack'] = true; @@ -950,7 +997,7 @@ to single-step. " stack[--vm.sp] = a === b ? vm.trueObj : vm.falseObj;\n", "} else if (a === b && a.float === a.float) {\n", // NaN check " stack[--vm.sp] = vm.trueObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(6); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(6); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x7: // NEQ ~= this.needsVar['stack'] = true; @@ -959,7 +1006,7 @@ to single-step. " stack[--vm.sp] = a !== b ? vm.trueObj : vm.falseObj;\n", "} else if (a === b && a.float === a.float) {\n", // NaN check " stack[--vm.sp] = vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(7); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(7); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x8: // TIMES * this.source.push("vm.success = true; vm.resultIsFloat = false; if(!vm.pop2AndPushNumResult(vm.stackIntOrFloat(1) * vm.stackIntOrFloat(0))) { vm.pc = ", this.pc, "; vm.sendSpecial(8); return}\n"); @@ -1002,7 +1049,7 @@ to single-step. } else { this.source.push("; vm.send(", prefix, num, suffix, ", ", numArgs, ", ", superSend, "); "); } - this.source.push("if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return;\n"); + this.source.push("if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return;\n"); this.needsBreak = false; // already checked // need a label for coming back after send this.needsLabel[this.pc] = true; @@ -1028,7 +1075,7 @@ to single-step. this.needsVar['stack'] = true; this.source.push( "var closure = vm.instantiateClass(vm.specialObjects[36], ", numCopied, ");\n", - "closure.pointers[0] = context; vm.reclaimableContextCount = 0;\n", + "closure.pointers[0] = vm.activeContextObj(); vm.reclaimableContextCount = 0;\n", "closure.pointers[1] = ", from + this.method.pointers.length * 4 + 1, ";\n", // encodeSqueakPC "closure.pointers[2] = ", numArgs, ";\n"); if (numCopied > 0) { diff --git a/jit2.js b/jit2.js new file mode 100644 index 00000000..35d0bac3 --- /dev/null +++ b/jit2.js @@ -0,0 +1,865 @@ +"use strict"; +/* + * jit2: stack-to-register mapping compiler for stack-zone mode. + * + * Compiles V3(+closures) methods to JS functions where operand-stack slots + * live in JS locals (s0, s1, ...) whose depth is tracked at compile time. + * Slots are spilled to the zone only at send sites (where the frame must be + * observable/resumable) and reloaded on trampoline re-entry, distinguished + * from internal jumps by a function-local `entering` flag. Each send site + * has a monomorphic inline cache (class -> method/primitive), bypassing + * findSelectorInClass on hits. Unsupported bytecodes bail the whole method + * to the jit1 template compiler. + * + * Only active in stack-zone mode (generated code assumes frames). + */ + +Squeak.LEAF_DEOPT = { leafDeopt: true }; // sentinel: el leaf no pudo, re-ejecutar framed + +Object.subclass('Squeak.Compiler2', +'initialization', { + initialize: function(vm) { + this.vm = vm; + this.fallback = new Squeak.Compiler(vm); + this.bailCount = 0; + this.okCount = 0; + }, +}, +'accessing', { + compile: function(method, optClassObj, optSelObj) { + if (method.compiled === undefined) { + method.compiled = false; // 1st time: defer (same policy as jit1) + return; + } + if (method.methodSignFlag() // Sista: not supported yet + || (typeof process !== "undefined" && process.env && process.env.JIT2BAIL)) { + return this.fallback.compile(method, optClassObj, optSelObj); + } + // bisección de bugs: JIT2SEL="div,resto" compila con jit2 solo métodos + // cuyo fingerprint % div === alguno de los restos listados + if (typeof process !== "undefined" && process.env && process.env.JIT2SEL) { + var parts = process.env.JIT2SEL.split(",").map(Number); + var div = parts[0]; + var fpr = method.bytes ? method.bytes.length : 0; + if (method.bytes) for (var bi = 0; bi < Math.min(method.bytes.length, 16); bi++) + fpr = ((fpr * 31) + method.bytes[bi]) | 0; + var rem = ((fpr % div) + div) % div; + if (parts.indexOf(rem, 1) < 0) + return this.fallback.compile(method, optClassObj, optSelObj); + } + var clsName = optClassObj && optClassObj.className(), + sel = optSelObj && optSelObj.bytesAsString(); + var fn = this.generate2(method, clsName, sel); + if (fn) { + this.okCount++; + method.compiled = fn; + method.compiledLeaf = this.generateLeaf(method, + ((clsName || "M") + "_" + (sel || "m")).replace(/[^a-zA-Z0-9_]/g, "ː")) || null; + if (method.compiledLeaf) this.leafCount = (this.leafCount || 0) + 1; + } else { + this.bailCount++; + method.compiledLeaf = null; + return this.fallback.compile(method, optClassObj, optSelObj); + } + }, + enableSingleStepping: function(method, optClass, optSel) { + // debugging always uses the jit1 debug/single-step generator + return this.fallback.enableSingleStepping(method, optClass, optSel); + }, +}, +'generating', { + generateLeaf: function(method, funcName) { + // Forma "leaf" de un método: función JS pura que recibe (vm, rcvr, args...) + // y devuelve el resultado o Squeak.LEAF_DEOPT. Sin frame, sin zona, sin + // alocaciones (el overflow aritmético deoptimiza — el restart re-ejecuta + // el camino framed y ahí sí se aloca, exactamente una vez). Stores a inst + // vars permitidos solo si no hay puntos de deopt posteriores (restart-safe: + // los temps son locals JS, invisibles). + if (method.methodPrimitiveIndex() !== 0) return null; + var bytes = method.bytes; + var numArgs = method.methodNumArgs(); + var tempCount = method.methodTempCount(); + var src = []; + var depth = 0, maxDepth = 0; + var pc = 0, endPC = 0; + var labelDepth = {}, emitted = {}; + var knownDepth = true; + var instStoreSeen = false, deoptSeen = false; + var MIN = Squeak.MinSmallInt, MAX = Squeak.MaxSmallInt; + function push(expr) { src.push("v" + depth + " = " + expr + ";\n"); depth++; if (depth > maxDepth) maxDepth = depth; } + function pop() { depth--; return "v" + depth; } + function top() { return "v" + (depth - 1); } + function deopt() { deoptSeen = true; if (instStoreSeen) throw { bail: true }; return "return Squeak.LEAF_DEOPT;\n"; } + function jumpTo(dest, atDepth) { + if (dest <= pc) throw { bail: true }; // sin loops: trabajo acotado, sin interrupt checks + if (labelDepth[dest] === undefined) labelDepth[dest] = atDepth; + else if (labelDepth[dest] !== atDepth) throw { bail: true }; + if (dest > endPC) endPC = dest; + } + try { + var done = false; + while (!done) { + var instStart = pc; + if (!knownDepth) { + if (labelDepth[instStart] === undefined) throw { bail: true }; + depth = labelDepth[instStart]; + knownDepth = true; + } else if (labelDepth[instStart] !== undefined && labelDepth[instStart] !== depth) { + throw { bail: true }; + } + if (labelDepth[instStart] !== undefined) src.push("case " + instStart + ":\n"); + emitted[instStart] = depth; + var b = bytes[pc++], b2; + switch (b & 0xF8) { + case 0x00: case 0x08: push("inst[" + (b & 0xF) + "]"); break; + case 0x10: case 0x18: push("t" + (b & 0xF)); break; + case 0x20: case 0x28: case 0x30: case 0x38: push("lit[" + (1 + (b & 0x1F)) + "]"); break; + case 0x40: case 0x48: case 0x50: case 0x58: push("lit[" + (1 + (b & 0x1F)) + "].pointers[1]"); break; + case 0x60: // storeAndPop inst (receiver garantizado no-casado por el gate del call site) + if (deoptSeen) throw { bail: true }; // conservador: store tras posible deopt previo re-ejecutable? el restart re-ejecuta TODO: stores previos a deopts posteriores son el problema; deopts previos ya retornaron. Permitir. + instStoreSeen = true; + src.push("inst[" + (b & 7) + "] = " + pop() + "; rcvr.dirty = true;\n"); break; + case 0x68: src.push("t" + (b & 7) + " = " + pop() + ";\n"); break; + case 0x70: + switch (b) { + case 0x70: push("rcvr"); break; + case 0x71: push("vm.trueObj"); break; + case 0x72: push("vm.falseObj"); break; + case 0x73: push("vm.nilObj"); break; + case 0x74: push("-1"); break; + case 0x75: push("0"); break; + case 0x76: push("1"); break; + case 0x77: push("2"); break; + } + break; + case 0x78: + switch (b) { + case 0x78: src.push("return rcvr;\n"); break; + case 0x79: src.push("return vm.trueObj;\n"); break; + case 0x7A: src.push("return vm.falseObj;\n"); break; + case 0x7B: src.push("return vm.nilObj;\n"); break; + case 0x7C: src.push("return " + pop() + ";\n"); break; + default: throw { bail: true }; + } + knownDepth = false; + done = pc > endPC; + break; + case 0x80: case 0x88: + switch (b) { + case 0x80: + b2 = bytes[pc++]; + switch (b2 >> 6) { + case 0: push("inst[" + (b2 & 0x3F) + "]"); break; + case 1: push("t" + (b2 & 0x3F)); break; + case 2: push("lit[" + (1 + (b2 & 0x3F)) + "]"); break; + case 3: push("lit[" + (1 + (b2 & 0x3F)) + "].pointers[1]"); break; + } + break; + case 0x81: + b2 = bytes[pc++]; + if ((b2 >> 6) === 1) { src.push("t" + (b2 & 0x3F) + " = " + top() + ";\n"); break; } + if ((b2 >> 6) === 0) { instStoreSeen = true; src.push("inst[" + (b2 & 0x3F) + "] = " + top() + "; rcvr.dirty = true;\n"); break; } + throw { bail: true }; + case 0x82: + b2 = bytes[pc++]; + if ((b2 >> 6) === 1) { src.push("t" + (b2 & 0x3F) + " = " + pop() + ";\n"); break; } + if ((b2 >> 6) === 0) { instStoreSeen = true; src.push("inst[" + (b2 & 0x3F) + "] = " + pop() + "; rcvr.dirty = true;\n"); break; } + throw { bail: true }; + case 0x87: depth--; break; + case 0x88: push(top()); break; + default: throw { bail: true }; + } + break; + case 0x90: { var d = (b & 7) + 1; jumpTo(pc + d, depth); src.push("{ pcx = " + (pc + d) + "; continue; }\n"); knownDepth = false; done = pc > endPC; break; } + case 0x98: { var d = (b & 7) + 1, dest = pc + d, c = pop(); + jumpTo(dest, depth); + src.push("if (" + c + " === vm.falseObj) { pcx = " + dest + "; continue; }\n"); + src.push("else if (" + c + " !== vm.trueObj) " + deopt()); + break; } + case 0xA0: { b2 = bytes[pc++]; var d = ((b & 7) - 4) * 256 + b2; jumpTo(pc + d, depth); src.push("{ pcx = " + (pc + d) + "; continue; }\n"); knownDepth = false; done = pc > endPC; break; } + case 0xA8: { b2 = bytes[pc++]; var d = (b & 3) * 256 + b2, dest = pc + d, c = pop(), ifTrue = b < 0xAC; + jumpTo(dest, depth); + src.push("if (" + c + " === vm." + ifTrue + "Obj) { pcx = " + dest + "; continue; }\n"); + src.push("else if (" + c + " !== vm." + !ifTrue + "Obj) " + deopt()); + break; } + case 0xB0: case 0xB8: { // aritmética: smallint-in smallint-out, si no deopt + var op = b & 0xF, bb = pop(), aa = top(); + var guard = "if (typeof " + aa + " !== 'number' || typeof " + bb + " !== 'number') " + deopt(); + switch (op) { + case 0x0: case 0x1: { + var o = op === 0 ? "+" : "-"; + src.push(guard); + src.push("var r" + depth + " = " + aa + " " + o + " " + bb + "; if (r" + depth + " < " + MIN + " || r" + depth + " > " + MAX + ") " + deopt()); + src.push(aa + " = r" + depth + ";\n"); + break; + } + case 0x2: case 0x3: case 0x4: case 0x5: { + var cmp = ["<", ">", "<=", ">="][op - 2]; + src.push(guard); + src.push(aa + " = " + aa + " " + cmp + " " + bb + " ? vm.trueObj : vm.falseObj;\n"); + break; + } + case 0x6: case 0x7: { + src.push(guard); + src.push(aa + " = " + aa + " " + (op === 6 ? "===" : "!==") + " " + bb + " ? vm.trueObj : vm.falseObj;\n"); + break; + } + case 0xE: case 0xF: { + src.push(guard); + src.push(aa + " = " + aa + " " + (op === 0xE ? "&" : "|") + " " + bb + ";\n"); + break; + } + default: throw { bail: true }; + } + break; + } + case 0xC0: case 0xC8: { + var lo = b & 0xF; + if (lo === 0x6) { var b6 = pop(), a6 = top(); src.push(a6 + " = " + a6 + " === " + b6 + " ? vm.trueObj : vm.falseObj;\n"); break; } + if (lo === 0x7) { var t7 = top(); src.push(t7 + " = typeof " + t7 + " === 'number' ? vm.specialObjects[5] : " + t7 + ".sqClass;\n"); break; } + throw { bail: true }; + } + default: throw { bail: true }; // sends reales, closures, thisContext: no-leaf + } + } + } catch (e) { + if (e && e.bail) return null; + throw e; + } + // ensamblar + var head = []; + var params = ["vm", "rcvr"]; + for (var i = 0; i < numArgs; i++) params.push("t" + i); + var decls = []; + for (var i = numArgs; i < tempCount; i++) decls.push("t" + i + " = vm.nilObj"); + for (var i = 0; i < maxDepth; i++) decls.push("v" + i); + head.push("'use strict';\nvar lit = arguments[0];\nreturn function LEAF_" + funcName + "(" + params.join(", ") + ") {\n"); + head.push("var inst = rcvr.pointers;\n"); + if (decls.length) head.push("var " + decls.join(", ") + ";\n"); + var hasLabels = Object.keys(labelDepth).length > 0; + var body; + if (hasLabels) { + body = "var pcx = 0;\nwhile (true) switch (pcx) {\ncase 0:\n" + src.join("") + "default: return Squeak.LEAF_DEOPT;\n}\n"; + } else { + body = src.join("") + "return Squeak.LEAF_DEOPT;\n"; + } + // lit se pasa via bind para no leer method.pointers por llamada + var full = head.join("") + body + "}"; + try { + return new Function(full)(method.pointers); // literales cerrados en la función + } catch (e) { + return null; + } + }, + generate2: function(method, optClass, optSel) { + try { + return this.generateV3R(method, optClass, optSel); + } catch (e) { + if (e && e.bail) return null; + throw e; + } + }, + envNo: function(tag) { + return typeof process !== "undefined" && process.env && process.env.JIT2NO + && process.env.JIT2NO.split(",").indexOf(tag) >= 0; + }, + bail: function(why) { + throw { bail: true, why: why }; + }, + generateV3R: function(method, optClass, optSel) { + this.method = method; + this.pc = 0; + this.endPC = 0; + this.depth = 0; // virtual operand-stack depth + this.maxDepth = 0; + this.knownDepth = true; // false after unconditional jump/return until a label + this.labelDepth = {}; // pc -> expected depth (from forward jumps / resumes) + this.resumeDepth = {}; // pc -> depth to reload on trampoline re-entry + this.needsLabel = {}; + this.sourceParts = []; // list of {pc, code:[]} segments, one per instruction + this.ics = []; + var tempCount = method.methodTempCount(); + this.tempCount = tempCount; + this.blockRegions = []; // {from, to, ceil}: dentro de un block V3, los temps + // con índice >= args+copied ALIASAN slots del stack de + // operandos (los aloca el pushNil inicial del block); + // jit2 los tiene en locals, así que esos métodos bailean + // pass over bytecodes + this.done = false; + while (!this.done) { + this.instStart = this.pc; + this.beginInstruction(); + var b = method.bytes[this.pc++]; + this.generateByte(b); + } + // assemble + var parts = this.sourceParts; + var src = []; + var maxD = this.maxDepth; + var decl = []; + for (var i = 0; i < maxD; i++) decl.push("s" + i); + src.push("var zone = vm.stack, fp = vm.fp, page = vm.zonePage;\n"); + src.push("var rcvr = vm.receiver, inst = rcvr.pointers, lit = vm.method.pointers;\n"); + // spBase = base del stack de operandos de ESTA activación (métodos y + // blocks difieren); en entrada fresca depth=0 así que spBase = vm.sp, + // en re-entrada el prelude del label lo deriva de la depth de resume + src.push("var tB = vm.tempOffset, spBase = vm.sp;\n"); + if (decl.length) src.push("var ", decl.join(", "), ";\n"); + src.push("var entering = true;\n"); + src.push("while (true) switch (vm.pc) {\n"); + this.needsLabel[0] = true; + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + if (this.needsLabel[part.pc]) { + src.push("case ", part.pc, ":\n"); + // profundidad a recargar: la depth estática de este label — + // cubre resumes de sends Y transiciones intérprete->compilado + // a mitad de método (loop heads con contador en el stack) + var rd = this.emittedDepth[part.pc] || 0; + src.push("if (entering) { entering = false; spBase = vm.sp - ", rd, ";"); + for (var k = 0; k < rd; k++) + src.push(" s", k, " = zone[spBase + ", 1 + k, "];"); + src.push(" }\n"); + } + for (var j = 0; j < part.code.length; j++) src.push(part.code[j]); + } + src.push("default: if (vm.jit2Debug) { console.error('jit2 default pc=' + vm.pc + ' mbytes=' + vm.method.bytes.length); console.error('bytes: ' + Array.prototype.join.call(vm.method.bytes, ',')); console.error(String(vm.method.compiled).slice(0, 2000)); vm.jit2Debug = false; } vm.interpretOne(true); return;\n}"); + var funcName = (optClass && optSel) + ? (optClass + "_" + optSel).replace(/[^a-zA-Z0-9_]/g, "ː") + : "R_METHOD"; + var body = "'use strict';\nvar ics = arguments[0];\nreturn function " + funcName + + "(vm) {\n" + src.join("") + "}"; + try { + return new Function(body)(this.ics); + } catch (e) { + this.bail("syntax: " + e.message); + } + }, + beginInstruction: function() { + // labels: if this pc is a known jump target, depths must agree + var expected = this.labelDepth[this.instStart]; + if (!this.knownDepth) { + if (expected === undefined) this.bail("unreachable code with unknown depth"); + this.depth = expected; + this.knownDepth = true; + } else if (expected !== undefined && expected !== this.depth) { + this.bail("stack depth mismatch at label"); + } + if (!this.emittedDepth) this.emittedDepth = {}; + this.emittedDepth[this.instStart] = this.depth; + this.part = { pc: this.instStart, code: [] }; + this.sourceParts.push(this.part); + }, + emit: function(/* ... */) { + for (var i = 0; i < arguments.length; i++) this.part.code.push(arguments[i]); + }, + push: function(expr) { + this.emit("s", this.depth, " = ", expr, ";\n"); + this.depth++; + if (this.depth > this.maxDepth) this.maxDepth = this.depth; + }, + top: function() { return "s" + (this.depth - 1); }, + pop: function() { this.depth--; return "s" + this.depth; }, + slotAt: function(depthFromTop) { return "s" + (this.depth - 1 - depthFromTop); }, + checkTempAccess: function(index) { + // buscar la región de block MÁS INTERNA que contenga el pc actual + for (var i = this.blockRegions.length - 1; i >= 0; i--) { + var r = this.blockRegions[i]; + if (this.instStart >= r.from && this.instStart < r.to) { + if (index >= r.ceil) this.bail("block stack-temp aliasing"); + return; + } + } + }, + spillAll: function() { + for (var k = 0; k < this.depth; k++) + this.emit("zone[spBase + ", 1 + k, "] = s", k, ";\n"); + this.emit("vm.sp = spBase + ", this.depth, ";\n"); + }, + activationCheck: function() { + return "if (vm.fp !== fp || vm.zonePage !== page || vm.breakOutOfInterpreter !== false) return;\n"; + }, + markJumpTarget: function(dest) { + this.needsLabel[dest] = true; + if (dest > this.endPC) this.endPC = dest; + if (dest <= this.instStart) { // backward: el label ya se emitió + if (this.emittedDepth[dest] !== this.depth) this.bail("backward jump depth mismatch"); + return; + } + var expected = this.labelDepth[dest]; + if (expected === undefined) this.labelDepth[dest] = this.depth; + else if (expected !== this.depth) this.bail("jump depth mismatch"); + }, + markResume: function(pc, depthAfter) { + this.needsLabel[pc] = true; + if (pc > this.endPC) this.endPC = pc; // el label de resume debe generarse + if (this.resumeDepth[pc] !== undefined && this.resumeDepth[pc] !== depthAfter) + this.bail("conflicting resume depths"); + this.resumeDepth[pc] = depthAfter; + if (pc <= this.instStart) { // backward (interrupt check de back-jump) + if (this.emittedDepth[pc] !== depthAfter) this.bail("backward resume depth mismatch"); + return; + } + var expected = this.labelDepth[pc]; + if (expected === undefined) this.labelDepth[pc] = depthAfter; + else if (expected !== depthAfter) this.bail("resume depth mismatch"); + }, + endOfSegment: function() { + // after return/unconditional jump: depth unknown until next label + this.knownDepth = false; + this.done = this.pc > this.endPC; + }, +}, +'bytecodes', { + generateByte: function(byte) { + var b2, b3; + switch (byte & 0xF8) { + case 0x00: case 0x08: // push inst var + this.push("inst[" + (byte & 0x0F) + "]"); break; + case 0x10: case 0x18: // push temp + this.checkTempAccess(byte & 0xF); + this.push("zone[tB + " + (byte & 0xF) + "]"); break; + case 0x20: case 0x28: case 0x30: case 0x38: // push literal + this.push("lit[" + (1 + (byte & 0x1F)) + "]"); break; + case 0x40: case 0x48: case 0x50: case 0x58: // push literal indirect + this.push("lit[" + (1 + (byte & 0x1F)) + "].pointers[1]"); break; + case 0x60: // storeAndPop inst var + this.generateStoreInst(byte & 7, true); break; + case 0x68: // storeAndPop temp + this.checkTempAccess(byte & 7); + this.emit("zone[tB + ", byte & 7, "] = ", this.pop(), ";\n"); break; + case 0x70: // quick pushes + switch (byte) { + case 0x70: this.push("rcvr"); break; + case 0x71: this.push("vm.trueObj"); break; + case 0x72: this.push("vm.falseObj"); break; + case 0x73: this.push("vm.nilObj"); break; + case 0x74: this.push("-1"); break; + case 0x75: this.push("0"); break; + case 0x76: this.push("1"); break; + case 0x77: this.push("2"); break; + } + break; + case 0x78: // quick returns + switch (byte) { + case 0x78: this.generateReturn("rcvr"); break; + case 0x79: this.generateReturn("vm.trueObj"); break; + case 0x7A: this.generateReturn("vm.falseObj"); break; + case 0x7B: this.generateReturn("vm.nilObj"); break; + case 0x7C: this.generateReturn(this.pop()); break; + case 0x7D: this.generateBlockReturn(); break; + default: this.bail("unusedBytecode " + byte); + } + break; + case 0x80: case 0x88: + this.generateExtended(byte); break; + case 0x90: // short jump + this.generateJump((byte & 7) + 1); break; + case 0x98: // short conditional jump (jumpIfFalse) + this.generateJumpIf(false, (byte & 7) + 1); break; + case 0xA0: // long jump + b2 = this.method.bytes[this.pc++]; + this.generateJump(((byte & 7) - 4) * 256 + b2); break; + case 0xA8: // long conditional jump + b2 = this.method.bytes[this.pc++]; + this.generateJumpIf(byte < 0xAC, (byte & 3) * 256 + b2); break; + case 0xB0: case 0xB8: // arithmetic special sends + this.generateNumericOp(byte); break; + case 0xC0: case 0xC8: // quick prims + this.generateQuickPrim(byte); break; + case 0xD0: case 0xD8: // send literal selector, 0 args + this.generateSend(1 + (byte & 0xF), 0); break; + case 0xE0: case 0xE8: // 1 arg + this.generateSend(1 + (byte & 0xF), 1); break; + case 0xF0: case 0xF8: // 2 args + this.generateSend(1 + (byte & 0xF), 2); break; + default: this.bail("bytecode " + byte); + } + }, + generateExtended: function(byte) { + var b2, b3; + switch (byte) { + case 0x80: // extended push + b2 = this.method.bytes[this.pc++]; + switch (b2 >> 6) { + case 0: this.push("inst[" + (b2 & 0x3F) + "]"); break; + case 1: this.checkTempAccess(b2 & 0x3F); this.push("zone[tB + " + (b2 & 0x3F) + "]"); break; + case 2: this.push("lit[" + (1 + (b2 & 0x3F)) + "]"); break; + case 3: this.push("lit[" + (1 + (b2 & 0x3F)) + "].pointers[1]"); break; + } + break; + case 0x81: // extended store (no pop) + b2 = this.method.bytes[this.pc++]; + switch (b2 >> 6) { + case 0: this.generateStoreInst(b2 & 0x3F, false); break; + case 1: this.checkTempAccess(b2 & 0x3F); this.emit("zone[tB + ", b2 & 0x3F, "] = ", this.top(), ";\n"); break; + case 2: this.bail("store into literal"); + case 3: this.emit("var assoc = lit[", 1 + (b2 & 0x3F), "]; assoc.dirty = true; assoc.pointers[1] = ", this.top(), ";\n"); break; + } + break; + case 0x82: // extended store-pop + b2 = this.method.bytes[this.pc++]; + switch (b2 >> 6) { + case 0: this.generateStoreInst(b2 & 0x3F, true); break; + case 1: this.checkTempAccess(b2 & 0x3F); this.emit("zone[tB + ", b2 & 0x3F, "] = ", this.pop(), ";\n"); break; + case 2: this.bail("store into literal"); + case 3: this.emit("var assoc = lit[", 1 + (b2 & 0x3F), "]; assoc.dirty = true; assoc.pointers[1] = ", this.pop(), ";\n"); break; + } + break; + case 0x83: // single extended send + b2 = this.method.bytes[this.pc++]; + this.generateSend(1 + (b2 & 31), b2 >> 5); + break; + case 0x84: // double extended do-anything + b2 = this.method.bytes[this.pc++]; + b3 = this.method.bytes[this.pc++]; + switch (b2 >> 5) { + case 0: this.generateSend(1 + b3, b2 & 31); break; + case 1: if (this.envNo("super")) this.bail("super"); this.generateSend(1 + b3, b2 & 31, true); break; + case 2: this.push("inst[" + b3 + "]"); break; + case 3: this.push("lit[" + (1 + b3) + "]"); break; + case 4: this.push("lit[" + (1 + b3) + "].pointers[1]"); break; + default: this.bail("doubleExtended op " + (b2 >> 5)); + } + break; + case 0x85: // single extended send to super + if (this.envNo("super")) this.bail("super"); + b2 = this.method.bytes[this.pc++]; + this.generateSend(1 + (b2 & 31), b2 >> 5, true); + break; + case 0x86: // second extended send + if (this.envNo("send2")) this.bail("send2"); + b2 = this.method.bytes[this.pc++]; + this.generateSend(1 + (b2 & 0x3F), b2 >> 6); + break; + case 0x87: // pop + this.depth--; break; + case 0x88: // dup + this.push(this.top()); break; + case 0x89: // thisContext + this.spillAll(); + this.push("vm.exportThisContext()"); + break; + case 0x8A: { // closure temps (array de indirección) + if (this.envNo("ctemps")) this.bail("ctemps"); + b2 = this.method.bytes[this.pc++]; + var popValues = b2 > 127, count = b2 & 127; + this.emit("var arr = vm.instantiateClass(vm.specialObjects[7], ", count, ");\n"); + if (popValues) { + for (var ci = 0; ci < count; ci++) + this.emit("arr.pointers[", ci, "] = ", this.slotAt(count - ci - 1), ";\n"); + this.depth -= count; + } + this.push("arr"); + break; + } + case 0x8C: + if (this.envNo("vec")) this.bail("vec"); + // remote push from temp vector + b2 = this.method.bytes[this.pc++]; + b3 = this.method.bytes[this.pc++]; + this.push("zone[tB + " + b3 + "].pointers[" + b2 + "]"); + break; + case 0x8D: { // remote store into temp vector + if (this.envNo("vec")) this.bail("vec"); + b2 = this.method.bytes[this.pc++]; + b3 = this.method.bytes[this.pc++]; + this.emit("var tv = zone[tB + ", b3, "]; tv.pointers[", b2, "] = ", this.top(), "; tv.dirty = true;\n"); + break; + } + case 0x8E: { // remote store and pop + if (this.envNo("vec")) this.bail("vec"); + b2 = this.method.bytes[this.pc++]; + b3 = this.method.bytes[this.pc++]; + this.emit("var tv = zone[tB + ", b3, "]; tv.pointers[", b2, "] = ", this.pop(), "; tv.dirty = true;\n"); + break; + } + case 0x8F: // pushClosureCopy + this.generateClosureCopy(); break; + default: + this.bail("extended " + byte); // 0x8B callPrimitive: jit1 + } + }, + generateStoreInst: function(index, popIt) { + var val = popIt ? this.pop() : this.top(); + // married-context write-through, same protocol as jit1 frames templates + this.emit("if (rcvr.sqClass !== vm.contextClass_ || rcvr.frame == null) { inst[", index, "] = ", val, "; rcvr.dirty = true; }\n"); + this.emit("else { "); + this.spillAll(); + this.emit("vm.pc = ", this.pc, "; vm.storeToMarriedContext(rcvr, ", index, ", ", val, "); ", + this.activationCheck(), " }\n"); + this.markResume(this.pc, this.depth); + }, + generateReturn: function(what) { + this.emit("vm.pc = ", this.pc, "; vm.doReturn(", what, "); return;\n"); + this.endOfSegment(); + }, + generateBlockReturn: function() { + this.emit("vm.pc = ", this.pc, "; vm.doBlockReturn(", this.pop(), "); return;\n"); + this.endOfSegment(); + }, + generateJump: function(distance) { + var dest = this.pc + distance; + if (distance < 0) { + // backward jump: interrupt check; a process switch must find the + // frame resumable, so spill live slots (loop back-edges: usually 0) + this.spillAll(); + this.emit("if (vm.interruptCheckCounter-- <= 0) {\n", + " vm.pc = ", dest, "; vm.checkForInterrupts();\n", + " ", this.activationCheck(), "}\n"); + this.markResume(dest, this.depth); + } + this.markJumpTarget(dest); + this.emit("vm.pc = ", dest, "; continue;\n"); + this.endOfSegment(); + }, + generateJumpIf: function(condition, distance) { + var dest = this.pc + distance; + var cond = this.pop(); + this.emit("if (", cond, " === vm.", condition, "Obj) { vm.pc = ", dest, "; continue; }\n"); + this.emit("else if (", cond, " !== vm.", !condition, "Obj) {\n"); + this.depth++; this.spillAll(); this.depth--; // cond back on stack as receiver + this.emit(" vm.pc = ", this.pc, "; vm.send(vm.specialObjects[25], 0, false); return; }\n"); + this.markJumpTarget(dest); + this.markResume(this.pc, this.depth); + }, + generateSend: function(litIndex, argCount, superFlag) { + var ic = this.ics.length; + this.ics.push({ c: null, m: null, a: 0, p: 0, k: null }); + var rcvrSlot = this.slotAt(argCount); + var argSlots = []; + for (var i = argCount - 1; i >= 0; i--) argSlots.push(this.slotAt(i)); + this.emit("vm.pc = ", this.pc, ";\n"); + this.emit("var ic = ics[", ic, "], rc = typeof ", rcvrSlot, " === 'number' ? vm.smallIntClass_ : ", rcvrSlot, ".sqClass;\n"); + if (superFlag) { + // super: la clase de lookup es estática (superclase de la clase del + // método) — el IC se llena una sola vez y vale para todo receiver + this.emit("if (ic.c === null) vm.jit2FillSuperIC(ic, lit[", litIndex, "], ", argCount, ");\n"); + } else { + this.emit("if (rc !== ic.c) vm.jit2FillIC(ic, lit[", litIndex, "], ", argCount, ", rc);\n"); + } + // leaf fast path: sin spill, sin frame, args como argumentos JS. Solo con + // interruptCheckCounter > 0 (paridad exacta de cadencia con el golden: + // vencido => camino framed, donde executeNewMethod chequea como siempre) + this.emit("var lr = vm.LEAF_DEOPT_;\n"); + this.emit("if (ic.a === ", argCount, " && ic.m.compiledLeaf != null && rc !== vm.contextClass_ && vm.interruptCheckCounter > 0) {\n"); + this.emit(" var sc = vm.sendCount++; vm.interruptCheckCounter--;\n"); + this.emit(" lr = ic.m.compiledLeaf(vm, ", [rcvrSlot].concat(argSlots).join(", "), ");\n"); + this.emit(" if (lr === vm.LEAF_DEOPT_) { vm.sendCount--; vm.interruptCheckCounter++; vm.nLeafDeopts++; }\n"); + this.emit(" else { vm.nLeafCalls++; if (vm.jit2LeafHook) vm.jit2LeafHook(ic.m, sc, ", rcvrSlot, ", lit[", litIndex, "]); }\n"); + this.emit("}\n"); + var resultSlot = "s" + (this.depth - argCount - 1); + this.emit("if (lr !== vm.LEAF_DEOPT_) { ", resultSlot, " = lr; }\n"); + this.emit("else {\n"); + this.spillAll(); + this.emit("if (rc === vm.contextClass_ && ", rcvrSlot, ".frame != null) vm.syncMarriedContext(", rcvrSlot, ");\n"); + this.emit("if (ic.p) { vm.verifyAtSelector = lit[", litIndex, "]; vm.verifyAtClass = ", superFlag ? "ic.c" : "rc", "; }\n"); + this.emit("vm.executeNewMethod(", rcvrSlot, ", ic.m, ic.a, ic.p, ic.k, lit[", litIndex, "]);\n"); + this.emit(this.activationCheck()); + this.emit(resultSlot, " = zone[vm.sp];\n"); + this.emit("}\n"); + this.depth -= argCount + 1; + this.depth++; if (this.depth > this.maxDepth) this.maxDepth = this.depth; + this.markResume(this.pc, this.depth); + }, + generateNumericOp: function(byte) { + var a, b; + switch (byte & 0xF) { + case 0x0: case 0x1: { // + - + var op = (byte & 0xF) === 0 ? "+" : "-"; + b = this.pop(); a = this.top(); + this.emit("if (typeof ", a, " === 'number' && typeof ", b, " === 'number') ", + a, " = vm.primHandler.signed32BitIntegerFor(", a, " ", op, " ", b, ");\n"); + this.generateNumericFallback(a, b, byte & 0xF); + break; + } + case 0x2: case 0x3: case 0x4: case 0x5: { // < > <= >= + var cmp = ["<", ">", "<=", ">="][(byte & 0xF) - 2]; + b = this.pop(); a = this.top(); + this.emit("if (typeof ", a, " === 'number' && typeof ", b, " === 'number') ", + a, " = ", a, " ", cmp, " ", b, " ? vm.trueObj : vm.falseObj;\n"); + this.generateNumericFallback(a, b, byte & 0xF); + break; + } + case 0x6: case 0x7: { // = ~= + var eq = (byte & 0xF) === 6; + b = this.pop(); a = this.top(); + this.emit("if (typeof ", a, " === 'number' && typeof ", b, " === 'number') ", + a, " = ", a, " ", eq ? "===" : "!==", " ", b, " ? vm.trueObj : vm.falseObj;\n"); + this.emit("else if (", a, " === ", b, " && ", a, ".float === ", a, ".float) ", + a, " = vm.", eq ? "true" : "false", "Obj;\n"); + this.generateNumericFallback(a, b, byte & 0xF); + break; + } + case 0xE: case 0xF: { // bitAnd: bitOr: + var bop = (byte & 0xF) === 0xE ? "&" : "|"; + b = this.pop(); a = this.top(); + this.emit("if (typeof ", a, " === 'number' && typeof ", b, " === 'number' && (", a, "|0) === ", a, " && (", b, "|0) === ", b, ") ", + a, " = ", a, " ", bop, " ", b, ";\n"); + this.generateNumericFallback(a, b, byte & 0xF); + break; + } + default: { // * / \\ @ bitShift: // : usar el camino genérico de vm + b = this.pop(); a = this.top(); + this.generateNumericSlow(byte & 0xF); + break; + } + } + }, + generateNumericFallback: function(a, b, opIndex) { + // a/b son locals; a ya tiene el resultado si el fast path aplicó. + // Si no aplicó, mandar el send especial (stack: rcvr, arg). + this.emit("else {\n"); + this.depth++; this.spillAll(); this.depth--; + this.emit(" vm.pc = ", this.pc, "; vm.sendSpecial(", opIndex, "); ", this.activationCheck()); + this.emit(" ", a, " = zone[vm.sp];\n}\n"); + this.markResume(this.pc, this.depth); + }, + generateNumericSlow: function(opIndex) { + // sin fast path inline: siempre via camino del vm (pop2AndPush... via sendSpecial-like) + this.depth++; this.spillAll(); this.depth--; + var a = "s" + (this.depth - 1); + this.emit("vm.pc = ", this.pc, "; vm.sp = spBase + ", this.depth + 1, ";\n"); + // replicar jit1: success+pop2AndPushResult según op + switch (opIndex) { + case 0x8: this.emit("vm.success = true; vm.resultIsFloat = false; if (!vm.pop2AndPushNumResult(vm.stackIntOrFloat(1) * vm.stackIntOrFloat(0))) { vm.sendSpecial(8); ", this.activationCheck(), "}\n"); break; + case 0x9: this.emit("vm.success = true; if (!vm.pop2AndPushIntResult(vm.quickDivide(vm.stackInteger(1), vm.stackInteger(0)))) { vm.sendSpecial(9); ", this.activationCheck(), "}\n"); break; + case 0xA: this.emit("vm.success = true; if (!vm.pop2AndPushIntResult(vm.mod(vm.stackInteger(1), vm.stackInteger(0)))) { vm.sendSpecial(10); ", this.activationCheck(), "}\n"); break; + case 0xB: this.emit("vm.success = true; if (!vm.primHandler.primitiveMakePoint(1, true)) { vm.sendSpecial(11); ", this.activationCheck(), "}\n"); break; + case 0xC: this.emit("vm.success = true; if (!vm.pop2AndPushIntResult(vm.safeShift(vm.stackInteger(1), vm.stackInteger(0)))) { vm.sendSpecial(12); ", this.activationCheck(), "}\n"); break; + case 0xD: this.emit("vm.success = true; if (!vm.pop2AndPushIntResult(vm.div(vm.stackInteger(1), vm.stackInteger(0)))) { vm.sendSpecial(13); ", this.activationCheck(), "}\n"); break; + default: this.bail("numeric op " + opIndex); + } + this.emit(a, " = zone[vm.sp];\n"); + this.markResume(this.pc, this.depth); + }, + generateQuickPrim: function(byte) { + var lo = byte & 0xF; + switch (lo) { + case 0x6: { // == + var b = this.pop(), a = this.top(); + this.emit(a, " = ", a, " === ", b, " ? vm.trueObj : vm.falseObj;\n"); + return; + } + case 0x7: { // class + var t = this.top(); + this.emit(t, " = typeof ", t, " === 'number' ? vm.specialObjects[5] : ", t, ".sqClass;\n"); + return; + } + case 0x0: { // at: + var idx = this.slotAt(0), arr = this.slotAt(1); + this.emit("if (", arr, ".sqClass === vm.specialObjects[7] && ", arr, ".pointers && typeof ", idx, " === 'number' && ", idx, " > 0 && ", idx, " <= ", arr, ".pointers.length) {\n", + " ", arr, " = ", arr, ".pointers[", idx, " - 1];\n} else {\n"); + this.spillAll(); + this.emit(" var c = vm.primHandler.objectAt(true,true,false); if (vm.primHandler.success) { vm.sp -= 1; ", arr, " = c; } else {\n", + " vm.pc = ", this.pc, "; vm.sendSpecial(16); ", this.activationCheck(), + " ", arr, " = zone[vm.sp]; }\n}\n"); + this.depth--; + this.markResume(this.pc, this.depth); + return; + } + case 0x1: { // at:put: + var val = this.slotAt(0), idx = this.slotAt(1), arr = this.slotAt(2); + this.emit("if (", arr, ".sqClass === vm.specialObjects[7] && ", arr, ".pointers && typeof ", idx, " === 'number' && ", idx, " > 0 && ", idx, " <= ", arr, ".pointers.length) {\n", + " ", arr, ".pointers[", idx, " - 1] = ", val, "; ", arr, ".dirty = true; ", arr, " = ", val, ";\n} else {\n"); + this.spillAll(); + // puede flushear (context casado): pc de re-ejecución idempotente + this.emit(" vm.pc = ", this.instStart, "; vm.primHandler.objectAtPut(true,true,false); if (vm.fp !== fp || vm.zonePage !== page) return;\n", + " if (vm.primHandler.success) { vm.sp -= 2; ", arr, " = ", val, "; } else {\n", + " vm.pc = ", this.pc, "; vm.sendSpecial(17); ", this.activationCheck(), + " ", arr, " = zone[vm.sp]; }\n}\n"); + this.depth -= 2; + this.needsLabel[this.instStart] = true; + this.labelDepth[this.instStart] = this.depth + 2; + this.resumeDepth[this.instStart] = this.depth + 2; + this.markResume(this.pc, this.depth); + return; + } + case 0x2: { // size + var t = this.top(); + this.emit("if (", t, ".sqClass === vm.specialObjects[7]) ", t, " = ", t, ".pointersSize();\n", + "else if (", t, ".sqClass === vm.specialObjects[6]) ", t, " = ", t, ".bytesSize();\n", + "else {\n"); + this.spillAll(); + this.emit(" vm.pc = ", this.pc, "; vm.sendSpecial(18); ", this.activationCheck(), + " ", t, " = zone[vm.sp];\n}\n"); + this.markResume(this.pc, this.depth); + return; + } + case 0x9: case 0xA: case 0xB: { // value value: do: + this.spillAll(); + var rcvrSlot = this.slotAt(lo === 0x9 ? 0 : 1); + this.emit("vm.pc = ", this.pc, "; if (!vm.primHandler.quickSendOther(", rcvrSlot, ", ", lo, ")) vm.sendSpecial(", lo + 16, "); return;\n"); + this.depth -= (lo === 0x9 ? 0 : 1); + this.markResume(this.pc, this.depth); + this.endOfSegment(); + // tras el return el resultado queda en zone: el resume lo recarga + return; + } + case 0x3: case 0x5: case 0xC: case 0xE: case 0xF: { // next atEnd new x y (0 args) + if (this.envNo("qp0")) this.bail("qp0"); + this.spillAll(); // receiver ya está en el stack virtual + this.emit("vm.primHandler.success = true; vm.pc = ", this.pc, "; vm.sendSpecial(", lo + 16, "); ", this.activationCheck()); + this.emit(this.top(), " = zone[vm.sp];\n"); + this.markResume(this.pc, this.depth); + return; + } + case 0x4: case 0xD: { // nextPut: new: (1 arg) + if (this.envNo("qp1")) this.bail("qp1"); + this.spillAll(); + this.emit("vm.primHandler.success = true; vm.pc = ", this.pc, "; vm.sendSpecial(", lo + 16, "); ", this.activationCheck()); + this.depth--; + this.emit(this.top(), " = zone[vm.sp];\n"); + this.markResume(this.pc, this.depth); + return; + } + default: // blockCopy: do: -> jit1 + this.bail("quick prim " + lo); + } + }, + generateClosureCopy: function() { + var b = this.method.bytes; + var numArgsNumCopied = b[this.pc++], + numArgs = numArgsNumCopied & 0xF, + numCopied = numArgsNumCopied >> 4, + blockSize = b[this.pc++] * 256 + b[this.pc++]; + var from = this.pc, to = from + blockSize; + this.blockRegions.push({ from: from, to: to, ceil: numArgs + numCopied }); + // marry del frame activo: la zona y vm.sp deben reflejar el estado real + // (el snapshot del context casado y el GC leen vm.sp) + this.spillAll(); + this.emit("var closure = vm.instantiateClass(vm.specialObjects[36], ", numCopied, ");\n"); + this.emit("closure.pointers[0] = vm.activeContextObj(); vm.reclaimableContextCount = 0;\n"); + this.emit("closure.pointers[1] = ", from + this.method.pointers.length * 4 + 1, ";\n"); + this.emit("closure.pointers[2] = ", numArgs, ";\n"); + for (var i = 0; i < numCopied; i++) + this.emit("closure.pointers[", 3 + i, "] = ", this.slotAt(numCopied - i - 1), ";\n"); + this.depth -= numCopied; + this.push("closure"); + this.emit("vm.pc = ", to, "; continue;\n"); + this.markJumpTarget(to); + // el cuerpo del block arranca con stack vacío (activación fresca) + this.needsLabel[from] = true; + this.labelDepth[from] = 0; + this.resumeDepth[from] = 0; + if (to > this.endPC) this.endPC = to; + this.endOfSegment(); + }, +}); + +// vm-side support +Object.extend(Squeak.Interpreter.prototype, 'jit2 support', { + jit2FillSuperIC: function(ic, selector, argCount) { + // super: lookup estático en la superclase de la clase del método activo + var lookupClass = this.method.methodClassForSuper().superclass(); + this.jit2FillIC(ic, selector, argCount, lookupClass); + }, + jit2FillIC: function(ic, selector, argCount, lookupClass) { + var entry = this.findSelectorInClass(selector, argCount, lookupClass); + ic.c = lookupClass; + ic.m = entry.method; + ic.a = entry.argCount; + ic.p = entry.primIndex; + ic.k = entry.mClass; + }, +}); diff --git a/perf/README.md b/perf/README.md new file mode 100644 index 00000000..b9ff72b8 --- /dev/null +++ b/perf/README.md @@ -0,0 +1,40 @@ +# perf/ — proyecto stack zone (frames planos + reificación perezosa de Contexts) + +Ver [stack-zone-design.md](stack-zone-design.md) para el diseño completo, las reglas +de reificación verificadas contra el código, y el plan por fases. + +## spike/ — benchmarks fase 0 (sintéticos) + +Cinco variantes del mismo `fib(32)` por sends (7.05M activaciones) que aíslan +(a) contexts heap vs frames planos y (b) JS vs WASM. Resultados 2026-07-05 +(Node 20, M-series): + +| variante | ms | vs V1 | +|---|---|---| +| V1 contexts + trampolín (≈ jit.js actual) | 198 | 1.00x | +| V2 frames + trampolín (JS puro, camino incremental) | 143 | **1.38x** | +| V3 frames + intérprete de bytecodes JS | 313 | 0.63x | +| V4 frames + intérprete de bytecodes WASM | 183 | 1.08x | +| V5 frames + métodos compilados WASM (estimador fase 2) | 74 | **2.7x** | + +Correr: + +``` +npm install --no-save assemblyscript +npx asc perf/spike/as/frames.ts --target release -O3 --noAssert --runtime stub \ + --initialMemory 2 --outFile perf/spike/as/frames.wasm +node perf/spike/bench2.js # desde perf/spike/ +``` + +## harness/ — oráculo diferencial (paso 0) + +Corre `ws/client/cuis.image` headless en Node con reloj virtual determinista y +acumula un hash de la traza de ejecución (pc/método/sends en cada checkpoint). +Dos corridas del mismo VM producen el mismo hash; un VM modificado que diverge +en semántica produce otro hash → detección automática de bugs sin tests en-imagen. + +``` +node perf/harness/difftrace.js --golden # graba perf/harness/golden.json +node perf/harness/difftrace.js # compara contra el golden +node perf/harness/difftrace.js --bench # mide tiempo real (reloj de verdad) +``` diff --git a/perf/harness/browser.js b/perf/harness/browser.js new file mode 100644 index 00000000..f52c73c2 --- /dev/null +++ b/perf/harness/browser.js @@ -0,0 +1,130 @@ +"use strict"; +// Driver de browser REAL (Chrome headless vía puppeteer-core) para tener feedback +// propio sin depender del usuario: levanta run/ con la imagen servida por HTTP, +// captura consola + errores, opcionalmente inyecta eventos de mouse REALES (que +// pasan por recordMouseEvent → display.runNow → interpret, la reentrancia que +// reproduce el crash de stackZone+jit2 que el harness headless no alcanzaba). +// +// node perf/harness/browser.js [url] [--drive] [--secs N] [--events file.json] +// default url: http://localhost:8081/run/#stackZone&jit2&image=/Dialogo.32bits.image +// --drive mueve el mouse por el canvas + clicks (dispara el crash) +// --secs N segundos a esperar tras el boot (default 8) +// --events F replay de una grabación {width,height,events:[{at,ev}]} +// --shot F.png guarda screenshot al final + +var fs = require("fs"); +var CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; + +var args = process.argv.slice(2); +function opt(name, d) { var i = args.indexOf(name); return i >= 0 && args[i+1] && !args[i+1].startsWith("--") ? args[i+1] : d; } +var url = args.find(function(a){ return a.startsWith("http"); }) + || "http://localhost:8081/run/#stackZone&jit2&image=/Dialogo.32bits.image"; +var drive = args.indexOf("--drive") >= 0; +var secs = parseInt(opt("--secs", "8"), 10); +var eventsFile = opt("--events", null); +var shot = opt("--shot", null); + +var HARD_TIMEOUT = parseInt(opt("--timeout", "60"), 10) * 1000; +var hardKill = setTimeout(function(){ console.error("HARD TIMEOUT — matando"); process.exit(3); }, HARD_TIMEOUT); + +(async function() { + var puppeteer = (await import("puppeteer-core")).default; + var browser = await puppeteer.launch({ + executablePath: CHROME, + headless: "new", + args: ["--no-sandbox", "--disable-gpu", "--window-size=1024,820", "--use-gl=swiftshader"], + }); + var page = await browser.newPage(); + await page.setViewport({ width: 1024, height: 820 }); + // capturar stacks completos de los errores (console.error(errorObj) pierde el stack en msg.text()) + await page.evaluateOnNewDocument(function() { + window.__sqErrors = []; + var oe = console.error; + console.error = function() { + for (var i = 0; i < arguments.length; i++) { + var a = arguments[i]; + if (a && a.stack) window.__sqErrors.push(String(a.stack)); + } + return oe.apply(console, arguments); + }; + window.addEventListener("error", function(e) { if (e.error && e.error.stack) window.__sqErrors.push(String(e.error.stack)); }); + }); + + var logs = [], errors = [], ready = false; + page.on("console", function(msg) { + var t = msg.text(); + logs.push("[" + msg.type() + "] " + t); + if (/squeak: ready/.test(t)) ready = true; + if (msg.type() === "error") errors.push(t); + }); + page.on("pageerror", function(err) { errors.push("PAGEERROR: " + err.message); }); + // los alert() de squeak.js tras un crash bloquean headless — auto-dismiss y registrar + page.on("dialog", function(d) { errors.push("ALERT: " + d.message().split("\n")[0]); d.dismiss().catch(function(){}); }); + page.on("requestfailed", function(req) { errors.push("REQFAIL: " + req.url() + " " + (req.failure()||{}).errorText); }); + + console.log("navegando: " + url); + await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 }).catch(function(e){ errors.push("GOTO: " + e.message); }); + + // esperar boot (squeak: ready) hasta 40s + var t0 = Date.now(); + while (!ready && Date.now() - t0 < 40000) await new Promise(function(r){ setTimeout(r, 200); }); + console.log("boot: " + (ready ? "READY en " + ((Date.now()-t0)/1000).toFixed(1) + "s" : "NO llegó a ready (timeout)")); + + // confirmar modo + var mode = await page.evaluate(function() { + var vm = window.SqueakJS && SqueakJS.vm; + return vm ? { useStackZone: !!vm.useStackZone, leafCount: vm.compiler && vm.compiler.leafCount, sends: vm.sendCount } : null; + }).catch(function(){ return null; }); + console.log("modo: " + JSON.stringify(mode)); + + var traceFile = opt("--trace", null); + if (traceFile) { await page.tracing.start({ path: traceFile, categories: ["devtools.timeline", "toplevel", "v8.cpu_profiler", "disabled-by-default-v8.cpu_profiler"] }); console.log("tracing → " + traceFile); } + + if (eventsFile && fs.existsSync(eventsFile)) { + var rec = JSON.parse(fs.readFileSync(eventsFile, "utf8")); + var evs = Array.isArray(rec) ? rec : rec.events; + console.log("replay de " + evs.length + " eventos grabados…"); + for (var i = 0; i < evs.length; i++) { + var e = evs[i].ev || evs[i]; + if (e[0] === 1) { // mouse + await page.mouse.move(e[2], e[3]); + if (e[4] & 4) await page.mouse.down(); + else if (evs[i-1] && ((evs[i-1].ev||evs[i-1])[4] & 4)) await page.mouse.up(); + } + if (i % 20 === 0) await new Promise(function(r){ setTimeout(r, 10); }); + } + } else if (drive) { + console.log("moviendo el mouse por el canvas + clicks…"); + for (var k = 0; k < 30; k++) { + await page.mouse.move(100 + (k*27) % 800, 100 + (k*19) % 600); + await new Promise(function(r){ setTimeout(r, 30); }); + } + await page.mouse.click(400, 300); + await new Promise(function(r){ setTimeout(r, 100); }); + await page.mouse.move(420, 320); await page.mouse.down(); + for (var k = 0; k < 10; k++) { await page.mouse.move(420 + k*20, 320 + k*10); await new Promise(function(r){ setTimeout(r, 20); }); } + await page.mouse.up(); + } + + await new Promise(function(r){ setTimeout(r, secs * 1000); }); + if (traceFile) { await page.tracing.stop(); console.log("trace guardado: " + traceFile); } + var sends2 = await page.evaluate(function(){ return window.SqueakJS && SqueakJS.vm ? SqueakJS.vm.sendCount : null; }).catch(function(){ return null; }); + if (shot) { await page.screenshot({ path: shot }).catch(function(){}); console.log("screenshot: " + shot); } + + var stacks = await page.evaluate(function(){ return window.__sqErrors || []; }).catch(function(){ return []; }); + if (stacks.length) { + var uniq = {}; + stacks.forEach(function(s){ var key = s.split("\n").slice(0,6).join("\n"); uniq[key] = (uniq[key]||0)+1; }); + console.log("\n=== STACKS ÚNICOS (" + stacks.length + " errores) ==="); + Object.keys(uniq).forEach(function(k){ console.log("\n×" + uniq[k] + ":\n" + k); }); + } + console.log("\n=== sendCount final: " + sends2 + " ==="); + console.log("=== ERRORES (" + errors.length + ") ==="); + var seen = {}; + errors.forEach(function(e){ var key = e.slice(0, 80); if (!seen[key]) { seen[key] = 0; } seen[key]++; }); + Object.keys(seen).forEach(function(k){ console.log(" ×" + seen[k] + " " + k); }); + if (process.env.FULLLOG) { console.log("\n=== LOG COMPLETO ==="); logs.forEach(function(l){ console.log(" " + l); }); } + + await browser.close(); + process.exit(errors.length ? 1 : 0); +})().catch(function(e){ console.error("FATAL:", e); process.exit(2); }); diff --git a/perf/harness/dialogo.sh b/perf/harness/dialogo.sh new file mode 100755 index 00000000..889395bd --- /dev/null +++ b/perf/harness/dialogo.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Workload automático de Dialogo para el oráculo/bench — sin abrir el browser. +# +# La imagen de Dialogo auto-carga sus proyectos del directorio al arrancar, así +# que el boot con los archivos al lado ya es un workload representativo (carga de +# proyectos + render), reproducible y determinista. Este script prepara un +# entorno estable (fuera del scratchpad, que se borra entre sesiones) y corre el +# workload headless comparando modos. +# +# perf/harness/dialogo.sh compare [sends] ctx vs frames+jit2: wall + correctitud +# perf/harness/dialogo.sh prof [ctx|frames] [sends] top hotspots (node --prof) +# perf/harness/dialogo.sh run [ctx|frames] [sends] una corrida, salida completa +# +# Env: DIALOGO_SRC (fuente, default ~/Desktop/Dialogo/Archivos), DIALOGO_RUN +# (dir de corrida, default /tmp/dialogo-run), EVENTS (grabación, default +# ~/Downloads/dialogo-events.json si existe; "none" para desactivar). +HERE="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "$HERE/../.." && pwd)" +SRC="${DIALOGO_SRC:-$HOME/Desktop/Dialogo/Archivos}" +RUN="${DIALOGO_RUN:-/tmp/dialogo-run}" +IMG="$RUN/Dialogo.32bits.image" +EVENTS="${EVENTS:-$RUN/events.json}" + +prepare() { + if [ ! -f "$IMG" ]; then + echo "preparando entorno en $RUN (copiando de $SRC)…" >&2 + mkdir -p "$RUN" + cp "$SRC/Dialogo.32bits.image" "$RUN/" + [ -d "$SRC/dictionaries" ] && cp -R "$SRC/dictionaries" "$RUN/" + [ -d "$SRC/projects" ] && cp -R "$SRC/projects" "$RUN/" + fi + # estabilizar la grabación de eventos en el dir de corrida (sobrevive a que + # se borre Downloads); se puede regrabar y volver a copiar + if [ ! -f "$RUN/events.json" ] && [ -f "$HOME/Downloads/dialogo-events.json" ]; then + cp "$HOME/Downloads/dialogo-events.json" "$RUN/events.json" + fi + # limpiar artefactos de la corrida anterior (el harness también lo hace) + rm -f "$RUN"/*.changes "$RUN"/CuisDebug-*.log 2>/dev/null || true +} + +evflag() { + if [ "$EVENTS" != "none" ] && [ -f "$EVENTS" ]; then echo "--events $EVENTS"; fi +} + +# corre un modo, devuelve la línea "trace:" y "display:" por stdout +run_one() { # $1=sends, $2..=flags + local sends="$1"; shift + node "$REPO/perf/harness/difftrace.js" --ui $(evflag) "$@" --image "$IMG" --sends "$sends" 2>&1 +} + +# mide best-of-2 de un modo; imprime "wall|traceHash|dispHash|sendCount" +measure() { # $1=sends $2..=flags + local sends="$1"; shift + local best=99999999 out="" o w + for i in 1 2; do + o=$(run_one "$sends" "$@") + w=$(echo "$o" | grep -oE "wall [0-9]+ ms" | grep -oE "[0-9]+") + if [ -n "$w" ] && [ "$w" -lt "$best" ]; then best=$w; out="$o"; fi + done + local th=$(echo "$out" | grep "^trace:" | grep -oE "hash=[0-9a-fx]+" | cut -d= -f2) + local dh=$(echo "$out" | grep "^display:" | grep -oE "hash=[0-9a-fx]+" | cut -d= -f2) + local sc=$(echo "$out" | grep "^trace:" | grep -oE "sends=[0-9]+" | cut -d= -f2) + echo "$best|$th|$dh|$sc" +} + +cmd="${1:-compare}" +prepare + +case "$cmd" in +compare) + SENDS="${2:-20000000}" + echo "workload: Dialogo auto-load, $SENDS sends, eventos=$([ -n "$(evflag)" ] && echo sí || echo no)" + ctx=$(measure "$SENDS"); frm=$(measure "$SENDS" --frames --jit2) + cw=${ctx%%|*}; ct=$(echo "$ctx"|cut -d'|' -f2); cd=$(echo "$ctx"|cut -d'|' -f3); cs=$(echo "$ctx"|cut -d'|' -f4) + fw=${frm%%|*}; ft=$(echo "$frm"|cut -d'|' -f2); fd=$(echo "$frm"|cut -d'|' -f3); fs=$(echo "$frm"|cut -d'|' -f4) + printf "%-14s %10s %10s %12s %s\n" "modo" "wall(ms)" "sends/s" "traceHash" "displayHash" + printf "%-14s %10s %9.2fM %12s %s\n" "ctx" "$cw" "$(awk "BEGIN{print $cs/($cw/1000)/1e6}")" "$ct" "$cd" + printf "%-14s %10s %9.2fM %12s %s\n" "frames+jit2" "$fw" "$(awk "BEGIN{print $fs/($fw/1000)/1e6}")" "$ft" "$fd" + echo "---" + if [ "$ct" = "$ft" ] && [ "$cd" = "$fd" ]; then + echo "✓ CORRECTO: traza y dibujo idénticos entre ctx y frames+jit2" + else + echo "✗ DIVERGE: ctx trace=$ct disp=$cd vs frames trace=$ft disp=$fd" + fi + awk "BEGIN{printf \"perf: frames+jit2 %+.1f%% vs ctx (%s vs %s ms)\n\", ($cw-$fw)/$cw*100, $fw, $cw}" + ;; + +run) + mode="${2:-frames}"; SENDS="${3:-20000000}" + [ "$mode" = "ctx" ] && run_one "$SENDS" || run_one "$SENDS" --frames --jit2 + ;; + +prof) + mode="${2:-frames}"; SENDS="${3:-20000000}" + PDIR="$RUN/prof"; mkdir -p "$PDIR"; rm -f "$PDIR"/isolate-*.log + flags=""; [ "$mode" = "frames" ] && flags="--frames --jit2" + ( cd "$PDIR" && node --prof "$REPO/perf/harness/difftrace.js" --ui $(evflag) $flags --image "$IMG" --sends "$SENDS" >/dev/null 2>&1 + node --prof-process isolate-*.log > proc.txt 2>/dev/null ) + echo "== $mode ($SENDS sends): top JS (nonlib %) ==" + awk '/\[JavaScript\]:/{f=1; next} /^ \[/{f=0} f' "$PDIR/proc.txt" | head -24 + rm -f "$PDIR"/isolate-*.log + ;; + +*) echo "uso: dialogo.sh {compare|run|prof} …"; exit 1 ;; +esac diff --git a/perf/harness/difftrace.js b/perf/harness/difftrace.js new file mode 100644 index 00000000..d0ae8bee --- /dev/null +++ b/perf/harness/difftrace.js @@ -0,0 +1,783 @@ +"use strict"; +// Oráculo diferencial para el proyecto stack-zone (ver ../stack-zone-design.md). +// +// Corre una imagen headless en Node con entorno determinista (reloj virtual, +// random sembrado, WebSocket inerte) y acumula un hash de la traza de ejecución +// (sendCount/pc/sp/método en cada slice). Dos corridas del mismo VM producen el +// mismo hash; cualquier divergencia semántica introducida por un VM modificado +// produce otro hash. +// +// node perf/harness/difftrace.js --golden graba perf/harness/golden.json +// node perf/harness/difftrace.js compara contra el golden (exit 1 si difiere) +// node perf/harness/difftrace.js --bench reloj real, mide wall-time y sends/s +// opciones: --sends N (default 20000000), --image ruta (default ws/client/cuis.image) + +var os = require("os"); +var fs = require("fs"); +var path = require("path"); + +var repoRoot = path.join(__dirname, "..", ".."); +var args = process.argv.slice(2); +var mode = args.indexOf("--golden") >= 0 ? "golden" + : args.indexOf("--bench") >= 0 ? "bench" + : "check"; +var useFrames = args.indexOf("--frames") >= 0; // correr con el stack zone activado +var useJit2 = args.indexOf("--jit2") >= 0; // stack-to-register jit (requiere --frames) +var noJit = args.indexOf("--nojit") >= 0; // apagar el jit (para aislar divergencias jit vs frames) +var useUI = args.indexOf("--ui") >= 0; // display offscreen real: levanta el World y ejercita la UI (ver uidisplay.js) +function argValue(name, deflt) { + var i = args.indexOf(name); + return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : deflt; +} +var maxSends = parseInt(argValue("--sends", "20000000"), 10); +var untilMs = parseInt(argValue("--until-ms", "0"), 10); // parar al alcanzar este tiempo-virtual (mide "tiempo para el mismo trabajo") +var untilStable = args.indexOf("--until-stable") >= 0; // parar cuando el dibujo se estabiliza (= trabajo real terminado; excluye idle-spin) +var uiW = parseInt(argValue("--width", "1024"), 10); +var uiH = parseInt(argValue("--height", "768"), 10); +// Grabación de eventos (--events): la leemos temprano para dimensionar el display +// headless igual que el browser donde se grabó (coordenadas → mismos morphs). +var evData = null; +if (useUI && argValue("--events", null)) { + evData = JSON.parse(fs.readFileSync(path.resolve(argValue("--events", null)), "utf8")); + if (!Array.isArray(evData)) { // wrapper {width, height, events} + if (args.indexOf("--width") < 0 && evData.width) uiW = evData.width; + if (args.indexOf("--height") < 0 && evData.height) uiH = evData.height; + } +} +var imagePath = path.resolve(repoRoot, argValue("--image", "ws/client/cuis.image")); +var goldenPath = path.join(__dirname, "golden.json"); +var logPath = argValue("--log", null); // traza por checkpoint, para ubicar divergencias +var logFrom = parseInt(argValue("--logfrom", "-1"), 10); // log fino por-send en [logfrom, logto] +var logTo = parseInt(argValue("--logto", "-1"), 10); + +// --------------------------------------------------------------------------- +// Entorno determinista (solo en modos de traza; --bench usa el reloj real) +// --------------------------------------------------------------------------- +var virtualMs = 0; +var VIRTUAL_EPOCH = 1735689600000; // 2025-01-01, fijo +// El reloj virtual queda congelado hasta que arranca el loop de interpretación: +// el boot de Node/carga de imagen consume Date.now un número variable de veces +// (estado frío vs caliente) y no debe correr la línea de tiempo. +var clockRunning = false; +if (mode !== "bench") { + var clockCalls = 0; + var virtualNow = function() { + if (clockRunning && ++clockCalls % 4 === 0) virtualMs++; + return VIRTUAL_EPOCH + virtualMs; + }; + // Stub de Date completo: new Date() sin args también debe ser virtual + // (el código de la imagen loguea timestamps; con reloj real divergen las trazas) + var RealDate = Date; + var FakeDate = function Date() { + if (arguments.length === 0) return new RealDate(virtualNow()); + return new (RealDate.bind.apply(RealDate, [null].concat(Array.prototype.slice.call(arguments))))(); + }; + FakeDate.prototype = RealDate.prototype; + FakeDate.now = virtualNow; + FakeDate.UTC = RealDate.UTC; + FakeDate.parse = RealDate.parse; + global.Date = FakeDate; + global.performance = { now: function() { return virtualMs; } }; + var seed = 42 >>> 0; + Math.random = function() { + seed = (seed * 1664525 + 1013904223) >>> 0; + return seed / 4294967296; + }; +} + +// --------------------------------------------------------------------------- +// Bootstrapping del VM: mismo esquema que squeak_node.js +// --------------------------------------------------------------------------- +Object.assign(global, { + self: new Proxy({}, { + get: function(obj, prop) { return global[prop]; }, + set: function(obj, prop, value) { global[prop] = value; return true; } + }) +}); + +// WebSocket inerte: la imagen de ws/client intenta conectarse al arrancar; acá +// queda CONNECTING para siempre (el timeout del lado Smalltalk corre con el +// reloj virtual, así que es determinista). +function FakeWebSocket(url) { + this.url = url; + this.readyState = 0; + this.onopen = null; this.onclose = null; this.onmessage = null; this.onerror = null; +} +FakeWebSocket.prototype.send = function() {}; +FakeWebSocket.prototype.close = function() { this.readyState = 3; }; +FakeWebSocket.prototype.addEventListener = function() {}; +FakeWebSocket.prototype.removeEventListener = function() {}; + +Object.assign(self, { + localStorage: {}, + // inerte también en --bench: el workload debe ser idéntico al de la traza + WebSocket: FakeWebSocket, + sha1: require(path.join(repoRoot, "lib/sha1")), + btoa: function(string) { return Buffer.from(string, "ascii").toString("base64"); }, + atob: function(string) { return Buffer.from(string, "base64").toString("ascii"); } +}); + +[ + "globals.js", "vm.js", "vm.object.js", "vm.object.spur.js", "vm.image.js", + "vm.interpreter.js", "vm.interpreter.proxy.js", "vm.instruction.stream.js", + "vm.instruction.stream.sista.js", "vm.instruction.printer.js", "vm.primitives.js", + "jit.js", "jit2.js", "vm.display.js", "vm.display.headless.js", "vm.input.js", + "vm.input.headless.js", "vm.plugins.js", "vm.plugins.file.node.js", + "vm.stackzone.js", +].forEach(function(f) { require(path.join(repoRoot, f)); }); + +Object.extend(Squeak, { + vmPath: path.dirname(imagePath) + path.sep, + platformSubtype: "Node.js", + osVersion: mode === "bench" + ? process.version + " " + os.platform() + " " + os.release() + " " + os.arch() + : "difftrace-deterministic", // la imagen puede leer esto; que no varíe por máquina + windowSystem: "none", +}); + +Object.extend(Squeak.Primitives.prototype, { + loadModuleDynamically: function(modName) { + try { + require(path.join(repoRoot, "plugins", modName)); + return Squeak.externalModules[modName]; + } catch (e) { + console.error("Plugin " + modName + " could not be loaded"); + } + return undefined; + } +}); + +// --------------------------------------------------------------------------- +// Driver síncrono + hash de traza +// --------------------------------------------------------------------------- +var hash = 2166136261 >>> 0; // FNV-1a +var mixLog = process.env.MIXLOG ? [] : null; +function mix(v) { + if (mixLog) mixLog.push(v); + hash = (hash ^ (v >>> 0)) >>> 0; + hash = Math.imul(hash, 16777619) >>> 0; +} +process.on("exit", function() { + if (mixLog) require("fs").writeFileSync(process.env.MIXLOG, mixLog.join("\n") + "\n"); +}); + +// La imagen escribe artefactos junto a sí misma (crea el .changes si falta, +// logs de debug). Limpiarlos antes de correr para que toda corrida parta del +// mismo estado de filesystem — si no, la primera corrida en un clone fresco +// difiere de las siguientes. +var imageDir = path.dirname(imagePath); +var imageBase = path.basename(imagePath, ".image"); +fs.readdirSync(imageDir).forEach(function(f) { + if (f === imageBase + ".changes" || /^CuisDebug-.*\.log$/.test(f)) { + fs.unlinkSync(path.join(imageDir, f)); + } +}); + +if (process.env.PERCLASS === "1") Squeak.perClassShape = true; // A/B: volver a constructores por-clase (revertir monomorfización) +var data = fs.readFileSync(imagePath); +var image = new Squeak.Image(imagePath.replace(/\.image$/, "")); +image.readFromBuffer(data.buffer, function startRunning() { + var uiMod = null, display; + if (useUI) { + uiMod = require(path.join(__dirname, "uidisplay.js")); + display = uiMod.install(Squeak, { width: uiW, height: uiH }); + } else { + display = { vmOptions: ["-vm-display-null", "-nodisplay"] }; + } + var vm = new Squeak.Interpreter(image, display, useFrames ? { stackZone: true, jit2: useJit2 } : {}); + if (noJit) vm.compiler = null; + if (process.env.LARGEINT === "0") vm.primHandler.largeIntPrims = false; // A/B: desactivar prims LargeInteger + if (process.env.STREAMPRIM === "0") vm.primHandler.streamPrims = false; // A/B: desactivar prims Stream 65/66/67 + if (process.env.JIT2DBG) vm.jit2Debug = true; + if (process.env.SEMDBG) { + var origSS = vm.primHandler.synchronousSignal; + vm.primHandler.synchronousSignal = function(sema) { + if (vm.sendCount >= 717670 && vm.sendCount <= 717685) + console.error("SIG s=" + vm.sendCount + " semaHash=" + sema.hash + + " excess=" + sema.pointers[Squeak.Sema_excessSignals] + + " empty=" + this.isEmptyList(sema) + + " firstLink=" + (sema.pointers[Squeak.LinkedList_firstLink].isNil ? "nil" : "proc:" + sema.pointers[Squeak.LinkedList_firstLink].hash)); + return origSS.call(this, sema); + }; + var origWait = vm.primHandler.primitiveWait || null; + } + if (process.env.GCDBG) { + var origFGR = vm.frameGCRoots; + var fgrCalls = 0; + vm.frameGCRoots = function() { + var roots = origFGR.call(vm); + if (++fgrCalls <= 2) { + var desc = []; + for (var i = 0; i < vm.zonePages.length; i++) { + var pg = vm.zonePages[i]; + desc.push((pg.live ? "L" : "d") + " fp=" + pg.fp + " sp=" + pg.sp + " len=" + pg.slots.length); + } + console.error("FGR#" + fgrCalls + " s=" + vm.sendCount + " roots=" + roots.length + " | " + desc.join(" | ")); + } + return roots; + }; + } + if (process.env.CHKDBG) { + var origCFI = vm.checkForInterrupts; + vm.checkForInterrupts = function() { + console.error("CHK s=" + vm.sendCount + " vms=" + virtualMs + " reset=" + vm.interruptCheckCounterFeedBackReset + " wake=" + vm.nextWakeupTick); + return origCFI.call(vm); + }; + } + if (process.env.ZDBG9) { + // volcar bytes+literales del método activado cuando mbytes coincide + var z9size = parseInt(process.env.ZDBG9_SIZE), z9From = parseInt(process.env.ZDBG9_FROM || "0"); + var origENM9 = vm.executeNewMethod; + var dumped = 0; + vm.executeNewMethod = function(newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel) { + if (vm.sendCount >= z9From && dumped < 2 && newMethod.bytes && newMethod.bytes.length === z9size) { + dumped++; + var lits = []; + for (var i = 0; i < newMethod.pointers.length; i++) { + var l = newMethod.pointers[i]; + lits.push(l && l.bytesAsString ? l.bytesAsString() : (l && l.sqClass ? l.sqClass.className() : String(l))); + } + console.error("MDUMP s=" + vm.sendCount + " sel=" + (optSel && optSel.bytesAsString ? optSel.bytesAsString() : "?") + + " bytes=[" + Array.prototype.join.call(newMethod.bytes, ",") + "] lits=" + JSON.stringify(lits)); + } + return origENM9.call(vm, newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel); + }; + } + if (process.env.PFNDBG) vm.primFnDebug = true; + if (process.env.COUNTBV) { + var ph = vm.primHandler, cnt = { blockValue:0, blockValueArgs:0, closureAct:0, closureActFull:0 }; + var o1 = ph.primitiveBlockValue.bind(ph); ph.primitiveBlockValue = function(a){ cnt.blockValue++; return o1(a); }; + var o2 = ph.primitiveBlockValueWithArgs.bind(ph); ph.primitiveBlockValueWithArgs = function(a){ cnt.blockValueArgs++; return o2(a); }; + if (ph.activateNewClosureMethod) { var o3 = ph.activateNewClosureMethod.bind(ph); ph.activateNewClosureMethod = function(b,a){ cnt.closureAct++; return o3(b,a); }; } + process.on("exit", function(){ console.error("COUNTBV " + JSON.stringify(cnt) + " byClosure(marriages)=" + (vm.nMarryClosure||0)); }); + } + if (process.env.CLEANBLK) { + // ¿cuántos closures son "clean" (nunca usan su outerContext)? Un clean block + // no necesitaría casar el frame → eliminaría marriages del stack zone. + // Conservador: clean = numCopied 0 Y ningún bytecode que toque estado externo + // (rcvr vars/self/thisContext/^/remote temps/extendidos que podrían). + var dirty = {}; [0x70,0x7C,0x81,0x82,0x84,0x85,0x89,0x8C,0x8D,0x8E].forEach(function(b){dirty[b]=1;}); + var stats = { total:0, clean:0, numCopied0:0, byCopied:{} }; + var origPCC = vm.pushClosureCopy.bind(vm); + vm.pushClosureCopy = function() { + var savedPc = vm.pc, m = vm.method; + var nac = m.bytes[vm.pc], numCopied = nac >> 4; + var bsHi = m.bytes[vm.pc+1], blockSize = bsHi*256 + m.bytes[vm.pc+2]; + var blockStart = vm.pc + 3, isClean = (numCopied === 0); + if (isClean) for (var p = blockStart; p < blockStart + blockSize; p++) { + var b = m.bytes[p]; + if (b <= 0x0F || (b >= 0x60 && b <= 0x67) || dirty[b]) { isClean = false; break; } + } + stats.total++; + if (numCopied === 0) stats.numCopied0++; + stats.byCopied[numCopied] = (stats.byCopied[numCopied]||0)+1; + if (isClean) stats.clean++; + return origPCC(); + }; + process.on("exit", function(){ + console.error("CLEANBLK closures=" + stats.total + " clean=" + stats.clean + + " (" + (100*stats.clean/stats.total).toFixed(1) + "%) numCopied0=" + stats.numCopied0 + + " (" + (100*stats.numCopied0/stats.total).toFixed(1) + "%) byCopied=" + JSON.stringify(stats.byCopied)); + }); + } + if (process.env.HOTSEL) { + // top métodos Smalltalk por activaciones (rcvrClass>>selector) — muestra + // en qué gasta sends la imagen real (¿desperdicio algorítmico o costo base?) + var hot = {}; + var origENMh = vm.executeNewMethod; + vm.executeNewMethod = function(r, m, ac, pi, oc, sel) { + var key = (oc ? oc.className() : (vm.getClass(r).className())) + ">>" + (sel && sel.bytesAsString ? sel.bytesAsString() : "?"); + hot[key] = (hot[key] || 0) + 1; + return origENMh.call(vm, r, m, ac, pi, oc, sel); + }; + process.on("exit", function() { + var arr = Object.keys(hot).map(function(k){ return [k, hot[k]]; }).sort(function(a,b){ return b[1]-a[1]; }); + var tot = arr.reduce(function(s,e){ return s+e[1]; }, 0); + console.error("HOTSEL top 30 de " + arr.length + " selectores (" + tot + " sends):"); + arr.slice(0, 30).forEach(function(e){ console.error(" " + (100*e[1]/tot).toFixed(1) + "% " + e[1] + " " + e[0]); }); + // agregado por clase receptora + var byC = {}; + arr.forEach(function(e){ var c = e[0].split(">>")[0]; byC[c] = (byC[c]||0) + e[1]; }); + var carr = Object.keys(byC).map(function(k){ return [k, byC[k]]; }).sort(function(a,b){ return b[1]-a[1]; }); + console.error("HOTSEL top 15 por CLASE receptora:"); + carr.slice(0, 15).forEach(function(e){ console.error(" " + (100*e[1]/tot).toFixed(1) + "% " + e[1] + " " + e[0]); }); + var largeInt = (byC["LargePositiveInteger"]||0) + (byC["LargeNegativeInteger"]||0); + console.error("→ LargeInteger (receptor): " + (100*largeInt/tot).toFixed(1) + "% de sends"); + }); + } + if (process.env.UNWINDDBG) { + // cobertura: contar activaciones por selector (watchlist de unwind/terminación) + // + process switches + resumes de contextos casados. Responde "¿el workload + // siquiera toca los paths donde crashea el browser?" + var watch = (process.env.UNWINDDBG_SEL || + "terminateTo:,resume:through:,resume:,aboutToReturn:through:,cannotReturn:,valueUninterruptably") + .split(","); + var counts = {}; watch.forEach(function(s){ counts[s]=0; }); + counts[""] = 0; + counts[""] = 0; + var origENMu = vm.executeNewMethod; + vm.executeNewMethod = function(r, m, ac, pi, oc, sel) { + if (sel && sel.bytesAsString) { var s = sel.bytesAsString(); if (counts[s] !== undefined) counts[s]++; } + return origENMu.call(vm, r, m, ac, pi, oc, sel); + }; + var origTTu = vm.primHandler.transferTo; + vm.primHandler.transferTo = function(p){ counts[""]++; return origTTu.call(this,p); }; + if (vm.newActiveContext) { + var origNACu = vm.newActiveContext; + vm.newActiveContext = function(c){ if (vm.useStackZone && c && c.frame != null) counts[""]++; return origNACu.call(vm,c); }; + } + process.on("exit", function(){ console.error("UNWIND cobertura:", JSON.stringify(counts)); }); + } + if (process.env.SSDBG) { + var c2 = vm.compiler; + if (c2) { + var origESS = c2.enableSingleStepping.bind(c2); + c2.enableSingleStepping = function(method, optClass, optSel) { + vm.nSingleStep = (vm.nSingleStep || 0) + 1; + if (vm.nSingleStep <= 5) console.error("SINGLESTEP #" + vm.nSingleStep + " s=" + vm.sendCount + " pc=" + vm.pc + " mbytes=" + (method.bytes ? method.bytes.length : "?")); + return origESS(method, optClass, optSel); + }; + } + } + if (process.env.FREEZE_SIM) { + // replica el patrón del FilePlugin del browser (fileContentsDo): + // el primitivo congela el VM y retorna true sin efecto de stack; un + // callback diferido hace unfreeze() y LUEGO aplica el efecto original + var wrapFrozen = function(mod, fnName) { + var orig = mod[fnName].bind(mod); + mod[fnName] = function(argCount) { + if (vm.frozen) return orig(argCount); // ya diferido: ejecutar directo + vm.nFreezeSim = (vm.nFreezeSim || 0) + 1; + vm.freeze(function(unfreeze) { + setImmediate(function() { + unfreeze(); + orig(argCount); + }); + }); + return true; + }; + }; + var origLM2 = vm.primHandler.loadModule.bind(vm.primHandler); + vm.primHandler.loadModule = function(name) { + var m = origLM2(name); + if (name === "FilePlugin" && m && !m._frozenSim) { + m._frozenSim = true; + for (var k in m) + if (typeof m[k] === "function" && /^primitive/.test(k)) + wrapFrozen(m, k); + } + return m; + }; + } + if (process.env.PAGEDBG) { vm.pageStats = {fresh:0, reused:0, flushActive:0, flushSusp:0, flushDead:0}; process.on("exit", function(){ console.error("PAGES:", JSON.stringify(vm.pageStats)); }); } + if (process.env.SMCDBG) { vm.smcStats = {}; process.on("exit", function() { console.error("SMC stores por índice:", JSON.stringify(vm.smcStats)); }); } + if (process.env.FADBG) { + // loguear llamadas a FloatArrayPlugin.primitiveAt: (sc, sp-antes, success-después, sp-después) + var faCount = 0; + var patchFA = function(mod) { + if (!mod || mod._faPatched) return !!mod; + mod._faPatched = true; + var orig = mod.primitiveAt; + mod.primitiveAt = function(argCount) { + var spBefore = vm.sp; + var r = orig.call(this, argCount); + if (++faCount <= 40) + console.error("FA#" + faCount + " s=" + vm.sendCount + " spB=" + spBefore + + " r=" + r + " succ=" + vm.primHandler.success + " spA=" + vm.sp); + return r; + }; + return true; + }; + var origLMD = vm.primHandler.loadModule.bind(vm.primHandler); + vm.primHandler.loadModule = function(name) { + var m = origLMD(name); + if (name === "FloatArrayPlugin") patchFA(m); + return m; + }; + } + if (process.env.ZDBG7) { + // dump de cadena al activar un método específico (por _traceId) + var z7id = parseInt(process.env.ZDBG7_ID), z7From = parseInt(process.env.ZDBG7_FROM || "0"); + var chainDesc = function() { + var desc = []; + if (vm.useStackZone) { + var page = vm.zonePage, fp = vm.fp, hops = 0; + while (fp >= 0 && hops++ < 20) { + var m = page.slots[fp + Squeak.Frame_method]; + var cl = page.slots[fp + Squeak.Frame_closure]; + desc.push("m" + (m.bytes ? m.bytes.length : "?") + (cl && !cl.isNil ? "b" : "")); + fp = page.slots[fp + Squeak.Frame_savedFp]; + } + if (fp < 0) desc.push("BASE:" + (page.baseCallerCtx && !page.baseCallerCtx.isNil ? "ctx" : "nil")); + } else { + var ctx = vm.activeContext, hops = 0; + while (!ctx.isNil && hops++ < 20) { + var m2 = ctx.pointers[Squeak.Context_method]; + var cl2 = ctx.pointers[Squeak.Context_closure]; + desc.push("m" + (m2.bytes ? m2.bytes.length : (vm.isSmallInt(m2) ? "INT" : "?")) + (cl2 && !cl2.isNil ? "b" : "")); + ctx = ctx.pointers[Squeak.Context_sender]; + } + } + return desc.join("<"); + }; + var origENM7 = vm.executeNewMethod; + vm.executeNewMethod = function(newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel) { + var r = origENM7.call(vm, newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel); + if (vm.sendCount >= z7From && newMethod._traceId === z7id) + console.error("ACT s=" + vm.sendCount + " chain: " + chainDesc()); + return r; + }; + } + if (process.env.ZDBG6) { + var origTT = vm.primHandler.transferTo; + vm.primHandler.transferTo = function(newProc) { + console.error("TT s=" + vm.sendCount); + return origTT.call(this, newProc); + }; + } + if (process.env.ZDBG5) { + // historial de activaciones de contexto explícitas (process switch / value de + // contextos dormidos) en ambos modos + var origNAC = vm.newActiveContext; + vm.newActiveContext = function(newContext) { + var m = newContext.pointers[Squeak.Context_method]; + console.error("NAC s=" + vm.sendCount + + " mbytes=" + (m && m.bytes ? m.bytes.length : "int?") + + " senderNil=" + (newContext.pointers[Squeak.Context_sender].isNil === true) + + (vm.useStackZone ? " frame=" + (newContext.frame != null) : "")); + return origNAC.call(vm, newContext); + }; + } + if (process.env.ZDBG4) { + // atrapar escrituras de campos de contexts en modo contexts: + // via bytecode (storeInstVar) y via primitivos (objectAtPut/storeStackp) + var ctxClass = vm.specialObjects[Squeak.splOb_ClassMethodContext]; + var origSIV = vm.storeInstVar; + var sivCount = 0; + vm.storeInstVar = function(index, value) { + if (++sivCount <= 3) console.error("SIV-ALIVE #" + sivCount + " s=" + vm.sendCount + " rcls=" + this.getClass(this.receiver).className()); + if (this.receiver.sqClass === ctxClass) + console.error("CTXSTORE s=" + vm.sendCount + " idx=" + index + " val=" + (value && value.isNil ? "nil" : typeof value)); + return origSIV.call(this, index, value); + }; + var origOAP = vm.primHandler.objectAtPut; + vm.primHandler.objectAtPut = function(a, b, c) { + var rcvr = this.stackNonInteger(2); + if (rcvr.sqClass === ctxClass) + console.error("CTXATPUT s=" + vm.sendCount + " idx=" + this.stackPos32BitInt(1)); + return origOAP.call(this, a, b, c); + }; + var origSSP = vm.primHandler.primitiveStoreStackp; + vm.primHandler.primitiveStoreStackp = function(argCount) { + console.error("CTXSTACKP s=" + vm.sendCount); + return origSSP.call(this, argCount); + }; + } + if (process.env.ZDBG3) { + // dump de cadenas en la ventana: frames (fp chain) vs contexts (sender chain) + var z3From = parseInt(process.env.ZDBG3_FROM || "0"), z3To = parseInt(process.env.ZDBG3_TO || "99999999"); + var origDR = vm.doReturn; + vm.doReturn = function(returnValue, targetContext) { + if (vm.sendCount >= z3From && vm.sendCount <= z3To) { + var desc = []; + if (vm.useStackZone) { + var page = vm.zonePage, fp = vm.fp, hops = 0; + while (fp >= 0 && hops++ < 12) { + var m = page.slots[fp + Squeak.Frame_method]; + var cl = page.slots[fp + Squeak.Frame_closure]; + desc.push(fp + ":m" + (m.bytes ? m.bytes.length : "?") + (cl && !cl.isNil ? "[blk]" : "")); + fp = page.slots[fp + Squeak.Frame_savedFp]; + } + if (fp < 0) desc.push("base->" + (page.baseCallerCtx && !page.baseCallerCtx.isNil ? "ctx" : "nil")); + } else { + var ctx = vm.activeContext, hops = 0; + while (!ctx.isNil && hops++ < 12) { + var m2 = ctx.pointers[Squeak.Context_method]; + var cl2 = ctx.pointers[Squeak.Context_closure]; + desc.push("m" + (m2.bytes ? m2.bytes.length : "?") + (cl2 && !cl2.isNil ? "[blk]" : "")); + ctx = ctx.pointers[Squeak.Context_sender]; + } + } + console.error("DR s=" + vm.sendCount + " pc=" + vm.pc + " chain: " + desc.join(" <- ")); + } + return origDR.call(vm, returnValue, targetContext); + }; + } + if (process.env.ZDBG) { + var zFrom = parseInt(process.env.ZDBG_FROM || "0"), zTo = parseInt(process.env.ZDBG_TO || "99999999"); + var origANC = vm.primHandler.activateNewClosureMethod; + vm.primHandler.activateNewClosureMethod = function(blockClosure, argCount) { + if (vm.sendCount >= zFrom && vm.sendCount <= zTo) { + var outer = blockClosure.pointers[Squeak.Closure_outerContext]; + var m = outer.frame != null ? outer.frame.page.slots[outer.frame.fp + Squeak.Frame_method] + : outer.pointers[Squeak.Context_method]; + console.error("ANC s=" + vm.sendCount + " startpc=" + blockClosure.pointers[Squeak.Closure_startpc] + + " mlits=" + (m.pointers ? m.pointers.length : "?") + " mbytes=" + (m.bytes ? m.bytes.length : "?") + + " outerMarried=" + (outer.frame != null) + " isNilM=" + (m.isNil === true)); + } + var r = origANC.call(this, blockClosure, argCount); + if (vm.sendCount >= zFrom && vm.sendCount <= zTo && !vm._zdbgArmed) { + vm._zdbgArmed = 60; + var origIO = vm.interpretOne.bind(vm); + vm.interpretOne = function(singleStep) { + if (vm._zdbgArmed-- > 0) + console.error("BC pc=" + vm.pc + " byte=" + vm.method.bytes[vm.pc] + + " mbytes=" + vm.method.bytes.length + " sp=" + vm.sp + " fp=" + vm.fp); + return origIO(singleStep); + }; + } + return r; + }; + } + var slices = 0, idleStreak = 0, stopReason = "maxSends"; + var logLines = logPath ? [] : null; + if (mode !== "bench") { + // Muestreo en checkpoints fijos de sendCount: independiente de la + // representación (contexts/frames) Y de la cadencia de interrupciones + // (que difiere con/sin jit). Es la señal que entra al hash. + var traceIdOf = function(m) { + if (m._traceId === undefined) { + var fpv = m.bytes ? m.bytes.length : 0; + if (m.bytes) for (var bi = 0; bi < Math.min(m.bytes.length, 16); bi++) + fpv = ((fpv * 31) + m.bytes[bi]) | 0; + m._traceId = fpv; + } + return m._traceId; + }; + // leaf-sends: mismo muestreo que executeNewMethod (sc = sendCount pre-incremento) + vm.jit2HookFires = 0; + vm.jit2LeafHook = function(method, sc, rcvr, sel) { + vm.jit2HookFires++; + if (sc < maxSends && (sc & 4095) === 0) { + mix(sc); + mix(traceIdOf(method)); + mix(vm.pc); + if (logLines) logLines.push("s=" + sc + " h=" + traceIdOf(method) + " pc=" + vm.pc); + } + if (logLines && sc >= logFrom && sc <= logTo) + logLines.push("S=" + sc + " h=" + traceIdOf(method) + " pc=" + vm.pc + " args=? prim=0" + + " cm=" + (vm.method ? traceIdOf(vm.method) : "?") + + " rcls=" + vm.getClass(rcvr).className() + " sel=" + (sel && sel.bytesAsString ? sel.bytesAsString() : "?") + " [leaf]"); + }; + var origENM = vm.executeNewMethod; + vm.executeNewMethod = function(newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel) { + // método identificado por fingerprint de contenido (los oops temporales + // interleavan contexts y difieren entre representaciones; el identity + // hash de métodos de imagen V3 es 0) + if (newMethod._traceId === undefined) { + var fp = newMethod.bytes ? newMethod.bytes.length : 0; + if (newMethod.bytes) for (var bi = 0; bi < Math.min(newMethod.bytes.length, 16); bi++) + fp = ((fp * 31) + newMethod.bytes[bi]) | 0; + newMethod._traceId = fp; + } + if (vm.sendCount < maxSends && (vm.sendCount & 4095) === 0) { + mix(vm.sendCount); + mix(newMethod._traceId); + mix(vm.pc); + if (logLines) logLines.push("s=" + vm.sendCount + " h=" + newMethod._traceId + " pc=" + vm.pc + " vms=" + virtualMs + " alloc=" + (vm.image.newSpaceCount + vm.image.allocationCount)); + } + if (logLines && vm.sendCount >= logFrom && vm.sendCount <= logTo) { + if (vm.method && vm.method._traceId === undefined) { + var fp2 = vm.method.bytes ? vm.method.bytes.length : 0; + if (vm.method.bytes) for (var bj = 0; bj < Math.min(vm.method.bytes.length, 16); bj++) + fp2 = ((fp2 * 31) + vm.method.bytes[bj]) | 0; + vm.method._traceId = fp2; + } + logLines.push("S=" + vm.sendCount + " h=" + newMethod._traceId + " pc=" + vm.pc + " args=" + argumentCount + " prim=" + primitiveIndex + + " cm=" + (vm.method ? vm.method._traceId : "?") + + " rcls=" + vm.getClass(newRcvr).className() + " sel=" + (optSel && optSel.bytesAsString ? optSel.bytesAsString() : "?")); + } + return origENM.call(vm, newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel); + }; + } + // Agenda de eventos de entrada (solo --ui). Determinista: cada evento se + // inyecta al cruzar su umbral de sendCount, idéntico entre representaciones. + var evSched = null, evIdx = 0; + if (useUI) { + var evFile = argValue("--events", null); + if (evFile) { + // Grabado con #record del browser: [ {at: sendCount, ev:[type,ts,...]}, ... ]. + // Se rebasa: el 1er evento cae en --evstart y los deltas de sendCount del + // browser se preservan pero capados a --evgap (comprime las pausas del + // usuario). También acepta el formato plano [ [type,ms,...], ... ]. + var raw = Array.isArray(evData) ? evData : evData.events; + var evStart = parseInt(argValue("--evstart", "800000"), 10); + var evGapCap = parseInt(argValue("--evgap", "250000"), 10); + if (raw.length && Array.isArray(raw[0])) { + evSched = raw.map(function(e, i) { return { at: Math.floor(maxSends * (0.3 + 0.6 * i / raw.length)), ev: e }; }); + } else { + var acc = evStart, prev = raw.length ? raw[0].at : 0; + evSched = raw.map(function(e) { + var gap = Math.min(Math.max(0, e.at - prev), evGapCap); + acc += gap; prev = e.at; + return { at: acc, ev: e.ev }; + }); + } + } else { + evSched = uiMod.syntheticScript(uiW, uiH, Math.floor(maxSends * 0.5), Math.floor(maxSends * 0.9)); + } + var lastAt = evSched.length ? evSched[evSched.length - 1].at : 0; + console.log("eventos agendados: " + evSched.length + (evFile ? " (de " + evFile + ", último en send " + lastAt + ")" : " (sintéticos)") + + (lastAt > maxSends ? " ⚠️ subí --sends a >" + lastAt + " para no cortar la interacción" : "")); + } + function injectDueEvents() { + if (!evSched) return; + while (evIdx < evSched.length && vm.sendCount >= evSched[evIdx].at) { + var e = evSched[evIdx++].ev.slice(); + e[1] = vm.primHandler.millisecondClockValue(); // ts en el dominio del reloj del VM + // reflejar posición/botones en el display para los prims de polling + if (e[0] === 1) { display.mouseX = e[2]; display.mouseY = e[3]; display.buttons = e[4]; } + display.eventQueue.push(e); + if (display.signalInputEvent) display.signalInputEvent(); + display.idle = 0; + } + } + + // Detección de quiescencia: el dibujo dejó de cambiar por 2 chequeos seguidos + // (~1M sends) y no quedan eventos → el trabajo real terminó. Mide el costo de + // la acción sin el idle-spin que el clock-warp sobre-representa. + var stLastHash = null, stStable = 0, stLastCheck = 0; + function checkStable() { + if (!untilStable || !uiMod) return false; + if (vm.sendCount - stLastCheck < 500000) return false; + stLastCheck = vm.sendCount; + var fp = uiMod.displayFingerprint(vm), h = fp ? fp.hash : null; + var eventsDone = !evSched || evIdx >= evSched.length; + if (h && h === stLastHash && eventsDone) { if (++stStable >= 2) return true; } + else stStable = 0; + stLastHash = h; + return false; + } + + var noop = function() {}; + var wallStart = process.hrtime.bigint(); + clockRunning = true; + mainLoop(); + async function mainLoop() { + try { + var frozenYields = 0; + while (vm.sendCount < maxSends) { + if (display.quitFlag) { stopReason = "quit"; break; } + if (untilMs > 0 && virtualMs >= untilMs) { stopReason = "untilMs"; break; } + if (vm.frozen) { + // ceder el event loop para que el unfreeze diferido corra + if (++frozenYields > 1000000) { stopReason = "frozen-livelock"; break; } + await new Promise(function(r) { setImmediate(r); }); + continue; + } + injectDueEvents(); + var result = vm.interpret(5, noop); // thenDo: freeze necesita continueFunc + if (result === "frozen") continue; + slices++; + if (checkStable()) { stopReason = "stable"; break; } + // el hash se alimenta solo de los checkpoints por sendCount (arriba); + // los límites de slice dependen de la cadencia de interrupciones, + // que varía entre modos (jit/no-jit) sin implicar divergencia semántica + if (result === "sleep") { + // todos los procesos esperan sin timer: nada más va a pasar + if (++idleStreak >= 3) { stopReason = "idle"; break; } + warpClock(vm, 10); + } else if (typeof result === "number" && result > 1) { + // todos esperan hasta un timer futuro: saltar la espera + warpClock(vm, result); + idleStreak = 0; + } else if (result === "break") { + stopReason = "breakpoint"; break; + } else { + idleStreak = 0; + } + } + } catch (e) { + stopReason = "error: " + e.message; + if (process.env.DIFFTRACE_DEBUG) console.error(e.stack); + } + var wallMs = Number(process.hrtime.bigint() - wallStart) / 1e6; + if (logLines) fs.writeFileSync(logPath, logLines.join("\n") + "\n"); + + function warpClock(vm, ms) { + if (mode === "bench") { + // adelantar el reloj del VM (startupTime) para no esperar de verdad + var now = vm.primHandler.millisecondClockValue(); + vm.primHandler.millisecondClockValueSet(now + ms); + } else { + virtualMs += ms; + } + } + + var fp = uiMod ? uiMod.displayFingerprint(vm) : null; + var report = { + image: path.relative(repoRoot, imagePath), + maxSends: maxSends, + sendCount: vm.sendCount, + slices: slices, + stopReason: stopReason, + hash: hash.toString(16), + virtualMs: virtualMs, + displayHash: fp ? fp.hash : undefined, + }; + + if (mode === "bench") { + console.log("bench: " + vm.sendCount + " sends en " + wallMs.toFixed(0) + " ms (" + + (vm.sendCount / (wallMs / 1000) / 1e6).toFixed(2) + "M sends/s), stop: " + stopReason); + if (vm.useStackZone) { + console.log("leaf calls=" + vm.nLeafCalls + " deopts=" + vm.nLeafDeopts + + " (de " + vm.sendCount + " sends)"); + var live = 0, maxSlots = 0; + for (var i = 0; i < vm.zonePages.length; i++) { + if (vm.zonePages[i].live) live++; + if (vm.zonePages[i].slots.length > maxSlots) maxSlots = vm.zonePages[i].slots.length; + } + var vacias = 0; + for (var i = 0; i < vm.zonePages.length; i++) + if (vm.zonePages[i].live && vm.zonePages[i].fp < 0) vacias++; + console.log("pages live vacías (fp<0): " + vacias + " flushPage=" + (vm.nFlushPage || 0)); + console.log("zona: pages=" + vm.zonePages.length + " live=" + live + " maxSlots=" + maxSlots + + " married=" + (vm.nMarriedContexts || 0) + " flushAll=" + (vm.nFlushAll || 0) + + " byClosure=" + (vm.nMarryClosure || 0) + " byThisCtx=" + (vm.nMarryThisCtx || 0) + + " bySenderFill=" + (vm.nMarrySenderFill || 0)); + } + return; + } + + if (vm.compiler && vm.compiler.okCount !== undefined) + console.log("jit2: ok=" + vm.compiler.okCount + " bail=" + vm.compiler.bailCount + + " leaves=" + (vm.compiler.leafCount || 0) + + " leafCalls=" + vm.nLeafCalls + " deopts=" + vm.nLeafDeopts); + if (vm.useStackZone) + console.log("married=" + (vm.nMarriedContexts||0) + " byClosure=" + (vm.nMarryClosure||0) + + " byThisCtx=" + (vm.nMarryThisCtx||0) + " bySenderFill=" + (vm.nMarrySenderFill||0)); + if (vm.nFreezeSim) console.log("freezes simulados: " + vm.nFreezeSim + " singleSteps: " + (vm.nSingleStep || 0)); + if (fp) console.log("display: " + fp.w + "x" + fp.h + " depth=" + fp.depth + + " hash=" + fp.hash + " nonzero=" + fp.nonzero + "/" + (fp.words || "?") + " words" + + " damage=" + (display.damage ? JSON.stringify(display.damage) : "none") + + " eventsLeft=" + (display.eventQueue ? display.eventQueue.length : "?")); + console.log("trace: sends=" + report.sendCount + " slices=" + report.slices + + " stop=" + report.stopReason + " hash=" + report.hash + " virtualMs=" + report.virtualMs + + " (wall " + wallMs.toFixed(0) + " ms)"); + + if (mode === "golden") { + fs.writeFileSync(goldenPath, JSON.stringify(report, null, 2) + "\n"); + console.log("golden grabado en " + path.relative(repoRoot, goldenPath)); + } else { + if (!fs.existsSync(goldenPath)) { + console.error("no hay golden.json — corré primero con --golden"); + process.exit(2); + } + var golden = JSON.parse(fs.readFileSync(goldenPath, "utf8")); + // slices/virtualMs/sendCount-final quedan fuera de la comparación: + // dependen de la cadencia de interrupciones y la granularidad del slice, + // no de la semántica + var keys = ["image", "maxSends", "stopReason", "hash"]; + if (golden.displayHash !== undefined && report.displayHash !== undefined) keys.push("displayHash"); + var diffs = keys.filter(function(k) { return String(golden[k]) !== String(report[k]); }); + if (diffs.length === 0) { + console.log("OK: traza idéntica al golden"); + } else { + diffs.forEach(function(k) { + console.error("DIVERGE " + k + ": golden=" + golden[k] + " actual=" + report[k]); + }); + process.exit(1); + } + } + } // fin mainLoop +}); diff --git a/perf/harness/uidisplay.js b/perf/harness/uidisplay.js new file mode 100644 index 00000000..2052e23a --- /dev/null +++ b/perf/harness/uidisplay.js @@ -0,0 +1,207 @@ +"use strict"; +// Display+input headless "que mide" para el oráculo diferencial. +// +// El display headless normal (vm.display.headless.js) reporta que NO hay +// pantalla (primitiveScreenSize devuelve false) y no entrega eventos, así que +// la imagen se cierra al arrancar (por eso existe -ignoreQuit) y el oráculo +// solo cubría el boot-hasta-quit. Esta variante reporta una pantalla real +// (offscreen), acepta el Display form y entrega eventos desde una cola, de modo +// que el World de Morphic LEVANTA y corre su loop: BitBlt dibuja al Display Form +// en memoria de objetos (no hace falta canvas). Con eso el oráculo ejercita el +// código interactivo — el mismo donde crashea el browser — y podemos hashear los +// bits del dibujo como fingerprint de estado final (idéntico en browser). +// +// install(Squeak, {width, height}) parchea Squeak.Primitives.prototype con las +// versiones "que miden" (ganan sobre las headless requeridas antes) y devuelve +// el objeto display para pasar al Interpreter. Llamar SOLO en modo --ui: el +// parche del prototipo es global. + +var EventTypeNone = 0; + +function makeDisplayObject(width, height) { + var display = { + width: width, + height: height, + // el cursor arranca en el centro + mouseX: width >> 1, + mouseY: height >> 1, + buttons: 0, + keys: [], + eventQueue: [], // [ [type, ms, ...campos], ... ] en dominio de reloj del VM + signalInputEvent: null, // seteado por primitiveInputSemaphore + idle: 0, + deferDisplayUpdates: false, + damage: null, // {l,t,r,b} acumulado por showDisplayRect (para saber si dibujó) + vmOptions: ["-headless"], + }; + display.getNextEvent = function(evtBuf, timeOffset) { + var evt = display.eventQueue.shift(); + if (!evt) { evtBuf[0] = EventTypeNone; return; } + evtBuf[0] = evt[0]; + // los timestamps ya están en el dominio de reloj del VM; sin offset + evtBuf[1] = (evt[1] - 0) & 0x1FFFFFFF; // MillisecondClockMask + for (var i = 2; i < evt.length; i++) evtBuf[i] = evt[i]; + }; + return display; +} + +function install(Squeak, opts) { + opts = opts || {}; + var width = opts.width || 1024, height = opts.height || 768; + var display = makeDisplayObject(width, height); + + Object.extend(Squeak.Primitives.prototype, "measuring-display", { + // --- display --- + primitiveScreenSize: function(argCount) { + return this.popNandPushIfOK(argCount + 1, this.makePointWithXandY(this.display.width, this.display.height)); + }, + primitiveScreenDepth: function(argCount) { + return this.popNandPushIfOK(argCount + 1, 32); + }, + primitiveScreenScaleFactor: function(argCount) { + return this.popNandPushIfOK(argCount + 1, 1); + }, + primitiveBeDisplay: function(argCount) { + var displayObj = this.vm.stackValue(0); + this.vm.specialObjects[Squeak.splOb_TheDisplay] = displayObj; + this.vm.popN(argCount); // return self + return true; + }, + primitiveReverseDisplay: function(argCount) { return true; }, + primitiveDeferDisplayUpdates: function(argCount) { + var flag = this.stackBoolean(0); + if (!this.success) return false; + this.display.deferDisplayUpdates = flag; + this.vm.popN(argCount); + return true; + }, + primitiveForceDisplayUpdate: function(argCount) { + this.vm.popN(argCount); + return true; + }, + primitiveShowDisplayRect: function(argCount) { + // registrar el rect como "dibujado" (BitBlt ya escribió al Display Form + // en memoria); no hay canvas al que copiar + var d = this.display; + var r = { l: this.stackInteger(3), t: this.stackInteger(1), r: this.stackInteger(2), b: this.stackInteger(0) }; + if (this.success) { + if (!d.damage) d.damage = { l: r.l, t: r.t, r: r.r, b: r.b }; + else { d.damage.l = Math.min(d.damage.l, r.l); d.damage.t = Math.min(d.damage.t, r.t); + d.damage.r = Math.max(d.damage.r, r.r); d.damage.b = Math.max(d.damage.b, r.b); } + } + this.vm.popN(argCount); + return true; + }, + primitiveSetFullScreen: function(argCount) { this.vm.popN(argCount); return true; }, + primitiveTestDisplayDepth: function(argCount) { + // responder true para las profundidades que soporta el image (todas acá) + return this.popNandPushIfOK(argCount + 1, this.vm.trueObj); + }, + + // --- input --- + primitiveInputSemaphore: function(argCount) { + var semaIndex = this.stackInteger(0); + if (!this.success) return false; + this.inputEventSemaIndex = semaIndex; + this.display.signalInputEvent = function() { + this.signalSemaphoreWithIndex(this.inputEventSemaIndex); + }.bind(this); + return this.popNIfOK(argCount); + }, + primitiveInputWord: function(argCount) { return this.popNandPushIfOK(1, 0); }, + primitiveGetNextEvent: function(argCount) { + this.display.idle++; + var evtBuf = this.stackNonInteger(0); + this.display.getNextEvent(evtBuf.pointers, this.vm.startupTime); + return this.popNIfOK(argCount); + }, + primitiveMouseButtons: function(argCount) { + this.popNandPushIfOK(argCount + 1, this.ensureSmallInt(this.display.buttons)); + if (this.display.idle++ > 20) this.vm.goIdle(); + return true; + }, + primitiveMousePoint: function(argCount) { + return this.popNandPushIfOK(argCount + 1, + this.makePointWithXandY(this.ensureSmallInt(this.display.mouseX), this.ensureSmallInt(this.display.mouseY))); + }, + primitiveKeyboardNext: function(argCount) { + return this.popNandPushIfOK(argCount + 1, + this.display.keys.length ? this.ensureSmallInt(this.display.keys.shift()) : this.vm.nilObj); + }, + primitiveKeyboardPeek: function(argCount) { + return this.popNandPushIfOK(argCount + 1, + this.display.keys.length ? this.ensureSmallInt(this.display.keys[0] || 0) : this.vm.nilObj); + }, + primitiveBeep: function(argCount) { return true; }, + primitiveClipboardText: function(argCount) { return false; }, + }); + + return display; +} + +// Hash FNV-1a de los bits del Display Form (fingerprint del dibujo). +// Devuelve {hash, nonzero, w, h, depth} o null si no hay Display todavía. +function displayFingerprint(vm) { + var disp = vm.specialObjects[Squeak.splOb_TheDisplay]; + if (!disp || disp.isNil || !disp.pointers) return null; + // DisplayScreen/Form: pointers = [bits, width, height, depth, ...] + var bits = disp.pointers[0]; + var w = disp.pointers[1], h = disp.pointers[2], depth = disp.pointers[3]; + if (!bits || !bits.words) return { hash: "nobits", nonzero: 0, w: w, h: h, depth: depth }; + var words = bits.words; + var hash = 2166136261 >>> 0, nonzero = 0; + for (var i = 0; i < words.length; i++) { + var v = words[i] >>> 0; + if (v !== 0) nonzero++; + hash = (hash ^ (v & 0xffff)) >>> 0; hash = Math.imul(hash, 16777619) >>> 0; + hash = (hash ^ (v >>> 16)) >>> 0; hash = Math.imul(hash, 16777619) >>> 0; + } + return { hash: hash.toString(16), nonzero: nonzero, words: words.length, w: w, h: h, depth: depth }; +} + +// Constantes de evento (espejo de Squeak.events / vm.input.js) +var Ev = { + Mouse: 1, Keyboard: 2, + Red: 4, Yellow: 2, Blue: 1, // botones + Shift: 8, Ctrl: 16, Alt: 32, Cmd: 64, + KeyChar: 0, KeyDown: 1, KeyUp: 2, +}; + +// Script sintético de interacción, agendado por sendCount (determinista e +// idéntico entre representaciones). No apunta a botones concretos de Dialogo +// (no conocemos su layout) pero ejercita la maquinaria interactiva pesada que +// el boot estático nunca toca: tracking del hand, hit-testing de morphs, +// cursor, drag (down→move→up) y teclado. Para "dibujar" con coordenadas reales, +// pasar --events con una traza grabada del browser. +// from/to: ventana de sendCount donde ocurre la interacción +// devuelve [ {at, ev:[type, tsPlaceholder, ...]}, ... ] ordenado por at +function syntheticScript(width, height, from, to) { + var sched = []; + var span = Math.max(1, to - from); + var cx = width >> 1, cy = height >> 1; + var t = from; + var step = Math.max(1, Math.floor(span / 64)); + function moveTo(x, y, buttons) { sched.push({ at: t, ev: [Ev.Mouse, 0, x | 0, y | 0, buttons | 0, 0] }); t += step; } + // 1) barrido diagonal (hand tracking + hit-test en toda la pantalla) + for (var i = 0; i <= 16; i++) moveTo(50 + (width - 100) * i / 16, 50 + (height - 100) * i / 16, 0); + // 2) mover al centro y hacer click (down/up) — dispara menús/halos según el morph + moveTo(cx, cy, 0); + moveTo(cx, cy, Ev.Red); // mouse down (botón izquierdo) + moveTo(cx, cy, 0); // mouse up + // 3) drag: down, arrastrar en L, up (maquinaria de drag/pickup) + moveTo(cx - 200, cy - 100, Ev.Red); + for (var j = 1; j <= 12; j++) moveTo(cx - 200 + j * 30, cy - 100, Ev.Red); + for (var k = 1; k <= 8; k++) moveTo(cx + 160, cy - 100 + k * 25, Ev.Red); + moveTo(cx + 160, cy + 100, 0); // soltar + // 4) teclado: unas teclas (macRoman == unicode para ASCII) + "hola".split("").forEach(function(ch) { + var u = ch.charCodeAt(0); + sched.push({ at: t, ev: [Ev.Keyboard, 0, u, Ev.KeyChar, 0, u] }); t += step; + }); + return sched; +} + +module.exports = { + install: install, displayFingerprint: displayFingerprint, + EventTypeNone: EventTypeNone, syntheticScript: syntheticScript, Ev: Ev, +}; diff --git a/perf/spike/as/frames.ts b/perf/spike/as/frames.ts new file mode 100644 index 00000000..39ae6bce --- /dev/null +++ b/perf/spike/as/frames.ts @@ -0,0 +1,273 @@ +// V4: frames planos + intérprete de bytecodes en WASM (espejo de V3 en variants.js). +// Diferencias deliberadas con el spike anterior (que perdió 2.2x): +// - load/store crudos sobre memoria lineal, sin objetos Int32Array +// (sin bounds checks ni indirección dataStart) +// - sin recursión del host: un solo loop de dispatch, frames en nuestra memoria +// - valores taggeados i32: smallint = (v<<1)|1, oop = dirección par + +// Mapa de memoria (offsets en bytes; --initialMemory 2 páginas = 128KB) +const METHOD_BASE: i32 = 4096; // bytecodes del método fib (u8) +const CACHE_BASE: i32 = 8192; // cache de métodos: 512 × [check:i32, method:i32] +const RCVR_OOP: i32 = 16384; // objeto receiver: [classId:i32]; oop = dirección +const ZONE_BASE: i32 = 65536; // stack zone + +const SEL_FIB: i32 = 7; +const CLASS_SMALLINT: i32 = 99; +const CLASS_FIB: i32 = 5; +const METH_FIB: i32 = 1; +const CACHE_MASK: i32 = 511; +const HASCTX_BIT: i32 = 1 << 17; + +// offsets en bytes de los slots del frame +const F_SAVEDFP: i32 = 0; +const F_SAVEDPC: i32 = 4; +const F_METHOD: i32 = 8; +const F_FLAGS: i32 = 12; +const F_CTX: i32 = 16; +const F_FIXED: i32 = 20; + +let sends: i32 = 0; + +export function init(): void { + // fib: n<2 ifTrue:[^n] ifFalse:[^(self fib: n-1) + (self fib: n-2)] + // opcodes: 0 PUSH_ARG0, 1 PUSH_C1, 2 PUSH_C2, 3 PUSH_SELF, 4 SUB, 5 LT, + // 6 JMPF , 7 SEND_FIB, 8 ADD, 9 RET + const b: i32 = METHOD_BASE; + store(b + 0, 0); // PUSH_ARG0 + store(b + 1, 2); // PUSH_C2 + store(b + 2, 5); // LT + store(b + 3, 6); // JMPF + store(b + 4, 7); // target: pc 7 + store(b + 5, 0); // PUSH_ARG0 + store(b + 6, 9); // RET + store(b + 7, 3); // PUSH_SELF + store(b + 8, 0); // PUSH_ARG0 + store(b + 9, 1); // PUSH_C1 + store(b + 10, 4); // SUB + store(b + 11, 7); // SEND_FIB + store(b + 12, 3); // PUSH_SELF + store(b + 13, 0); // PUSH_ARG0 + store(b + 14, 2); // PUSH_C2 + store(b + 15, 4); // SUB + store(b + 16, 7); // SEND_FIB + store(b + 17, 8); // ADD + store(b + 18, 9); // RET + store(RCVR_OOP, CLASS_FIB); + memory.fill(CACHE_BASE, 0, 4096); +} + +export function run(n: i32): i32 { + sends = 0; + let icc: i32 = 1000; + // frame base: rcvr @ ZONE_BASE, arg @ +4, frame @ +8 + store(ZONE_BASE, RCVR_OOP); + store(ZONE_BASE + 4, (n << 1) | 1); + const bfp: i32 = ZONE_BASE + 8; + store(bfp + F_SAVEDFP, 0); + store(bfp + F_SAVEDPC, 0); + store(bfp + F_METHOD, METH_FIB); + store(bfp + F_FLAGS, 1); + store(bfp + F_CTX, 0); + let fp: i32 = bfp; + let sp: i32 = bfp + F_FIXED - 4; + let pc: i32 = METHOD_BASE; + while (true) { + const op: i32 = load(pc); + pc++; + switch (op) { + case 0: { // PUSH_ARG0 + sp += 4; store(sp, load(fp - 4)); break; + } + case 1: { sp += 4; store(sp, 3); break; } // 1 taggeado + case 2: { sp += 4; store(sp, 5); break; } // 2 taggeado + case 3: { // PUSH_SELF + sp += 4; store(sp, load(fp - 8)); break; + } + case 4: { // SUB taggeado: (a-1)-(b-1)+1 = a-b+1 + const b2: i32 = load(sp); sp -= 4; const a: i32 = load(sp); + if ((a & b2 & 1) != 0) store(sp, a - b2 + 1); + else unreachable(); + break; + } + case 5: { // LT (comparación taggeada es monótona) + const b2: i32 = load(sp); sp -= 4; const a: i32 = load(sp); + if ((a & b2 & 1) != 0) store(sp, a < b2 ? 3 : 1); + else unreachable(); + break; + } + case 6: { // JMPF + const t: i32 = load(pc); pc++; + const c: i32 = load(sp); sp -= 4; + if (c == 1) pc = METHOD_BASE + t; + break; + } + case 7: { // SEND_FIB + sends++; + const numArgs: i32 = 1; + const rcvr: i32 = load(sp - (numArgs << 2)); + const classId: i32 = (rcvr & 1) != 0 ? CLASS_SMALLINT : load(rcvr); + const k: i32 = ((classId ^ (SEL_FIB * 31)) & CACHE_MASK) << 3; + const check: i32 = (classId << 8) | SEL_FIB; + let methodId: i32; + if (load(CACHE_BASE + k) == check) { + methodId = load(CACHE_BASE + k + 4); + } else { + store(CACHE_BASE + k, check); + store(CACHE_BASE + k + 4, METH_FIB); + methodId = METH_FIB; + } + const nfp: i32 = sp + 4; + store(nfp + F_SAVEDFP, fp); + store(nfp + F_SAVEDPC, pc); + store(nfp + F_METHOD, methodId); + store(nfp + F_FLAGS, numArgs); + store(nfp + F_CTX, 0); + const numTemps: i32 = 0; // nil-fill presente, 0 iteraciones en fib + for (let i: i32 = 0; i < numTemps; i++) store(nfp + F_FIXED + (i << 2), 0); + fp = nfp; + sp = nfp + F_FIXED - 4; + pc = METHOD_BASE; // methodStart(methodId); un solo método en el bench + icc--; if (icc <= 0) icc = 1000; + break; + } + case 8: { // ADD taggeado: a+b-1 + const b2: i32 = load(sp); sp -= 4; const a: i32 = load(sp); + if ((a & b2 & 1) != 0) store(sp, a + b2 - 1); + else unreachable(); + break; + } + case 9: { // RET + const rv: i32 = load(sp); + const flags: i32 = load(fp + F_FLAGS); + if ((flags & HASCTX_BIT) != 0) unreachable(); // widowCold: no pasa en el bench + const sfp: i32 = load(fp + F_SAVEDFP); + if (sfp == 0) return rv >> 1; + const numArgs: i32 = flags & 0xFFFF; + const rs: i32 = fp - 4 - (numArgs << 2); + store(rs, rv); + sp = rs; + pc = load(fp + F_SAVEDPC); + fp = sfp; + break; + } + default: unreachable(); + } + } + return 0; // inalcanzable +} + +export function getSends(): i32 { + return sends; +} + +// --------------------------------------------------------------------------- +// V5: frames + "métodos compilados" en WASM + trampolín (estimador de fase 2: +// codegen WASM por método). Misma estructura que V2 en JS, pero sobre memoria +// lineal. El dispatch del trampolín usa switch sobre methodId (aprox. de +// call_indirect; subestima levemente su costo). +// --------------------------------------------------------------------------- +let g_fp: i32 = 0; +let g_sp: i32 = 0; +let g_pc: i32 = 0; +let g_method: i32 = 0; +let g_running: bool = false; +let g_result: i32 = 0; +let g_icc: i32 = 1000; + +function send5(numArgs: i32): void { + sends++; + const rcvr: i32 = load(g_sp - (numArgs << 2)); + const classId: i32 = (rcvr & 1) != 0 ? CLASS_SMALLINT : load(rcvr); + const k: i32 = ((classId ^ (SEL_FIB * 31)) & CACHE_MASK) << 3; + const check: i32 = (classId << 8) | SEL_FIB; + let methodId: i32; + if (load(CACHE_BASE + k) == check) { + methodId = load(CACHE_BASE + k + 4); + } else { + store(CACHE_BASE + k, check); + store(CACHE_BASE + k + 4, METH_FIB); + methodId = METH_FIB; + } + const nfp: i32 = g_sp + 4; + store(nfp + F_SAVEDFP, g_fp); + store(nfp + F_SAVEDPC, g_pc); + store(nfp + F_METHOD, methodId); + store(nfp + F_FLAGS, numArgs); + store(nfp + F_CTX, 0); + g_fp = nfp; + g_sp = nfp + F_FIXED - 4; + g_method = methodId; + g_pc = 0; + g_icc--; if (g_icc <= 0) g_icc = 1000; +} + +function ret5(rv: i32): void { + const flags: i32 = load(g_fp + F_FLAGS); + if ((flags & HASCTX_BIT) != 0) unreachable(); + const sfp: i32 = load(g_fp + F_SAVEDFP); + if (sfp == 0) { g_running = false; g_result = rv; return; } + const numArgs: i32 = flags & 0xFFFF; + const rs: i32 = g_fp - 4 - (numArgs << 2); + store(rs, rv); + g_sp = rs; + g_pc = load(g_fp + F_SAVEDPC); + g_method = load(sfp + F_METHOD); + g_fp = sfp; +} + +function fibCompiled5(): void { + const fp: i32 = g_fp; + while (true) { + switch (g_pc) { + case 0: { + const n: i32 = load(fp - 4); + // n < 2 con n taggeado: tagged(2) = 5, comparación monótona + if ((n & 1) != 0 && n < 5) { ret5(n); return; } + g_sp += 4; store(g_sp, load(fp - 8)); // self + if ((n & 1) != 0) { g_sp += 4; store(g_sp, n - 2); } // n-1 taggeado + else unreachable(); + g_pc = 1; send5(1); return; + } + case 1: { + g_sp += 4; store(g_sp, load(fp - 8)); // self + const n: i32 = load(fp - 4); + if ((n & 1) != 0) { g_sp += 4; store(g_sp, n - 4); } // n-2 taggeado + else unreachable(); + g_pc = 2; send5(1); return; + } + case 2: { + const b: i32 = load(g_sp); const a: i32 = load(g_sp - 4); + if ((a & b & 1) != 0) { g_sp -= 4; store(g_sp, a + b - 1); } + else unreachable(); + ret5(load(g_sp)); + return; + } + default: unreachable(); + } + } +} + +export function run5(n: i32): i32 { + sends = 0; + g_icc = 1000; + store(ZONE_BASE, RCVR_OOP); + store(ZONE_BASE + 4, (n << 1) | 1); + const bfp: i32 = ZONE_BASE + 8; + store(bfp + F_SAVEDFP, 0); + store(bfp + F_SAVEDPC, 0); + store(bfp + F_METHOD, METH_FIB); + store(bfp + F_FLAGS, 1); + store(bfp + F_CTX, 0); + g_fp = bfp; + g_sp = bfp + F_FIXED - 4; + g_pc = 0; + g_method = METH_FIB; + g_running = true; + while (g_running) { + switch (g_method) { + case METH_FIB: fibCompiled5(); break; + default: unreachable(); + } + } + return g_result >> 1; +} diff --git a/perf/spike/bench2.js b/perf/spike/bench2.js new file mode 100644 index 00000000..5acdf4ba --- /dev/null +++ b/perf/spike/bench2.js @@ -0,0 +1,68 @@ +"use strict"; +// Bench fase 0 del diseño stack-zone: 5 variantes, mismo fib(32) por sends. +const fs = require("fs"); +const path = require("path"); +const mimic = require("./js/mimic.js"); // V0: contexts + recursión JS (spike anterior) +const { makeContextVM, makeFrameTrampolineVM, makeFrameInterpVM } = require("./js/variants.js"); + +const N = 32, EXPECT_RESULT = 2178309, EXPECT_SENDS = 7049155; +const WARMUP = 3, RUNS = 7; + +async function loadWasm() { + const bytes = fs.readFileSync(path.join(__dirname, "as/frames.wasm")); + const { instance } = await WebAssembly.instantiate(bytes, { + env: { abort: () => { throw new Error("wasm abort"); } }, + }); + instance.exports.init(); + return instance.exports; +} + +function bench(name, runFn) { + let best = Infinity, r; + for (let i = 0; i < WARMUP; i++) r = runFn(N); + for (let i = 0; i < RUNS; i++) { + r = runFn(N); + if (r.ns < best) best = r.ns; + } + if (r.result !== EXPECT_RESULT) throw Error(name + ": resultado " + r.result); + // V0 cuenta la activación raíz como send; V1-V4 no (la arman a mano) + if (r.sends !== EXPECT_SENDS && r.sends !== EXPECT_SENDS - 1) + throw Error(name + ": sends " + r.sends); + return { name, ms: best / 1e6, sendsPerSec: EXPECT_SENDS / (best / 1e9) }; +} + +(async () => { + const wasm = await loadWasm(); + const wasmRun = (n) => { + const t0 = process.hrtime.bigint(); + const result = wasm.run(n); + const t1 = process.hrtime.bigint(); + return { result, sends: wasm.getSends(), ns: Number(t1 - t0) }; + }; + + const rows = [ + bench("V0 contexts + recursión JS (spike anterior, referencia)", mimic), + bench("V1 contexts + trampolín (≈ jit.js actual)", makeContextVM()), + bench("V2 frames + trampolín (jit.js con stack zone, JS puro)", makeFrameTrampolineVM()), + bench("V3 frames + intérprete bytecodes JS", makeFrameInterpVM()), + bench("V4 frames + intérprete bytecodes WASM", wasmRun), + bench("V5 frames + métodos compilados WASM (estimador fase 2)", (n) => { + const t0 = process.hrtime.bigint(); + const result = wasm.run5(n); + const t1 = process.hrtime.bigint(); + return { result, sends: wasm.getSends(), ns: Number(t1 - t0) }; + }), + ]; + + const base = rows[1].ms; // V1 = arquitectura actual + console.log(`fib(${N}) = ${EXPECT_RESULT}, ${(EXPECT_SENDS / 1e6).toFixed(2)}M sends, best of ${RUNS} (tras ${WARMUP} warmup)\n`); + for (const r of rows) { + const ratio = base / r.ms; + console.log( + r.ms.toFixed(1).padStart(7) + " ms " + + (r.sendsPerSec / 1e6).toFixed(1).padStart(6) + "M sends/s " + + (ratio >= 1 ? (ratio.toFixed(2) + "x más rápido que V1") : ((r.ms / base).toFixed(2) + "x más lento que V1")).padStart(24) + " " + + r.name + ); + } +})(); diff --git a/perf/spike/js/mimic.js b/perf/spike/js/mimic.js new file mode 100644 index 00000000..b6c03eb7 --- /dev/null +++ b/perf/spike/js/mimic.js @@ -0,0 +1,90 @@ +"use strict"; +// Synthetic microbenchmark mimicking the shape of SqueakJS's hot loop as measured +// in the CPU profile: findSelectorInClass (linear-probe method dict lookup), +// executeNewMethod (context allocation from a recycle pool, arg copy, temp nil-fill), +// doReturn (context recycling). Not a real Smalltalk interpreter -- just the same +// data-structure shapes and control flow, run recursively via a fib(n) send tree +// so we get millions of sends of a realistic shape. + +function makeMethodDict(entries) { + var size = 1; + while (size < entries.length * 4) size *= 2; // keep load factor low, like a real MethodDict + var selectors = new Array(size).fill(-1); // -1 == nil + var methods = new Array(size).fill(-1); + var mask = size - 1; + for (var i = 0; i < entries.length; i++) { + var sel = entries[i][0], meth = entries[i][1]; + var idx = sel & mask; + while (selectors[idx] !== -1) idx = (idx + 1) & mask; + selectors[idx] = sel; + methods[idx] = meth; + } + return { selectors: selectors, methods: methods, mask: mask }; +} + +function lookupSelectorInDict(mDict, selectorHash) { + var idx = selectorHash & mDict.mask; + while (true) { + if (mDict.selectors[idx] === selectorHash) return mDict.methods[idx]; + if (mDict.selectors[idx] === -1) return -1; // nil + idx = (idx + 1) & mDict.mask; + } +} + +var SEL_FIB = 1; +var METH_FIB = 100; +var fibClassDict = makeMethodDict([[SEL_FIB, METH_FIB]]); + +var FRAME_SIZE = 8; // sender + arg + temps, like Squeak's Context_tempFrameStart layout +var freeList = []; +var sendCount = 0; + +function allocateContext() { + var ctx = freeList.pop(); + if (!ctx) ctx = { pointers: new Array(FRAME_SIZE) }; + return ctx; +} +function recycleContext(ctx) { + freeList.push(ctx); +} + +function executeNewMethod(senderCtx, arg) { + sendCount++; + var found = lookupSelectorInDict(fibClassDict, SEL_FIB); // findSelectorInClass equivalent + if (found !== METH_FIB) throw new Error("lookup failed"); + var ctx = allocateContext(); + ctx.sender = senderCtx; + ctx.pointers[0] = arg; + ctx.pointers[1] = 0; + for (var i = 2; i < FRAME_SIZE; i++) ctx.pointers[i] = null; // fill temps with nil + return ctx; +} + +function doReturn(ctx) { + var sender = ctx.sender; + recycleContext(ctx); + return sender; +} + +function fibSend(senderCtx, n) { + var ctx = executeNewMethod(senderCtx, n); + var result; + if (n < 2) { + result = n; + } else { + var a = fibSend(ctx, n - 1); + var b = fibSend(ctx, n - 2); + result = a + b; + } + doReturn(ctx); + return result; +} + +module.exports = function run(n) { + sendCount = 0; + freeList.length = 0; + var t0 = process.hrtime.bigint(); + var result = fibSend(null, n); + var t1 = process.hrtime.bigint(); + return { result: result, sends: sendCount, ns: Number(t1 - t0) }; +}; diff --git a/perf/spike/js/variants.js b/perf/spike/js/variants.js new file mode 100644 index 00000000..f6b887a7 --- /dev/null +++ b/perf/spike/js/variants.js @@ -0,0 +1,350 @@ +"use strict"; +// Spike fase 0 del diseño stack-zone (ver ../stack-zone-design.md). +// Tres variantes JS del mismo benchmark fib-por-sends, para aislar dos efectos: +// V1: contexts heap + trampolín ≈ arquitectura actual (jit.js + vm.interpreter.js) +// V2: frames planos + trampolín ≈ jit.js con stack zone (camino JS incremental) +// V3: frames planos + intérprete de bytecodes ≈ intérprete fase-1 (espejo del WASM V4) +// Todas hacen el mismo trabajo por send: probe de cache de métodos, chequeo de clase +// del receiver, contador de interrupciones, y (V2/V3) chequeo de hasContext al retornar. + +const SEL_FIB = 7; +const CLASS_SMALLINT = 99; +const CLASS_FIB = 5; +const METH_FIB = 1; +const CACHE_MASK = 511; + +function makeCache() { + // direct-mapped: [checkWord, methodId] × 512 + const cache = new Int32Array((CACHE_MASK + 1) * 2); + return cache; +} + +function cacheLookup(cache, classId, selId) { + const k = ((classId ^ (selId * 31)) & CACHE_MASK) << 1; + const check = (classId << 8) | selId; + if (cache[k] === check) return cache[k + 1]; + // slow path: "búsqueda" y fill (en el bench siempre encuentra METH_FIB) + cache[k] = check; + cache[k + 1] = METH_FIB; + return METH_FIB; +} + +// --------------------------------------------------------------------------- +// V1: contexts heap + trampolín (modela la arquitectura actual) +// Context = objeto con array `pointers`: [sender, pc, sp, method, receiver, arg0, ...opstack] +// Reciclado por free-list como allocateOrRecycleContext/recycleIfPossible. +// --------------------------------------------------------------------------- +const CTX_SENDER = 0, CTX_PC = 1, CTX_SP = 2, CTX_METHOD = 3, CTX_RCVR = 4, CTX_TEMP0 = 5; +const CTX_SIZE = 24; + +function makeContextVM() { + const cache = makeCache(); + const rcvrObj = { classId: CLASS_FIB }; + const vm = { + activeCtx: null, pc: 0, sp: 0, methodId: 0, + running: false, result: 0, sends: 0, icc: 1000, + freeCtx: null, + }; + + function allocCtx() { + let ctx = vm.freeCtx; + if (ctx) { vm.freeCtx = ctx.pointers[CTX_SENDER]; return ctx; } + return { pointers: new Array(CTX_SIZE).fill(null) }; + } + + function send(selId, numArgs) { + vm.sends++; + const stack = vm.activeCtx.pointers; + const rcvr = stack[vm.sp - numArgs]; + const classId = typeof rcvr === "number" ? CLASS_SMALLINT : rcvr.classId; + const methodId = cacheLookup(cache, classId, selId); + // storeContextRegisters: guardar pc/sp en el context actual + stack[CTX_PC] = vm.pc; + stack[CTX_SP] = vm.sp - numArgs - 1; // sp tras popear rcvr+args + const ctx = allocCtx(); + const p = ctx.pointers; + p[CTX_SENDER] = vm.activeCtx; + p[CTX_METHOD] = methodId; + // copiar receiver + args al nuevo context (como executeNewMethod:1046) + p[CTX_RCVR] = rcvr; + for (let i = 0; i < numArgs; i++) p[CTX_TEMP0 + i] = stack[vm.sp - numArgs + 1 + i]; + // nil-fill de temps (fib: 0 temps extra — loop presente igual) + const numTemps = 0; + for (let i = 0; i < numTemps; i++) p[CTX_TEMP0 + numArgs + i] = null; + vm.activeCtx = ctx; + vm.methodId = methodId; + vm.pc = 0; + vm.sp = CTX_TEMP0 + numArgs + numTemps - 1; // tope del opstack (vacío) + if (--vm.icc <= 0) vm.icc = 1000; + } + + function doReturn(rv) { + const ctx = vm.activeCtx; + const sender = ctx.pointers[CTX_SENDER]; + // escaneo mínimo de unwind (target === sender, un compare como hoy) + // nil sender/ip + reciclar (doReturn:1103-1107) + ctx.pointers[CTX_SENDER] = vm.freeCtx; // reuso del slot como link de free-list + ctx.pointers[CTX_PC] = null; + vm.freeCtx = ctx; + if (sender === null) { vm.running = false; vm.result = rv; return; } + // fetchContextRegisters + const sp = sender.pointers[CTX_SP]; + vm.activeCtx = sender; + vm.methodId = sender.pointers[CTX_METHOD]; + vm.pc = sender.pointers[CTX_PC]; + vm.sp = sp + 1; + sender.pointers[sp + 1] = rv; // push del resultado + } + + // fib "compilado" al estilo jit.js: switch sobre pc, return al trampolín en cada send + function fibCompiled(vm) { + const stack = vm.activeCtx.pointers; + while (true) switch (vm.pc) { + case 0: { + const n = stack[CTX_TEMP0]; + if (typeof n === "number" && n < 2) { doReturn(n); return; } + stack[++vm.sp] = stack[CTX_RCVR]; + const a = n; // inline #- con chequeo como generateNumericOp + if (typeof a === "number") stack[++vm.sp] = a - 1; else throw Error("fail"); + vm.pc = 1; send(SEL_FIB, 1); return; + } + case 1: { + stack[++vm.sp] = stack[CTX_RCVR]; + const a = stack[CTX_TEMP0]; + if (typeof a === "number") stack[++vm.sp] = a - 2; else throw Error("fail"); + vm.pc = 2; send(SEL_FIB, 1); return; + } + case 2: { + const b = stack[vm.sp], a = stack[vm.sp - 1]; + if (typeof a === "number" && typeof b === "number") stack[--vm.sp] = a + b; + else throw Error("fail"); + doReturn(stack[vm.sp]); + return; + } + } + } + + const methodFns = [null, fibCompiled]; + + return function run(n) { + vm.freeCtx = null; vm.sends = 0; vm.icc = 1000; + const base = { pointers: new Array(CTX_SIZE).fill(null) }; + base.pointers[CTX_SENDER] = null; + base.pointers[CTX_METHOD] = METH_FIB; + base.pointers[CTX_RCVR] = rcvrObj; + base.pointers[CTX_TEMP0] = n; + vm.activeCtx = base; vm.methodId = METH_FIB; vm.pc = 0; + vm.sp = CTX_TEMP0; // opstack vacío arriba de arg0 + vm.running = true; + const t0 = process.hrtime.bigint(); + while (vm.running) methodFns[vm.methodId](vm); + const t1 = process.hrtime.bigint(); + return { result: vm.result, sends: vm.sends, ns: Number(t1 - t0) }; + }; +} + +// --------------------------------------------------------------------------- +// Maquinaria de frames compartida por V2/V3 (layout del diseño): +// rcvr @ fp-1-numArgs, args hasta fp-1, +// fp+0 savedFp | fp+1 savedPc | fp+2 method | fp+3 flags | fp+4 ctxOop | temps | opstack +// --------------------------------------------------------------------------- +const F_SAVEDFP = 0, F_SAVEDPC = 1, F_METHOD = 2, F_FLAGS = 3, F_CTX = 4, F_FIXED = 5; +const HASCTX_BIT = 1 << 17; + +// --------------------------------------------------------------------------- +// V2: frames planos + trampolín (jit.js con stack zone, sin WASM) +// --------------------------------------------------------------------------- +function makeFrameTrampolineVM() { + const cache = makeCache(); + const rcvrObj = { classId: CLASS_FIB }; + const zone = new Array(1 << 16).fill(0); + const vm = { + fp: 0, sp: 0, pc: 0, methodId: 0, + running: false, result: 0, sends: 0, icc: 1000, + }; + + function send(selId, numArgs) { + vm.sends++; + const rcvr = zone[vm.sp - numArgs]; + const classId = typeof rcvr === "number" ? CLASS_SMALLINT : rcvr.classId; + const methodId = cacheLookup(cache, classId, selId); + const nfp = vm.sp + 1; + zone[nfp + F_SAVEDFP] = vm.fp; + zone[nfp + F_SAVEDPC] = vm.pc; + zone[nfp + F_METHOD] = methodId; + zone[nfp + F_FLAGS] = numArgs; + zone[nfp + F_CTX] = 0; + const numTemps = 0; // nil-fill presente + for (let i = 0; i < numTemps; i++) zone[nfp + F_FIXED + i] = null; + vm.fp = nfp; + vm.sp = nfp + F_FIXED - 1 + numTemps; + vm.methodId = methodId; + vm.pc = 0; + if (--vm.icc <= 0) vm.icc = 1000; + } + + function doReturn(rv) { + const fp = vm.fp; + const flags = zone[fp + F_FLAGS]; + if (flags & HASCTX_BIT) widowCold(fp); + const sfp = zone[fp + F_SAVEDFP]; + if (sfp === 0) { vm.running = false; vm.result = rv; return; } + const numArgs = flags & 0xFFFF; + const rs = fp - 1 - numArgs; + zone[rs] = rv; + vm.sp = rs; + vm.pc = zone[fp + F_SAVEDPC]; + vm.methodId = zone[sfp + F_METHOD]; + vm.fp = sfp; + } + + function widowCold() { throw Error("no contexts casados en este bench"); } + + function fibCompiled(vm) { + const fp = vm.fp; + while (true) switch (vm.pc) { + case 0: { + const n = zone[fp - 1]; + if (typeof n === "number" && n < 2) { doReturn(n); return; } + zone[++vm.sp] = zone[fp - 2]; + if (typeof n === "number") zone[++vm.sp] = n - 1; else throw Error("fail"); + vm.pc = 1; send(SEL_FIB, 1); return; + } + case 1: { + zone[++vm.sp] = zone[fp - 2]; + const a = zone[fp - 1]; + if (typeof a === "number") zone[++vm.sp] = a - 2; else throw Error("fail"); + vm.pc = 2; send(SEL_FIB, 1); return; + } + case 2: { + const b = zone[vm.sp], a = zone[vm.sp - 1]; + if (typeof a === "number" && typeof b === "number") zone[--vm.sp] = a + b; + else throw Error("fail"); + doReturn(zone[vm.sp]); + return; + } + } + } + + const methodFns = [null, fibCompiled]; + + return function run(n) { + vm.sends = 0; vm.icc = 1000; + // frame base: rcvr@0, arg@1, frame@2 + zone[0] = rcvrObj; zone[1] = n; + zone[2 + F_SAVEDFP] = 0; zone[2 + F_SAVEDPC] = 0; + zone[2 + F_METHOD] = METH_FIB; zone[2 + F_FLAGS] = 1; zone[2 + F_CTX] = 0; + vm.fp = 2; vm.sp = 2 + F_FIXED - 1; vm.pc = 0; vm.methodId = METH_FIB; + vm.running = true; + const t0 = process.hrtime.bigint(); + while (vm.running) methodFns[vm.methodId](vm); + const t1 = process.hrtime.bigint(); + return { result: vm.result, sends: vm.sends, ns: Number(t1 - t0) }; + }; +} + +// --------------------------------------------------------------------------- +// V3: frames planos + intérprete de bytecodes en JS (espejo exacto del WASM V4) +// --------------------------------------------------------------------------- +// mini-ISA: ver diseño; fib: n < 2 ifTrue:[^n] ifFalse:[^(self fib: n-1) + (self fib: n-2)] +const OP_PUSH_ARG0 = 0, OP_PUSH_C1 = 1, OP_PUSH_C2 = 2, OP_PUSH_SELF = 3, + OP_SUB = 4, OP_LT = 5, OP_JMPF = 6, OP_SEND_FIB = 7, OP_ADD = 8, OP_RET = 9; +const FIB_BYTES = new Uint8Array([ + OP_PUSH_ARG0, OP_PUSH_C2, OP_LT, OP_JMPF, 7, // pc 0-4: n<2? si no → pc7 + OP_PUSH_ARG0, OP_RET, // pc 5-6: ^n + OP_PUSH_SELF, OP_PUSH_ARG0, OP_PUSH_C1, OP_SUB, OP_SEND_FIB, // pc 7-11 + OP_PUSH_SELF, OP_PUSH_ARG0, OP_PUSH_C2, OP_SUB, OP_SEND_FIB, // pc 12-16 + OP_ADD, OP_RET, // pc 17-18 +]); + +function makeFrameInterpVM() { + const cache = makeCache(); + const rcvrObj = { classId: CLASS_FIB }; + const zone = new Array(1 << 16).fill(0); + const methodBytes = [null, FIB_BYTES]; + let sends = 0; + + function run(n) { + sends = 0; + let icc = 1000; + zone[0] = rcvrObj; zone[1] = n; + zone[2 + F_SAVEDFP] = 0; zone[2 + F_SAVEDPC] = 0; + zone[2 + F_METHOD] = METH_FIB; zone[2 + F_FLAGS] = 1; zone[2 + F_CTX] = 0; + let fp = 2, sp = 2 + F_FIXED - 1, pc = 0; + let bytes = FIB_BYTES; + const t0 = process.hrtime.bigint(); + let result; + loop: while (true) { + switch (bytes[pc++]) { + case OP_PUSH_ARG0: zone[++sp] = zone[fp - 1]; break; + case OP_PUSH_C1: zone[++sp] = 1; break; + case OP_PUSH_C2: zone[++sp] = 2; break; + case OP_PUSH_SELF: zone[++sp] = zone[fp - 2]; break; + case OP_SUB: { + const b = zone[sp--], a = zone[sp]; + if (typeof a === "number" && typeof b === "number") zone[sp] = a - b; + else throw Error("fail"); + break; + } + case OP_LT: { + const b = zone[sp--], a = zone[sp]; + if (typeof a === "number" && typeof b === "number") zone[sp] = a < b ? 1 : 0; + else throw Error("fail"); + break; + } + case OP_JMPF: { + const target = bytes[pc++]; + if (zone[sp--] === 0) pc = target; + break; + } + case OP_SEND_FIB: { + sends++; + const numArgs = 1; + const rcvr = zone[sp - numArgs]; + const classId = typeof rcvr === "number" ? CLASS_SMALLINT : rcvr.classId; + const methodId = cacheLookup(cache, classId, SEL_FIB); + const nfp = sp + 1; + zone[nfp + F_SAVEDFP] = fp; + zone[nfp + F_SAVEDPC] = pc; + zone[nfp + F_METHOD] = methodId; + zone[nfp + F_FLAGS] = numArgs; + zone[nfp + F_CTX] = 0; + const numTemps = 0; + for (let i = 0; i < numTemps; i++) zone[nfp + F_FIXED + i] = null; + fp = nfp; sp = nfp + F_FIXED - 1 + numTemps; pc = 0; + bytes = methodBytes[methodId]; + if (--icc <= 0) icc = 1000; + break; + } + case OP_ADD: { + const b = zone[sp--], a = zone[sp]; + if (typeof a === "number" && typeof b === "number") zone[sp] = a + b; + else throw Error("fail"); + break; + } + case OP_RET: { + const rv = zone[sp]; + const flags = zone[fp + F_FLAGS]; + if (flags & HASCTX_BIT) throw Error("no contexts casados en este bench"); + const sfp = zone[fp + F_SAVEDFP]; + if (sfp === 0) { result = rv; break loop; } + const numArgs = flags & 0xFFFF; + const rs = fp - 1 - numArgs; + zone[rs] = rv; + sp = rs; + pc = zone[fp + F_SAVEDPC]; + const callerMethod = zone[sfp + F_METHOD]; + bytes = methodBytes[callerMethod]; + fp = sfp; + break; + } + default: throw Error("bytecode ilegal"); + } + } + const t1 = process.hrtime.bigint(); + return { result: result, sends: sends, ns: Number(t1 - t0) }; + } + return run; +} + +module.exports = { makeContextVM, makeFrameTrampolineVM, makeFrameInterpVM }; diff --git a/perf/spikes/linmem-vs-jsobj.js b/perf/spikes/linmem-vs-jsobj.js new file mode 100644 index 00000000..080e09a1 --- /dev/null +++ b/perf/spikes/linmem-vs-jsobj.js @@ -0,0 +1,166 @@ +"use strict"; +// Spike: acceso a campos de objetos en el patrón del intérprete, 3 representaciones: +// A: objetos JS monomórficos (= default actual del VM) +// B: heap Int32Array accedido desde JS (memoria lineal SIN WASM) +// C: memoria lineal accedida desde WASM (loads/stores crudos) +// +// Decide la apuesta WASM: si C no gana claramente a A, el "impuesto de objetos +// JS" no es una palanca real y el rewrite WASM (meses) no vale. Si C >> A, sí. +// +// Loop plano que modela el trabajo de campos de un send+return: escribir 9 slots +// del contexto (method/sender/receiver/pc/sp/args/temps), leer 4 inst-vars del +// receiver + 2 args, computar, acumular. Contexto reciclado (dirección fija) = +// el caso común del free-list del VM. Las 3 reps hacen el MISMO trabajo lógico. + +var ITERS = 40000000; +var REPS = 7; + +// ---- A: objetos JS monomórficos ---- +function CtxA(){ this.method=0;this.sender=0;this.pc=0;this.sp=0;this.receiver=0;this.t0=0;this.t1=0;this.a0=0;this.a1=0; } +function RcvA(){ this.cls=0;this.iv0=0;this.iv1=0;this.iv2=0;this.iv3=0; } +function runA(iters){ + var rcvr=new RcvA(); rcvr.cls=42;rcvr.iv0=1;rcvr.iv1=2;rcvr.iv2=3;rcvr.iv3=4; + var ctx=new CtxA(); var acc=0; + for(var i=0;i>>=7;if(n)x|=0x80;b.push(x);}while(n); } + function i32(n){ // signed LEB128 + var more=1; while(more){ var x=n&0x7f; n>>=7; if((n===0&&!(x&0x40))||(n===-1&&(x&0x40)))more=0; else x|=0x80; b.push(x);} } + // opcodes + var GET=0x20,SET=0x21,CONST=0x41,ADD=0x6a,AND=0x71,GES=0x4e,STORE=0x36,LOAD=0x28, + LOOP=0x03,BLOCK=0x02,BRIF=0x0d,BR=0x0c,END=0x0b,VOID=0x40; + // memarg = align(u32) offset(u32); usamos align=2 (i32), offset=0 (dir absoluta en operando) + function store(){ b.push(STORE); u32(2); u32(0); } + function load(){ b.push(LOAD); u32(2); u32(0); } + // --- header --- + b.push(0,0x61,0x73,0x6d, 1,0,0,0); + // --- type section: () one type (i32)->i32 --- + var ts=[0x01, 0x60,0x01,0x7f,0x01,0x7f]; sect(1,ts); + // --- function section: 1 func, type 0 --- + sect(3,[0x01,0x00]); + // --- memory section: 1 mem, min 1 page --- + sect(5,[0x01,0x00,0x01]); + // --- export section: "run" func0, "mem" mem0 --- + var es=[0x02, 0x03,0x72,0x75,0x6e,0x00,0x00, 0x03,0x6d,0x65,0x6d,0x02,0x00]; sect(7,es); + // --- code section --- + // locals: i(1)=local1, acc=local2, x=local3 (param iters=local0) + var body=[]; + var save=b; b=body; + // local decls: 1 group of 3 i32 + u32(1); u32(3); b.push(0x7f); + // acc=0 + b.push(CONST); i32(0); b.push(SET); u32(2); + // i=0 + b.push(CONST); i32(0); b.push(SET); u32(1); + b.push(BLOCK,VOID); + b.push(LOOP,VOID); + // if i>=iters break: local.get i; local.get iters; ge_s; br_if 1 + b.push(GET);u32(1); b.push(GET);u32(0); b.push(GES); b.push(BRIF);u32(1); + // write ctx fields: mem[128]=7 + function wr(addr,emitVal){ b.push(CONST);i32(addr); emitVal(); store(); } + wr(128,function(){b.push(CONST);i32(7);}); + wr(132,function(){b.push(GET);u32(1);}); // sender=i + wr(136,function(){b.push(CONST);i32(0);}); + wr(140,function(){b.push(CONST);i32(0);}); + wr(144,function(){b.push(CONST);i32(32);}); // receiver=32 + wr(148,function(){b.push(CONST);i32(0);}); + wr(152,function(){b.push(CONST);i32(0);}); + wr(156,function(){b.push(GET);u32(1);}); // a0=i + wr(160,function(){b.push(GET);u32(1);b.push(CONST);i32(1);b.push(ADD);}); // a1=i+1 + // x = mem[36]+mem[40]+mem[44]+mem[48]+mem[156]+mem[160] + b.push(CONST);i32(36);load(); + b.push(CONST);i32(40);load();b.push(ADD); + b.push(CONST);i32(44);load();b.push(ADD); + b.push(CONST);i32(48);load();b.push(ADD); + b.push(CONST);i32(156);load();b.push(ADD); + b.push(CONST);i32(160);load();b.push(ADD); + b.push(SET);u32(3); + // mem[148]=x (t0) + b.push(CONST);i32(148); b.push(GET);u32(3); store(); + // acc += mem[148] & 1 + b.push(GET);u32(2); b.push(CONST);i32(148);load(); b.push(CONST);i32(1);b.push(AND); b.push(ADD); b.push(SET);u32(2); + // i++ + b.push(GET);u32(1);b.push(CONST);i32(1);b.push(ADD);b.push(SET);u32(1); + b.push(BR);u32(0); + b.push(END); // loop + b.push(END); // block + b.push(GET);u32(2); // return acc + b.push(END); // func + var code=b; b=save; + var cs=[0x01]; // 1 body + var bl=code.length; // body length + var lenBytes=[]; (function(n){do{var x=n&0x7f;n>>>=7;if(n)x|=0x80;lenBytes.push(x);}while(n);})(bl); + cs=cs.concat(lenBytes).concat(code); + sect(10,cs); + function sect(id,content){ save2=b; b=[]; u32(content.length); var lb=b; b=save2; b.push(id); for(var k=0;k + + + +SqueakJS en Web Worker — spike + + + +
arrancando…
+
main thread: —
+
+ + + — grabá abriendo un proyecto y descargá el JSON para el test +
+ + + + diff --git a/perf/worker/replay.js b/perf/worker/replay.js new file mode 100644 index 00000000..3a281e97 --- /dev/null +++ b/perf/worker/replay.js @@ -0,0 +1,65 @@ +"use strict"; +// Reproduce una grabación de eventos del worker (dialogo-worker-events.json, hecha con +// los botones de perf/worker/index.html) en el worker REAL vía puppeteer headless. +// Sirve para reproducir bugs interactivos (ej. el OOM al abrir un proyecto) y validar +// fixes sin depender de que el usuario pruebe a mano. +// +// node perf/worker/replay.js [--secs 30] [--url http://localhost:8081/perf/worker/index.html] [--shot /tmp/replay.png] [--hash grep] +// +// Reporta: sends a lo largo del tiempo, líneas de consola que matcheen /oom|memory|error| +// crash|space|missing/i, y un screenshot final. +var fs = require("fs"); + +function arg(name, def) { var i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : def; } + +var eventsPath = process.argv[2]; +if (!eventsPath || eventsPath.startsWith("--")) { console.error("uso: node replay.js [--secs N] [--url U] [--shot P]"); process.exit(1); } +var secs = parseInt(arg("--secs", "30"), 10); +var url = arg("--url", "http://localhost:8081/perf/worker/index.html"); +var shot = arg("--shot", "/tmp/worker-replay.png"); +var chrome = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; + +(async () => { + var evData = JSON.parse(fs.readFileSync(eventsPath, "utf8")); + var events = Array.isArray(evData) ? evData : evData.events; + console.log("replay:", eventsPath, "|", events.length, "eventos |", secs + "s"); + + var puppeteer = (await import("puppeteer-core")).default; + var b = await puppeteer.launch({ executablePath: chrome, headless: "new", args: ["--no-sandbox", "--use-gl=swiftshader"] }); + var p = await b.newPage(); + await p.setViewport({ width: (evData.width || 1024) + 16, height: (evData.height || 768) + 120 }); + + var log = []; + b.on("targetcreated", async t => { if (t.type() === "worker") { try { var w = await t.worker(); w.on("console", m => log.push(m.text())); } catch (e) {} } }); + p.on("console", m => log.push(m.text())); + p.on("dialog", d => d.dismiss()); + + await p.goto(url, { waitUntil: "domcontentloaded" }); + // esperar a que el worker bootee (status con sends) + await new Promise(r => setTimeout(r, 6000)); + var sendsOf = () => p.evaluate(() => { var m = (document.getElementById("status").textContent || "").match(/sends: (\d+)/); return m ? +m[1] : 0; }); + console.log("boot listo, sends:", await sendsOf()); + + // disparar el replay + var n = await p.evaluate(evs => window.__replayEvents(evs), events); + console.log("replay lanzado:", n, "eventos"); + + // muestrear sends + detectar OOM/errores mientras corre + var oomSeen = 0; + for (var s = 0; s < secs; s += 3) { + await new Promise(r => setTimeout(r, 3000)); + var sc = await sendsOf(); + var oom = log.filter(l => /out of memory|failing allocation/i.test(l)).length; + var errs = log.filter(l => /error|not defined|not a function|crash/i.test(l)).length; + console.log(" t=" + (s + 3) + "s sends=" + sc + " oom=" + oom + " errs=" + errs); + oomSeen = oom; + } + + console.log("--- consola relevante ---"); + var rel = [...new Set(log.filter(l => /oom|out of memory|failing allocation|error|not defined|not a function|crash|low space|missing primitive/i.test(l)))]; + console.log(rel.slice(0, 20).join("\n") || "(nada relevante)"); + await p.screenshot({ path: shot }); + console.log("screenshot:", shot); + await b.close(); + process.exit(oomSeen > 0 ? 2 : 0); +})().catch(e => { console.error("ERR", e.message); process.exit(1); }); diff --git a/pharo/README.md b/pharo/README.md new file mode 100644 index 00000000..53a41431 --- /dev/null +++ b/pharo/README.md @@ -0,0 +1,66 @@ +# Pharo support for SqueakJS + +Two startup scripts live here. Both use Pharo's own mechanism: name the file `startup.st`, +place it next to the `.image`, and `StartupPreferencesLoader` runs it at boot. The hosted +zip bundles ship them that way. + +State of Pharo support (see `../ESTADO.md` for the full picture): + +- **32-bit** — boots clean and is fully interactive. +- **64-bit** — boots clean and is interactive too (menus, list selection, dragging + windows), provided `startup-compat64.st` ships next to the image as `startup.st`. + +Pharo's git integration (Iceberg → libgit2 through native FFI) used to open a post-mortem +debugger over an otherwise healthy world on *both* builds. The VM now neuters +`LGitLibrary class>>startUp:` (see `hackImage` in `../vm.interpreter.js`), so Pharo runs +git-less — exactly as on a machine without libgit2. No image patching needed. + +## `startup-compat64.st` + +Restores, at image startup, the classic VM display/input classes that Pharo's **64-bit** +builds no longer ship (they render exclusively through SDL2/OSWindow via native FFI, +which does not exist in the browser): + +- `EventSensorConstants`, `InputEventHandler`, `InputEventSensor`, `InputEventFetcher` + (classic VM event polling), the classic `HandMorph` event-queue methods, +- the classic `DisplayScreen` protocol (`beDisplay`, `forceToScreen:` …), +- `VMWorldRenderer` + its update modes (rendering via Display/BitBlt), +- cursor primitives (`beCursor`, `beCursorWithMask:`), + +all extracted verbatim from the Pharo 10 32-bit build (which still ships them), then +reinstalls the UI (`UIManager default:` after clearing `MainWorldRenderer`). + +**Usage:** ships inside `Pharo64.zip`. 32-bit images don't need it. + +**Also restores `DisplayScreen>>deferUpdates:`** (instance and class side, plus its +`DeferringUpdates` class variable). The 64-bit build's own version never reaches primitive 126, +so nothing was ever batched: each BitBlt hit the screen on its own and Pharo's fading notifications +redrew ~300 times a second, reading as a flicker. With it, drawing matches 32-bit exactly — one +frame per Morphic cycle. + +**Gotcha worth keeping:** the script also has to `addSharedPool: EventSensorConstants` to +`HandMorph` *before* compiling the methods it injects there. Those methods reference pool +variables (`EventTypeMouse`, `EventKeyDown`, …); without the pool they compile as undeclared +globals — silently `nil`, since the compiler does not fail — so every event was classified +`#invalid` and the world rendered but ignored all input. Injecting methods into an existing +class does not bring along the scope they need. See `../ESTADO.md` §4.2. + +## `demo-startup.st` + +A small self-contained Morphic app, so visitors see a real Pharo application running (and +animating) in the browser rather than just the IDE. Closes the Welcome window, draws a +caption next to Pharo's desktop logo, and bounces 14 balls around the world. Core Morphic +only — no FFI, no network. The full IDE stays one menu-bar click away. + +**Usage:** publish `Pharo-demo.zip` = the contents of `Pharo.zip` plus this file renamed to +`startup.st` at the root of the zip. + +**Also restores `DisplayScreen>>deferUpdates:`** (instance and class side, plus its +`DeferringUpdates` class variable). The 64-bit build's own version never reaches primitive 126, +so nothing was ever batched: each BitBlt hit the screen on its own and Pharo's fading notifications +redrew ~300 times a second, reading as a flicker. With it, drawing matches 32-bit exactly — one +frame per Morphic cycle. + +**Gotcha worth keeping:** the animation process must run at `userSchedulingPriority - 1` and +invalidate explicitly (`m changed` per ball, then `world changed` per frame). Forking it at +`userBackgroundPriority` never got scheduled — the balls rendered once and stayed frozen. diff --git a/pharo/demo-startup.st b/pharo/demo-startup.st new file mode 100644 index 00000000..fb27019e --- /dev/null +++ b/pharo/demo-startup.st @@ -0,0 +1,50 @@ +"SqueakJS Pharo demo — opens a small self-contained Morphic app at boot so visitors +see a real Pharo application running (and animating smoothly) in the browser, not just +the IDE. Pure core Morphic: no FFI, no network. Runs from the image's working directory +via Pharo's startup.st mechanism." +[ +| title subtitle balls world | +world := World. + +"Start from a clean desktop: close the Welcome window (and any other window) so the +demo is the star. The IDE is still one menu-bar click away." +(world submorphs select: [:m | m isSystemWindow]) do: [:w | w delete]. + +"Caption, placed to the right of Pharo's own desktop logo (top-left) so both read. +Dark text on the light desktop." +title := StringMorph contents: 'running in your browser'. +title font: (LogicalFont familyName: 'Source Sans Pro' pointSize: 26). +title color: (Color r: 0.11 g: 0.15 b: 0.33). +title position: 440 @ 44. +title openInWorld. +subtitle := StringMorph contents: 'a live Morphic app — no plugins, no server, just SqueakJS.'. +subtitle font: (LogicalFont familyName: 'Source Sans Pro' pointSize: 14). +subtitle color: (Color r: 0.33 g: 0.36 b: 0.45). +subtitle position: 442 @ 82. +subtitle openInWorld. + +"A handful of bouncing balls (the classic Smalltalk 'atoms' demo)." +balls := (1 to: 14) collect: [:i | | m | + m := EllipseMorph new. + m color: (Color random alpha: 0.85). + m extent: (28 + 24 atRandom) asPoint. + m position: (60 + 500 atRandom) @ (70 + 380 atRandom). + m setProperty: #vx toValue: ((6 atRandom) - 3 + (2 atRandom)) sign * (2 + 3 atRandom). + m setProperty: #vy toValue: ((6 atRandom) - 3 + (2 atRandom)) sign * (2 + 3 atRandom). + m openInWorld. + m]. + +"Animate from a process just below the UI so it always gets scheduled between UI +cycles; each ball invalidates its own region so the World redraws it." +[[true] whileTrue: [ + balls do: [:m | | p vx vy w h | + vx := m valueOfProperty: #vx. vy := m valueOfProperty: #vy. + w := m width. h := m height. + p := m position + (vx @ vy). + (p x < 0 or: [p x > (world width - w)]) ifTrue: [vx := vx negated. m setProperty: #vx toValue: vx]. + (p y < 0 or: [p y > (world height - h)]) ifTrue: [vy := vy negated. m setProperty: #vy toValue: vy]. + m position: (p x max: 0) @ (p y max: 0). + m changed]. + world changed. + (Delay forMilliseconds: 25) wait]] forkAt: Processor userSchedulingPriority - 1. +] on: Error do: [:e | Transcript showln: 'demo failed: ', e messageText]. diff --git a/pharo/startup-compat64.st b/pharo/startup-compat64.st new file mode 100644 index 00000000..67fcc888 --- /dev/null +++ b/pharo/startup-compat64.st @@ -0,0 +1,1434 @@ +"SqueakJS compatibility: restore the classic VM display/input path that 64-bit Pharo builds dropped (they are SDL2/FFI-only). Filed in automatically at startup."! + +SharedPool subclass: #EventSensorConstants + instanceVariableNames: '' + classVariableNames: 'BlueButtonBit ButtonsBitMask CommandKeyBit CtrlKeyBit EventKeyChar EventKeyDown EventKeyUp EventTypeDragDropFiles EventTypeIndexInBuffer EventTypeKeyboard EventTypeMenu EventTypeMouse EventTypeNone EventTypeWindow KeyboardModifierIndexInEventBuffer ModifierKeysBitMask MouseButtonIndexInEventBuffer MouseModifierIndexInEventBuffer MouseXCoordinateIndexInEventBuffer MouseYCoordinateIndexInEventBuffer OptionKeyBit RedButtonBit ShiftKeyBit WindowEventActivated WindowEventClose WindowEventIconise WindowEventMetricChange WindowEventPaint YellowButtonBit' + package: 'System-VMEvents-Support'! + +!EventSensorConstants class methodsFor: 'private - initialization' stamp: ''! +initializeEventKeyConstants + "Press codes for keyboard events" + + EventKeyChar := 0. + EventKeyDown := 1. + EventKeyUp := 2.! ! + +!EventSensorConstants class methodsFor: 'class initialization' stamp: ''! +initialize + + self + initializeButtonBits; + initializeSpecialKeyBits; + initializeEventTypeConstants; + initializeEventKeyConstants; + initializeWindowEventConstants; + initializeEventIndexesConstants! ! + +!EventSensorConstants class methodsFor: 'private - initialization' stamp: ''! +initializeEventTypeConstants + "Types of events" + + EventTypeNone := 0. + EventTypeMouse := 1. + EventTypeKeyboard := 2. + EventTypeDragDropFiles := 3. + EventTypeMenu := 4. + EventTypeWindow := 5! ! + +!EventSensorConstants class methodsFor: 'private - initialization' stamp: ''! +initializeButtonBits + + RedButtonBit := 4. + BlueButtonBit := 1. + YellowButtonBit := 2. + ButtonsBitMask := RedButtonBit | BlueButtonBit | YellowButtonBit! ! + +!EventSensorConstants class methodsFor: 'private - initialization' stamp: ''! +initializeSpecialKeyBits + + ShiftKeyBit := 1. + CtrlKeyBit := 2. + OptionKeyBit := 4. + CommandKeyBit := 8. + ModifierKeysBitMask := OptionKeyBit | CommandKeyBit | CtrlKeyBit! ! + +!EventSensorConstants class methodsFor: 'private - initialization' stamp: ''! +initializeWindowEventConstants + "Window event action codes" + + WindowEventMetricChange := 1. "Size or position of window changed - value1-4 are left/top/right/bottom values" + WindowEventClose := 2. "Window close icon pressed" + WindowEventIconise := 3. "Window iconised or hidden etc." + WindowEventActivated :=4. "Window made active - some platforms only - do not rely upon this" + WindowEventPaint := 5. "Window area (in value1-4) needs updating. Some platforms do not need to send this, do not rely on it in image" ! ! + +!EventSensorConstants class methodsFor: 'private - initialization' stamp: ''! +initializeEventIndexesConstants + EventTypeIndexInBuffer := 1. + MouseModifierIndexInEventBuffer := 6. + MouseButtonIndexInEventBuffer := 5. + MouseXCoordinateIndexInEventBuffer := 3. + MouseYCoordinateIndexInEventBuffer := 4. + KeyboardModifierIndexInEventBuffer := 5! ! + +EventSensorConstants initialize! + +Object subclass: #InputEventHandler + instanceVariableNames: 'eventFetcher' + classVariableNames: '' + poolDictionaries: 'EventSensorConstants' + package: 'System-VMEvents-Base'! + +!InputEventHandler methodsFor: 'events' stamp: ''! +handleEvent: eventBuffer + self subclassResponsibility! ! + +!InputEventHandler methodsFor: 'events' stamp: ''! +flushEvents! ! + +!InputEventHandler methodsFor: 'events - testing' stamp: ''! +isKeyboardEvent: eventBuffer + + ^self class isKeyboardEvent: eventBuffer! ! + +!InputEventHandler methodsFor: 'initialization' stamp: ''! +registerIn: anEventFetcher + eventFetcher := anEventFetcher. + eventFetcher registerHandler: self! ! + +!InputEventHandler methodsFor: 'initialization' stamp: ''! +unregister + eventFetcher ifNotNil: [ + eventFetcher unregisterHandler: self. + eventFetcher := nil. ]! ! + +!InputEventHandler class methodsFor: 'events - testing' stamp: ''! +isKeyboardEvent: eventBuffer + ^ eventBuffer first = EventTypeKeyboard and: [ eventBuffer fourth = EventKeyChar ]! ! + +InputEventHandler subclass: #InputEventSensor + instanceVariableNames: 'eventQueue modifiers mouseButtons mousePosition' + classVariableNames: 'ButtonDecodeTable KeyDecodeTable' + poolDictionaries: 'EventSensorConstants' + package: 'System-VMEvents-Base'! + +!InputEventSensor methodsFor: 'events' stamp: ''! +nextEvent + "Return the next event from the receiver." + | evt | + + evt := eventQueue nextOrNil. + ^ evt ifNotNil: [ self processEvent: evt ] +! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +nextKeyboardEvent + "Allows for use of old Sensor protocol to get at the keyboard, + as when running kbdTest or the InterpreterSimulator in Morphic" + + | evtBuf | + evtBuf := eventQueue findFirst: [:buf | self isKeyboardEvent: buf]. + self flushNonKbdEvents. + ^evtBuf! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +processEvent: evt + "Process a single event. This method is run at high priority." + + | type | + type := evt at: EventTypeIndexInBuffer. + + "Treat menu events first" + type = EventTypeMenu + ifTrue: + [ "Do we still have EventTypeMenu? All the code handling this was removed from Pharo. I keep this guard just in case." ^ nil ]. + + "Tackle mouse events first" + type = EventTypeMouse + ifTrue: [ ^ self processMouseEvent: evt ]. + + + "Finally keyboard" + type = EventTypeKeyboard + ifTrue: [ ^ self processKeyboardEvent: evt ]. + + "Handle all events other than Keyboard or Mouse." + ^ evt! ! + +!InputEventSensor methodsFor: 'initialization' stamp: ''! +initialize + "Initialize the receiver" + super initialize. + eventQueue := WaitfreeQueue new. + mouseButtons := 0. + mousePosition := 0 @ 0. + modifiers := 0! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +characterForEvent: evtBuf + + | keycode | + evtBuf ifNil: [^nil]. + keycode := evtBuf sixth. + ^keycode + ifNotNil: [Unicode value: keycode]! ! + +!InputEventSensor methodsFor: 'modifier keys' stamp: ''! +controlKeyPressed + "Answer whether the control key on the keyboard is being held down." + + ^self modifiers anyMask: CtrlKeyBit! ! + +!InputEventSensor methodsFor: 'keyboard' stamp: ''! +keyboardPressed + "Answer true if keystrokes are available." + + ^self peekKeyboardEvent notNil! ! + +!InputEventSensor methodsFor: 'private' stamp: ''! +primReadJoystick: index + "Return the joystick input word for the joystick with the given index in the range [1..16]. Returns zero if the index does not correspond to a currently installed joystick." + + + ^ 0 + + ! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +queueEvent: evt + "Queue the given event in the event queue" + eventQueue nextPut: evt! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +mouseButtons + "button state; bitfield with the following entries: + 1 - yellow (e.g., right) button + 2 - blue (e.g., middle) button + 4 - red (e.g., left) button + [all other bits are currently undefined]" + + "Fetch the next event if any to update state. + Makes sure that the old polling methods consume events" + self nextEvent. + + ^mouseButtons! ! + +!InputEventSensor methodsFor: 'mouse' stamp: ''! +waitButtonOrKeyboard + "Wait for the user to press either any mouse button or any key. + Answer the current cursor location or nil if a keypress occurred." + + | delay | + delay := Delay forMilliseconds: 50. + [self anyButtonPressed] + whileFalse: [delay wait. + self keyboardPressed + ifTrue: [^ nil]]. + ^ self cursorPoint +! ! + +!InputEventSensor methodsFor: 'mouse' stamp: ''! +waitClickButton + "Wait for the user to click (press and then release) any mouse button and + then answer with the current location of the cursor." + + self waitButton. + ^self waitNoButton! ! + +!InputEventSensor methodsFor: 'events' stamp: ''! +peekEvent + "Look ahead at the next event." + + | nextEvent | + nextEvent := eventQueue peek. + ^ (nextEvent notNil + and: [ (nextEvent at: EventTypeIndexInBuffer) ~= EventTypeMenu ]) + ifTrue: [ self processEvent: nextEvent ]! ! + +!InputEventSensor methodsFor: 'mouse' stamp: ''! +waitNoButton + "Wait for the user to release any mouse button and then answer the current location of the cursor." + + | delay | + delay := Delay forMilliseconds: 50. + [self anyButtonPressed] whileTrue: [ delay wait]. + ^self cursorPoint +! ! + +!InputEventSensor methodsFor: 'events' stamp: ''! +someEventInQueue + ^eventQueue isEmpty not! ! + +!InputEventSensor methodsFor: 'mouse' stamp: ''! +noButtonPressed + "Answer whether any mouse button is not being pressed." + + ^self anyButtonPressed not +! ! + +!InputEventSensor methodsFor: 'joystick' stamp: ''! +joystickOn: index + + ^ (((self primReadJoystick: index) bitShift: -27) bitAnd: 1) ~= 0 + ! ! + +!InputEventSensor methodsFor: 'joystick' stamp: ''! +joystickButtons: index + + ^ ((self primReadJoystick: index) bitShift: -22) bitAnd: 16r71F + ! ! + +!InputEventSensor methodsFor: 'modifier keys' stamp: ''! +shiftPressed + "Answer whether the shift key on the keyboard is being held down." + + ^self modifiers anyMask: ShiftKeyBit +! ! + +!InputEventSensor methodsFor: 'keyboard' stamp: ''! +flushKeyboard + "Remove all characters from the keyboard buffer." + + [self keyboardPressed] + whileTrue: [self keyboard]! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +flushNonKbdEvents + eventQueue ifNil: [^ self]. + eventQueue flushAllSuchThat: + [:buf | (self isKeyboardEvent: buf) not]! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +processKeyboardEvent: evt + modifiers := evt at: KeyboardModifierIndexInEventBuffer. "Update state for polling calls" + ^ evt! ! + +!InputEventSensor methodsFor: 'private' stamp: ''! +primTabletRead: cursorIndex + "Answer the pen tablet data for the cursor having the given index. Answer nil if there is no pen tablet. The data is: + 1. index of the cursor to which this data applies + 2. timestamp of the last state chance for this cursor + 3., 4., and 5. x, y, and z coordinates of the cursor (z is typically 0) + 6. and 7. xTilt and yTilt of the cursor; (signed) + 8. type of cursor (0 = unknown, 1 = pen, 2 = puck, 3 = eraser) + 9. cursor buttons + 10. cursor pressure, downward + 11. cursor pressure, tangential + 12. flags" + + + self primitiveFailed +! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +peekKeyboardEvent + "Allows for use of old Sensor protocol to get at the keyboard, + as when running kbdTest or the InterpreterSimulator in Morphic" + + ^eventQueue findFirst: [:buf | self isKeyboardEvent: buf]! ! + +!InputEventSensor methodsFor: 'private' stamp: ''! +primTabletGetParameters: cursorIndex + "Answer the pen tablet parameters. For parameters that differ from cursor to cursor, answer those associated with the cursor having the given index. Answer nil if there is no pen tablet. The parameters are: + 1. tablet width, in tablet units + 2. tablet height, in tablet units + 3. number of tablet units per inch + 4. number of cursors (pens, pucks, etc; some tablets have more than one) + 5. this cursor index + 6. and 7. x scale and x offset for scaling tablet coordinates (e.g., to fit the screen) + 8. and 9. y scale and y offset for scaling tablet coordinates (e.g., to fit the screen) + 10. number of pressure levels + 11. presure threshold needed close pen tip switch + 12. number of pen tilt angles" + + + ^ nil +! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +modifiers + "modifier keys; bitfield with the following entries: + 1 - shift key + 2 - ctrl key + 4 - (Mac specific) option key + 8 - Cmd/Alt key" + + "Fetch the next event if any to update state. + Makes sure that the old polling methods consume events" + +" self nextEvent." + + + ^modifiers! ! + +!InputEventSensor methodsFor: 'modifier keys' stamp: ''! +commandKeyPressed + "Answer whether the command key on the keyboard is being held down." + + ^self modifiers anyMask: CommandKeyBit! ! + +!InputEventSensor methodsFor: 'mouse' stamp: ''! +waitButton + "Wait for the user to press any mouse button and then answer with the + current location of the cursor." + + | delay | + delay := Delay forMilliseconds: 50. + [self anyButtonPressed] whileFalse: [ delay wait ]. + ^self cursorPoint +! ! + +!InputEventSensor methodsFor: 'tablet' stamp: ''! +tabletPressure + "Answer the current pressure of the first tablet pointing device (pen, puck, or eraser), a number between 0.0 (no pressure) and 1.0 (max pressure)" + + | params data | + params := self primTabletGetParameters: 1. + params ifNil: [^ self]. + data := self primTabletRead: 1. "state of first/primary pen" + ^ (data at: 10) asFloat / ((params at: 10) - 1) +! ! + +!InputEventSensor methodsFor: 'tablet' stamp: ''! +hasTablet + "Answer true if there is a pen tablet available on this computer." + + ^ (self primTabletGetParameters: 1) notNil +! ! + +!InputEventSensor methodsFor: 'tablet' stamp: ''! +tabletPoint + "Answer the current position of the first tablet pointing device (pen, puck, or eraser) in tablet coordinates." + + | data | + data := self primTabletRead: 1. "state of first/primary pen" + ^ (data at: 3) @ (data at: 4) +! ! + +!InputEventSensor methodsFor: 'mouse' stamp: ''! +redButtonPressed + "Answer true if only the red mouse button is being pressed. + This is the first mouse button, usually the left one." + + ^(self mouseButtons bitAnd: ButtonsBitMask) = RedButtonBit +! ! + +!InputEventSensor methodsFor: 'events' stamp: ''! +handleEvent: evt + + self queueEvent: evt shallowCopy! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +mapButtons: buttons modifiers: anInteger + "Map the buttons to yellow or blue based on the given modifiers. + If only the red button is pressed, then map + Ctrl-RedButton -> YellowButtonBit. + Cmd-RedButton -> BlueButtonBit. + " + + buttons = RedButtonBit + ifFalse: [ ^ buttons ]. + (anInteger allMask: CtrlKeyBit) + ifTrue: [ ^ YellowButtonBit ]. + (anInteger allMask: CommandKeyBit) + ifTrue: [ ^ BlueButtonBit ]. + ^ buttons! ! + +!InputEventSensor methodsFor: 'joystick' stamp: ''! +joystickXY: index + + | inputWord x y | + inputWord := self primReadJoystick: index. + x := (inputWord bitAnd: 16r7FF) - 16r400. + y := ((inputWord bitShift: -11) bitAnd: 16r7FF) - 16r400. + ^ x@y + ! ! + +!InputEventSensor methodsFor: 'tablet' stamp: ''! +tabletTimestamp + "Answer the time (in tablet clock ticks) at which the tablet's primary pen last changed state. This can be used in polling loops; if this timestamp hasn't changed, then the pen state hasn't changed either." + + | data | + data := self primTabletRead: 1. "state of first/primary pen" + ^ data at: 2 +! ! + +!InputEventSensor methodsFor: 'keyboard' stamp: ''! +keyboardPeek + "Answer the next character in the keyboard buffer without removing it, or nil if it is empty." + + ^ self characterForEvent: self peekKeyboardEvent! ! + +!InputEventSensor methodsFor: 'events' stamp: ''! +flushAllButDandDEvents! ! + +!InputEventSensor methodsFor: 'mouse' stamp: ''! +blueButtonPressed + "Answer whether only the blue mouse button is being pressed. + This is the third mouse button or cmd+click on the Mac." + + ^(self mouseButtons bitAnd: ButtonsBitMask) = BlueButtonBit +! ! + +!InputEventSensor methodsFor: 'mouse' stamp: ''! +yellowButtonPressed + "Answer whether only the yellow mouse button is being pressed. + This is the second mouse button or option+click on the Mac." + + ^(self mouseButtons bitAnd: ButtonsBitMask) = YellowButtonBit +! ! + +!InputEventSensor methodsFor: 'private - events' stamp: ''! +processMouseEvent: evt + "Transmogrify the button state according to the platform's button map definition" + evt + at: MouseButtonIndexInEventBuffer + put: (ButtonDecodeTable at: (evt at: MouseButtonIndexInEventBuffer) + 1). + "Map the mouse buttons depending on modifiers" + evt + at: MouseButtonIndexInEventBuffer + put: + (self + mapButtons: (evt at: MouseButtonIndexInEventBuffer) + modifiers: (evt at: MouseModifierIndexInEventBuffer)). + + "Update state for polling calls" + mousePosition := (evt at: MouseXCoordinateIndexInEventBuffer) @ (evt at: MouseYCoordinateIndexInEventBuffer). + modifiers := evt at: MouseModifierIndexInEventBuffer. + mouseButtons := evt at: MouseButtonIndexInEventBuffer. + ^ evt! ! + +!InputEventSensor methodsFor: 'events' stamp: ''! +flushEvents + eventQueue ifNotNil:[eventQueue flush]! ! + +!InputEventSensor methodsFor: 'system startup' stamp: ''! +startUp + self initialize! ! + +!InputEventSensor methodsFor: 'mouse' stamp: ''! +anyButtonPressed + "Answer whether at least one mouse button is currently being pressed. + This can be checked by looking if any of the 3 bits representing the 3 mouse buttons states is set to 1." + + ^self mouseButtons anyMask: ButtonsBitMask! ! + +!InputEventSensor methodsFor: 'tablet' stamp: ''! +tabletExtent + "Answer the full tablet extent in tablet coordinates." + + | params | + params := self primTabletGetParameters: 1. + params ifNil: [^ self error: 'no tablet available']. + ^ (params at: 1)@(params at: 2) +! ! + +!InputEventSensor methodsFor: 'modifier keys' stamp: ''! +anyModifierKeyPressed + "Answer whether at least one modifier key is currently being pressed. + This can be checked by looking if any of the 3 bits representing the state of cmd, opt and ctrl modifiers is set to 1." + + ^self modifiers anyMask: ModifierKeysBitMask! ! + +!InputEventSensor methodsFor: 'cursor' stamp: ''! +cursorPoint + "Answer a Point indicating the cursor location." + + "Fetch the next event if any to update state. + Makes sure that the old polling methods consume events + self nextEvent." + + ^ mousePosition! ! + +!InputEventSensor methodsFor: 'keyboard' stamp: ''! +keyboard + "Answer the next character from the keyboard." + + ^self characterForEvent: self nextKeyboardEvent! ! + +!InputEventSensor methodsFor: 'system startup' stamp: ''! +shutDown! ! + +!InputEventSensor class methodsFor: 'class initialization' stamp: ''! +installMouseDecodeTable + "Create a decode table that swaps the lowest-order 2 bits" + ButtonDecodeTable := self swapMouseButtons + ifTrue: [ByteArray withAll: + ((0 to: 255) collect: [:i | + ((i bitAnd: 1) << 1 + bitOr: (i bitAnd: 2) >> 1) + bitOr: (i bitAnd: 16rFC) ])] + ifFalse: [ByteArray withAll: (0 to: 255)]! ! + +!InputEventSensor class methodsFor: 'class initialization' stamp: ''! +installEventSensorFramework: fetcherClass + "Installs the new sensor framework." + + | newSensor | + "Do some extra cleanup" + SessionManager default unregisterClassNamed: #InputEventFetcher. + InputEventFetcher deinstall. + + newSensor := self new. + fetcherClass install. + newSensor registerIn: InputEventFetcher default. + + "Shut down old sensor" + Sensor shutDown. + SessionManager default unregisterClassNamed: Sensor class name. + + "Note: We must use #become: here to replace all references to the old sensor with the new one, since Sensor is referenced from all the existing controllers." + Sensor becomeForward: newSensor. "done" + + "Register the interrupt handler" + UserInterruptHandler new registerIn: InputEventFetcher default. + + SessionManager default + registerGuiClassNamed: Sensor class name + atPriority: 20. + "Project spawnNewProcessAndTerminateOld: true"! ! + +!InputEventSensor class methodsFor: 'public' stamp: ''! +default + "Answer the default system InputEventSensor, Sensor." + + ^ Sensor! ! + +!InputEventSensor class methodsFor: 'class initialization' stamp: ''! +defaultCrossPlatformKeys + "Answer a list of key letters that are used for common editing operations + on different platforms." + ^{ $c . $x . $v . $a . $s . $f . $g . $z } +! ! + +!InputEventSensor class methodsFor: 'settings' stamp: ''! +swapMouseButtons + ^ Smalltalk os isWindows not! ! + +!InputEventSensor class methodsFor: 'class initialization' stamp: ''! +installEventSensorFramework + "Installs the new sensor framework." + "InputEventSensor installEventSensorFramework" + + self installEventSensorFramework: InputEventFetcher! ! + +Object subclass: #InputEventFetcher + instanceVariableNames: 'eventHandlers fetcherProcess inputSemaphore' + classVariableNames: 'Default' + poolDictionaries: 'EventSensorConstants' + package: 'System-VMEvents-Base'! + +!InputEventFetcher methodsFor: 'initialization' stamp: ''! +terminateEventLoop + "Terminate the event loop process. Terminate the old process if any." + "InputEventFetcher default terminateEventLoop" + + fetcherProcess ifNotNil: + [ fetcherProcess terminate. + fetcherProcess := nil ]! ! + +!InputEventFetcher methodsFor: 'handlers' stamp: ''! +unregisterHandler: handler + self eventHandlers remove: handler ifAbsent: []! ! + +!InputEventFetcher methodsFor: 'initialization' stamp: ''! +startUp + inputSemaphore := Semaphore new. + self primSetInputSemaphore: (Smalltalk registerExternalObject: inputSemaphore). + inputSemaphore consumeAllSignals. + self installEventLoop! ! + +!InputEventFetcher methodsFor: 'private - events' stamp: ''! +primInterruptSemaphore: aSemaphore + "Primitive. Install the argument as the semaphore to be signalled whenever the user presses the interrupt key. The semaphore will be signaled once each time the interrupt key is pressed." + + + ^self primitiveFailed +"Note: This primitive was marked obsolete but is still used and actually quite useful. It could bre replace with a check in the event loop though, without a need for the now obsolete event tickler as event fetching isn't bound to the Morphic loop."! ! + +!InputEventFetcher methodsFor: 'events' stamp: ''! +waitForInput + inputSemaphore wait.! ! + +!InputEventFetcher methodsFor: 'accessing' stamp: ''! +fetcherProcess + ^ fetcherProcess ! ! + +!InputEventFetcher methodsFor: 'private - events' stamp: ''! +primSetInputSemaphore: semaIndex + "Set the input semaphore the VM should use for asynchronously signaling the availability of events. Primitive. Optional." + + ^nil! ! + +!InputEventFetcher methodsFor: 'events' stamp: ''! +eventLoop + "Fetch pending raw events from the VM. + This method is run at high priority." + | eventBuffer | + + eventBuffer := Array new: 8. + + [true] whileTrue: [ + | type window | + self waitForInput. + + [self primGetNextEvent: eventBuffer. + type := eventBuffer at: 1. + type = EventTypeNone] + whileFalse: [ + "Patch up the window index in case we don't get one" + window := eventBuffer at: 8. + (window isNil + or: [window isZero]) + ifTrue: [eventBuffer at: 8 put: 1]. + + self signalEvent: eventBuffer]]! ! + +!InputEventFetcher methodsFor: 'private' stamp: ''! +eventHandlers + ^eventHandlers ifNil: [eventHandlers := OrderedCollection new]! ! + +!InputEventFetcher methodsFor: 'handlers' stamp: ''! +registerHandler: handler + self eventHandlers add: handler! ! + +!InputEventFetcher methodsFor: 'initialization' stamp: ''! +shutDown + self terminateEventLoop. + inputSemaphore ifNotNil: [Smalltalk unregisterExternalObject: inputSemaphore]! ! + +!InputEventFetcher methodsFor: 'events' stamp: ''! +signalEvent: eventBuffer + "Signal the event buffer to all registered event handlers. + Handlers need make sure to copy the buffer or extract the data otherwise, as the buffer will be reused." + + self eventHandlers do: [:handler | + handler handleEvent: eventBuffer]! ! + +!InputEventFetcher methodsFor: 'initialize-release' stamp: ''! +installEventLoop + "Initialize the event loop process. Terminate the old process if any." + "InputEventFetcher default installEventLoop" + + self terminateEventLoop. + fetcherProcess := [self eventLoop] forkAt: Processor lowIOPriority. + fetcherProcess name: 'Input Event Fetcher Process'! ! + +!InputEventFetcher methodsFor: 'private' stamp: ''! +primGetNextEvent: array + "Store the next OS event available into the provided array. + Essential." + + array at: 1 put: EventTypeNone. + ^nil + ! ! + +!InputEventFetcher class methodsFor: 'accessing' stamp: ''! +default + "InputEventFetcher default" + + ^Default ifNil: [Default := InputEventFetcher new]! ! + +!InputEventFetcher class methodsFor: 'class initialization' stamp: ''! +install + "InputEventFetcher install" + Default := self new. + Default startUp.! ! + +!InputEventFetcher class methodsFor: 'class initialization' stamp: ''! +deinstall + "InputEventFetcher deinstall" + + Default + ifNotNil: [ + Default shutDown. + Default := nil]. +! ! + +"The HandMorph methods below use EventSensorConstants pool variables (EventTypeMouse, +EventTypeKeyboard, EventKeyDown, ...). The 32-bit build declares that pool in HandMorph; +the 64-bit build dropped it along with the classic event classes. Without it those names +compile as undeclared globals -- silently nil -- so every `type = EventTypeMouse` test is +false, nextEventFrom: falls through to ^ #invalid, and processEventsFromQueue: returns +before ever calling handleEvent:. The world then renders but ignores every click, which +is exactly the 'opens but does not respond' symptom. Add the pool BEFORE compiling them." +(HandMorph sharedPools includes: EventSensorConstants) + ifFalse: [ HandMorph addSharedPool: EventSensorConstants ]! + +!HandMorph methodsFor: 'squeakjs-compat'! +eventQueue + ^ Sensor! ! + +!HandMorph methodsFor: 'squeakjs-compat'! +generateDropFilesEvent: evtBuf + "Generate the appropriate mouse event for the given raw event buffer" + + "Note: This is still in an experimental phase and will need more work" + + | position buttons modifiers stamp numFiles dragType | + stamp := evtBuf second. + stamp = 0 ifTrue: [stamp := Time millisecondClockValue]. + dragType := evtBuf third. + position := evtBuf fourth @ evtBuf fifth. + buttons := 0. + modifiers := evtBuf sixth. + buttons := buttons bitOr: (modifiers bitShift: 3). + numFiles := evtBuf seventh. + dragType = 4 + ifTrue: + ["e.g., drop" + + owner borderWidth: 0. + ^DropFilesEvent new + setPosition: position + contents: numFiles + hand: self]. + "the others are currently not handled by morphs themselves" + dragType = 1 + ifTrue: + ["experimental drag enter" + + owner + borderWidth: 4; + borderColor: owner color asColor negated]. + dragType = 2 + ifTrue: + ["experimental drag move" + + ]. + dragType = 3 + ifTrue: + ["experimental drag leave" + + owner borderWidth: 0]. + ^nil! ! + +!HandMorph methodsFor: 'squeakjs-compat'! +generateKeyboardEvent: evtBuf + "Generate the appropriate mouse event for the given raw event buffer" + + | buttons modifiers type pressType stamp charCode keyValue keyEvent | + stamp := evtBuf second. + stamp = 0 ifTrue: [stamp := Time millisecondClockValue]. + pressType := evtBuf fourth. + pressType = EventKeyDown + ifTrue: [ + type := #keyDown. + lastKeyScanCode := evtBuf third]. + pressType = EventKeyUp ifTrue: [type := #keyUp]. + pressType = EventKeyChar ifTrue: [ + type := #keystroke]. + modifiers := evtBuf fifth. + buttons := modifiers bitShift: 3. + keyValue := evtBuf third. + charCode := evtBuf sixth. + + "This piece of code handles the creation of scrolling events. When a scroll is done by the user, the VM forwards a keystroke event with the up/down key. So we reconvert it to a MouseWheelEvent in that case." + (type = #keystroke + and: [(buttons anyMask: 16) + and: [{Character arrowUp. Character arrowDown} includes: (charCode asCharacter)]]) + ifTrue: [^ MouseWheelEvent + fromCharacter: charCode asCharacter + position: lastMouseEvent cursorPoint + buttons: buttons + hand: self + stamp: stamp]. + + "All control keys (tab, cr...) should be managed as keydown and ignored as text" + (type = #keystroke and: [ + charCode <= 31 or: [ (127 to: 159) includes: charCode ]]) + ifTrue: [ ^ #invalid ]. + + "If charCode is not single-byte, we definately have Unicode input. Zero keyValue to avoid garbage values from som VMs." + self flag: #pharoFixMe. "I need to look at the windows vm side, to see why this keyvalue is generated in the first place". + + (charCode notNil and:[charCode > 255]) ifTrue: [keyValue:=0]. + keyEvent := KeyboardEvent new + setType: type + buttons: buttons + position: self position + keyValue: keyValue + charCode: (type == #keystroke + ifTrue: [ charCode ] + ifFalse: [ self mapSymbolToKeyValue: keyValue ]) + hand: self + stamp: stamp. + + "If a modifier key is pressed the keystroke event is handled by #visitMouseDownEvent:" + (type == #keystroke + and: [ keyEvent commandKeyPressed + or: [ keyEvent controlKeyPressed ] ]) + ifTrue: [ ^ #invalid ]. + + keyEvent scanCode: lastKeyScanCode. + ^keyEvent! ! + +!HandMorph methodsFor: 'squeakjs-compat'! +generateMouseEvent: evtBuf + "Generate the appropriate mouse event for the given raw event buffer" + + | position buttons modifiers type trail stamp oldButtons evtChanged | + self class environment at: #KMBuffer ifPresent: [ :kmBuffer | kmBuffer uniqueInstance clearBuffer]. + evtBuf first = lastEventBuffer first + ifTrue: + ["Workaround for Mac VM bug, *always* generating 3 events on clicks" + + evtChanged := false. + 3 to: evtBuf size + do: [:i | (lastEventBuffer at: i) = (evtBuf at: i) ifFalse: [evtChanged := true]]. + evtChanged ifFalse: [^nil]]. + stamp := evtBuf second. + stamp = 0 ifTrue: [stamp := Time millisecondClockValue]. + position := evtBuf third @ evtBuf fourth. + buttons := evtBuf fifth. + modifiers := evtBuf sixth. + type := buttons = 0 + ifTrue: + [lastEventBuffer fifth = 0 ifTrue: [#mouseMove] ifFalse: [#mouseUp]] + ifFalse: + [lastEventBuffer fifth = 0 + ifTrue: [#mouseDown] + ifFalse: [#mouseMove]]. + buttons := buttons bitOr: (modifiers bitShift: 3). + oldButtons := lastEventBuffer fifth + bitOr: (lastEventBuffer sixth bitShift: 3). + lastEventBuffer := evtBuf. + type == #mouseMove + ifTrue: + [trail := self mouseTrailFrom: evtBuf. + ^MouseMoveEvent basicNew + setType: type + startPoint: (self position) + endPoint: trail last + trail: trail + buttons: buttons + hand: self + stamp: stamp]. + ^MouseButtonEvent basicNew + setType: type + position: position + which: (oldButtons bitXor: buttons) + buttons: buttons + hand: self + stamp: stamp! ! + +!HandMorph methodsFor: 'squeakjs-compat'! +generateWindowEvent: evtBuf + "Generate the appropriate window event for the given raw event buffer" + + | evt | + evt := WindowEvent new. + evt setTimeStamp: evtBuf second. + evt timeStamp = 0 ifTrue: [evt setTimeStamp: Time millisecondClockValue]. + evt action: evtBuf third. + evt rectangle: (Rectangle origin: evtBuf fourth @ evtBuf fifth corner: evtBuf sixth @ evtBuf seventh ). + + ^evt! ! + +!HandMorph methodsFor: 'squeakjs-compat'! +mouseTrailFrom: currentBuf + "Current event, a mouse event buffer, is about to be processed. If there are other similar mouse events queued up, then drop them from the queue, and report the positions inbetween." + + | nextEvent trail | + trail := (Array new: 1) writeStream. + trail nextPut: currentBuf third @ currentBuf fourth. + [(nextEvent := Sensor peekEvent) isNil] whileFalse: + [nextEvent first = currentBuf first + ifFalse: [^trail contents "different event type"]. + nextEvent fifth = currentBuf fifth + ifFalse: [^trail contents "buttons changed"]. + nextEvent sixth = currentBuf sixth + ifFalse: [^trail contents "modifiers changed"]. + "nextEvent is similar. Remove it from the queue, and check the next." + nextEvent := Sensor nextEvent. + nextEvent + ifNotNil: [ trail nextPut: nextEvent third @ nextEvent fourth ]]. + ^trail contents! ! + +!HandMorph methodsFor: 'squeakjs-compat'! +nextEventFrom: anEventBufferQueue + "Returns the next morphic event to handle. + Three cases may arise: + - If possible, a valid morphic event is returned + - If no more events are available, nil is returned + - If we find an invalid event in the queue, we consume it and return the #invalid symbol + + The first morphic events available are those in the pending queue. + An event handling cycle should generate and manage a single event. + In the case a cycle needs to generate several events (see MouseClickState), only one event is handled. + Remaining events are queued for later processing. + " + | eventBuffer morphicEvent type pendingMorphicEvent | + + pendingMorphicEvent := self pendingEventQueue nextOrNil. + pendingMorphicEvent ifNotNil: [ ^ pendingMorphicEvent ]. + + anEventBufferQueue ifNil: [ ^ nil ]. + eventBuffer := anEventBufferQueue nextEvent. + eventBuffer ifNil: [ ^ nil ]. + + morphicEvent := nil. "for unknown event types" + type := eventBuffer first. + type = EventTypeMouse ifTrue: [ self world beCursorOwner. + recentModifiers := eventBuffer sixth. + morphicEvent := self generateMouseEvent: eventBuffer + ]. + type = EventTypeKeyboard ifTrue: [ + recentModifiers := eventBuffer fifth. + morphicEvent := self generateKeyboardEvent: eventBuffer + ]. + type = EventTypeDragDropFiles ifTrue: [ + morphicEvent := self generateDropFilesEvent: eventBuffer ]. + type = EventTypeWindow ifTrue: [ + morphicEvent := self generateWindowEvent: eventBuffer ]. + + "If the event did not match any of the categories above, it is an invalid event" + ( type ~= EventTypeDragDropFiles and: [ morphicEvent isNil ] ) + ifTrue: [ ^ #invalid ]. + ^ morphicEvent! ! + +!HandMorph methodsFor: 'squeakjs-compat'! +processEventsFromQueue: anEventBufferQueue + + "Process user input events from the local input devices." + + | evt | + + [ (evt := self nextEventFrom: anEventBufferQueue) notNil ] whileTrue: [ + evt == #invalid ifTrue: [ ^ self ]. + evt ifNotNil: [ "Finally, handle it" + self handleEvent: evt. + "For better user feedback, return immediately after a mouse event has been processed." + ( evt isMouse and: [ evt isMouseWheel not ] ) + ifTrue: [ ^ self ] + ] + ]. + + "note: if we come here we didn't have any mouse events" + mouseClickState notNil ifTrue: [ + "No mouse events during this cycle. Make sure click states time out accordingly" + mouseClickState handleEvent: lastMouseEvent asMouseMove from: self ].! ! + +!HandMorph methodsFor: 'squeakjs-compat'! +processEvents + ^ self processEventsFromQueue: self eventQueue! ! + +"--- DisplayScreen classic protocol (dropped from 64-bit builds) ---"! + +!DisplayScreen methodsFor: 'private' stamp: ''! +beDisplay + "Primitive. Tell the interpreter to use the receiver as the current display + image. Fail if the form is too wide to fit on the physical display. + Essential. See Object documentation whatIsAPrimitive." + + + self primitiveFailed! ! + +!DisplayScreen methodsFor: 'other' stamp: ''! +primitiveDeferUpdates: aBoolean + "Set the deferUpdates flag in the virtual machine. When this flag is true, BitBlt operations on the Display are not automatically propagated to the screen. If this underlying platform does not support deferred updates, this primitive will fail. Answer the receiver if the primitive succeeds, nil if it fails." + + + ^ nil "answer nil if primitive fails" +! ! + +!DisplayScreen methodsFor: 'displaying' stamp: ''! +forceDamageToScreen: allDamage + "Force all the damage rects to the screen." + | regions rectList | + rectList := allDamage. + "Note: Reset extra regions at the beginning to prevent repeated errors" + regions := extraRegions. + extraRegions := nil. + regions ifNotNil:[ + "exclude extra regions" + regions do:[:drawerAndRect| | excluded remaining | + excluded := drawerAndRect at: 2. + remaining := Array new writeStream. + rectList do:[:r| + remaining nextPutAll:(r areasOutside: excluded)]. + rectList := remaining contents]. + ]. + rectList do:[:r| self forceToScreen: r]. + regions ifNotNil:[ + "Have the drawers paint what is needed" + regions do:[:drawerAndRect| (drawerAndRect at: 1) forceToScreen]. + ].! ! + +!DisplayScreen methodsFor: 'private' stamp: ''! +primShowRectLeft: l right: r top: t bottom: b + "Copy the given rectangular section of the Display to to the screen. This primitive is not implemented on all platforms. If this fails, retry integer coordinates." + + + "if this fails, coerce coordinates to integers and try again" + self primRetryShowRectLeft: l truncated + right: r rounded + top: t truncated + bottom: b rounded. +! ! + +!DisplayScreen methodsFor: 'other' stamp: ''! +forceToScreen + "Force the entire display area to the screen" + ^self forceToScreen: self boundingBox! ! + +!DisplayScreen methodsFor: 'private' stamp: ''! +primRetryShowRectLeft: l right: r top: t bottom: b + "Copy the given rectangular section of the Display to to the screen. This primitive is not implemented on all platforms. Do nothing if it fails. " + + + "do nothing if primitive fails" +! ! + +!DisplayScreen methodsFor: 'other' stamp: ''! +forceDisplayUpdate + "On platforms that buffer screen updates, force the screen to be updated immediately. On other platforms, or if the primitive is not implemented, do nothing." + + + "do nothing if primitive fails"! ! + +!DisplayScreen methodsFor: 'other' stamp: ''! +forceToScreen: aRectangle + "Force the given rectangular section of the Display to be copied to the screen. The primitive call does nothing if the primitive is not implemented. Typically used when the deferUpdates flag in the virtual machine is on; see deferUpdates:." + + self primShowRectLeft: aRectangle left + right: aRectangle right + top: aRectangle top + bottom: aRectangle bottom. +! ! + +!DisplayScreen class methodsFor: 'host window access' stamp: ''! +primitiveWindowSize: id width: width heigth: height + + "ignore failure"! ! + +"deferUpdates: is what actually switches the VM into batched (deferred) drawing. The 64-bit +build's version never reaches primitive 126, so every single BitBlt was pushed straight to +the screen: a fading growl notification redrew ~300 times per second and looked like it was +flickering / reopening, instead of fading in one frame per Morphic cycle (32-bit does 1 draw +per cycle). Restore the 32-bit implementation. DeferringUpdates is a class variable there; +add it first, since compiling against a name that does not exist would silently bind it to +an undeclared nil global." +[ DisplayScreen addClassVarNamed: #DeferringUpdates ] on: Error do: [ :e | ]! + +!DisplayScreen methodsFor: 'deferring' stamp: ''! +deferUpdates: aBoolean + ^ self class deferUpdates: aBoolean! ! + +!DisplayScreen class methodsFor: 'deferring' stamp: ''! +deferUpdates: aBoolean + "Set the deferUpdates flag in the virtual machine. When this flag is true, BitBlt operations on the Display are not automatically propagated to the screen. If this underlying platform does not support deferred updates, this primitive will fail. Answer whether updates were deferred before if the primitive succeeds, nil if it fails." + | wasDeferred | + wasDeferred := DeferringUpdates == true. + DeferringUpdates := aBoolean. + ^ (self primitiveDeferUpdates: aBoolean) ifNotNil: [ wasDeferred ]! ! + +!DisplayScreen class methodsFor: 'deferring' stamp: ''! +primitiveDeferUpdates: aBoolean + "Set the deferUpdates flag in the virtual machine. When this flag is true, BitBlt operations on the Display are not automatically propagated to the screen. If this underlying platform does not support deferred updates, this primitive will fail. Answer the receiver if the primitive succeeds, nil if it fails." + + + ^ nil "answer nil if primitive fails"! ! + +!DisplayScreen class methodsFor: 'host window access' stamp: ''! +primitiveWindowTitle: id string: titleString + + "ignore failure"! ! + +!DisplayScreen class methodsFor: 'display box access' stamp: ''! +depth: depthInteger width: widthInteger height: heightInteger fullscreen: aBoolean + "Force Pharo's window (if there's one) into a new size and depth." + "DisplayScreen depth: 8 width: 1024 height: 768 fullscreen: false" + + + self primitiveFailed ! ! + +!DisplayScreen class methodsFor: 'host window access' stamp: ''! +refreshHostWindowTitle + Title ifNotNil: [ self hostWindowTitle: Title ]! ! + +!DisplayScreen class methodsFor: 'host window access' stamp: ''! +primitiveWindowIcon: id path: pathString + + (pathString isKindOf: AbstractFileReference) ifTrue: [ ^ self primitiveWindowIcon: id path: pathString fullName ]. + ec == #'bad argument' + ifTrue: [ pathString isString ifFalse: [ ^ self error: 'The path should be a String' ]. + ^ pathString asFileReference + ifExists: [ self error: 'File existing but not found by the VM sorry, try another location...' "Not reproductible in the tests but can happen" ] + ifAbsent: [ :file | FileDoesNotExistException signalOnFile: file ] ]. + self primitiveFailed! ! + +!DisplayScreen class methodsFor: 'host window access' stamp: ''! +hostWindowIndex + ^ 1! ! + +!DisplayScreen class methodsFor: 'host window access' stamp: ''! +resetTitle +