Skip to content

Plug memory leaks during multi-model loads#1493

Merged
pablo-mayrgundter merged 2 commits intobldrs-ai:mainfrom
pablo-mayrgundter:fix/memory-leaks-multimodel
May 1, 2026
Merged

Plug memory leaks during multi-model loads#1493
pablo-mayrgundter merged 2 commits intobldrs-ai:mainfrom
pablo-mayrgundter:fix/memory-leaks-multimodel

Conversation

@pablo-mayrgundter
Copy link
Copy Markdown
Member

Summary

Cherry-pick of the memory-leak fixes from @MarkusSteinbrecher's #1477, on a focused branch (without the unrelated Google Drive / theme / floor-plan / project-hierarchy work in that PR).

The headline fix: initViewer() was building a new IfcViewerAPIExtended on every model load without disposing the previous one. The old WebGL context, renderer, geometries, materials, and textures all stayed alive, which is the resource creep we've seen across multi-model sessions. Now disposeViewer() runs first, releasing GPU resources before allocating new ones.

Three small extensions on top of Markus's viewer disposal:

  • WebGLRenderer.forceContextLoss() after renderer.dispose() — Three.js's dispose() frees programs/render targets but the WebGL context itself stays allocated. Chrome caps active contexts at ~16, so without this we could still hit the cap after enough loads.
  • currentViewer.dispose?.() — optional call to any built-in dispose on IfcViewerAPIExtended. Belt-and-suspenders against subsystems the manual scene traverse misses (controls, post-processor, highlighter, isolator, viewsManager).
  • Expanded texture-map disposal — Markus's loop only disposed material.map. Now iterates all common slots (normalMap, roughnessMap, metalnessMap, emissiveMap, aoMap, bumpMap, displacementMap, alphaMap, lightMap, envMap).

The other 7 leaks fixed (per Markus's MEMORY_LEAK_FIXES.md):

  • Theme listener accumulation (onModelPath re-registered without removal)
  • Canvas event listener accumulation (handler refs didn't match between add/remove; useEffect had no cleanup)
  • Hash listeners had no removal mechanism — added removeHashListener(name)
  • Object URLs created via URL.createObjectURL and never revoked (4 sites). For the two sites whose URL is consumed by a Web Worker, revocation is deferred until the worker reports completion to avoid the worker fetching a dead URL.
  • IFrameCommunicationChannel had no dispose() — added one; AppIFrame tracks the channel via a ref and disposes on unmount/recreate
  • setDeletedNotes left orphaned entries in editBodies / editModes / editOriginalBodies
  • SaveModelControl snack timeouts piled up — now dedup'd via a single tracked timer

Test plan

  • yarn lint && yarn typecheck — clean
  • yarn test-src — 160 suites, 958 tests, all pass
  • Deploy preview: load 3 IFCs in sequence; in DevTools confirm only one WebGLRenderingContext exists, JS heap returns toward baseline between loads, and theme.themeChangeListeners doesn't grow
  • Mobile/desktop smoke: search, section planes, notes, save flow

Notes

  • Branched off bldrs-ai/Share:main, not pablo-mayrgundter/Share:main, so the PR is a clean diff against the org's main.
  • See MEMORY_LEAK_FIXES.md for the full per-leak breakdown (Markus's doc, with a one-line preamble noting the cherry-pick origin and the three extensions).

🤖 Generated with Claude Code

Cherry-picked from bldrs-ai#1477. Disposes the prior IfcViewerAPIExtended on
each new model load (WebGL context, geometries, materials, textures,
ResizeObserver hook, global mouse handlers) so GPU/heap don't grow
across loads. Also cleans up theme/hash/canvas listeners, revokes
object URLs (worker-aware: deferred until the worker is done with
the URL), disposes IFrameCommunicationChannel ports, prunes per-note
edit state on deletion, and dedupes snack-clear timeouts.

Extends Markus's viewer disposal with:
- WebGLRenderer.forceContextLoss() so the GL context itself is
  reclaimed (renderer.dispose alone leaves it allocated and browsers
  cap active contexts)
- optional currentViewer.dispose?.() to reach subsystems the manual
  scene traverse misses (controls, post-processor, highlighter,
  isolator, viewsManager)
- disposal of additional texture-map slots: normalMap, roughnessMap,
  metalnessMap, emissiveMap, aoMap, bumpMap, displacementMap,
  alphaMap, lightMap, envMap

See MEMORY_LEAK_FIXES.md for the full per-leak breakdown.

Co-Authored-By: Markus Steinbrecher <74647806+MarkusSteinbrecher@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 1, 2026

Deploy Preview for bldrs-share-dev ready!

Name Link
🔨 Latest commit c9af756
🔍 Latest deploy log https://app.netlify.com/projects/bldrs-share-dev/deploys/69f521c0da2f4f0008571016
😎 Deploy Preview https://deploy-preview-1493--bldrs-share-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 54 (no change from production)
Accessibility: 88 (no change from production)
Best Practices: 100 (no change from production)
SEO: 100 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

Adds focused unit tests for each leak fix from the prior commit:

- src/Containers/viewer.test.js (NEW, 12 tests): verifies disposeViewer
  removes the same window mouse handlers it added (same fn refs),
  traverses the scene and disposes geometry + every texture-map slot
  + the material itself, calls renderer.dispose() AND
  forceContextLoss(), invokes the viewer's built-in dispose() when
  present, disconnects the ResizeObserver, nulls glbClipper, and is
  idempotent.  Also verifies initViewer disposes the previous viewer
  before constructing the new one.
- src/Components/Apps/AppsMessagesHandler.test.js (NEW, 7 tests):
  verifies the channel posts port2 with init, attaches a message
  handler, and dispose() nulls onmessage, closes the port, drops all
  references, is idempotent, and survives port.close throwing.
  Polyfills MessageChannel for jest-fixed-jsdom.
- src/store/NotesSlice.test.js: 4 new tests covering setDeletedNotes
  pruning behavior — prunes editBodies/editModes/editOriginalBodies
  for the deleted id, leaves others untouched, and no-ops on null /
  legacy-shape inputs.
- src/utils/loader.test.js: 3 new tests verifying URL.revokeObjectURL
  is called in loadLocalFile (sync fallback), loadLocalFileFallback,
  and saveDnDFileToOpfsFallback — and revoke happens before the
  callback fires.
- src/Components/Camera/CameraControl.test.jsx: 1 new test verifying
  every canvas listener added by onLoad gets removed on unmount with
  the same fn ref (this was the original leak's root cause).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pablo-mayrgundter pablo-mayrgundter merged commit 61ad190 into bldrs-ai:main May 1, 2026
6 checks passed
@pablo-mayrgundter pablo-mayrgundter deleted the fix/memory-leaks-multimodel branch May 1, 2026 22:04
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