Skip to content

?perf instrumentation suite: real GPU timestamps, system tracks, action receipts, DOM panel - #755

Merged
wass08 merged 9 commits into
mainfrom
feat/perf-instrumentation
Sep 2, 2026
Merged

?perf instrumentation suite: real GPU timestamps, system tracks, action receipts, DOM panel#755
wass08 merged 9 commits into
mainfrom
feat/perf-instrumentation

Conversation

@wass08

@wass08 wass08 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Measurement layer for the editor performance pass — everything gated on ?perf, zero cost without it.

What's inside

  • Panel fixes: DRAW read info.render.calls (lifetime renderer.render() count, never cleared by reset()) — now drawCalls. "GPU ms" was a wall-clock fence (onSubmittedWorkDone) bundling CPU encode + queue wait + microtask latency — now real WebGPU timestamp queries (trackTimestamp + resolveTimestampsAsync), with the fence kept as a separate QUEUE (backpressure) line and ENCODE (CPU) split out.
  • Panel as a tool: headless in-canvas collector + DOM panel portaled to <body> (drei <Html> wrappers carry a camera transform that defeats position:fixed — the old overlay drifted with the camera). Draggable, dockable to an edge tab, placement persisted.
  • System tracks: perf-tracks sink emits Chrome DevTools custom lanes (trackGroup "Pascal") + per-window buckets for the panel: frame-cpu, geometry builders, wall miter/rebuild/CSG, door/window rebuilds, pointer raycast, react-render, long tasks.
  • Action-cost ledger: edit gestures produce receipts (drag / settle / per-track breakdown) in the panel, console, and a DevTools "Actions" lane. Settle = dirty queue empty + deferred wall rebuilds flushed + one more GPU frame. The generic bracket is the interaction scope store; specific call sites (drag-action, 2D floorplan, undo/redo/delete/level-switch/place) take precedence.
  • Probe hooks: window.__pascalPerf.{listNodes,projectNode} for scripted perf runs.
  • Deleted the dormant DebugRenderer (unreferenced; would double-render if mounted).

Diagnosis results and the optimization backlog live in the private repo's plans/performance/editor-scalable-scene-runtime.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK


Note

Low Risk
Instrumentation is gated on ?perf with no-op fast paths in production; changes are diagnostic UI and timing hooks rather than core edit or auth logic.

Overview
Adds a ?perf-gated measurement stack so editor performance work can see real frame cost, per-system work, and end-to-end edit latency without affecting normal sessions.

Viewer overlay moves from a drei Html overlay inside the canvas to a DOM panel portaled to document.body (draggable, dockable, persisted placement). A headless PerfMonitor still runs in the canvas, publishes aggregates via perf-panel-store, and exposes window.__pascalPerf probe hooks for scripted runs. GPU reporting switches to WebGPU timestamp queries when available (trackTimestamp), with encode, gpu-render, and queue fence lines separated; draw stats use drawCalls instead of lifetime calls.

perf-tracks centralizes spans (timeSpan, recordPerfSample) into DevTools “Pascal” lanes and per-window buckets; hot systems (wall miter/rebuild/CSG, geometry, door/window, frame-cpu, pointer, react-render, long tasks) are instrumented.

perf-actions brackets editor gestures (drag, place, delete, undo/redo, level-switch) with begin/commit/cancel and markPerfAction, coordinated with interaction scope and 2D floorplan affordances. PerfActionSettleSystem closes receipts only after live dirty nodes clear, deferred wall rebuilds flush, and a post-quiet GPU sample.

The editor wraps the main tree in React Profiler when perf is on. react-dom is added as a viewer peer dependency for the portal panel.

Reviewed by Cursor Bugbot for commit e51e2a6. Bugbot is set up for automated code reviews on this repo. Configure here.

wass08 and others added 7 commits September 2, 2026 10:38
…system User Timing tracks

- DRAW read info.render.calls (lifetime render() count, never reset) — now drawCalls
- GPU time now from WebGPU timestamp queries (trackTimestamp + resolveTimestampsAsync);
  the old queue-fence delta stays as QUEUE (backpressure), encode CPU as ENCODE
- perf-tracks: shared sink emitting DevTools custom tracks (trackGroup Pascal) +
  per-window counter buckets; perf-observers: longtask observer
- spans: frame-cpu (FrameLimiter advance), geometry builders, wall miter/rebuild/CSG,
  door/window rebuilds, pointer raycast, react-render Profiler boundary
- panel: FRAME cpu avg/max, MEM (info.memory + JS heap), visible-only census at 2s,
  TRACKS readout, clearPerfMeasures per drain; FPS threshold matches the 50fps cap
- deleted dormant DebugRenderer (unreferenced; would double-render if mounted)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
drei <Html> wrappers carry a camera-driven transform, which turns the old
overlay's position:fixed into 'fixed relative to the wrapper' — the panel
drifted with the camera. PerfMonitor is now a headless in-canvas collector
publishing to perf-panel-store; PerfPanel portals to <body>: draggable by
header, dockable to the nearest edge as a live fps tab, placement persisted
in localStorage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
Edit gestures now produce receipts: begin/commit/cancel bracket a gesture,
every perf-tracks sample in between is attributed to it, and the action
settles only when dirty nodes + deferred wall rebuilds hit zero and one more
GPU sample lands. Receipts surface in the panel (last action + breakdown),
the console, and a DevTools 'Actions' lane.

Call sites: the interaction scope store as the generic bracket (yields to
more specific ones), use-drag-action, 2D floorplan gestures, place/undo/
redo/delete/level-switch; markToolCancelConsumed finalizes cancels.
Settle system feeds the ledger per frame at priority 100.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
A node deleted while dirty (undo of a wall split) leaves its mark in
dirtyNodes forever — no system clears marks for missing nodes — and that
phantom dirt kept every action receipt from settling in furnished scenes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
Comment thread packages/editor/src/store/use-interaction-scope.ts
Comment thread packages/editor/src/store/use-interaction-scope.ts Outdated
Comment thread packages/editor/src/lib/history.ts Outdated
Comment thread packages/viewer/src/lib/perf-actions.ts
wass08 and others added 2 commits September 2, 2026 11:00
…s, no-op undo

- the interaction scope now commits only the action IT began (id token), and
  yields only to an UNCOMMITTED action, so a settling receipt can't swallow
  the next gesture and a specific call site's cancel is never committed by
  the generic bracket
- devices without timestamp-query settle on the queue fence instead of
  timing every receipt out
- a no-op undo/redo no longer opens a receipt
- import order (CI quality)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
CI typecheck resolves per-package: the perf panel's createPortal import needs
react-dom declared, not inherited from hoisting. Same peer + types pattern as
packages/editor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e51e2a6. Configure here.

if (historyCommandDelegate) {
const result = historyCommandDelegate.redo()
if (result.kind !== 'empty') markPerfAction('redo')
return result

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delegate undo measured after the jump

Medium Severity

Collaborative undo/redo calls markPerfAction after the delegate has already applied the jump, so the receipt misses the operation itself and only attributes later settle work. The local path marks before temporal.undo()/redo(), so the two history modes report different costs for the same gesture.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e51e2a6. Configure here.


adoptedRef.current = false
originalStateRef.current = null
commitPerfAction()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Place action interrupts in-flight gesture

Medium Severity

beginPerfAction('place:item') runs at commit time, not at gesture start. beginPerfAction finalizes any active action as interrupted, so a placing scope opened by setMovingNode (the generic bracket) is cut off at drop. The drag is stored as interrupted and the new receipt only covers the create write.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e51e2a6. Configure here.

<>
{/* DOM overlay, deliberately outside <Canvas> — drei Html wrappers carry
a camera transform that defeats position:fixed (see perf-panel.tsx). */}
{(perf || PERF_OVERLAY_ENABLED) && <PerfPanel />}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perf panel hydration mismatches Canvas

Medium Severity

PerfPanel is a DOM sibling of Canvas gated on PERF_OVERLAY_ENABLED, a module const that is false during SSR (window missing) and true on the client when ?perf is set. The server tree has only Canvas; the client tree inserts the panel first, which can fail hydration and remount the WebGPU canvas.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e51e2a6. Configure here.

@wass08
wass08 merged commit 3a5fdde into main Sep 2, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant