Skip to content

Commit fd9f115

Browse files
committed
perf
1 parent 781d1bd commit fd9f115

1 file changed

Lines changed: 110 additions & 45 deletions

File tree

transformer_nuggets/utils/memory_viz.py

Lines changed: 110 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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(
937957
let searchMatcher = null;
938958
let hoveredAlloc = null;
939959
940-
// Precompute colors for each alloc
960+
// Precompute colors and start/end arrays for fast access
941961
const 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
9431004
function 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
10041068
function 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(
10361101
function 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

Comments
 (0)