Skip to content

Commit 2544426

Browse files
authored
UnloadTilesPlugin: Fix memory leak, cleanup (#1493)
* Add comments * Fix memory leak * Fix memory usage tracking * Clear LRUCache on dispose * Add disposal listener cleanup * Adjust memory estimation
1 parent 894c5ff commit 2544426

1 file changed

Lines changed: 52 additions & 14 deletions

File tree

src/three/plugins/UnloadTilesPlugin.js

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,15 @@ export class UnloadTilesPlugin {
5959
this.tiles = tiles;
6060

6161
const { lruCache, deferCallbacks } = this;
62-
deferCallbacks.callback = tile => {
63-
64-
lruCache.markUnused( tile );
65-
lruCache.scheduleUnload( false );
66-
67-
};
6862

6963
const unloadCallback = tile => {
7064

65+
// trigger a tile unload from the GPU if it's not currently visible
7166
const scene = tile.engineData.scene;
7267
const visible = tiles.visibleTiles.has( tile );
73-
7468
if ( ! visible ) {
7569

70+
// extra visible check in case another plugin or system has forced it to be visible
7671
tiles.invokeOnePlugin( plugin => plugin.unloadTileFromGPU && plugin.unloadTileFromGPU( scene, tile ) );
7772

7873
}
@@ -83,47 +78,75 @@ export class UnloadTilesPlugin {
8378

8479
// update lruCache in "update" in case the callback values change
8580
lruCache.unloadPriorityCallback = tiles.lruCache.unloadPriorityCallback;
86-
lruCache.computeMemoryUsageCallback = tiles.lruCache.computeMemoryUsageCallback;
81+
82+
// adjust the settings so we don't reject tiles added
8783
lruCache.minSize = Infinity;
8884
lruCache.maxSize = Infinity;
8985
lruCache.maxBytesSize = Infinity;
86+
87+
// unload all tiles possible at once
9088
lruCache.unloadPercent = 1;
89+
90+
// do not run mark unused without an explicit call
9191
lruCache.autoMarkUnused = false;
9292

9393
};
9494

95-
this._onVisibilityChangeCallback = ( { tile, visible } ) => {
95+
this._onVisibilityChangeCallback = ( { tile, scene, visible } ) => {
9696

9797
if ( visible ) {
9898

99+
// if the tile is visible then do not trigger disposal - for the tile to have at least 1 byte of
100+
// memory usage to ensure the lru cache will always remove the item to reduce memory pressure
99101
lruCache.add( tile, unloadCallback );
102+
lruCache.setMemoryUsage( tile, tiles.calculateBytesUsed( tile, scene ) || 1 );
100103
tiles.markTileUsed( tile );
101104
deferCallbacks.cancel( tile );
102105

103106
} else {
104107

108+
// mark the tile as unused in our cache and trigger an unload after the delay
105109
deferCallbacks.run( tile );
106110

107111
}
108112

109113
};
110114

115+
this._onDisposeModel = ( { tile } ) => {
116+
117+
lruCache.remove( tile );
118+
deferCallbacks.cancel( tile );
119+
120+
};
121+
122+
deferCallbacks.callback = tile => {
123+
124+
// try to unload the tile up to our limit
125+
lruCache.markUnused( tile );
126+
lruCache.scheduleUnload();
127+
128+
};
129+
130+
// initialize all existing tiles
111131
tiles.forEachLoadedModel( ( scene, tile ) => {
112132

113133
const visible = tiles.visibleTiles.has( tile );
114-
this._onVisibilityChangeCallback( { scene, visible } );
134+
this._onVisibilityChangeCallback( { tile, visible } );
115135

116136
} );
117137

118138
tiles.addEventListener( 'tile-visibility-change', this._onVisibilityChangeCallback );
119139
tiles.addEventListener( 'update-before', this._onUpdateBefore );
140+
tiles.addEventListener( 'dispose-model', this._onDisposeModel );
120141

121142
}
122143

123144
unloadTileFromGPU( scene, tile ) {
124145

125146
if ( scene ) {
126147

148+
// disposes of all GPU memory and materials associated with the tile but does not
149+
// close any image bitmaps so we can reupload them as needed
127150
scene.traverse( c => {
128151

129152
if ( c.material ) {
@@ -158,9 +181,19 @@ export class UnloadTilesPlugin {
158181

159182
dispose() {
160183

161-
this.tiles.removeEventListener( 'tile-visibility-change', this._onVisibilityChangeCallback );
162-
this.tiles.removeEventListener( 'update-before', this._onUpdateBefore );
163-
this.deferCallbacks.cancelAll();
184+
const { lruCache, tiles, deferCallbacks } = this;
185+
186+
tiles.removeEventListener( 'tile-visibility-change', this._onVisibilityChangeCallback );
187+
tiles.removeEventListener( 'update-before', this._onUpdateBefore );
188+
tiles.removeEventListener( 'dispose-model', this._onDisposeModel );
189+
deferCallbacks.cancelAll();
190+
191+
// clear the lru cache
192+
lruCache.minBytesSize = 0;
193+
lruCache.minSize = 0;
194+
lruCache.maxSize = 0;
195+
lruCache.markAllUnused();
196+
lruCache.scheduleUnload();
164197

165198
}
166199

@@ -192,7 +225,12 @@ class DeferCallbackManager {
192225

193226
} else {
194227

195-
map.set( tile, setTimeout( () => this.callback( tile ), delay ) );
228+
map.set( tile, setTimeout( () => {
229+
230+
this.callback( tile );
231+
map.delete( tile );
232+
233+
}, delay ) );
196234

197235
}
198236

0 commit comments

Comments
 (0)