Skip to content

Latest commit

 

History

History
96 lines (75 loc) · 4.63 KB

File metadata and controls

96 lines (75 loc) · 4.63 KB

Process Stack

The rendering engine is iterative, not recursive. Work is described as typed frames pushed onto renderRuntime.renderStack (a plain SimpRenderFrame[]). A single processStack loop pops and dispatches them until the stack is empty.

This avoids JavaScript call-stack overflows for deep trees and makes it straightforward to interleave mount, patch, unmount, and DOM placement operations on the same pass.

Frame types

Kind constant Value Handler
MOUNT_ENTER 10 mountEnter(frame)
MOUNT_EXIT 11 mountExit(frame)
MOUNT_CHILDREN_ENTER 12 mountChildren(frame)
PATCH_ENTER 20 patchEnter(frame)
PATCH_EXIT 21 patchExit(frame)
UNMOUNT_ENTER 30 unmountEnter(frame)
UNMOUNT_EXIT 31 unmountExit(frame)
UNMOUNT_CHILDREN_ENTER 32 unmountChildren(frame)
HOST_OPS_PLACE_ELEMENT_BEFORE_ANCHOR 40 placeElementBeforeAnchor
HOST_OPS_REPLACE_CHILD 42 hostAdapter.replaceChild

The main loop

flowchart TD
    START(["processStack(renderRuntime)"]) --> CHECK{stackCursor >= 0?}
    CHECK -->|no| END(["sweep slots 0..stackPeak, return"])
    CHECK -->|yes| POP["frame = stack[stackCursor--]"]
    POP --> DISPATCH{frame.kind}

    DISPATCH -->|MOUNT_ENTER 10| ME["mountEnter(frame)"]
    DISPATCH -->|MOUNT_EXIT 11| MX["mountExit(frame)"]
    DISPATCH -->|MOUNT_CHILDREN_ENTER 12| MC["mountChildren(frame)"]
    DISPATCH -->|PATCH_ENTER 20| PE["patchEnter(frame)"]
    DISPATCH -->|PATCH_EXIT 21| PX["patchExit(frame)"]
    DISPATCH -->|UNMOUNT_ENTER 30| UE["unmountEnter(frame)"]
    DISPATCH -->|UNMOUNT_EXIT 31| UX["unmountExit(frame)"]
    DISPATCH -->|UNMOUNT_CHILDREN_ENTER 32| UC["unmountChildren(frame)"]
    DISPATCH -->|PLACE 40| PL["placeElementBeforeAnchor"]
    DISPATCH -->|REPLACE 42| RP["hostAdapter.replaceChild"]

    ME --> DEC["stackCursor--\n(frame processed, loop continues)"]
    MX --> DEC
    MC --> DEC
    PE --> DEC
    PX --> DEC
    UE --> DEC
    UX --> DEC
    UC --> DEC
    PL --> DEC
    RP --> DEC

    DEC --> CHECK
Loading

The stack is popped by decrementing renderRuntime.stackCursor (no Array.pop), so the frame object itself is left in place at that slot — ready to be reused by a later acquireFrame call without reallocating. Once the whole processStack call drains (stackCursor back to -1), a bounded sweep nulls out the large fields (node, prevElement, children, subtreeRightBoundary, placeHolderElement) on every slot up to stackPeak, so a deep render's element references don't outlive the call.

ENTER / EXIT pairs

Most element types use an enter/exit pair to bracket work that must happen before and after children are processed. The stack is LIFO, so pushing EXIT before CHILDREN means CHILDREN is processed first:

sequenceDiagram
    participant S as renderStack
    participant P as processStack loop

    Note over S: initial state (empty)
    S->>S: push MOUNT_EXIT
    S->>S: push MOUNT_CHILDREN_ENTER
    P->>S: pop → MOUNT_CHILDREN_ENTER
    Note over P: mounts children (may push more frames)
    P->>S: pop → MOUNT_EXIT
    Note over P: mounts props, places element in DOM
Loading

This pattern is used identically by HOST, FC, PORTAL, and FRAGMENT elements for mount and unmount. Patch uses PATCH_ENTER / PATCH_EXIT the same way.

Frame pool

Frame objects are expensive to GC at high frequency. Unlike a separate pool keyed per frame kind, renderRuntime.renderStack is the pool: it's a single flat array indexed by stackCursor (the index of the top active frame; -1 when empty) and stackPeak (the highest index acquired during the current top-level call). acquireFrame increments stackCursor and reuses whatever frame object already sits at that slot, allocating a new one only the first time a slot is reached:

flowchart LR
    ACQUIRE["acquireFrame(renderRuntime)"] --> INC["cursor = ++stackCursor"]
    INC --> PEAK{cursor > stackPeak?}
    PEAK -->|yes| BUMP["stackPeak = cursor"]
    PEAK -->|no| SLOT
    BUMP --> SLOT["stack[cursor] ??= createFrame()"]
    SLOT --> FRAME["frame (caller overwrites all fields)"]
Loading

acquireFrame is the single constructor for every frame kind — there's no per-kind pool or per-kind acquire function. Callers set all fields on the returned frame themselves (nulling ones their kind doesn't use).

Reentrancy guard

mount(), patch(), and unmount() all assert renderRuntime.stackCursor === -1 before pushing their initial frame. This prevents concurrent entry into processStack from the same runtime, which would produce undefined behavior (two interleaved stack walks over the same array).