Skip to content

Improve robustness and memory usage when loading many meshes#9684

Open
hotzenklotz wants to merge 3 commits into
masterfrom
improve-mesh-loading-robustness
Open

Improve robustness and memory usage when loading many meshes#9684
hotzenklotz wants to merge 3 commits into
masterfrom
improve-mesh-loading-robustness

Conversation

@hotzenklotz

@hotzenklotz hotzenklotz commented Jun 12, 2026

Copy link
Copy Markdown
Member

When meshes are loaded for many segments at once (e.g., for a whole segment group), large meshes could exhaust RAM, fail ungracefully during geometry merging, spam error toasts on flaky chunk requests, and stall the UI. This PR addresses the four smaller issues from that problem set (memory accounting/LOD eviction is left for a follow-up):

  • Dispose geometries and materials on mesh removal: SegmentMeshController.removeMeshById now disposes the geometries/materials of removed meshes so that three.js actually frees the GPU buffers. This also covers the eagerly added chunk meshes that are replaced by the merged geometry after every precomputed mesh load, as well as mesh reloads and removals.
  • Limit concurrent segment mesh loads: A token-channel semaphore limits how many segments are meshed simultaneously (PARALLEL_MESH_LOADING_SEGMENT_COUNT = 4). Loading indicators still appear immediately for all queued segments, but the download/decode/merge pipelines run for at most 4 segments at a time. This way, the first meshes are fully visible earlier and the memory pressure of in-flight buffers stays bounded. Cancellation via REMOVE_MESH keeps working for both queued and running loads.
  • Graceful fallback when merging fails: mergeGeometries + BVH computation are now wrapped in try/catch. If merging fails (e.g., buffer allocation failure under memory pressure), the eagerly rendered, unmerged chunk meshes are kept as a fallback instead of the error killing the saga. Only features that need the merged geometry (proofreading range highlighting, fast raycasting) are unavailable for that mesh then.
  • Less error spam for retried chunk requests: Mesh chunk (data and listing) requests were already retried 3 times with exponential backoff, but each failed attempt showed an error toast. The per-attempt toast is now suppressed; the calling saga still shows one consolidated warning if all retries fail.
  • Yield to the event loop in the chunk decode loop to avoid a tight loop that starves rendering/GC and can silently stop the saga middleware (Saga stopped inside loop without throwing any error redux-saga/redux-saga#1592).

Steps to test:

  • Open a dataset with a precomputed mesh file and load meshes for a segment group with many (large) segments.
  • Observe that at most 4 segment meshes are loading their chunks at a time and that early segments become fully visible before later ones start.
  • Remove loaded meshes / reload meshes and verify (e.g., via a heap snapshot or renderer.info.memory.geometries) that memory is released.
  • Remove a mesh while it is still queued or loading and verify the load is cancelled.
  • Verify proofreading mesh interactions (hover/active highlighting) still work for merged meshes.

TODOs:

  • Changelog entry

Issues:

fixes #9541


(Please delete unneeded items, merge only when none are left open)

  • Added changelog entry (create a $PR_NUMBER.md file in unreleased_changes or use ./tools/create-changelog-entry.py)
  • Removed dev-only changes like prints and application.conf edits
  • Considered common edge cases

🤖 Generated with Claude Code

hotzenklotz and others added 2 commits June 12, 2026 16:24
- Dispose geometries and materials when meshes are removed from the
  scene so that their GPU buffers are actually freed (also frees the
  eagerly added chunk meshes that are replaced by the merged geometry).
- Limit how many segments are meshed simultaneously via a token channel
  so that the first meshes finish early and the memory pressure of
  in-flight chunk buffers stays bounded.
- Handle failures of mergeGeometries/BVH computation (e.g., when buffers
  cannot be allocated under memory pressure) gracefully by keeping the
  unmerged chunk meshes as a fallback.
- Yield to the event loop after each decoded chunk to avoid a tight loop
  that starves rendering and can silently stop the saga middleware
  (redux-saga#1592).
- Don't show an error toast for each failed (and retried) mesh chunk
  request; the final error is still surfaced by the calling saga.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR optimizes precomputed mesh loading by throttling concurrency to 4 segments, yielding event loop control during chunk processing, gracefully handling merge failures, and ensuring GPU memory cleanup on mesh removal. Request retry behavior documentation and changelog entries document these improvements.

Changes

Mesh Loading Optimization and Resource Management

Layer / File(s) Summary
Parallel Loading Configuration and Imports
frontend/javascripts/viewer/constants.ts, frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts
A new constant PARALLEL_MESH_LOADING_SEGMENT_COUNT set to 4 limits concurrent segment loading. Imports updated to include sleep and redux-saga channel utilities.
Token Channel Initialization and Semaphore Control
frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts
Module-level token channel initialized with fixed-size tokens matching the parallel limit. loadPrecomputedMeshForSegmentId acquires tokens before chunk loading and releases in finally block to enforce concurrency.
Event Loop Responsiveness and Merge Error Fallback
frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts, frontend/javascripts/admin/api/mesh.ts
Chunk processing yields to event loop after each iteration via sleep(0). Merged-geometry computation wrapped in try/catch with graceful fallback: on failure, unmerged chunks are kept. Request retry behavior documented to suppress per-attempt error toasts.
GPU Memory Disposal on Mesh Removal
frontend/javascripts/viewer/controller/segment_mesh_controller.ts
New disposeMeshGroup helper releases Three.js geometry and material buffers. Called in removeMeshById to prevent GPU memory leaks when meshes are removed.
Documentation and Changelog
CLAUDE.md, unreleased_changes/9684.md
Frontend formatting command updated to yarn fix-frontend. Changelog documents concurrency throttling, GPU cleanup, merge failure resilience, and retry toast suppression.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • scalableminds/webknossos#9541: Addresses the same mesh-loading issues including mergeGeometries failures, tight addMeshFromGeometry loops, retry error toasts, and uncontrolled parallel segment loads.

Possibly related PRs

Suggested labels

frontend, performance, bug

Suggested reviewers

  • philippotto
  • daniel-wer
  • fm3

Poem

🐰 Four meshes at a time, no more, no less,
GPU memory freed, event loops blessed,
Merge folds gracefully when it must fail,
Concurrent loading tells a swift tale! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Improve robustness and memory usage when loading many meshes' directly and concisely summarizes the PR's main objective of addressing memory and robustness issues in mesh loading.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description comprehensively explains the motivation, implementation details, and testing approach for the changes across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improve-mesh-loading-robustness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hotzenklotz hotzenklotz self-assigned this Jun 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts`:
- Around line 502-513: The catch block currently sets mergedGeometry = null
without disposing the BufferGeometry returned by mergeGeometries, causing a GPU
leak when computeBvhAsync throws; update the catch to check if mergedGeometry is
non-null and call mergedGeometry.dispose() (and also dispose
mergedGeometry.boundsTree if applicable) before setting mergedGeometry = null so
the merged BufferGeometry and any associated BVH resources are released;
reference the mergedGeometry variable and the functions mergeGeometries and
computeBvhAsync to locate the code to update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0dc9b86a-384b-49a8-b3fd-ae3786e1b719

📥 Commits

Reviewing files that changed from the base of the PR and between 10382f3 and 5fecafd.

📒 Files selected for processing (6)
  • CLAUDE.md
  • frontend/javascripts/admin/api/mesh.ts
  • frontend/javascripts/viewer/constants.ts
  • frontend/javascripts/viewer/controller/segment_mesh_controller.ts
  • frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts
  • unreleased_changes/9684.md

Comment on lines +502 to +513
try {
mergedGeometry = (
sortedBufferGeometries.length > 0 ? mergeGeometries(sortedBufferGeometries, false) : null
) as BufferGeometryWithInfo | null;
if (mergedGeometry != null) {
mergedGeometry.vertexSegmentMapping = new VertexSegmentMapping(sortedBufferGeometries);
mergedGeometry.boundsTree = yield* call(computeBvhAsync, mergedGeometry);
}
} catch (exception) {
mergedGeometry = null;
console.error(`Failed to merge mesh chunks for segment ${segmentId}:`, exception);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts"

# Show the exact section around the reported lines
sed -n '450,560p' "$FILE" | nl -ba

# Locate any other disposal/cleanup of mergedGeometry or buffer geometries
rg -n "mergedGeometry|dispose\(|BufferGeometryWithInfo" "$FILE"

# Find definition of BufferGeometryWithInfo (type/interface/class)
rg -n "type BufferGeometryWithInfo|interface BufferGeometryWithInfo|class BufferGeometryWithInfo|BufferGeometryWithInfo" frontend/javascripts/viewer -S

# Find computeBvhAsync implementation/usage
rg -n "computeBvhAsync" frontend/javascripts/viewer -S

# Find mergeGeometries usage and whether callers dispose merged outputs
rg -n "mergeGeometries\(" frontend/javascripts/viewer -S

Repository: scalableminds/webknossos

Length of output: 111


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts"

# Section around the reported lines (with awk line numbers)
sed -n '450,560p' "$FILE" | awk '{printf "%6d\t%s\n", NR+449, $0}'

echo "----"
# Locate disposal/cleanup related to mergedGeometry
rg -n "mergedGeometry|dispose\(|new VertexSegmentMapping|boundsTree" "$FILE"

echo "----"
# Find BufferGeometryWithInfo definition
rg -n "type BufferGeometryWithInfo|interface BufferGeometryWithInfo|class BufferGeometryWithInfo|BufferGeometryWithInfo" frontend/javascripts/viewer -S

echo "----"
# Find computeBvhAsync definition/usages
rg -n "function\s+computeBvhAsync|computeBvhAsync\s*\(" frontend/javascripts/viewer -S

echo "----"
# Find mergeGeometries definition/usages
rg -n "function\s+mergeGeometries|mergeGeometries\s*\(" frontend/javascripts/viewer -S

Repository: scalableminds/webknossos

Length of output: 7256


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE_SAGA="frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts"
FILE_HELPERS="frontend/javascripts/viewer/controller/mesh_helpers.ts"
FILE_CONTROLLER="frontend/javascripts/viewer/controller/segment_mesh_controller.ts"

echo "---- mesh_helpers.ts (computeBvhAsync, types)"
sed -n '1,220p' "$FILE_HELPERS" | awk '{print NR "\t" $0}'

echo "---- segment_mesh_controller.ts (addMeshFromGeometry/removeMeshById)"
# Print a focused region where add/remove are implemented
rg -n "addMeshFromGeometry|removeMeshById|computeBvhAsync" "$FILE_CONTROLLER"
sed -n '120,260p' "$FILE_CONTROLLER" | awk '{print NR "\t" $0}'

echo "---- precomputed_mesh_saga.ts imports for mergeGeometries/computeBvhAsync"
sed -n '1,120p' "$FILE_SAGA" | awk '{print NR "\t" $0}'

echo "---- find mergeGeometries import source and any local helpers"
rg -n "mergeGeometries" "$FILE_SAGA" "$FILE_CONTROLLER" "$FILE_HELPERS"

Repository: scalableminds/webknossos

Length of output: 16273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE_CONTROLLER="frontend/javascripts/viewer/controller/segment_mesh_controller.ts"
FILE_SAGA="frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts"

echo "---- occurrences of dispose() in controller"
rg -n "dispose\(" "$FILE_CONTROLLER"

echo "---- implementation of removeMeshById and any geometry disposal"
# print around removeMeshById function
rg -n "removeMeshById\(" "$FILE_CONTROLLER"
# rough region around it
sed -n '260,360p' "$FILE_CONTROLLER" | awk '{print NR+259 "\t" $0}'

echo "---- any disposal/cleanup for mergedGeometry in saga"
rg -n "mergedGeometry\.dispose|dispose\(" "$FILE_SAGA"
rg -n "Falling back to the unmerged mesh chunks|mergedGeometry == null" "$FILE_SAGA"
sed -n '480,540p' "$FILE_SAGA" | awk '{print NR+479 "\t" $0}'

Repository: scalableminds/webknossos

Length of output: 4519


Dispose merged geometry when merge/BVH fails

In the catch block, mergedGeometry is set to null without calling dispose(). On this path the merged geometry was never handed to segmentMeshController/scene management (and removeMeshById/disposal isn’t invoked because the saga returns immediately), so the allocated Three.js BufferGeometry from mergeGeometries can leak GPU resources when computeBvhAsync throws.

Proposed fix
   let mergedGeometry: BufferGeometryWithInfo | null = null;
   try {
     mergedGeometry = (
       sortedBufferGeometries.length > 0 ? mergeGeometries(sortedBufferGeometries, false) : null
     ) as BufferGeometryWithInfo | null;
     if (mergedGeometry != null) {
       mergedGeometry.vertexSegmentMapping = new VertexSegmentMapping(sortedBufferGeometries);
       mergedGeometry.boundsTree = yield* call(computeBvhAsync, mergedGeometry);
     }
   } catch (exception) {
+    mergedGeometry?.dispose();
     mergedGeometry = null;
     console.error(`Failed to merge mesh chunks for segment ${segmentId}:`, exception);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/javascripts/viewer/model/sagas/meshes/precomputed_mesh_saga.ts`
around lines 502 - 513, The catch block currently sets mergedGeometry = null
without disposing the BufferGeometry returned by mergeGeometries, causing a GPU
leak when computeBvhAsync throws; update the catch to check if mergedGeometry is
non-null and call mergedGeometry.dispose() (and also dispose
mergedGeometry.boundsTree if applicable) before setting mergedGeometry = null so
the merged BufferGeometry and any associated BVH resources are released;
reference the mergedGeometry variable and the functions mergeGeometries and
computeBvhAsync to locate the code to update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Loading many meshes at once can cause problems

1 participant