From 395e92dc05cbeb3f44890c7103a4f837490fe1f6 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Tue, 26 Dec 2017 14:10:02 +0000 Subject: [PATCH] [client] Utilise symbosl to clean up chunk cache & reduce memory usage. Also add primitive chunk unloading system - we'll need to keep an eye on how effective this is. --- Nibriboard/ClientFiles/Chunk.js | 4 +- Nibriboard/ClientFiles/ChunkCache.js | 74 +++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/Nibriboard/ClientFiles/Chunk.js b/Nibriboard/ClientFiles/Chunk.js index 7d187e1..aa52454 100644 --- a/Nibriboard/ClientFiles/Chunk.js +++ b/Nibriboard/ClientFiles/Chunk.js @@ -22,6 +22,8 @@ class Chunk this.size = inSize; /** @type {[ { Points: [Vector], Width: number, Color: string }] */ this.lines = []; + /** The last time this chunk was seen on (or near) the user's screen. @type {Date} */ + this.lastSeen = new Date(); } /** @@ -111,7 +113,7 @@ class Chunk update(dt) { - + this.lastSeen = new Date(); } /** diff --git a/Nibriboard/ClientFiles/ChunkCache.js b/Nibriboard/ClientFiles/ChunkCache.js index 8d8d43c..1ce42f5 100644 --- a/Nibriboard/ClientFiles/ChunkCache.js +++ b/Nibriboard/ClientFiles/ChunkCache.js @@ -16,6 +16,14 @@ class ChunkCache this.showRenderedChunks = false; this.boardWindow.rippleLink.on("ChunkUpdate", this.handleChunkUpdate.bind(this)); + + /** A few presetsymbols for various non-chunk entries in the cache. */ + this.cacheTypes = { + /** An empty chunk @type {Symbol} */ + empty: Symbol("empty-chunk"), + /** A chunk that's been requested from the server, but hasn't arrived yet. @type {Symbol} */ + requested: Symbol("requested-chunk") + } } /** @@ -31,7 +39,7 @@ class ChunkCache return null; let requestedChunk = this.cache.get(chunkRef.toString()); - if(!(requestedChunk instanceof Chunk)) { + if(!this.isChunk(requestedChunk)) { if(!quiet) console.warn(`Attempt to access a chunk at ${chunkRef} that's not loaded yet.`); return null; @@ -52,7 +60,7 @@ class ChunkCache let currentChunk = this.fetchChunk(startingChunkRef); let nextUniqueId = lineUniqueId; - while(currentChunk instanceof Chunk) + while(currentChunk instanceof Chunk) // No need to search empty chunks { let nextLineFragment = currentChunk.getLineByUniqueId(nextUniqueId); if(nextLineFragment == null) @@ -70,10 +78,14 @@ class ChunkCache return lineFragments; } + /** + * Deletes all the stray chunk requests we've filed in the chunk cache. + * @return {[type]} [description] + */ clearRequestedChunks() { for(let [chunkRefStr, chunk] of this.cache.entries()) { - if(!(chunk instanceof Chunk)) + if(chunk == this.cacheTypes.requested) this.cache.delete(chunkRefStr); } } @@ -97,6 +109,44 @@ class ChunkCache return !hasChunk; } + /** + * Prunes the cache of all the chunks that haven't been seen on (or near) + * the screen for at least the specified number of milliseconds. + * @param {number} msSinceSeen The minimum number of milliseconds since a chunk as been seen for it to be unloaded. + * @return {number} The number of chunks unloaded. + */ + prune(msSinceSeen) + { + let now = +new Date(), chunksPruned = 0; + for(let [cRefStr, chunk] of this.cache) { + // Skip over symbols and other such oddities + // future: handle these + if(!(chunk instanceof Chunk)) + continue; + + if(typeof chunk.lastSeen == "undefined") debugger; + + if(now - chunk.lastSeen.getTime() >= msSinceSeen) { + this.cache.delete(cRefStr); + chunksPruned++; + } + } + return chunksPruned; + } + + /** + * Works out whether _thing_ is a chunk or not. + * @param {object} thing The thing to analyze. + * @return {bool} Whether _thing_ is a chunk or not. + */ + isChunk(thing) { + if(thing instanceof Chunk) + return true; + if(thing === this.cacheTypes.empty) + return true; + return false; + } + /** * Updates the chunk cache ready for the next frame. * @param {number} dt The amount of time, in milliseconds, since that last frame was rendered. @@ -117,10 +167,15 @@ class ChunkCache this.boardWindow.currentPlaneName, cx / chunkSize, cy / chunkSize ); - if(!this.cache.has(cChunk.toString())) { + let chunk = this.cache.get(cChunk.toString()); + if(!this.isChunk(chunk) && chunk != this.cacheTypes.requested) { console.info(`Requesting ${cChunk}`); missingChunks.push(cChunk); - this.cache.set(cChunk.toString(), { requestedFromServer: true }); + this.cache.set(cChunk.toString(), this.cacheTypes.requested); + } + else if(chunk instanceof Chunk){ + // It's a real (non-empty), so update it + chunk.update(dt); } } } @@ -134,6 +189,12 @@ class ChunkCache "ForgottenChunks": missingChunks }); } + + // Prune chunks from the cache that haven't been accessed in 60 + // seconds or more + let prunedChunkCount = this.prune(60 * 1000); + if(prunedChunkCount > 0) + console.debug(`Pruned ${prunedChunkCount} from the local chunk cache, leaving ${this.cache.size} entries total remaining.`); } /** @@ -185,7 +246,7 @@ class ChunkCache chunkData.Location.Y ); - console.info(`Chunk Update @ ${newChunkRef}`) + console.info(`Chunk Update @ ${newChunkRef}`); let newChunk = new Chunk(newChunkRef, chunkData.Size); let newLines = chunkData.lines.map((line) => { @@ -229,7 +290,6 @@ ChunkCache.CalculateChunkArea = function(visibleArea, chunkSize) { (Math.ceil((Math.abs(visibleArea.x) + (visibleArea.width)) / chunkSize) * chunkSize), (Math.ceil((Math.abs(visibleArea.y) + (visibleArea.height)) / chunkSize) * chunkSize) ); - } export default ChunkCache;