Skip to content

Eliminate lag-spike sources in the engine and host - #3

Merged
GoatInAHat merged 2 commits into
mainfrom
claude/site-lag-spikes-733sqk
Aug 24, 2026
Merged

GoatInAHat merged 2 commits into
mainfrom
claude/site-lag-spikes-733sqk

Conversation

@GoatInAHat

Copy link
Copy Markdown
Owner

Hunts down and removes the causes of the intermittent frame hitches on the site. Each fix targets one measured mechanism.

The spikes and their causes

  • Every ~40s during the demo rotation — the enabled rotation (Demo3 → Demo4 → Demo7 → Demo2) flips gridCellSize 40 → 16 → 40, and each flip destroyed and reallocated GPU grid buffers sized by the whole world (~120 MB at cell 16 on this page) mid-transition. createGridStorage is now grow-only, so after boot no transition reallocates anything. A 70s headless run across both flips now shows zero long tasks (previously a ~1s task).
  • Random GC pauses — the cell census sliced ~1.9 MB of fresh typed arrays out of every readback, every frame. It now copies into per-slot reused arrays (contract documented in the vendor README). The render pipeline also rebuilt every module's webgpu() descriptor and multi-KB per-pass WGSL strings each frame; all are now cached.
  • ~600 ms freezes on height-only resizes — the mobile URL-bar collapse and any body-height change triggered the full name/Voronoi + text-field rebuild. The resize path now rebuilds the name geometry only when the page width (or zoom) actually changed, and the content field only when the document reflowed. Headless: 4 height-only resizes now produce zero long tasks (previously ~600 ms each). layout() is also memoized so spurious ResizeObserver fires stop discarding the overlay canvas backing stores.
  • Page-load freeze — boot ran name Voronoi, the text distance field, and the 80k spawn in one long task (3.5s in headless software rendering); it now yields a frame between the heavy steps, and WebGPU compute pipelines compile via createComputePipelineAsync off the main thread.
  • CPU fallback (non-WebGPU browsers) — the runtime allocated per particle per frame: fresh cpu() descriptors and input objects per pass (with read() called once per key, quadratic in key count), getState/setState closures per particle, five objects per particle in integration, a reallocated rows×cols grid every frame, and a new rgba() style string per particle. All removed. The 2D context is now acquired with willReadFrequently, so the sensors module's per-particle getImageData calls (two per particle per frame in most rotation demos) stop being synchronous GPU readback stalls.
  • Dev-only density-stat array copies no longer run in production builds, and Lloyd relaxation early-exits once seeds settle.

Deliberately not changed

  • .party-canvas { filter: invert(1) contrast(2) } — constant compositor cost, but it is the site's visual baseline, not a spike.
  • The CPU renderer's 2,500 individual arc/fill dispatches — constant ~2-5 ms, and batching would change alpha blending at overlaps.
  • The settings panel's rAF sync loop — runs only while the panel is open.

Verification

  • npm run lint, npm run build, check:force (816 assertions), check:density (139,500 assertions), agent-config checks, pre-commit all green.
  • Headless Chromium (SwiftShader WebGPU + CPU runtime): full rotation cycle across both cellSize flips with zero long tasks; height-only resizes clean; density enforcement active and settling (0 teleports/s at rest); screenshots of both runtimes identical to pre-change baseline.
  • Every vendor divergence is recorded in vendor/party/README.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01F9Fq5ub3QM8igyjzkU92pn


Generated by Claude Code

claude added 2 commits August 24, 2026 22:20
The site hitched at predictable moments; each fix removes one cause.

- Grid storage is grow-only: the demo rotation flips gridCellSize
  (40 -> 16 -> 40), and each flip destroyed and reallocated grid buffers
  sized by the whole world (~120 MB) mid-transition.
- The cell census copies readbacks into per-slot reused arrays instead of
  slicing ~1.9 MB of fresh typed arrays per frame -- steady GC-pause fuel.
- webgpu() descriptors, array-input lists, and per-pass WGSL are cached;
  all were rebuilt (multi-KB template strings included) every frame.
- Compute pipelines compile via createComputePipelineAsync, off the boot
  task.
- The CPU fallback no longer allocates per particle per frame (descriptor
  and input caching, hoisted closures, in-place integration, reused grid
  cells, memoized fill styles) and acquires its 2D context with
  willReadFrequently so per-particle sensor reads stop stalling on GPU
  readback.
- The host skips the several-hundred-ms name/Voronoi and text-field
  rebuilds when only the page height changed (the mobile URL-bar case),
  memoizes layout(), stops collecting dev-only density stats in
  production, early-exits Lloyd relaxation once seeds settle, and yields
  between the heavy boot steps so page load stays responsive.

Verified: lint, build, force/density checks green; headless runs show the
height-only resize stall (~600 ms) and the idle-rotation long task
(~1 s) gone, with both runtimes rendering identically to baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F9Fq5ub3QM8igyjzkU92pn
The module is enabled in nearly every preset, so the two per-particle
Vector allocations ran for every particle of every frame on the CPU
fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F9Fq5ub3QM8igyjzkU92pn
@GoatInAHat
GoatInAHat marked this pull request as ready for review August 24, 2026 22:22
@GoatInAHat
GoatInAHat merged commit 6ff6dcb into main Aug 24, 2026
1 check passed

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cfabf0893

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +37 to +40
private positionState: Map<
number,
{ prev: { x: number; y: number }; post: { x: number; y: number } }
> = new Map();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear cached positions when replacing particles

On the CPU fallback, every width change calls spawnAll() and then setParticles(), creating 8,000 particles with new monotonically increasing IDs, while this persistent map retains every record from the previous swarm. Repeated resizes therefore grow positionState without bound—three objects per retired particle—and clear(), reset(), and destroy() also leave it populated; clear or prune the cache whenever the particle collection is replaced or discarded.

Useful? React with 👍 / 👎.

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.

2 participants