Event Loop — Spec-Compliant Microtask/Macrotask Drain + RAF/Idle #98

Closed
opened 2026-06-19 12:09:49 +00:00 by Artur · 0 comments
Owner

Issue #98 — Event Loop — Spec-Compliant Microtask/Macrotask Drain

Problembeschreibung

Der Browser hat keinen spec-konformen Event Loop. Aktuell gibt es:

  • SimpleRAF (src/fakes/simple-raf.ts) — simuliert requestAnimationFrame via setInterval
  • Microtask-Queue existiert nicht explizit (Bun/Babel-handled)
  • Kein queueMicrotask() Promise-Verhalten (kein await-Drain)
  • Macrotask-Order: setTimeout, setInterval, setImmediate ohne Priorisierung
  • IdleCallbacks (requestIdleCallback) fehlen

Warum das blockiert:

  • AWS WAF Challenge: Async-Generator-Drain in _drainAsyncGenerators() pollt alle 50ms — kein spec-konformer Microtask-Drain
  • Moderne Frameworks: React 18+ Concurrency, Vue Async-Scheduler, Svelte tick() hängen von korrekter Microtask-Order ab
  • CMPs (Consent Management): Nutzen requestIdleCallback für non-critical Scripts
  • Performance-Timing: DOMContentLoaded, load Event-Timing ist inkorrekt

Architektur-Analyse

Aktueller Stand

Script modifies DOM → Happy DOM dispatches MutationObserver
                    → SimpleRAF.tick() (alle 16ms im setInterval)
                    → Stabilizer pollt alle 50ms
                    → Kein expliziter Microtask-Drain

Stabilizer:

async stabilize(doc, scriptLoader) {
    while (!this.isStable()) {
        await new Promise(r => setTimeout(r, 20));
        // pollt: scriptLoader.isEmpty(), keine pending fetches
    }
}

Das ist nicht spec-konform. Der Spec sagt:

flowchart TD
    A[Task Queue] --> B[Select oldest task]
    B --> C[Run task]
    C --> D[Microtask Queue]
    D --> E[Run all microtasks]
    E --> F{RAF Callbacks?}
    F -->|Yes| G[Fire Animation Frames]
    F -->|No| H[Render Opportunities]
    H --> I[Idle Callbacks?]
    I -->|Yes| J[Fire Idle Callbacks]
    I -->|No| K[Next Event Loop Iteration]
    J --> K

Lösungsansätze

Option A — Full Spec Event Loop (Empfohlen)

Eigener EventLoop-Klasse mit:

src/event-loop/
├── event-loop.ts      (Hauptklasse: Task Queue, Microtask Queue, RAF, Idle)
├── task-queue.ts       (Priorisierte Macrotask-Queue: Timer, Events, User-Interaction)
├── microtask-queue.ts  (Promise, queueMicrotask, MutationObserver Callbacks)
└── render-steps.ts     (Animation Frames, Style Recalc, Layout, Paint)

Task Sources (nach Spec):

  1. DOM Manipulation Tasks
  2. User Interaction Events
  3. Networking Tasks
  4. History Traversal Tasks
  5. Timer Tasks

Processing Model (exakt nach Spec):

while (true) {
    // Step 1: Select oldest task from task queue
    task = taskQueue.dequeue()
    
    // Step 2: Run task
    run(task)
    
    // Step 3: Microtask checkpoint
    while (microtaskQueue.length > 0) {
        microtask = microtaskQueue.dequeue()
        run(microtask)
    }
    
    // Step 4: Update rendering
    if (needsRender(timeSinceNavigationStart)) {
        // Step 5: Run beforeunload + pagehide
        // Step 6: Resize/Scroll/MediaQuery callbacks
        // Step 7: RAF callbacks
        fireAnimationFrames()
        // Step 8: Style Recalc
        // Step 9: Layout
        // Step 10: Paint
    }
    
    // Step 11: Idle period
    fireIdleCallbacks()
}

Integration in Page:

class EventLoop {
    private taskQueue: TaskQueue
    private microtaskQueue: MicrotaskQueue
    private rafCallbacks: Map<number, FrameCallback>
    private idleCallbacks: Map<number, IdleCallback>
    
    install(win: Window) {
        win.setTimeout = (fn, ms) => this.taskQueue.schedule(fn, ms, 'timer')
        win.requestAnimationFrame = (fn) => this.scheduleRAF(fn)
        win.requestIdleCallback = (fn, opts) => this.scheduleIdle(fn, opts)
        win.queueMicrotask = (fn) => this.microtaskQueue.enqueue(fn)
    }
    
    async tick(): Promise<void> {
        this.processMicrotasks()
        this.processTasks()
        this.processAnimationFrames()
        processIdleCallbacks()
    }
}

Vorteile:

  • 100% nach HTML5 Event Loop Spec
  • Korrektes Timing für alle Frameworks
  • RAF + IdleCallbacks spec-konform
  • Stablizer kann durch Event-Loop-Driven-Approach ersetzt werden

Nachteile:

  • Ersetzt Bun's nativen Event Loop — muss per-Context isoliert sein
  • Async/await braucht Integration mit JS-Promise-Microtasks

Option B — SimpleRAF + Stabilizer verbessern

Bestehende Komponenten optimieren:

  • SimpleRAF auf echte requestAnimationFrame-Timing (16ms frames, nicht setInterval)
  • Stabilizer: Microtask-Drain + Promise-Settlement-Prüfung
  • requestIdleCallback als setTimeout(fn, 50) Simulierung

Vorteile: Minimaler Code (~200 Zeilen)
Nachteile: Niemals spec-konform, Framework-Inkompatibilitäten bleiben

Option C — Hybrid: Mikro-Processing per Page

  • Event-Loop-ähnliches Processing pro Page.tick()
  • Page.goto() führt Page.tick() nach jedem await aus
  • Stabilizer waitForMicrotasks() drainiert explizit

Entscheidung

Option A — Full Spec Event Loop. Der Event Loop ist fundamental für spec-konformes Browser-Verhalten. SimpleRAF + Stabilizer zu flicken lohnt nicht — wir brauchen die korrekte Task-Queue-Ordnung.

Akzeptanzkriterien

  • Microtask-Queue wird nach JEDEM Task geleert (Promise.then(), queueMicrotask())
  • Macrotasks: Timer-Events haben niedrigere Priorität als UI-Events
  • requestAnimationFrame feuert genau 1x pro Frame (60fps / 16.6ms)
  • requestIdleCallback feuert in Idle-Perioden (keine pending Tasks)
  • DOMContentLoaded feuert nach Parser-Complete + defer-Script-Execution
  • load-Event feuert nach allen Ressourcen + RAF-Idle-Period
  • setTimeout(fn, 0) WIRD von Microtasks VOR der Funktion ausgeführt
  • Stabilizer ersetzt durch Event-Loop-basiertes Warten
  • Promise.then() wird VOR dem nächsten setTimeout ausgeführt
  • MutationObserver Callbacks laufen als Microtask (nicht synchron)

Betroffene Dateien

Datei Änderung Status
src/fakes/simple-raf.ts Ersetzen durch EventLoop.RAF Löschen
src/pages/stabilizer.ts Ersetzen durch EventLoop Löschen
src/event-loop/event-loop.ts Neu Neu
src/event-loop/task-queue.ts Neu Neu
src/event-loop/microtask-queue.ts Neu Neu
src/event-loop/render-steps.ts Neu Neu
src/pages/page.ts EventLoop instanziieren + install Ändern
src/runtime-isolation.ts EventLoop als Context hinzufügen Ändern
tests/unit/event-loop.test.ts Neu Neu
tests/integration/event-loop.test.ts Neu Neu

Dependencies

  • Keine externen Dependencies
  • Integriert mit StyleEngine (Style Recalc trigger)
  • Integriert mit LayoutEngine (Layout trigger)
  • Nutzt Bun's queueMicrotask für interne Microtask-Queue?

Technische Risiken

  • Async/Await: Bun's native Promise-Microtasks sind nicht unterbrechbar
  • Per-Event-Loop: Events von Happy DOM (click, submit) müssen durch Event Loop laufen
  • Blocking: Ein langer Task blockiert alle anderen

Performance-Impact

  • ~5% Overhead für Event-Loop-Buchhaltung
  • RAF + IdleCallbacks = 0 Kosten wenn nichts scheduled ist
  • Stabilizer-Polling (50ms setInterval) entfällt → Strom-/CPU-Ersparnis

Testplan

Unit (25+)

  • taskQueue.enqueue/dequeue Reihenfolge
  • MicrotaskQueue Leerung nach Task
  • RAF-Frame-Timing
  • IdleCallbacks feuern nur bei Idle
  • setTimeout vs Promise.then vs queueMicrotask Order
  • MutationObserver als Microtask

Integration (15+)

  • Event-Loop-Driven Page.goto()
  • DOMContentLoaded Timing
  • load-Event Timing
  • React Concurrent Mode Verhalten
  • AWS WAF Async-Generator-Microtask-Drain

E2E (3+)

  • qwik.dev initial render
  • Amazon CMP-Banner (idleCallback)
  • SPA mit setTimeout-basiertem Router
# Issue #98 — Event Loop — Spec-Compliant Microtask/Macrotask Drain ## Problembeschreibung Der Browser hat keinen spec-konformen Event Loop. Aktuell gibt es: - `SimpleRAF` (`src/fakes/simple-raf.ts`) — simuliert requestAnimationFrame via setInterval - Microtask-Queue existiert nicht explizit (Bun/Babel-handled) - Kein `queueMicrotask()` Promise-Verhalten (kein `await`-Drain) - Macrotask-Order: `setTimeout`, `setInterval`, `setImmediate` ohne Priorisierung - IdleCallbacks (`requestIdleCallback`) fehlen **Warum das blockiert:** - **AWS WAF Challenge**: Async-Generator-Drain in `_drainAsyncGenerators()` pollt alle 50ms — kein spec-konformer Microtask-Drain - **Moderne Frameworks**: React 18+ Concurrency, Vue Async-Scheduler, Svelte tick() hängen von korrekter Microtask-Order ab - **CMPs (Consent Management)**: Nutzen `requestIdleCallback` für non-critical Scripts - **Performance-Timing**: `DOMContentLoaded`, `load` Event-Timing ist inkorrekt ## Architektur-Analyse ### Aktueller Stand ``` Script modifies DOM → Happy DOM dispatches MutationObserver → SimpleRAF.tick() (alle 16ms im setInterval) → Stabilizer pollt alle 50ms → Kein expliziter Microtask-Drain ``` Stabilizer: ```ts async stabilize(doc, scriptLoader) { while (!this.isStable()) { await new Promise(r => setTimeout(r, 20)); // pollt: scriptLoader.isEmpty(), keine pending fetches } } ``` Das ist **nicht spec-konform**. Der Spec sagt: ```mermaid flowchart TD A[Task Queue] --> B[Select oldest task] B --> C[Run task] C --> D[Microtask Queue] D --> E[Run all microtasks] E --> F{RAF Callbacks?} F -->|Yes| G[Fire Animation Frames] F -->|No| H[Render Opportunities] H --> I[Idle Callbacks?] I -->|Yes| J[Fire Idle Callbacks] I -->|No| K[Next Event Loop Iteration] J --> K ``` ## Lösungsansätze ### Option A — Full Spec Event Loop (Empfohlen) Eigener EventLoop-Klasse mit: ``` src/event-loop/ ├── event-loop.ts (Hauptklasse: Task Queue, Microtask Queue, RAF, Idle) ├── task-queue.ts (Priorisierte Macrotask-Queue: Timer, Events, User-Interaction) ├── microtask-queue.ts (Promise, queueMicrotask, MutationObserver Callbacks) └── render-steps.ts (Animation Frames, Style Recalc, Layout, Paint) ``` **Task Sources (nach Spec):** 1. DOM Manipulation Tasks 2. User Interaction Events 3. Networking Tasks 4. History Traversal Tasks 5. Timer Tasks **Processing Model (exakt nach Spec):** ``` while (true) { // Step 1: Select oldest task from task queue task = taskQueue.dequeue() // Step 2: Run task run(task) // Step 3: Microtask checkpoint while (microtaskQueue.length > 0) { microtask = microtaskQueue.dequeue() run(microtask) } // Step 4: Update rendering if (needsRender(timeSinceNavigationStart)) { // Step 5: Run beforeunload + pagehide // Step 6: Resize/Scroll/MediaQuery callbacks // Step 7: RAF callbacks fireAnimationFrames() // Step 8: Style Recalc // Step 9: Layout // Step 10: Paint } // Step 11: Idle period fireIdleCallbacks() } ``` **Integration in Page:** ```ts class EventLoop { private taskQueue: TaskQueue private microtaskQueue: MicrotaskQueue private rafCallbacks: Map<number, FrameCallback> private idleCallbacks: Map<number, IdleCallback> install(win: Window) { win.setTimeout = (fn, ms) => this.taskQueue.schedule(fn, ms, 'timer') win.requestAnimationFrame = (fn) => this.scheduleRAF(fn) win.requestIdleCallback = (fn, opts) => this.scheduleIdle(fn, opts) win.queueMicrotask = (fn) => this.microtaskQueue.enqueue(fn) } async tick(): Promise<void> { this.processMicrotasks() this.processTasks() this.processAnimationFrames() processIdleCallbacks() } } ``` **Vorteile:** - 100% nach HTML5 Event Loop Spec - Korrektes Timing für alle Frameworks - RAF + IdleCallbacks spec-konform - Stablizer kann durch Event-Loop-Driven-Approach ersetzt werden **Nachteile:** - Ersetzt Bun's nativen Event Loop — muss per-Context isoliert sein - Async/await braucht Integration mit JS-Promise-Microtasks ### Option B — SimpleRAF + Stabilizer verbessern Bestehende Komponenten optimieren: - SimpleRAF auf echte `requestAnimationFrame`-Timing (16ms frames, nicht setInterval) - Stabilizer: Microtask-Drain + Promise-Settlement-Prüfung - requestIdleCallback als setTimeout(fn, 50) Simulierung **Vorteile:** Minimaler Code (~200 Zeilen) **Nachteile:** Niemals spec-konform, Framework-Inkompatibilitäten bleiben ### Option C — Hybrid: Mikro-Processing per Page - Event-Loop-ähnliches Processing pro Page.tick() - Page.goto() führt Page.tick() nach jedem await aus - Stabilizer waitForMicrotasks() drainiert explizit ## Entscheidung **Option A** — Full Spec Event Loop. Der Event Loop ist fundamental für spec-konformes Browser-Verhalten. SimpleRAF + Stabilizer zu flicken lohnt nicht — wir brauchen die korrekte Task-Queue-Ordnung. ## Akzeptanzkriterien - [ ] Microtask-Queue wird nach JEDEM Task geleert (Promise.then(), queueMicrotask()) - [ ] Macrotasks: Timer-Events haben niedrigere Priorität als UI-Events - [ ] requestAnimationFrame feuert genau 1x pro Frame (60fps / 16.6ms) - [ ] requestIdleCallback feuert in Idle-Perioden (keine pending Tasks) - [ ] DOMContentLoaded feuert nach Parser-Complete + defer-Script-Execution - [ ] load-Event feuert nach allen Ressourcen + RAF-Idle-Period - [ ] setTimeout(fn, 0) WIRD von Microtasks VOR der Funktion ausgeführt - [ ] Stabilizer ersetzt durch Event-Loop-basiertes Warten - [ ] Promise.then() wird VOR dem nächsten setTimeout ausgeführt - [ ] MutationObserver Callbacks laufen als Microtask (nicht synchron) ## Betroffene Dateien | Datei | Änderung | Status | |-------|----------|--------| | `src/fakes/simple-raf.ts` | Ersetzen durch EventLoop.RAF | Löschen | | `src/pages/stabilizer.ts` | Ersetzen durch EventLoop | Löschen | | `src/event-loop/event-loop.ts` | Neu | Neu | | `src/event-loop/task-queue.ts` | Neu | Neu | | `src/event-loop/microtask-queue.ts` | Neu | Neu | | `src/event-loop/render-steps.ts` | Neu | Neu | | `src/pages/page.ts` | EventLoop instanziieren + install | Ändern | | `src/runtime-isolation.ts` | EventLoop als Context hinzufügen | Ändern | | `tests/unit/event-loop.test.ts` | Neu | Neu | | `tests/integration/event-loop.test.ts` | Neu | Neu | ## Dependencies - Keine externen Dependencies - Integriert mit StyleEngine (Style Recalc trigger) - Integriert mit LayoutEngine (Layout trigger) - Nutzt Bun's `queueMicrotask` für interne Microtask-Queue? ## Technische Risiken - **Async/Await**: Bun's native Promise-Microtasks sind nicht unterbrechbar - **Per-Event-Loop**: Events von Happy DOM (click, submit) müssen durch Event Loop laufen - **Blocking**: Ein langer Task blockiert alle anderen ## Performance-Impact - ~5% Overhead für Event-Loop-Buchhaltung - RAF + IdleCallbacks = 0 Kosten wenn nichts scheduled ist - Stabilizer-Polling (50ms setInterval) entfällt → Strom-/CPU-Ersparnis ## Testplan ### Unit (25+) - taskQueue.enqueue/dequeue Reihenfolge - MicrotaskQueue Leerung nach Task - RAF-Frame-Timing - IdleCallbacks feuern nur bei Idle - setTimeout vs Promise.then vs queueMicrotask Order - MutationObserver als Microtask ### Integration (15+) - Event-Loop-Driven Page.goto() - DOMContentLoaded Timing - load-Event Timing - React Concurrent Mode Verhalten - AWS WAF Async-Generator-Microtask-Drain ### E2E (3+) - qwik.dev initial render - Amazon CMP-Banner (idleCallback) - SPA mit setTimeout-basiertem Router
Artur closed this issue 2026-06-19 13:57:32 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
glow-all/true-headless-browser#98
No description provided.