Skip to content

Commit 7cd3bb5

Browse files
authored
Fix memory cache (#762)
* Initial pass at fixing the memory cache * Account for loading state in lru cache unload logic * Remove nodes after the fact * Simplify LRUCache eviction logic * Add "loading" field for cache tiles * Handle unloading correctly * Remove log * Don't try to dispose of any items above the max cache sizes unless we have tiles that are still loading * Comment * comments * Comments * Fix tests, ensure we only exit eviction loops when both size conditions are met * Update tests * Update LRUCache.js
1 parent fd64f40 commit 7cd3bb5

File tree

5 files changed

+196
-47
lines changed

5 files changed

+196
-47
lines changed

src/base/TilesRendererBase.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ const lruPriorityCallback = ( a, b ) => {
5252
// dispose of deeper tiles first
5353
return a.__depthFromRenderedParent > b.__depthFromRenderedParent ? 1 : - 1;
5454

55+
} else if ( a.__loadingState !== b.__loadingState ) {
56+
57+
// dispose of tiles that are earlier along in the loading process first
58+
return a.__loadingState > b.__loadingState ? - 1 : 1;
59+
5560
} else if ( a.__lastFrameVisited !== b.__lastFrameVisited ) {
5661

5762
// dispose of least recent tiles first
@@ -646,6 +651,7 @@ export class TilesRendererBase {
646651
console.error( `TilesRenderer : Failed to load tile at url "${ tile.content.uri }".` );
647652
console.error( e );
648653
tile.__loadingState = FAILED;
654+
lruCache.setLoaded( tile, true );
649655

650656
} else {
651657

@@ -736,16 +742,24 @@ export class TilesRendererBase {
736742

737743
stats.parsing --;
738744
tile.__loadingState = LOADED;
745+
lruCache.setLoaded( tile, true );
739746

740-
// if the cache is full due to newly loaded memory then lets discard this tile - it will
741-
// be loaded again later from the disk cache if needed.
742-
if ( lruCache.isFull() ) {
747+
// If the memory of the item hasn't been registered yet then that means the memory usage hasn't
748+
// been accounted for by the cache yet so we need to check if it fits or if we should remove it.
749+
if ( lruCache.getMemoryUsage( tile ) === null ) {
743750

744-
lruCache.remove( tile );
751+
if ( lruCache.isFull() && lruCache.computeMemoryUsageCallback( tile ) > 0 ) {
745752

746-
} else {
753+
// And if the cache is full due to newly loaded memory then lets discard this tile - it will
754+
// be loaded again later from the disk cache if needed.
755+
lruCache.remove( tile );
747756

748-
lruCache.updateMemoryUsage( tile );
757+
} else {
758+
759+
// Otherwise update the item to the latest known value
760+
lruCache.updateMemoryUsage( tile );
761+
762+
}
749763

750764
}
751765

src/base/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
// FAILED is negative so lru cache priority sorting will unload it first
2+
export const FAILED = - 1;
13
export const UNLOADED = 0;
24
export const LOADING = 1;
35
export const PARSING = 2;
46
export const LOADED = 3;
5-
export const FAILED = 4;
67

78
// https://en.wikipedia.org/wiki/World_Geodetic_System
89
// https://en.wikipedia.org/wiki/Flattening

src/three/TilesRenderer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class TilesRenderer extends TilesRendererBase {
7676
this._eventDispatcher = new EventDispatcher();
7777
this._upRotationMatrix = new Matrix4();
7878

79-
this.lruCache.getMemoryUsageCallback = tile => tile.cached.bytesUsed || 0;
79+
this.lruCache.computeMemoryUsageCallback = tile => tile.cached.bytesUsed ?? null;
8080

8181
// flag indicating whether frustum culling should be disabled
8282
this._autoDisableRendererCulling = true;

src/utilities/LRUCache.js

Lines changed: 99 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ class LRUCache {
5252
this.unloadingHandle = - 1;
5353
this.cachedBytes = 0;
5454
this.bytesMap = new Map();
55+
this.loadedSet = new Set();
5556

5657
this._unloadPriorityCallback = null;
57-
this.getMemoryUsageCallback = () => 0;
58+
this.computeMemoryUsageCallback = () => null;
5859

5960
const itemSet = this.itemSet;
6061
this.defaultPriorityCallback = item => itemSet.get( item );
@@ -68,6 +69,12 @@ class LRUCache {
6869

6970
}
7071

72+
getMemoryUsage( item ) {
73+
74+
return this.bytesMap.get( item ) ?? null;
75+
76+
}
77+
7178
add( item, removeCb ) {
7279

7380
if ( this.markUnusedQueued ) {
@@ -98,8 +105,9 @@ class LRUCache {
98105
itemSet.set( item, Date.now() );
99106
callbacks.set( item, removeCb );
100107

101-
const bytes = this.getMemoryUsageCallback( item );
102-
this.cachedBytes += bytes;
108+
// computeMemoryUsageCallback can return "null" if memory usage is not known, yet
109+
const bytes = this.computeMemoryUsageCallback( item );
110+
this.cachedBytes += bytes || 0;
103111
bytesMap.set( item, bytes );
104112

105113
return true;
@@ -113,10 +121,11 @@ class LRUCache {
113121
const itemList = this.itemList;
114122
const bytesMap = this.bytesMap;
115123
const callbacks = this.callbacks;
124+
const loadedSet = this.loadedSet;
116125

117126
if ( itemSet.has( item ) ) {
118127

119-
this.cachedBytes -= bytesMap.get( item );
128+
this.cachedBytes -= bytesMap.get( item ) || 0;
120129
bytesMap.delete( item );
121130

122131
callbacks.get( item )( item );
@@ -126,6 +135,7 @@ class LRUCache {
126135
usedSet.delete( item );
127136
itemSet.delete( item );
128137
callbacks.delete( item );
138+
loadedSet.delete( item );
129139

130140
return true;
131141

@@ -135,6 +145,28 @@ class LRUCache {
135145

136146
}
137147

148+
// Marks whether tiles in the cache have been completely loaded or not. Tiles that have not been completely
149+
// loaded are subject to being disposed early if the cache is full above its max size limits, even if they
150+
// are marked as used.
151+
setLoaded( item, value ) {
152+
153+
const { itemSet, loadedSet } = this;
154+
if ( itemSet.has( item ) ) {
155+
156+
if ( value === true ) {
157+
158+
loadedSet.add( item );
159+
160+
} else {
161+
162+
loadedSet.delete( item );
163+
164+
}
165+
166+
}
167+
168+
}
169+
138170
updateMemoryUsage( item ) {
139171

140172
const itemSet = this.itemSet;
@@ -145,9 +177,9 @@ class LRUCache {
145177

146178
}
147179

148-
this.cachedBytes -= bytesMap.get( item );
180+
this.cachedBytes -= bytesMap.get( item ) || 0;
149181

150-
const bytes = this.getMemoryUsageCallback( item );
182+
const bytes = this.computeMemoryUsageCallback( item );
151183
bytesMap.set( item, bytes );
152184
this.cachedBytes += bytes;
153185

@@ -196,32 +228,44 @@ class LRUCache {
196228
itemList,
197229
itemSet,
198230
usedSet,
231+
loadedSet,
199232
callbacks,
200233
bytesMap,
201234
minBytesSize,
202235
maxBytesSize,
203236
} = this;
204237

205238
const unused = itemList.length - usedSet.size;
239+
const unloaded = itemList.length - loadedSet.size;
206240
const excessNodes = Math.max( Math.min( itemList.length - minSize, unused ), 0 );
207241
const excessBytes = this.cachedBytes - minBytesSize;
208242
const unloadPriorityCallback = this.unloadPriorityCallback || this.defaultPriorityCallback;
209243
let needsRerun = false;
210244

211-
const hasNodesToUnload = excessNodes > 0 && unused > 0 || itemList.length > maxSize;
212-
const hasBytesToUnload = unused && this.cachedBytes > minBytesSize || this.cachedBytes > maxBytesSize;
245+
const hasNodesToUnload = excessNodes > 0 && unused > 0 || unloaded && itemList.length > maxSize;
246+
const hasBytesToUnload = unused && this.cachedBytes > minBytesSize || unloaded && this.cachedBytes > maxBytesSize;
213247
if ( hasBytesToUnload || hasNodesToUnload ) {
214248

215-
// used items should be at the end of the array
249+
// used items should be at the end of the array, "unloaded" items in the middle of the array
216250
itemList.sort( ( a, b ) => {
217251

218252
const usedA = usedSet.has( a );
219253
const usedB = usedSet.has( b );
220254
if ( usedA === usedB ) {
221255

222-
// Use the sort function otherwise
223-
// higher priority should be further to the left
224-
return - unloadPriorityCallback( a, b );
256+
const loadedA = loadedSet.has( a );
257+
const loadedB = loadedSet.has( b );
258+
if ( loadedA === loadedB ) {
259+
260+
// Use the sort function otherwise
261+
// higher priority should be further to the left
262+
return - unloadPriorityCallback( a, b );
263+
264+
} else {
265+
266+
return loadedA ? 1 : - 1;
267+
268+
}
225269

226270
} else {
227271

@@ -241,27 +285,44 @@ class LRUCache {
241285

242286
let removedNodes = 0;
243287
let removedBytes = 0;
244-
while ( true ) {
288+
289+
// evict up to the max node or bytes size, keeping one more item over the max bytes limit
290+
// so the "full" function behaves correctly.
291+
while (
292+
this.cachedBytes - removedBytes > maxBytesSize ||
293+
itemList.length - removedNodes > maxSize
294+
) {
245295

246296
const item = itemList[ removedNodes ];
247-
const bytes = bytesMap.get( item );
297+
const bytes = bytesMap.get( item ) || 0;
298+
if (
299+
usedSet.has( item ) && loadedSet.has( item ) ||
300+
this.cachedBytes - removedBytes - bytes < maxBytesSize &&
301+
itemList.length - removedNodes <= maxSize
302+
) {
248303

249-
// note that these conditions ensure we keep one tile over the byte cap so we can
250-
// align with the the isFull function reports.
304+
break;
305+
306+
}
307+
308+
removedBytes += bytes;
309+
removedNodes ++;
310+
311+
}
251312

252-
// base while condition
253-
const doContinue =
254-
removedNodes < nodesToUnload
255-
|| removedBytes < bytesToUnload
256-
|| this.cachedBytes - removedBytes - bytes > maxBytesSize
257-
|| itemList.length - removedNodes > maxSize;
313+
// evict up to the min node or bytes size, keeping one more item over the min bytes limit
314+
// so we're meeting it
315+
while (
316+
removedBytes < bytesToUnload ||
317+
removedNodes < nodesToUnload
318+
) {
258319

259-
// don't unload any used tiles unless we're above our size cap
320+
const item = itemList[ removedNodes ];
321+
const bytes = bytesMap.get( item ) || 0;
260322
if (
261-
! doContinue
262-
|| removedNodes >= unused
263-
&& this.cachedBytes - removedBytes - bytes <= maxBytesSize
264-
&& itemList.length - removedNodes <= maxSize
323+
usedSet.has( item ) ||
324+
this.cachedBytes - removedBytes - bytes < minBytesSize &&
325+
removedNodes >= nodesToUnload
265326
) {
266327

267328
break;
@@ -271,15 +332,21 @@ class LRUCache {
271332
removedBytes += bytes;
272333
removedNodes ++;
273334

274-
bytesMap.delete( item );
335+
}
336+
337+
// remove the nodes
338+
itemList.splice( 0, removedNodes ).forEach( item => {
339+
340+
this.cachedBytes -= bytesMap.get( item ) || 0;
341+
275342
callbacks.get( item )( item );
343+
bytesMap.delete( item );
276344
itemSet.delete( item );
277345
callbacks.delete( item );
346+
loadedSet.delete( item );
347+
usedSet.delete( item );
278348

279-
}
280-
281-
itemList.splice( 0, removedNodes );
282-
this.cachedBytes -= removedBytes;
349+
} );
283350

284351
// if we didn't remove enough nodes or we still have excess bytes and there are nodes to removed
285352
// then we want to fire another round of unloading

0 commit comments

Comments
 (0)