Skip to content

Commit 2fdedc8

Browse files
committed
Optimize: Replace O(N) blockList based lookups with O(1) blockIndex property
1 parent f488f0b commit 2fdedc8

File tree

4 files changed

+119
-50
lines changed

4 files changed

+119
-50
lines changed

js/activity.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7133,7 +7133,7 @@ class Activity {
71337133
pasteDy = 0;
71347134
const map = new Map();
71357135
for (let i = 0; i < blocksArray.length; i++) {
7136-
const idx = this.blocks.blockList.indexOf(blocksArray[i]);
7136+
const idx = blocksArray[i].blockIndex;
71377137
map.set(
71387138
idx,
71397139
blocksArray[i].connections.filter(blk => blk !== null)

js/block.js

Lines changed: 78 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@ class Block {
274274
this.name = protoblock.name;
275275
this.activity = this.blocks.activity;
276276

277+
// Cached index in blocks.blockList — set when block is added
278+
// to blockList. Avoids O(N) blockList.indexOf() lookups that
279+
// are scattered across the codebase (33+ call sites).
280+
this.blockIndex = -1;
281+
277282
this.collapsed = false; // Is this collapsible block collapsed?
278283
this.inCollapsed = false; // Is this block in a collapsed stack?
279284
this.trash = false; // Is this block in the trash?
@@ -1039,7 +1044,7 @@ class Block {
10391044
generateArtwork(firstTime) {
10401045
// Get the block labels from the protoblock.
10411046
const that = this;
1042-
const thisBlock = this.blocks.blockList.indexOf(this);
1047+
const thisBlock = this.blockIndex;
10431048
let block_label = "";
10441049

10451050
/**
@@ -1071,7 +1076,7 @@ class Block {
10711076

10721077
const __callback = (that, firstTime) => {
10731078
that.activity.refreshCanvas();
1074-
const thisBlock = that.blocks.blockList.indexOf(that);
1079+
const thisBlock = that.blockIndex;
10751080

10761081
if (firstTime) {
10771082
that._loadEventHandlers();
@@ -1289,7 +1294,7 @@ class Block {
12891294
artwork = artwork.replace("arg_label_" + i, this.protoblock.staticLabels[i]);
12901295
}
12911296

1292-
that.blocks.blockArt[that.blocks.blockList.indexOf(that)] = artwork;
1297+
that.blocks.blockArt[that.blockIndex] = artwork;
12931298

12941299
_blockMakeBitmap(artwork, __processBitmap, this);
12951300
}
@@ -1300,7 +1305,7 @@ class Block {
13001305
* @returns {void}
13011306
*/
13021307
_finishImageLoad() {
1303-
// const thisBlock = this.blocks.blockList.indexOf(this);
1308+
// const thisBlock = this.blockIndex;
13041309
let proto, obj, label, attr;
13051310
// Value blocks get a modifiable text label.
13061311
if (SPECIALINPUTS.includes(this.name)) {
@@ -1490,7 +1495,7 @@ class Block {
14901495
* @returns {void}
14911496
*/
14921497
_generateCollapseArtwork(postProcess) {
1493-
const thisBlock = this.blocks.blockList.indexOf(this);
1498+
const thisBlock = this.blockIndex;
14941499

14951500
/**
14961501
* Run the postprocess function after the artwork is loaded.
@@ -1746,7 +1751,7 @@ class Block {
17461751
that._ensureDecorationOnTop();
17471752

17481753
// Save the collapsed block artwork for export.
1749-
that.blocks.blockCollapseArt[that.blocks.blockList.indexOf(that)] = that.collapseArtwork
1754+
that.blocks.blockCollapseArt[that.blockIndex] = that.collapseArtwork
17501755
.replace(/fill_color/g, PALETTEFILLCOLORS[that.protoblock.palette.name])
17511756
.replace(/stroke_color/g, PALETTESTROKECOLORS[that.protoblock.palette.name])
17521757
.replace("block_label", safeSVG(that.collapseText.text));
@@ -2076,7 +2081,7 @@ class Block {
20762081
*/
20772082
getBlockId() {
20782083
// Generate a UID based on the block index into the blockList.
2079-
const number = blockBlocks.blockList.indexOf(this);
2084+
const number = this.blockIndex;
20802085
return "_" + number.toString();
20812086
}
20822087

@@ -2099,7 +2104,7 @@ class Block {
20992104
*/
21002105
loadThumbnail(imagePath) {
21012106
// Load an image thumbnail onto block.
2102-
const thisBlock = this.blocks.blockList.indexOf(this);
2107+
const thisBlock = this.blockIndex;
21032108
const that = this;
21042109

21052110
if (this.blocks.blockList[thisBlock].value === null && imagePath === null) {
@@ -2236,7 +2241,7 @@ class Block {
22362241
collapseToggle() {
22372242
// Find the blocks to collapse/expand inside of a collapable
22382243
// block.
2239-
const thisBlock = this.blocks.blockList.indexOf(this);
2244+
const thisBlock = this.blockIndex;
22402245
this.blocks.findDragGroup(thisBlock);
22412246

22422247
if (this.collapseBlockBitmap === null) {
@@ -2355,7 +2360,7 @@ class Block {
23552360
const intervals = [];
23562361
let i = 0;
23572362

2358-
let c = this.blocks.blockList.indexOf(this),
2363+
let c = this.blockIndex,
23592364
lastIntervalBlock;
23602365
while (c !== null) {
23612366
lastIntervalBlock = c;
@@ -2868,7 +2873,7 @@ class Block {
28682873
*/
28692874
_loadEventHandlers() {
28702875
const that = this;
2871-
const thisBlock = this.blocks.blockList.indexOf(this);
2876+
const thisBlock = this.blockIndex;
28722877

28732878
this._calculateBlockHitArea();
28742879

@@ -2890,6 +2895,12 @@ class Block {
28902895
let locked = false;
28912896
let getInput = window.hasMouse;
28922897

2898+
// Cached drag state: computed once at mousedown, reused during pressmove.
2899+
// This avoids redundant O(N) findDragGroup and O(D) rest2 chain walks
2900+
// on every mouse move event (which fires 60+ times per second).
2901+
let _cachedDragGroup = null;
2902+
let _dragHasRest2 = false;
2903+
28932904
/**
28942905
* Handles the click event on the block container.
28952906
* @param {Event} event - The click event.
@@ -3012,7 +3023,7 @@ class Block {
30123023
that.blocks.mouseDownTime = new Date().getTime();
30133024

30143025
that.blocks.longPressTimeout = setTimeout(() => {
3015-
that.blocks.activeBlock = that.blocks.blockList.indexOf(that);
3026+
that.blocks.activeBlock = that.blockIndex;
30163027
that._triggerLongPress = true;
30173028
that.blocks.triggerLongPress();
30183029
}, LONGPRESSTIME);
@@ -3056,6 +3067,26 @@ class Block {
30563067
x: Math.round(that.container.x - that.original.x),
30573068
y: Math.round(that.container.y - that.original.y)
30583069
};
3070+
3071+
// Pre-compute the drag group and rest2 check once at mousedown.
3072+
// The drag group doesn't change during a drag (blocks only
3073+
// connect/disconnect on mouseup), so caching it avoids
3074+
// redundant O(N) recursive traversal on every mouse move.
3075+
that.blocks.findDragGroup(thisBlock);
3076+
_cachedDragGroup = that.blocks.dragGroup.slice();
3077+
3078+
// Pre-compute whether the chain below contains a rest2 block.
3079+
// This avoids walking the entire connection chain on every
3080+
// mouse move event.
3081+
_dragHasRest2 = false;
3082+
let checkBlock = that.blocks.blockList[that.connections[1]];
3083+
while (checkBlock != null) {
3084+
if (checkBlock?.name === "rest2") {
3085+
_dragHasRest2 = true;
3086+
break;
3087+
}
3088+
checkBlock = checkBlock?.blocks.blockList[checkBlock.connections[1]];
3089+
}
30593090
});
30603091

30613092
/**
@@ -3079,14 +3110,11 @@ class Block {
30793110
return;
30803111
}
30813112

3082-
// Do not allow a stack of blocks to be dragged if the stack contains a silence block.
3083-
let block = that.blocks.blockList[that.connections[1]];
3084-
while (block != null) {
3085-
if (block?.name === "rest2") {
3086-
this.activity.errorMsg(_("Silence block cannot be removed."), block);
3087-
return;
3088-
}
3089-
block = block?.blocks.blockList[block.connections[1]];
3113+
// Use the cached rest2 check from mousedown instead of
3114+
// walking the chain on every mouse move event.
3115+
if (_dragHasRest2) {
3116+
this.activity.errorMsg(_("Silence block cannot be removed."), that);
3117+
return;
30903118
}
30913119

30923120
if (window.hasMouse) {
@@ -3138,6 +3166,10 @@ class Block {
31383166
that.label.style.display = "none";
31393167
}
31403168

3169+
// Defer checkBounds during the entire block movement to avoid
3170+
// N separate checkBounds calls (one per block in the drag group).
3171+
that.blocks._beginDeferCheckBounds();
3172+
31413173
that.blocks.moveBlockRelative(thisBlock, dx, dy);
31423174

31433175
// If we are over the trash, warn the user.
@@ -3157,17 +3189,19 @@ class Block {
31573189
that.container.setChildIndex(that.text, that.container.children.length - 1);
31583190
}
31593191

3160-
// ...and move any connected blocks.
3161-
that.blocks.findDragGroup(thisBlock);
3162-
if (that.blocks.dragGroup.length > 0) {
3163-
for (let b = 0; b < that.blocks.dragGroup.length; b++) {
3164-
const blk = that.blocks.dragGroup[b];
3192+
// Move connected blocks using the cached drag group from
3193+
// mousedown, avoiding redundant O(N) recursive traversal.
3194+
if (_cachedDragGroup !== null && _cachedDragGroup.length > 0) {
3195+
for (let b = 0; b < _cachedDragGroup.length; b++) {
3196+
const blk = _cachedDragGroup[b];
31653197
if (b !== 0) {
31663198
that.blocks.moveBlockRelative(blk, dx, dy);
31673199
}
31683200
}
31693201
}
31703202

3203+
that.blocks._endDeferCheckBounds();
3204+
31713205
that.activity.refreshCanvas();
31723206
});
31733207

@@ -3191,6 +3225,9 @@ class Block {
31913225
}
31923226
that.blocks.activeBlock = null;
31933227

3228+
// Clear cached drag state.
3229+
_cachedDragGroup = null;
3230+
_dragHasRest2 = false;
31943231
moved = false;
31953232
});
31963233

@@ -3213,6 +3250,9 @@ class Block {
32133250
that.blocks.unhighlight(thisBlock, true);
32143251
that.blocks.activeBlock = null;
32153252

3253+
// Clear cached drag state.
3254+
_cachedDragGroup = null;
3255+
_dragHasRest2 = false;
32163256
moved = false;
32173257
});
32183258
}
@@ -3228,7 +3268,7 @@ class Block {
32283268
* @returns {void}
32293269
*/
32303270
_mouseoutCallback(event, moved, haveClick, hideDOM) {
3231-
const thisBlock = this.blocks.blockList.indexOf(this);
3271+
const thisBlock = this.blockIndex;
32323272
if (!this.activity.logo.runningLilypond) {
32333273
document.body.style.cursor = "default";
32343274
}
@@ -3270,7 +3310,7 @@ class Block {
32703310
// Just in case the blocks are not properly docked after
32713311
// the move (workaround for issue #38 -- Blocks fly
32723312
// apart). Still need to get to the root cause.
3273-
this.blocks.adjustDocks(this.blocks.blockList.indexOf(this), true);
3313+
this.blocks.adjustDocks(this.blockIndex, true);
32743314
}
32753315
} else if (SPECIALINPUTS.includes(this.name) || ["media", "loadFile"].includes(this.name)) {
32763316
if (!haveClick) {
@@ -3328,7 +3368,7 @@ class Block {
33283368
}
33293369

33303370
// Numeric pie menus
3331-
const blk = this.blocks.blockList.indexOf(this);
3371+
const blk = this.blockIndex;
33323372

33333373
if (this.blocks.octaveNumber(blk)) {
33343374
return true;
@@ -3385,7 +3425,7 @@ class Block {
33853425
return false;
33863426
}
33873427

3388-
return this.blocks.blockList[cblk].connections[1] === this.blocks.blockList.indexOf(this);
3428+
return this.blocks.blockList[cblk].connections[1] === this.blockIndex;
33893429
}
33903430

33913431
/**
@@ -3407,7 +3447,7 @@ class Block {
34073447
return false;
34083448
}
34093449

3410-
return this.blocks.blockList[cblk].connections[2] === this.blocks.blockList.indexOf(this);
3450+
return this.blocks.blockList[cblk].connections[2] === this.blockIndex;
34113451
}
34123452

34133453
/**
@@ -3429,7 +3469,7 @@ class Block {
34293469
return false;
34303470
}
34313471

3432-
return this.blocks.blockList[cblk].connections[3] === this.blocks.blockList.indexOf(this);
3472+
return this.blocks.blockList[cblk].connections[3] === this.blockIndex;
34333473
}
34343474

34353475
/**
@@ -3976,7 +4016,7 @@ class Block {
39764016
} else {
39774017
// If the number block is connected to a pitch block, then
39784018
// use the pie menu for octaves. Other special cases as well.
3979-
const blk = this.blocks.blockList.indexOf(this);
4019+
const blk = this.blockIndex;
39804020
if (this.blocks.octaveNumber(blk)) {
39814021
piemenuNumber(this, [8, 7, 6, 5, 4, 3, 2, 1], this.value);
39824022
} else if (this.blocks.noteValueNumber(blk, 2)) {
@@ -4124,7 +4164,7 @@ class Block {
41244164
}
41254165
}
41264166

4127-
// const blk = this.blocks.blockList.indexOf(this);
4167+
// const blk = this.blockIndex;
41284168
if (!this._usePiemenu()) {
41294169
let focused = false;
41304170

@@ -4257,7 +4297,7 @@ class Block {
42574297
_checkWidgets(closeInput) {
42584298
// Detect if label is changed, then reinit widget windows
42594299
// if they are open.
4260-
const thisBlock = this.blocks.blockList.indexOf(this);
4300+
const thisBlock = this.blockIndex;
42614301
const topBlock = this.blocks.findTopBlock(thisBlock);
42624302
const widgetTitle = document.getElementsByClassName("wftTitle");
42634303
let lockInit = false;
@@ -4436,7 +4476,7 @@ class Block {
44364476
}
44374477

44384478
if (isNaN(this.value)) {
4439-
const thisBlock = this.blocks.blockList.indexOf(this);
4479+
const thisBlock = this.blockIndex;
44404480
this.activity.errorMsg(newValue + ": " + _("Not a number"), thisBlock);
44414481
this.activity.refreshCanvas();
44424482
this.value = oldValue;
@@ -4447,15 +4487,15 @@ class Block {
44474487
this.blocks.blockList[cblk1].name === "pitch" &&
44484488
(this.value > 8 || this.value < 1)
44494489
) {
4450-
const thisBlock = this.blocks.blockList.indexOf(this);
4490+
const thisBlock = this.blockIndex;
44514491
this.activity.errorMsg(_("Octave value must be between 1 and 8."), thisBlock);
44524492
this.activity.refreshCanvas();
44534493
this.label.value = oldValue;
44544494
this.value = oldValue;
44554495
}
44564496

44574497
if (String(this.value).length > 10) {
4458-
const thisBlock = this.blocks.blockList.indexOf(this);
4498+
const thisBlock = this.blockIndex;
44594499
this.activity.errorMsg(_("Numbers can have at most 10 digits."), thisBlock);
44604500
this.activity.refreshCanvas();
44614501
this.label.value = oldValue;
@@ -4563,10 +4603,7 @@ class Block {
45634603
// Check to see which connection we are using in
45644604
// cblock. We only do something if blk is attached to
45654605
// the name connection (1).
4566-
if (
4567-
cblock.connections[1] === this.blocks.blockList.indexOf(this) &&
4568-
closeInput
4569-
) {
4606+
if (cblock.connections[1] === this.blockIndex && closeInput) {
45704607
// If the label was the name of a storein, update the
45714608
// associated box this.blocks and the palette buttons.
45724609
if (this.value !== "box") {

0 commit comments

Comments
 (0)