Skip to content

Fix unbounded tree node growth in XYZ globe image tiles#1555

Closed
WilliamLiu-1997 wants to merge 1 commit intoNASA-AMMOS:masterfrom
WilliamLiu-1997:treeNodeFix
Closed

Fix unbounded tree node growth in XYZ globe image tiles#1555
WilliamLiu-1997 wants to merge 1 commit intoNASA-AMMOS:masterfrom
WilliamLiu-1997:treeNodeFix

Conversation

@WilliamLiu-1997
Copy link
Copy Markdown
Contributor

@WilliamLiu-1997 WilliamLiu-1997 commented Apr 9, 2026

Tree Node Memory Leak in XYZ Globe Image Tiles

Relate to #1554

Problem

When using ImageFormatPlugin (XYZ / TMS / WMS image tile sources) on a globe, the tile node tree grows unboundedly as the camera pans. Unlike standard 3D Tiles where the tree structure is finite and defined by tileset.json, image tile trees are infinite and dynamically generatedImageFormatPlugin.expandChildren creates child nodes on the fly as the quadtree subdivides to meet the screen-space error target.

The root cause is that ensureChildrenArePreprocessed(tile) was called inside resetFrameState (optimized path) and markUsedTiles / recursivelyMarkUsed (non-optimized path). This means every tile visited during traversal unconditionally triggered child creation, even when the traversal ultimately decided not to descend. As the camera panned across the globe, children were created for every visited tile but never reclaimed — the tree grew monotonically.

The leak is in node objects themselves, not in content/textures/geometry (those are managed by the LRU cache). Over time, the accumulated tile node objects cause significant memory growth that does not recover even after returning to a previous view.

Fix

The fix has two parts:

1. Lazy expansion — Move ensureChildrenArePreprocessed from the "visit" phase (resetFrameState / markUsedTiles) to the "descend" phase (canTraverse). Children are only created when the traversal actually decides to refine deeper (error target not yet met). This is a change in the core traversal files and applies to all tile types.

2. Subtree pruning — After each traversal, ImageFormatPlugin walks the tree and removes subtrees where no descendant is retained (not in the LRU cache, not used, not visible, not active). expandChildren is made idempotent (deduplicates against existing children) so that pruned subtrees can be safely re-created on demand when the camera returns. A _imageChildrenComplete flag provides a fast path to skip the dedup check on steady-state frames.

Why this only affects ImageFormatPlugin

Standard 3D Tiles Image Tiles (ImageFormatPlugin)
Tree size Finite, defined by tileset.json Infinite, dynamically generated
Children source JSON parsing (one-time) expandChildren in preprocessNode (on demand)
Can recreate children No (would need to re-fetch JSON) Yes (expandChildren is idempotent)
Node leak No Yes (fixed by this change)

Other plugins that dynamically create children (ImplicitTilingPlugin, QuantizedMeshPlugin, ImageOverlayPlugin) do so in parseTile (after content loads), not in preprocessNode, so they are not affected by the eager expansion in resetFrameState. Their child lifecycle is already managed through LRU eviction callbacks.

Move ensureChildrenArePreprocessed from resetFrameState/markUsedTiles
to canTraverse so children are only created when the traversal decides
to refine deeper. Add per-frame subtree pruning in ImageFormatPlugin
to reclaim node objects that are no longer retained by the LRU cache
or traversal state. Make expandChildren idempotent so pruned subtrees
can be safely re-created on demand.
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