Fixed memory problems of deepzoom images.

This commit is contained in:
Uwe Oestermeier 2024-05-24 10:29:24 +02:00
parent 13ea23186f
commit 81f18ea2e9
4 changed files with 85 additions and 36 deletions

View File

@ -98,7 +98,7 @@ export default class PIXIApp extends PIXI.Application {
roundPixels = true, roundPixels = true,
monkeyPatchMapping = true, monkeyPatchMapping = true,
adaptive = true, adaptive = true,
graphql = false, graphql = false
}) { }) {
const fullScreen = !width || !height const fullScreen = !width || !height
@ -116,7 +116,7 @@ export default class PIXIApp extends PIXI.Application {
resolution, resolution,
autoResize, autoResize,
backgroundColor, backgroundColor,
forceCanvas, forceCanvas
}) })
this.width = width this.width = width
@ -137,7 +137,7 @@ export default class PIXIApp extends PIXI.Application {
this.graphql = graphql this.graphql = graphql
if (fullScreen || autoResize) { if (fullScreen || autoResize) {
console.log('App is in fullScreen mode or autoResize mode') 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) window.addEventListener('resize', resizeDebounced)
document.body.addEventListener('orientationchange', this.checkOrientation.bind(this)) document.body.addEventListener('orientationchange', this.checkOrientation.bind(this))
} }
@ -167,25 +167,25 @@ export default class PIXIApp extends PIXI.Application {
// GraphQL // GraphQL
if (this.graphql && typeof apollo !== 'undefined') { if (this.graphql && typeof apollo !== 'undefined') {
const networkInterface = apollo.createNetworkInterface({ const networkInterface = apollo.createNetworkInterface({
uri: '/graphql', uri: '/graphql'
}) })
const wsClient = new subscriptions.SubscriptionClient(`wss://${location.hostname}/subscriptions`, { const wsClient = new subscriptions.SubscriptionClient(`wss://${location.hostname}/subscriptions`, {
reconnect: true, reconnect: true,
connectionParams: {}, connectionParams: {}
}) })
const networkInterfaceWithSubscriptions = subscriptions.addGraphQLSubscriptions(networkInterface, wsClient) const networkInterfaceWithSubscriptions = subscriptions.addGraphQLSubscriptions(networkInterface, wsClient)
this.apolloClient = new apollo.ApolloClient({ this.apolloClient = new apollo.ApolloClient({
networkInterface: networkInterfaceWithSubscriptions, networkInterface: networkInterfaceWithSubscriptions
}) })
} }
// progress // progress
this._progress = new Progress( this._progress = new Progress(
Object.assign({ theme: this.theme }, this.progressOpts, { Object.assign({ theme: this.theme }, this.progressOpts, {
app: this, app: this
}) })
) )
this._progress.visible = false this._progress.visible = false
@ -214,7 +214,7 @@ export default class PIXIApp extends PIXI.Application {
if (value != this.orient) { if (value != this.orient) {
setTimeout( setTimeout(
100, 100,
function () { function() {
this.orientationChanged(true) this.orientationChanged(true)
}.bind(this) }.bind(this)
) )
@ -469,7 +469,7 @@ export default class PIXIApp extends PIXI.Application {
loadSprites(resources, loaded = null, { resolutionDependent = true, progress = false } = {}) { loadSprites(resources, loaded = null, { resolutionDependent = true, progress = false } = {}) {
this.loadTextures( this.loadTextures(
resources, resources,
(textures) => { textures => {
let sprites = new Map() let sprites = new Map()
for (let [key, texture] of textures) { for (let [key, texture] of textures) {
@ -528,7 +528,7 @@ export default class PIXIApp extends PIXI.Application {
} }
if (progress) { if (progress) {
loader.on('progress', (e) => { loader.on('progress', e => {
this.progress(e.progress) this.progress(e.progress)
}) })
} }
@ -719,7 +719,7 @@ class FpsDisplay extends PIXI.Graphics {
fontWeight: 'bold', fontWeight: 'bold',
fill: '#f6f6f6', fill: '#f6f6f6',
stroke: '#434f4f', stroke: '#434f4f',
strokeThickness: 3, strokeThickness: 3
}) })
) )
this.text.position.set(6, 6) this.text.position.set(6, 6)

View File

@ -155,7 +155,7 @@ export class DeepZoomInfo {
this.baseURL = this.urlForTile(this.baseLevel, 0, 0, false) this.baseURL = this.urlForTile(this.baseLevel, 0, 0, false)
if (loadBaseImage) { if (loadBaseImage) {
this.imageForURL(this.baseURL, (e) => { this.imageForURL(this.baseURL, e => {
this.size = [e.target.naturalWidth, e.target.naturalHeight] this.size = [e.target.naturalWidth, e.target.naturalHeight]
this.baseImage = e.target this.baseImage = e.target
}) })
@ -332,7 +332,7 @@ export class DeepZoomImage extends PIXI.Container {
useWorker = '', useWorker = '',
minimumLevel = 0, minimumLevel = 0,
alpha = 1, alpha = 1,
app = window.app, app = window.app
} = {} } = {}
) { ) {
super() super()
@ -708,7 +708,7 @@ export class DeepZoomImage extends PIXI.Container {
let changed = { added: [], removed: [] } let changed = { added: [], removed: [] }
let newNeeded = new Map() let newNeeded = new Map()
let { centerCol, centerRow, needed } = this.neededTiles(tiles, level) let { centerCol, centerRow, needed } = this.neededTiles(tiles, level)
needed.forEach((d) => { needed.forEach(d => {
let [url, col, row] = d let [url, col, row] = d
newNeeded.set(url, [col, row]) newNeeded.set(url, [col, row])
if (!tiles.requested.has(url)) { if (!tiles.requested.has(url)) {
@ -856,7 +856,7 @@ export class DeepZoomImage extends PIXI.Container {
} }
hideTilesAboveLevel(level) { hideTilesAboveLevel(level) {
Object.keys(this.tileLayers).forEach((key) => { Object.keys(this.tileLayers).forEach(key => {
let tiles = this.tileLayers[key] let tiles = this.tileLayers[key]
if (tiles.level > level) { if (tiles.level > level) {
tiles.visible = false tiles.visible = false
@ -869,7 +869,7 @@ export class DeepZoomImage extends PIXI.Container {
* @param {number} level - The zoom level of the grid * @param {number} level - The zoom level of the grid
*/ */
destroyTilesAboveLevel(level) { destroyTilesAboveLevel(level) {
Object.keys(this.tileLayers).forEach((key) => { Object.keys(this.tileLayers).forEach(key => {
let tiles = this.tileLayers[key] let tiles = this.tileLayers[key]
if (tiles.level > level && !tiles.keep) { if (tiles.level > level && !tiles.keep) {
for (let url of tiles.available.keys()) { for (let url of tiles.available.keys()) {
@ -882,18 +882,38 @@ export class DeepZoomImage extends PIXI.Container {
} }
destroyAllTiles() { destroyAllTiles() {
Object.keys(this.tileLayers).forEach((key) => { Object.keys(this.tileLayers).forEach(key => {
this.destroyTiles(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 * Tint tiles in all layers that are no longer needed
* *
* @memberof DeepZoomImage * @memberof DeepZoomImage
*/ */
tintObsoleteTiles() { tintObsoleteTiles() {
Object.keys(this.tileLayers).forEach((key) => { Object.keys(this.tileLayers).forEach(key => {
let tiles = this.tileLayers[key] let tiles = this.tileLayers[key]
tiles.untintTiles() tiles.untintTiles()
if (!tiles.keep) { if (!tiles.keep) {
@ -908,7 +928,7 @@ export class DeepZoomImage extends PIXI.Container {
* @memberof DeepZoomImage * @memberof DeepZoomImage
*/ */
destroyUnneededTiles() { destroyUnneededTiles() {
Object.keys(this.tileLayers).forEach((key) => { Object.keys(this.tileLayers).forEach(key => {
let tiles = this.tileLayers[key] let tiles = this.tileLayers[key]
if (!tiles.keep) { if (!tiles.keep) {
tiles.destroyUnneededTiles() tiles.destroyUnneededTiles()
@ -922,7 +942,7 @@ export class DeepZoomImage extends PIXI.Container {
* @memberof DeepZoomImage * @memberof DeepZoomImage
*/ */
destroyObsoleteTiles() { destroyObsoleteTiles() {
Object.keys(this.tileLayers).forEach((key) => { Object.keys(this.tileLayers).forEach(key => {
let tiles = this.tileLayers[key] let tiles = this.tileLayers[key]
if (!tiles.keep) { if (!tiles.keep) {
tiles.destroyObsoleteTiles() tiles.destroyObsoleteTiles()
@ -937,7 +957,7 @@ export class DeepZoomImage extends PIXI.Container {
* @memberof DeepZoomImage * @memberof DeepZoomImage
*/ */
destroyTiles() { destroyTiles() {
Object.keys(this.tileLayers).forEach((key) => { Object.keys(this.tileLayers).forEach(key => {
let tiles = this.tileLayers[key] let tiles = this.tileLayers[key]
if (!tiles.keep) { if (!tiles.keep) {
tiles.destroyTiles(this.quadTrees) tiles.destroyTiles(this.quadTrees)
@ -949,7 +969,7 @@ export class DeepZoomImage extends PIXI.Container {
* @param {number} level - The zoom level of the grid * @param {number} level - The zoom level of the grid
*/ */
tintTilesBelowLevel(level) { tintTilesBelowLevel(level) {
Object.keys(this.tileLayers).forEach((key) => { Object.keys(this.tileLayers).forEach(key => {
let tiles = this.tileLayers[key] let tiles = this.tileLayers[key]
if (tiles.level < level) { if (tiles.level < level) {
tiles.tintTiles(this.quadTrees) tiles.tintTiles(this.quadTrees)
@ -989,7 +1009,7 @@ export class DeepZoomImage extends PIXI.Container {
this.fastLoads += 1 this.fastLoads += 1
this.populateTiles(currentTiles, this.currentLevel, { this.populateTiles(currentTiles, this.currentLevel, {
onlyone: false, onlyone: false,
about: event.about, about: event.about
}) })
if (this.fastLoads == 3) { if (this.fastLoads == 3) {
this.fastLoads = 0 this.fastLoads = 0

View File

@ -40,7 +40,7 @@ export class TileLoader {
if (this.loaded.has(url)) this.loaded.delete(url) if (this.loaded.has(url)) this.loaded.delete(url)
if (this.loading.has(url)) this.loading.delete(url) if (this.loading.has(url)) this.loading.delete(url)
//Tile.unschedule(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 **/ /** Cancels loading by clearing the load queue **/
@ -246,7 +246,7 @@ export class RequestTileLoader extends TileLoader {
let xhr = new XMLHttpRequest() let xhr = new XMLHttpRequest()
xhr.open('GET', url, false) xhr.open('GET', url, false)
xhr.responseType = 'arraybuffer' xhr.responseType = 'arraybuffer'
xhr.onload = (e) => { xhr.onload = e => {
let CompressedImage = PIXI.compressedTextures.CompressedImage let CompressedImage = PIXI.compressedTextures.CompressedImage
let compressed = CompressedImage.loadFromArrayBuffer(xhr.response, url) let compressed = CompressedImage.loadFromArrayBuffer(xhr.response, url)
let base = new PIXI.BaseTexture(compressed) let base = new PIXI.BaseTexture(compressed)
@ -317,10 +317,10 @@ export class RequestTileLoader extends TileLoader {
export class WorkerTileLoader extends TileLoader { export class WorkerTileLoader extends TileLoader {
constructor(tiles, workerPath) { constructor(tiles, workerPath) {
super(tiles) super(tiles)
this.debug = false
let worker = (this.worker = new Worker(workerPath)) let worker = (this.worker = new Worker(workerPath))
worker.onmessage = (event) => { worker.onmessage = event => {
if (event.data.success) { if (event.data.success) {
let { url, col, row, buffer } = event.data let { url, col, row, buffer } = event.data
//console.log("WorkerTileLoader.loaded", url, buffer) //console.log("WorkerTileLoader.loaded", url, buffer)
@ -353,11 +353,13 @@ export class WorkerTileLoader extends TileLoader {
} }
cancel() { cancel() {
if (this.debug) console.log('canceling worker')
super.cancel() super.cancel()
this.worker.postMessage({ command: 'abort' }) this.worker.postMessage({ command: 'abort' })
} }
destroy() { destroy() {
if (this.debug) console.log('destroying worker')
this.worker.postMessage({ command: 'abort' }) this.worker.postMessage({ command: 'abort' })
this.worker.terminate() this.worker.terminate()
this.worker = null this.worker = null

View File

@ -1,35 +1,59 @@
let loadQueue = [] let loadQueue = []
let requestPool = []
let pendingRequests = new Map() let pendingRequests = new Map()
let requestCount = 0
const batchSize = 8 const batchSize = 8
const debug = false 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() { function load() {
while (loadQueue.length > 0 && pendingRequests.size < batchSize) { while (loadQueue.length > 0 && pendingRequests.size < batchSize) {
let tile = loadQueue.shift() let tile = loadQueue.shift()
let [col, row, url] = tile let [col, row, url] = tile
let xhr = new XMLHttpRequest() let xhr = recycledXMLHttpRequest()
xhr.responseType = 'arraybuffer' xhr.onload = event => {
xhr.onload = (event) => {
pendingRequests.delete(url)
let buffer = xhr.response let buffer = xhr.response
postMessage({ success: true, url, col, row, buffer }, [buffer]) postMessage({ success: true, url, col, row, buffer }, [buffer])
}
xhr.onerror = (event) => {
pendingRequests.delete(url) pendingRequests.delete(url)
}
xhr.onerror = event => {
let buffer = null let buffer = null
postMessage({ success: false, url, col, row, buffer }) 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.open('GET', url, true)
xhr.responseType = 'arraybuffer'
if (debug) console.log('send XMLHttpRequest')
xhr.send() xhr.send()
pendingRequests.set(url, xhr) pendingRequests.set(url, xhr)
} }
if (loadQueue.length > 0) setTimeout(load, 1000 / 120) if (loadQueue.length > 0) setTimeout(load, 1000 / 120)
else { else {
if (debug) console.log('Ready') if (debug) console.log('tileloader ready')
} }
} }
self.onmessage = (event) => { self.onmessage = event => {
let msg = event.data let msg = event.data
switch (msg.command) { switch (msg.command) {
case 'load': case 'load':
@ -40,10 +64,13 @@ self.onmessage = (event) => {
break break
case 'abort': case 'abort':
loadQueue = [] loadQueue = []
requestPool = []
for (let xhr of pendingRequests.values()) { for (let xhr of pendingRequests.values()) {
xhr.abort() xhr.abort()
} }
if (debug) console.log('Abort') pendingRequests.clear()
if (debug) console.log('tileloader aborted')
console.log('tileloader aborted')
break break
default: default:
console.warn('Unknown worker command: ' + msg.command) console.warn('Unknown worker command: ' + msg.command)