Skip to content

Perf: QuickJS interop hot-path optimizations (audit findings A-F) #106

Description

@Singtaa

Tracking issue for the QuickJS interop hot-path performance work, implemented from an audit of the shipped-app runtime hot path (native / QuickJS interpreter). Findings are labeled A-F. Work spans three repos; commit refs are noted per item.

Done

  • D - Batched Painter2D API. PainterBridge replays a flat command buffer onto Painter2D in one JS->C# crossing instead of one reflection crossing per op (and per new Vector2/new Color). JS-side Painter recorder + batchedVisualContent / useBatchedVectorContent.
    • OneJS 393d426, onejs-react feeb7fd, Website c20d0e2
  • E - Fast value-type ctors. Zero-alloc ctor fast path (resolved by type-name hash) for Vector2/3/4, Color, Quaternion. Same InteropValue packing as the reflection ctor path, so JS sees an identical value.
    • OneJS 393d426
  • C - FastPath not seeded for hot UI Toolkit types. Addressed per-type after hitting the concrete-type keying wall (instance-method fast paths key by the concrete element type, so VisualElement.Add can't be seeded for Button/Label/etc.):
    • Element ctors -> fast ctor path (keying-safe; ctors resolve by type name).
    • VisualElement.Add/Insert/RemoveFromHierarchy -> NodeBridge (static, registered by type name, type-agnostic).
    • IStyle members -> StyleBridge typed setters (S1).
    • Vector2/Color -> E; Painter2D -> D.
    • OneJS 393d426, onejs-react feeb7fd
  • B - Element-handle resolution under a global lock. FindElementHandle's parent-chain walk now runs under a single lock acquisition (GetHandleForElementOrAncestor) instead of one lock + dict lookup per hop. Behavior-identical; collapses N locks to 1 on deep (composite-control) event targets.
    • OneJS fbc379f

(Prior related work, before this audit pass: a6e6eba delivered the GC delta-gate, per-element pointer + wheel fast paths, and batched task drain.)

Deferred (intentional)

  • A - ExecutePendingJobs after every event dispatch. pointermove is already throttled to ~1/frame, and each drain is load-bearing (dispatch returns the suppression-flag bitmask synchronously, so the reconcile must complete per event). Real but low value, with correctness risk. Revisit only if a profiler flags it.
  • F - FastPath/ZeroAlloc hand-registration DX. A [OneJSFastPath] attribute / source generator would remove the re-register-in-Awake step. DX-only (built-in registrations self-heal on domain reload), but it is also the AOT-safe path to broad fast member invocation (see the I2 note below). Defer until custom FastPath usage is common or I2-style coverage is wanted.

Next pickup (broader structural tier, not part of the A-F audit)

I2 (general cached-delegate invokers for the reflection slow path) - dropped as a standalone item. It does not survive scrutiny: Expression.Compile is not AOT-safe (IL2CPP, i.e. the no-JIT native targets QuickJS exists for), and Delegate.CreateDelegate can't be invoked generically without DynamicInvoke (slower than reflection). The AOT-safe way to get broad fast invokers is FastPath registration, ideally automated by F (a source generator). So I2 folds into F + FastPath expansion rather than being a separate runtime-delegate layer. The remaining reflection hits after D/E/C/B are the cold long tail anyway.

Notes

Architectural lessons worth keeping: FastPath instance methods key by concrete runtime type; ctors key by type name (keying-safe); Expression.Compile is not AOT-safe, so S1 uses a static typed-setter switch rather than compiled delegates.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions