Skip to content

Commit 09ee79b

Browse files
authored
Improve memory cleanup performance (#506)
# Why? The memory cleanup routine would resort on every run, this is quite expensive to do during frame generation time. Instead keep a list of orphaned textures, once we need to clean up pick the first one added to that list until we reach our threshold using a simple while loop.
2 parents 3f5e0ef + c2a11c8 commit 09ee79b

File tree

5 files changed

+50
-47
lines changed

5 files changed

+50
-47
lines changed

examples/tests/texture-cleanup-critical.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,5 @@ See docs/ManualRegressionTests.md for more information.
7878
cacheId: Math.floor(Math.random() * 100000),
7979
});
8080
screen.textureOptions.preload = true;
81-
}, 10);
81+
}, 100);
8282
}

src/core/CoreTextureManager.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { RenderTexture } from './textures/RenderTexture.js';
2727
import type { Texture } from './textures/Texture.js';
2828
import { EventEmitter } from '../common/EventEmitter.js';
2929
import { getTimeStamp } from './platform.js';
30+
import type { Stage } from './Stage.js';
3031

3132
/**
3233
* Augmentable map of texture class types
@@ -178,6 +179,7 @@ export class CoreTextureManager extends EventEmitter {
178179
private priorityQueue: Array<Texture> = [];
179180
private uploadTextureQueue: Array<Texture> = [];
180181
private initialized = false;
182+
private stage: Stage;
181183

182184
imageWorkerManager: ImageWorkerManager | null = null;
183185
hasCreateImageBitmap = !!self.createImageBitmap;
@@ -208,8 +210,9 @@ export class CoreTextureManager extends EventEmitter {
208210
*/
209211
frameTime = 0;
210212

211-
constructor(numImageWorkers: number) {
213+
constructor(stage: Stage, numImageWorkers: number) {
212214
super();
215+
this.stage = stage;
213216
this.validateCreateImageBitmap()
214217
.then((result) => {
215218
this.hasCreateImageBitmap =
@@ -387,13 +390,19 @@ export class CoreTextureManager extends EventEmitter {
387390
return texture as InstanceType<TextureMap[Type]>;
388391
}
389392

393+
orphanTexture(texture: Texture): void {
394+
this.stage.txMemManager.addToOrphanedTextures(texture);
395+
}
396+
390397
/**
391398
* Override loadTexture to use the batched approach.
392399
*
393400
* @param texture - The texture to load
394401
* @param immediate - Whether to prioritize the texture for immediate loading
395402
*/
396403
loadTexture(texture: Texture, priority?: boolean): void {
404+
this.stage.txMemManager.removeFromOrphanedTextures(texture);
405+
397406
if (texture.state === 'loaded' || texture.state === 'loading') {
398407
return;
399408
}

src/core/Stage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export class Stage {
151151
} = options;
152152

153153
this.eventBus = options.eventBus;
154-
this.txManager = new CoreTextureManager(numImageWorkers);
154+
this.txManager = new CoreTextureManager(this, numImageWorkers);
155155

156156
// Wait for the Texture Manager to initialize
157157
// once it does, request a render

src/core/TextureMemoryManager.ts

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export interface MemoryInfo {
109109
export class TextureMemoryManager {
110110
private memUsed = 0;
111111
private loadedTextures: Map<Texture, number> = new Map();
112+
private orphanedTextures: Texture[] = [];
112113
private criticalThreshold: number;
113114
private targetThreshold: number;
114115
private cleanupInterval: number;
@@ -161,6 +162,36 @@ export class TextureMemoryManager {
161162
}
162163
}
163164

165+
/**
166+
* Add a texture to the orphaned textures list
167+
*
168+
* @param texture - The texture to add to the orphaned textures list
169+
*/
170+
addToOrphanedTextures(texture: Texture) {
171+
// If the texture can be cleaned up, add it to the orphaned textures list
172+
if (texture.preventCleanup === false) {
173+
this.orphanedTextures.push(texture);
174+
}
175+
}
176+
177+
/**
178+
* Remove a texture from the orphaned textures list
179+
*
180+
* @param texture - The texture to remove from the orphaned textures list
181+
*/
182+
removeFromOrphanedTextures(texture: Texture) {
183+
const index = this.orphanedTextures.indexOf(texture);
184+
if (index !== -1) {
185+
this.orphanedTextures.splice(index, 1);
186+
}
187+
}
188+
189+
/**
190+
* Set the memory usage of a texture
191+
*
192+
* @param texture - The texture to set memory usage for
193+
* @param byteSize - The size of the texture in bytes
194+
*/
164195
setTextureMemUse(texture: Texture, byteSize: number) {
165196
if (this.loadedTextures.has(texture)) {
166197
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -193,7 +224,7 @@ export class TextureMemoryManager {
193224
this.lastCleanupTime = this.frameTime;
194225
this.criticalCleanupRequested = false;
195226

196-
if (critical) {
227+
if (critical === true) {
197228
this.stage.queueFrameEvent('criticalCleanup', {
198229
memUsed: this.memUsed,
199230
criticalThreshold: this.criticalThreshold,
@@ -206,48 +237,14 @@ export class TextureMemoryManager {
206237
);
207238
}
208239

209-
/**
210-
* Sort the loaded textures by renderability, then by last touch time.
211-
*
212-
* This will ensure that the array is ordered by the following:
213-
* - Non-renderable textures, starting at the least recently rendered
214-
* - Renderable textures, starting at the least recently rendered
215-
*/
216-
const textures = [...this.loadedTextures.keys()].sort(
217-
(textureA, textureB) => {
218-
const txARenderable = textureA.renderable;
219-
const txBRenderable = textureB.renderable;
220-
if (txARenderable === txBRenderable) {
221-
return (
222-
textureA.lastRenderableChangeTime -
223-
textureB.lastRenderableChangeTime
224-
);
225-
} else if (txARenderable) {
226-
return 1;
227-
} else if (txBRenderable) {
228-
return -1;
229-
}
230-
return 0;
231-
},
232-
);
233-
234240
// Free non-renderable textures until we reach the target threshold
235241
const memTarget = this.targetThreshold;
236242
const txManager = this.stage.txManager;
237-
for (const texture of textures) {
238-
if (texture.renderable) {
239-
// Stop at the first renderable texture (The rest are renderable because of the sort above)
240-
// We don't want to free renderable textures because they will just likely be reloaded in the next frame
241-
break;
242-
}
243-
if (texture.preventCleanup === false) {
244-
texture.free();
245-
txManager.removeTextureFromCache(texture);
246-
}
247-
if (this.memUsed <= memTarget) {
248-
// Stop once we've freed enough textures to reach under the target threshold
249-
break;
250-
}
243+
244+
while (this.memUsed >= memTarget && this.orphanedTextures.length > 0) {
245+
const texture = this.orphanedTextures.shift()!;
246+
texture.free();
247+
txManager.removeTextureFromCache(texture);
251248
}
252249

253250
if (this.memUsed >= this.criticalThreshold) {

src/core/textures/Texture.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,6 @@ export abstract class Texture extends EventEmitter {
170170

171171
readonly renderable: boolean = false;
172172

173-
readonly lastRenderableChangeTime = 0;
174-
175173
public type: TextureType = TextureType.generic;
176174

177175
public preventCleanup = false;
@@ -210,7 +208,6 @@ export abstract class Texture extends EventEmitter {
210208
const newSize = this.renderableOwners.size;
211209
if (newSize > oldSize && newSize === 1) {
212210
(this.renderable as boolean) = true;
213-
(this.lastRenderableChangeTime as number) = this.txManager.frameTime;
214211
this.onChangeIsRenderable?.(true);
215212
this.load();
216213
}
@@ -219,8 +216,8 @@ export abstract class Texture extends EventEmitter {
219216
const newSize = this.renderableOwners.size;
220217
if (newSize < oldSize && newSize === 0) {
221218
(this.renderable as boolean) = false;
222-
(this.lastRenderableChangeTime as number) = this.txManager.frameTime;
223219
this.onChangeIsRenderable?.(false);
220+
this.txManager.orphanTexture(this);
224221
}
225222
}
226223
}

0 commit comments

Comments
 (0)