Skip to content

Commit 3283b55

Browse files
committed
fix: reduce canvas and bitmap memory by lazy-caching grids and shrinking scroll buffer
- Remove eager bitmap.cache() from _createGrid() — 8 grids were each allocating a 1200x900x4 (~4.3 MB) backing canvas at startup even though at most 1 grid is visible at a time (~35 MB wasted) - Add cache(0,0,1200,900) in _show*() methods so grids are only cached when made visible, and uncache() in _hide*() to free the backing canvas immediately when hidden - Skip trashed blocks in clearCache() to avoid re-creating backing canvases for invisible blocks on every theme/resize event - Uncache trashed block containers in sendStackToTrash() and delete their blockArt/blockCollapseArt SVG strings to free memory - Cap trashStacks undo history at 100 entries to prevent unbounded growth during long editing sessions - Reduce scroll canvas from 3x to 2x viewport dimensions in doScrollXY(), saving ~40 MB at 1920x1080 (75 MB -> 33 MB) - Update scroll boundary clamps to match the new 2x canvas size Estimated RAM savings: ~70-120 MB depending on viewport size and number of trashed blocks.
1 parent 81cefcb commit 3283b55

File tree

3 files changed

+62
-25
lines changed

3 files changed

+62
-25
lines changed

js/activity.js

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2665,13 +2665,15 @@ class Activity {
26652665

26662666
const bitmap = new createjs.Bitmap(img);
26672667
container.addChild(bitmap);
2668-
bitmap.cache(0, 0, 1200, 900);
2668+
// Do NOT cache the bitmap here. Each cached grid allocates a
2669+
// 1200x900x4 = ~4.3 MB backing canvas, and with 8 grids that
2670+
// totals ~35 MB even though at most 1 grid is visible at a time.
2671+
// Instead, we cache lazily in _show*() and uncache in _hide*().
26692672

26702673
bitmap.x = (this.canvas.width - 1200) / 2;
26712674
bitmap.y = (this.canvas.height - 900) / 2;
26722675
bitmap.scaleX = bitmap.scaleY = bitmap.scale = 1;
26732676
bitmap.visible = false;
2674-
bitmap.updateCache();
26752677

26762678
return bitmap;
26772679
};
@@ -4269,6 +4271,11 @@ class Activity {
42694271
*/
42704272
this.clearCache = () => {
42714273
this.blocks.blockList.forEach(block => {
4274+
// Skip trashed blocks — they are hidden and their backing
4275+
// canvases are freed in sendStackToTrash(). Re-caching them
4276+
// here would waste ~0.5–2 MB per trashed block.
4277+
if (block.trash) return;
4278+
42724279
if (block.container) {
42734280
block.container.uncache();
42744281
block.container.cache();
@@ -5427,7 +5434,7 @@ class Activity {
54275434
*/
54285435
this._hideCartesian = () => {
54295436
this.cartesianBitmap.visible = false;
5430-
this.cartesianBitmap.updateCache();
5437+
this.cartesianBitmap.uncache();
54315438
this.update = true;
54325439
};
54335440

@@ -5436,6 +5443,7 @@ class Activity {
54365443
*/
54375444
this._showCartesian = () => {
54385445
this.cartesianBitmap.visible = true;
5446+
this.cartesianBitmap.cache(0, 0, 1200, 900);
54395447
this.cartesianBitmap.updateCache();
54405448
this.update = true;
54415449
};
@@ -5445,7 +5453,7 @@ class Activity {
54455453
*/
54465454
this._hidePolar = () => {
54475455
this.polarBitmap.visible = false;
5448-
this.polarBitmap.updateCache();
5456+
this.polarBitmap.uncache();
54495457
this.update = true;
54505458
};
54515459

@@ -5454,6 +5462,7 @@ class Activity {
54545462
*/
54555463
this._showPolar = () => {
54565464
this.polarBitmap.visible = true;
5465+
this.polarBitmap.cache(0, 0, 1200, 900);
54575466
this.polarBitmap.updateCache();
54585467
this.update = true;
54595468
};
@@ -5514,7 +5523,7 @@ class Activity {
55145523
*/
55155524
this._hideTreble = () => {
55165525
this.trebleBitmap.visible = false;
5517-
this.trebleBitmap.updateCache();
5526+
this.trebleBitmap.uncache();
55185527
this._hideAccidentals();
55195528
this.update = true;
55205529
};
@@ -5524,6 +5533,7 @@ class Activity {
55245533
*/
55255534
this._showTreble = () => {
55265535
this.trebleBitmap.visible = true;
5536+
this.trebleBitmap.cache(0, 0, 1200, 900);
55275537
this.trebleBitmap.updateCache();
55285538
this._hideAccidentals();
55295539
// eslint-disable-next-line no-console
@@ -5573,7 +5583,7 @@ class Activity {
55735583
*/
55745584
this._hideGrand = () => {
55755585
this.grandBitmap.visible = false;
5576-
this.grandBitmap.updateCache();
5586+
this.grandBitmap.uncache();
55775587
this._hideAccidentals();
55785588
this.update = true;
55795589
};
@@ -5583,6 +5593,7 @@ class Activity {
55835593
*/
55845594
this._showGrand = () => {
55855595
this.grandBitmap.visible = true;
5596+
this.grandBitmap.cache(0, 0, 1200, 900);
55865597
this.grandBitmap.updateCache();
55875598
this._hideAccidentals();
55885599
// eslint-disable-next-line no-console
@@ -5631,7 +5642,7 @@ class Activity {
56315642
*/
56325643
this._hideSoprano = () => {
56335644
this.sopranoBitmap.visible = false;
5634-
this.sopranoBitmap.updateCache();
5645+
this.sopranoBitmap.uncache();
56355646
this.update = true;
56365647
};
56375648

@@ -5640,6 +5651,7 @@ class Activity {
56405651
*/
56415652
this._showSoprano = () => {
56425653
this.sopranoBitmap.visible = true;
5654+
this.sopranoBitmap.cache(0, 0, 1200, 900);
56435655
this.sopranoBitmap.updateCache();
56445656
this._hideAccidentals();
56455657
// eslint-disable-next-line no-console
@@ -5689,7 +5701,7 @@ class Activity {
56895701
*/
56905702
this._hideAlto = () => {
56915703
this.altoBitmap.visible = false;
5692-
this.altoBitmap.updateCache();
5704+
this.altoBitmap.uncache();
56935705
this._hideAccidentals();
56945706
this.update = true;
56955707
};
@@ -5703,6 +5715,7 @@ class Activity {
57035715
*/
57045716
this._showAlto = () => {
57055717
this.altoBitmap.visible = true;
5718+
this.altoBitmap.cache(0, 0, 1200, 900);
57065719
this.altoBitmap.updateCache();
57075720
this._hideAccidentals();
57085721
// eslint-disable-next-line no-console
@@ -5752,7 +5765,7 @@ class Activity {
57525765
*/
57535766
this._hideTenor = () => {
57545767
this.tenorBitmap.visible = false;
5755-
this.tenorBitmap.updateCache();
5768+
this.tenorBitmap.uncache();
57565769
this.update = true;
57575770
};
57585771

@@ -5761,6 +5774,7 @@ class Activity {
57615774
*/
57625775
this._showTenor = () => {
57635776
this.tenorBitmap.visible = true;
5777+
this.tenorBitmap.cache(0, 0, 1200, 900);
57645778
this.tenorBitmap.updateCache();
57655779
this._hideAccidentals();
57665780
// eslint-disable-next-line no-console
@@ -5810,7 +5824,7 @@ class Activity {
58105824
*/
58115825
this._hideBass = () => {
58125826
this.bassBitmap.visible = false;
5813-
this.bassBitmap.updateCache();
5827+
this.bassBitmap.uncache();
58145828
this._hideAccidentals();
58155829
this.update = true;
58165830
};
@@ -5820,6 +5834,7 @@ class Activity {
58205834
*/
58215835
this._showBass = () => {
58225836
this.bassBitmap.visible = true;
5837+
this.bassBitmap.cache(0, 0, 1200, 900);
58235838
this.bassBitmap.updateCache();
58245839
this._hideAccidentals();
58255840
// eslint-disable-next-line no-console
@@ -7796,11 +7811,12 @@ define(["domReady!"].concat(MYDEFINES), doc => {
77967811
const initialize = () => {
77977812
// Defensive check for multiple critical globals that may be delayed
77987813
// due to 'defer' execution timing variances.
7799-
const globalsReady = typeof createDefaultStack !== "undefined" &&
7800-
typeof createjs !== "undefined" &&
7801-
typeof Tone !== "undefined" &&
7802-
typeof GIFAnimator !== "undefined" &&
7803-
typeof SuperGif !== "undefined";
7814+
const globalsReady =
7815+
typeof createDefaultStack !== "undefined" &&
7816+
typeof createjs !== "undefined" &&
7817+
typeof Tone !== "undefined" &&
7818+
typeof GIFAnimator !== "undefined" &&
7819+
typeof SuperGif !== "undefined";
78047820

78057821
if (globalsReady) {
78067822
activity.setupDependencies();

js/blocks.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6921,6 +6921,13 @@ class Blocks {
69216921
/** Add this block to the list of blocks in the trash so we can undo this action. */
69226922
this.trashStacks.push(thisBlock);
69236923

6924+
// Cap the undo history to prevent unbounded memory growth.
6925+
// Keep the 100 most recent trashed stacks.
6926+
const MAX_TRASH_UNDO = 100;
6927+
if (this.trashStacks.length > MAX_TRASH_UNDO) {
6928+
this.trashStacks = this.trashStacks.slice(-MAX_TRASH_UNDO);
6929+
}
6930+
69246931
/** Disconnect block. */
69256932
const parentBlock = myBlock.connections[0];
69266933
if (parentBlock != null) {
@@ -6966,6 +6973,21 @@ class Blocks {
69666973
const blk = this.dragGroup[b];
69676974
this.blockList[blk].trash = true;
69686975
this.blockList[blk].hide();
6976+
6977+
// Free the backing canvas memory for trashed blocks.
6978+
// Each cached block holds a bitmap canvas (~0.5-2 MB).
6979+
if (this.blockList[blk].container) {
6980+
this.blockList[blk].container.uncache();
6981+
}
6982+
6983+
// Clean up SVG art strings for trashed blocks to free memory.
6984+
if (this.blockArt[blk]) {
6985+
delete this.blockArt[blk];
6986+
}
6987+
if (this.blockCollapseArt[blk]) {
6988+
delete this.blockCollapseArt[blk];
6989+
}
6990+
69696991
const title = this.blockList[blk].protoblock.staticLabels[0];
69706992
closeBlkWidgets(_(title));
69716993
this.activity.refreshCanvas();

js/turtle-painter.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,14 +1358,16 @@ class Painter {
13581358
turtles.gx = this.turtle.ctx.canvas.width;
13591359
turtles.gy = this.turtle.ctx.canvas.height;
13601360
turtles.canvas1 = document.createElement("canvas");
1361-
turtles.canvas1.width = 3 * this.turtle.ctx.canvas.width;
1362-
turtles.canvas1.height = 3 * this.turtle.ctx.canvas.height;
1361+
// Use 2x viewport instead of 3x to save ~40 MB of memory.
1362+
// At 1920x1080: 2x = 3840x2160x4 = 33 MB vs 3x = 5760x3240x4 = 75 MB.
1363+
turtles.canvas1.width = 2 * this.turtle.ctx.canvas.width;
1364+
turtles.canvas1.height = 2 * this.turtle.ctx.canvas.height;
13631365
turtles.c1ctx = turtles.canvas1.getContext("2d");
13641366
turtles.c1ctx.rect(
13651367
0,
13661368
0,
1367-
3 * this.turtle.ctx.canvas.width,
1368-
3 * this.turtle.ctx.canvas.height
1369+
2 * this.turtle.ctx.canvas.width,
1370+
2 * this.turtle.ctx.canvas.height
13691371
);
13701372
turtles.c1ctx.fillStyle = "#F9F9F9";
13711373
turtles.c1ctx.fill();
@@ -1375,15 +1377,12 @@ class Painter {
13751377

13761378
turtles.gy -= dy;
13771379
turtles.gx -= dx;
1380+
// Clamp to the bounds of the 2x scroll canvas (max offset = 1x viewport)
13781381
turtles.gx =
1379-
2 * this.turtle.ctx.canvas.width > turtles.gx
1380-
? turtles.gx
1381-
: 2 * this.turtle.ctx.canvas.width;
1382+
this.turtle.ctx.canvas.width > turtles.gx ? turtles.gx : this.turtle.ctx.canvas.width;
13821383
turtles.gx = 0 > turtles.gx ? 0 : turtles.gx;
13831384
turtles.gy =
1384-
2 * this.turtle.ctx.canvas.height > turtles.gy
1385-
? turtles.gy
1386-
: 2 * this.turtle.ctx.canvas.height;
1385+
this.turtle.ctx.canvas.height > turtles.gy ? turtles.gy : this.turtle.ctx.canvas.height;
13871386
turtles.gy = 0 > turtles.gy ? 0 : turtles.gy;
13881387

13891388
const newImgData = turtles.c1ctx.getImageData(

0 commit comments

Comments
 (0)