From 81f18ea2e95bc8bf1e1f6675f519ff45590edbf8 Mon Sep 17 00:00:00 2001 From: Uwe Oestermeier Date: Fri, 24 May 2024 10:29:24 +0200 Subject: [PATCH] Fixed memory problems of deepzoom images. --- lib/pixi/app.js | 22 ++++++++-------- lib/pixi/deepzoom/image.js | 44 +++++++++++++++++++++++--------- lib/pixi/deepzoom/loader.js | 10 +++++--- lib/pixi/deepzoom/tileloader.js | 45 ++++++++++++++++++++++++++------- 4 files changed, 85 insertions(+), 36 deletions(-) diff --git a/lib/pixi/app.js b/lib/pixi/app.js index 5af3a86..67d004e 100644 --- a/lib/pixi/app.js +++ b/lib/pixi/app.js @@ -98,7 +98,7 @@ export default class PIXIApp extends PIXI.Application { roundPixels = true, monkeyPatchMapping = true, adaptive = true, - graphql = false, + graphql = false }) { const fullScreen = !width || !height @@ -116,7 +116,7 @@ export default class PIXIApp extends PIXI.Application { resolution, autoResize, backgroundColor, - forceCanvas, + forceCanvas }) this.width = width @@ -137,7 +137,7 @@ export default class PIXIApp extends PIXI.Application { this.graphql = graphql if (fullScreen || autoResize) { console.log('App is in fullScreen mode or autoResize mode') - const resizeDebounced = debounce((event) => this.resizeApp(event), 50) + const resizeDebounced = debounce(event => this.resizeApp(event), 50) window.addEventListener('resize', resizeDebounced) document.body.addEventListener('orientationchange', this.checkOrientation.bind(this)) } @@ -167,25 +167,25 @@ export default class PIXIApp extends PIXI.Application { // GraphQL if (this.graphql && typeof apollo !== 'undefined') { const networkInterface = apollo.createNetworkInterface({ - uri: '/graphql', + uri: '/graphql' }) const wsClient = new subscriptions.SubscriptionClient(`wss://${location.hostname}/subscriptions`, { reconnect: true, - connectionParams: {}, + connectionParams: {} }) const networkInterfaceWithSubscriptions = subscriptions.addGraphQLSubscriptions(networkInterface, wsClient) this.apolloClient = new apollo.ApolloClient({ - networkInterface: networkInterfaceWithSubscriptions, + networkInterface: networkInterfaceWithSubscriptions }) } // progress this._progress = new Progress( Object.assign({ theme: this.theme }, this.progressOpts, { - app: this, + app: this }) ) this._progress.visible = false @@ -214,7 +214,7 @@ export default class PIXIApp extends PIXI.Application { if (value != this.orient) { setTimeout( 100, - function () { + function() { this.orientationChanged(true) }.bind(this) ) @@ -469,7 +469,7 @@ export default class PIXIApp extends PIXI.Application { loadSprites(resources, loaded = null, { resolutionDependent = true, progress = false } = {}) { this.loadTextures( resources, - (textures) => { + textures => { let sprites = new Map() for (let [key, texture] of textures) { @@ -528,7 +528,7 @@ export default class PIXIApp extends PIXI.Application { } if (progress) { - loader.on('progress', (e) => { + loader.on('progress', e => { this.progress(e.progress) }) } @@ -719,7 +719,7 @@ class FpsDisplay extends PIXI.Graphics { fontWeight: 'bold', fill: '#f6f6f6', stroke: '#434f4f', - strokeThickness: 3, + strokeThickness: 3 }) ) this.text.position.set(6, 6) diff --git a/lib/pixi/deepzoom/image.js b/lib/pixi/deepzoom/image.js index 8faa7d9..c982a5c 100755 --- a/lib/pixi/deepzoom/image.js +++ b/lib/pixi/deepzoom/image.js @@ -155,7 +155,7 @@ export class DeepZoomInfo { this.baseURL = this.urlForTile(this.baseLevel, 0, 0, false) if (loadBaseImage) { - this.imageForURL(this.baseURL, (e) => { + this.imageForURL(this.baseURL, e => { this.size = [e.target.naturalWidth, e.target.naturalHeight] this.baseImage = e.target }) @@ -332,7 +332,7 @@ export class DeepZoomImage extends PIXI.Container { useWorker = '', minimumLevel = 0, alpha = 1, - app = window.app, + app = window.app } = {} ) { super() @@ -708,7 +708,7 @@ export class DeepZoomImage extends PIXI.Container { let changed = { added: [], removed: [] } let newNeeded = new Map() let { centerCol, centerRow, needed } = this.neededTiles(tiles, level) - needed.forEach((d) => { + needed.forEach(d => { let [url, col, row] = d newNeeded.set(url, [col, row]) if (!tiles.requested.has(url)) { @@ -856,7 +856,7 @@ export class DeepZoomImage extends PIXI.Container { } hideTilesAboveLevel(level) { - Object.keys(this.tileLayers).forEach((key) => { + Object.keys(this.tileLayers).forEach(key => { let tiles = this.tileLayers[key] if (tiles.level > level) { tiles.visible = false @@ -869,7 +869,7 @@ export class DeepZoomImage extends PIXI.Container { * @param {number} level - The zoom level of the grid */ destroyTilesAboveLevel(level) { - Object.keys(this.tileLayers).forEach((key) => { + Object.keys(this.tileLayers).forEach(key => { let tiles = this.tileLayers[key] if (tiles.level > level && !tiles.keep) { for (let url of tiles.available.keys()) { @@ -882,18 +882,38 @@ export class DeepZoomImage extends PIXI.Container { } destroyAllTiles() { - Object.keys(this.tileLayers).forEach((key) => { + Object.keys(this.tileLayers).forEach(key => { this.destroyTiles(key) }) } + /** Destroys all tiles and laywer above the current level to ensure that the memory can + * be reused. This must be called from time to time to free load worker which otherwise would + * consume memory and threads permanentely. + */ + destroyUnusedTilesAndLayers() { + this.destroyTilesAboveLevel(this.currentLevel) + let candidates = [] + Object.keys(this.tileLayers).forEach(key => { + let tiles = this.tileLayers[key] + if (tiles.level > this.currentLevel) { + candidates.push(key) + } + }) + for (let key of candidates) { + let tiles = this.tileLayers[key] + tiles.destroy() + delete this.tileLayers[key] + } + } + /** * Tint tiles in all layers that are no longer needed * * @memberof DeepZoomImage */ tintObsoleteTiles() { - Object.keys(this.tileLayers).forEach((key) => { + Object.keys(this.tileLayers).forEach(key => { let tiles = this.tileLayers[key] tiles.untintTiles() if (!tiles.keep) { @@ -908,7 +928,7 @@ export class DeepZoomImage extends PIXI.Container { * @memberof DeepZoomImage */ destroyUnneededTiles() { - Object.keys(this.tileLayers).forEach((key) => { + Object.keys(this.tileLayers).forEach(key => { let tiles = this.tileLayers[key] if (!tiles.keep) { tiles.destroyUnneededTiles() @@ -922,7 +942,7 @@ export class DeepZoomImage extends PIXI.Container { * @memberof DeepZoomImage */ destroyObsoleteTiles() { - Object.keys(this.tileLayers).forEach((key) => { + Object.keys(this.tileLayers).forEach(key => { let tiles = this.tileLayers[key] if (!tiles.keep) { tiles.destroyObsoleteTiles() @@ -937,7 +957,7 @@ export class DeepZoomImage extends PIXI.Container { * @memberof DeepZoomImage */ destroyTiles() { - Object.keys(this.tileLayers).forEach((key) => { + Object.keys(this.tileLayers).forEach(key => { let tiles = this.tileLayers[key] if (!tiles.keep) { tiles.destroyTiles(this.quadTrees) @@ -949,7 +969,7 @@ export class DeepZoomImage extends PIXI.Container { * @param {number} level - The zoom level of the grid */ tintTilesBelowLevel(level) { - Object.keys(this.tileLayers).forEach((key) => { + Object.keys(this.tileLayers).forEach(key => { let tiles = this.tileLayers[key] if (tiles.level < level) { tiles.tintTiles(this.quadTrees) @@ -989,7 +1009,7 @@ export class DeepZoomImage extends PIXI.Container { this.fastLoads += 1 this.populateTiles(currentTiles, this.currentLevel, { onlyone: false, - about: event.about, + about: event.about }) if (this.fastLoads == 3) { this.fastLoads = 0 diff --git a/lib/pixi/deepzoom/loader.js b/lib/pixi/deepzoom/loader.js index ff04dd9..b948ce9 100644 --- a/lib/pixi/deepzoom/loader.js +++ b/lib/pixi/deepzoom/loader.js @@ -40,7 +40,7 @@ export class TileLoader { if (this.loaded.has(url)) this.loaded.delete(url) if (this.loading.has(url)) this.loading.delete(url) //Tile.unschedule(url) - this.loadQueue = this.loadQueue.filter((item) => item != url) + this.loadQueue = this.loadQueue.filter(item => item != url) } /** Cancels loading by clearing the load queue **/ @@ -246,7 +246,7 @@ export class RequestTileLoader extends TileLoader { let xhr = new XMLHttpRequest() xhr.open('GET', url, false) xhr.responseType = 'arraybuffer' - xhr.onload = (e) => { + xhr.onload = e => { let CompressedImage = PIXI.compressedTextures.CompressedImage let compressed = CompressedImage.loadFromArrayBuffer(xhr.response, url) let base = new PIXI.BaseTexture(compressed) @@ -317,10 +317,10 @@ export class RequestTileLoader extends TileLoader { export class WorkerTileLoader extends TileLoader { constructor(tiles, workerPath) { super(tiles) - + this.debug = false let worker = (this.worker = new Worker(workerPath)) - worker.onmessage = (event) => { + worker.onmessage = event => { if (event.data.success) { let { url, col, row, buffer } = event.data //console.log("WorkerTileLoader.loaded", url, buffer) @@ -353,11 +353,13 @@ export class WorkerTileLoader extends TileLoader { } cancel() { + if (this.debug) console.log('canceling worker') super.cancel() this.worker.postMessage({ command: 'abort' }) } destroy() { + if (this.debug) console.log('destroying worker') this.worker.postMessage({ command: 'abort' }) this.worker.terminate() this.worker = null diff --git a/lib/pixi/deepzoom/tileloader.js b/lib/pixi/deepzoom/tileloader.js index f72a881..66f869c 100644 --- a/lib/pixi/deepzoom/tileloader.js +++ b/lib/pixi/deepzoom/tileloader.js @@ -1,35 +1,59 @@ let loadQueue = [] +let requestPool = [] let pendingRequests = new Map() +let requestCount = 0 const batchSize = 8 const debug = false +console.log('tileloader.js') +function recycledXMLHttpRequest() { + // https://nullprogram.com/blog/2013/02/08/ + if (requestPool.length > 0) { + return requestPool.pop() + } + requestCount += 1 + if (debug) console.log('create XMLHttpRequest', requestCount) + let xhr = new XMLHttpRequest() + return xhr +} + function load() { while (loadQueue.length > 0 && pendingRequests.size < batchSize) { let tile = loadQueue.shift() let [col, row, url] = tile - let xhr = new XMLHttpRequest() - xhr.responseType = 'arraybuffer' - xhr.onload = (event) => { - pendingRequests.delete(url) + let xhr = recycledXMLHttpRequest() + xhr.onload = event => { let buffer = xhr.response postMessage({ success: true, url, col, row, buffer }, [buffer]) - } - xhr.onerror = (event) => { pendingRequests.delete(url) + } + xhr.onerror = event => { let buffer = null postMessage({ success: false, url, col, row, buffer }) + pendingRequests.delete(url) } + xhr.onreadystatechange = () => { + // In local files, status is 0 upon success in Mozilla Firefox + if (xhr.readyState === XMLHttpRequest.DONE) { + pendingRequests.delete(url) + requestPool.push(xhr) + if (debug) console.log('resuse XMLHttpRequest') + } + } + if (debug) console.log('open XMLHttpRequest') xhr.open('GET', url, true) + xhr.responseType = 'arraybuffer' + if (debug) console.log('send XMLHttpRequest') xhr.send() pendingRequests.set(url, xhr) } if (loadQueue.length > 0) setTimeout(load, 1000 / 120) else { - if (debug) console.log('Ready') + if (debug) console.log('tileloader ready') } } -self.onmessage = (event) => { +self.onmessage = event => { let msg = event.data switch (msg.command) { case 'load': @@ -40,10 +64,13 @@ self.onmessage = (event) => { break case 'abort': loadQueue = [] + requestPool = [] for (let xhr of pendingRequests.values()) { xhr.abort() } - if (debug) console.log('Abort') + pendingRequests.clear() + if (debug) console.log('tileloader aborted') + console.log('tileloader aborted') break default: console.warn('Unknown worker command: ' + msg.command)