@@ -65,14 +65,30 @@ def intern_frame(f: str) -> int:
6565 frames .append (f )
6666 return frame_to_idx [f ]
6767
68+ id_cache : dict [int , int ] = {}
69+ content_cache : dict [tuple , int ] = {}
70+
6871 def get_stack_idx (raw_frames : list [dict ]) -> int :
72+ raw_id = id (raw_frames )
73+ if raw_id in id_cache :
74+ return id_cache [raw_id ]
75+ content_key = tuple (
76+ (f .get ("filename" , "" ), f .get ("name" , "" ), f .get ("line" , 0 )) for f in raw_frames
77+ )
78+ if content_key in content_cache :
79+ result = content_cache [content_key ]
80+ id_cache [raw_id ] = result
81+ return result
6982 extracted = _extract_frames (raw_frames )
7083 frame_indices = [intern_frame (f ) for f in extracted ]
7184 key = tuple (frame_indices )
7285 if key not in stack_to_idx :
7386 stack_to_idx [key ] = len (stacks )
7487 stacks .append (frame_indices )
75- return stack_to_idx [key ]
88+ result = stack_to_idx [key ]
89+ id_cache [raw_id ] = result
90+ content_cache [content_key ] = result
91+ return result
7692
7793 allocated = 0
7894 reserved = 0
@@ -81,6 +97,7 @@ def get_stack_idx(raw_frames: list[dict]) -> int:
8197 last_time = 0
8298
8399 current_stack : list [int ] = []
100+ stack_pos : dict [int , int ] = {}
84101 alloc_id_by_addr : dict [int , int ] = {}
85102
86103 alloc_polys : list [dict ] = []
@@ -107,6 +124,7 @@ def get_stack_idx(raw_frames: list[dict]) -> int:
107124 "offsets" : [offset ],
108125 }
109126 )
127+ stack_pos [alloc_id ] = len (current_stack )
110128 current_stack .append (alloc_id )
111129 alloc_id_by_addr [addr ] = alloc_id
112130 timestep += 1
@@ -119,9 +137,11 @@ def get_stack_idx(raw_frames: list[dict]) -> int:
119137 poly ["ts" ].append (timestep )
120138 poly ["offsets" ].append (poly ["offsets" ][- 1 ])
121139
122- if freed_id in current_stack :
123- idx_in_stack = current_stack . index ( freed_id )
140+ idx_in_stack = stack_pos . pop ( freed_id , None )
141+ if idx_in_stack is not None :
124142 current_stack .pop (idx_in_stack )
143+ for j in range (idx_in_stack , len (current_stack )):
144+ stack_pos [current_stack [j ]] = j
125145 for above_id in current_stack [idx_in_stack :]:
126146 above = alloc_polys [above_id ]
127147 above ["ts" ].append (timestep )
@@ -937,8 +957,49 @@ def generate_memory_html(
937957let searchMatcher = null;
938958let hoveredAlloc = null;
939959
940- // Precompute colors for each alloc
960+ // Precompute colors and start/end arrays for fast access
941961const allocColors = ALLOCS.map(d => getColor(d.si));
962+ const allocStarts = new Float64Array(ALLOCS.length);
963+ const allocEnds = new Float64Array(ALLOCS.length);
964+ for (let i = 0; i < ALLOCS.length; i++) {
965+ allocStarts[i] = ALLOCS[i].ts[0];
966+ allocEnds[i] = ALLOCS[i].ts[ALLOCS[i].ts.length - 1];
967+ }
968+
969+ // Bucket index for O(bucket_size) hit testing instead of O(n)
970+ const NUM_HIT_BUCKETS = Math.max(1, Math.min(2000, META.max_timestep));
971+ const hitBucketSize = META.max_timestep / NUM_HIT_BUCKETS;
972+ const hitBuckets = new Array(NUM_HIT_BUCKETS + 1);
973+ for (let b = 0; b <= NUM_HIT_BUCKETS; b++) hitBuckets[b] = [];
974+ for (let ai = 0; ai < ALLOCS.length; ai++) {
975+ const b0 = Math.max(0, Math.floor(allocStarts[ai] / hitBucketSize));
976+ const b1 = Math.min(NUM_HIT_BUCKETS, Math.floor(allocEnds[ai] / hitBucketSize));
977+ for (let b = b0; b <= b1; b++) hitBuckets[b].push(ai);
978+ }
979+
980+ // Search match cache: precompute on search change instead of per-frame
981+ let searchMatchSet = null;
982+ function updateSearchCache() {
983+ if (!searchMatcher) { searchMatchSet = null; return; }
984+ searchMatchSet = new Set();
985+ for (let ai = 0; ai < ALLOCS.length; ai++) {
986+ const stack = resolveStack(ALLOCS[ai].si);
987+ if (stack.some(f => searchMatcher.test(f))) searchMatchSet.add(ai);
988+ }
989+ }
990+
991+ function tracePoly(ai, newX) {
992+ const d = ALLOCS[ai];
993+ const ts = d.ts, offsets = d.offsets, size = d.s;
994+ ctx.moveTo(newX(ts[0]), yScale(offsets[0]));
995+ for (let i = 1; i < ts.length; i++) {
996+ ctx.lineTo(newX(ts[i]), yScale(offsets[i]));
997+ }
998+ for (let i = ts.length - 1; i >= 0; i--) {
999+ ctx.lineTo(newX(ts[i]), yScale(offsets[i] + size));
1000+ }
1001+ ctx.closePath();
1002+ }
9421003
9431004function drawCanvas() {
9441005 const newX = currentTransform.rescaleX(xScale);
@@ -954,67 +1015,71 @@ def generate_memory_html(
9541015 const pxPerTs = width / (d1 - d0);
9551016 const minVisPx = 0.5;
9561017
1018+ // Batch visible allocs by color+alpha to minimize Canvas state changes
1019+ const batches = {};
1020+ let hoveredIdx = -1;
1021+
9571022 for (let ai = 0; ai < ALLOCS.length; ai++) {
958- const d = ALLOCS[ai];
959- const tStart = d.ts[0];
960- const tEnd = d.ts[d.ts.length - 1];
961- if (tEnd < d0 || tStart > d1) continue;
1023+ if (allocEnds[ai] < d0 || allocStarts[ai] > d1) continue;
9621024
963- const visW = (Math.min(tEnd , d1) - Math.max(tStart , d0)) * pxPerTs;
964- const visH = yScale(0) - yScale(d .s);
1025+ const visW = (Math.min(allocEnds[ai] , d1) - Math.max(allocStarts[ai] , d0)) * pxPerTs;
1026+ const visH = yScale(0) - yScale(ALLOCS[ai] .s);
9651027 if (visW < minVisPx && visH < minVisPx) continue;
9661028
967- const ts = d.ts;
968- const offsets = d.offsets;
969- const size = d.s;
970-
971- ctx.beginPath();
972- // Bottom edge left to right
973- for (let i = 0; i < ts.length; i++) {
974- const x = newX(ts[i]);
975- const y = yScale(offsets[i]);
976- if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
977- }
978- // Top edge right to left
979- for (let i = ts.length - 1; i >= 0; i--) {
980- ctx.lineTo(newX(ts[i]), yScale(offsets[i] + size));
981- }
982- ctx.closePath();
1029+ if (ALLOCS[ai] === hoveredAlloc) { hoveredIdx = ai; continue; }
9831030
9841031 let alpha = 0.85;
985- if (searchMatcher) {
986- const stack = resolveStack(d.si);
987- alpha = stack.some(f => searchMatcher.test(f)) ? 0.9 : 0.06;
1032+ if (searchMatchSet !== null) {
1033+ alpha = searchMatchSet.has(ai) ? 0.9 : 0.06;
9881034 }
989- if (d === hoveredAlloc) alpha = 1.0;
9901035
991- ctx.globalAlpha = alpha;
992- ctx.fillStyle = allocColors[ai];
1036+ const key = allocColors[ai] + alpha;
1037+ if (!batches[key]) batches[key] = { color: allocColors[ai], alpha, indices: [] };
1038+ batches[key].indices.push(ai);
1039+ }
1040+
1041+ for (const batch of Object.values(batches)) {
1042+ ctx.beginPath();
1043+ for (const ai of batch.indices) tracePoly(ai, newX);
1044+ ctx.globalAlpha = batch.alpha;
1045+ ctx.fillStyle = batch.color;
9931046 ctx.fill();
994- ctx.globalAlpha = d === hoveredAlloc ? 1.0 : 0.3;
995- ctx.strokeStyle = d === hoveredAlloc ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.5)';
996- ctx.lineWidth = d === hoveredAlloc ? 1.5 : 0.5;
1047+ ctx.globalAlpha = Math.min(batch.alpha, 0.3);
1048+ ctx.strokeStyle = 'rgba(0,0,0,0.5)';
1049+ ctx.lineWidth = 0.5;
1050+ ctx.stroke();
1051+ }
1052+
1053+ if (hoveredIdx >= 0) {
1054+ ctx.beginPath();
1055+ tracePoly(hoveredIdx, newX);
1056+ ctx.globalAlpha = 1.0;
1057+ ctx.fillStyle = allocColors[hoveredIdx];
1058+ ctx.fill();
1059+ ctx.strokeStyle = 'rgba(255,255,255,0.9)';
1060+ ctx.lineWidth = 1.5;
9971061 ctx.stroke();
9981062 }
9991063
10001064 ctx.restore();
10011065}
10021066
1003- // Hit testing: find allocation under mouse
1067+ // Hit testing: bucket lookup instead of full scan
10041068function hitTest(mx, my) {
10051069 const newX = currentTransform.rescaleX(xScale);
10061070 const dataX = newX.invert(mx - margin.left);
10071071 const dataY = yScale.invert(my - margin.top);
10081072
1073+ const bi = Math.max(0, Math.min(NUM_HIT_BUCKETS, Math.floor(dataX / hitBucketSize)));
1074+ const candidates = hitBuckets[bi];
1075+
10091076 let best = null;
10101077 let bestSize = Infinity;
10111078
1012- for (const d of ALLOCS) {
1013- const tStart = d.ts[0];
1014- const tEnd = d.ts[d.ts.length - 1];
1015- if (dataX < tStart || dataX > tEnd) continue;
1079+ for (const ai of candidates) {
1080+ if (dataX < allocStarts[ai] || dataX > allocEnds[ai]) continue;
10161081
1017- // Find the offset at this timestep via linear search of keyframes
1082+ const d = ALLOCS[ai];
10181083 let offset = d.offsets[0];
10191084 for (let i = 1; i < d.ts.length; i++) {
10201085 if (d.ts[i] > dataX) break;
@@ -1036,10 +1101,9 @@ def generate_memory_html(
10361101function getBaseYDomain(d0, d1) {
10371102 if (yMode === 'autofit') {
10381103 let maxY = 0;
1039- for (const d of ALLOCS) {
1040- const tStart = d.ts[0];
1041- const tEnd = d.ts[d.ts.length - 1];
1042- if (tEnd < d0 || tStart > d1) continue;
1104+ for (let ai = 0; ai < ALLOCS.length; ai++) {
1105+ if (allocEnds[ai] < d0 || allocStarts[ai] > d1) continue;
1106+ const d = ALLOCS[ai];
10431107 for (let i = 0; i < d.ts.length; i++) {
10441108 if (d.ts[i] >= d0 && d.ts[i] <= d1) {
10451109 maxY = Math.max(maxY, d.offsets[i] + d.s);
@@ -1278,6 +1342,7 @@ def generate_memory_html(
12781342 const q = query.toLowerCase();
12791343 searchMatcher = { test: (s) => s.toLowerCase().includes(q) };
12801344 }
1345+ updateSearchCache();
12811346 drawCanvas();
12821347}
12831348
0 commit comments