Skip to content

Commit a82f4c4

Browse files
committed
Deduplicate frames for 95% size reduction on large traces
Stack traces now use frame-level deduplication: a shared FRAMES array of unique strings, with STACKS storing integer indices. For a 600K allocation trace this reduces the stacks section from 1.3GB to ~62MB. Also: regex search toggle, title from file stem, tab reset fix.
1 parent e19a214 commit a82f4c4

2 files changed

Lines changed: 49 additions & 21 deletions

File tree

test/test_memory_viz.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
process_snapshot,
1212
)
1313

14+
def _unpack(snapshot, device=0):
15+
timeline, allocs, frames, stacks, categories, max_ts = process_snapshot(snapshot, device)
16+
resolved_stacks = [[frames[fi] for fi in s] for s in stacks]
17+
return timeline, allocs, resolved_stacks, categories, max_ts
18+
1419
DATA_DIR = Path(__file__).parent / "data"
1520
SNAPSHOT_PATH = DATA_DIR / "mini_snapshot.pickle"
1621

@@ -78,13 +83,20 @@ def test_is_cpython_c_frame(self):
7883

7984
class TestProcessSnapshot:
8085
def test_returns_correct_tuple_shape(self, snapshot):
81-
timeline, allocs, stacks, categories, max_ts = process_snapshot(snapshot)
86+
timeline, allocs, frames, stacks, categories, max_ts = process_snapshot(snapshot)
8287
assert len(timeline) > 0
8388
assert len(allocs) > 0
89+
assert len(frames) > 0
8490
assert len(stacks) > 0
8591
assert len(categories) == len(stacks)
8692
assert max_ts > 0
8793

94+
def test_stacks_reference_valid_frame_indices(self, snapshot):
95+
_, _, frames, stacks, *_ = process_snapshot(snapshot)
96+
for stack in stacks:
97+
for fi in stack:
98+
assert 0 <= fi < len(frames)
99+
88100
def test_timeline_fields(self, snapshot):
89101
timeline, *_ = process_snapshot(snapshot)
90102
entry = timeline[0]
@@ -111,15 +123,13 @@ def test_allocated_never_negative(self, snapshot):
111123
assert all(e["a"] >= 0 for e in timeline)
112124

113125
def test_stack_indices_valid(self, snapshot):
114-
timeline, allocs, stacks, *_ = process_snapshot(snapshot)
115-
for e in timeline:
116-
assert 0 <= e["si"] < len(stacks)
126+
_, allocs, _, stacks, *_ = process_snapshot(snapshot)
117127
for a in allocs:
118128
assert 0 <= a["si"] < len(stacks)
119129

120130
def test_empty_device_returns_empty(self, snapshot):
121131
result = process_snapshot(snapshot, device=99)
122-
assert result == ([], [], [], [], 0)
132+
assert result == ([], [], [], [], [], 0)
123133

124134
def test_polygon_offsets_non_negative(self, snapshot):
125135
_, allocs, *_ = process_snapshot(snapshot)

transformer_nuggets/utils/memory_viz.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,29 @@ def _categorize_stack(frames: list[str]) -> str:
5959

6060
def process_snapshot(
6161
snapshot: dict, device: int = 0
62-
) -> tuple[list[dict], list[dict], list[list[str]], list[str], int]:
62+
) -> tuple[list[dict], list[dict], list[str], list[list[int]], list[str], int]:
6363
traces = snapshot.get("device_traces", [])
6464
if device >= len(traces):
65-
return [], [], [], [], 0
66-
67-
stack_to_idx: dict[tuple[str, ...], int] = {}
68-
stacks: list[list[str]] = []
69-
70-
def get_stack_idx(frames: list[dict]) -> int:
71-
extracted = _extract_frames(frames)
72-
key = tuple(extracted)
65+
return [], [], [], [], [], 0
66+
67+
frame_to_idx: dict[str, int] = {}
68+
frames: list[str] = []
69+
stack_to_idx: dict[tuple[int, ...], int] = {}
70+
stacks: list[list[int]] = []
71+
72+
def intern_frame(f: str) -> int:
73+
if f not in frame_to_idx:
74+
frame_to_idx[f] = len(frames)
75+
frames.append(f)
76+
return frame_to_idx[f]
77+
78+
def get_stack_idx(raw_frames: list[dict]) -> int:
79+
extracted = _extract_frames(raw_frames)
80+
frame_indices = [intern_frame(f) for f in extracted]
81+
key = tuple(frame_indices)
7382
if key not in stack_to_idx:
7483
stack_to_idx[key] = len(stacks)
75-
stacks.append(extracted)
84+
stacks.append(frame_indices)
7685
return stack_to_idx[key]
7786

7887
allocated = 0
@@ -159,16 +168,18 @@ def get_stack_idx(frames: list[dict]) -> int:
159168
poly["ts"].append(timestep)
160169
poly["offsets"].append(poly["offsets"][-1])
161170

162-
categories = [_categorize_stack(stack) for stack in stacks]
163-
return timeline, alloc_polys, stacks, categories, timestep
171+
categories = [_categorize_stack([frames[fi] for fi in stack]) for stack in stacks]
172+
return timeline, alloc_polys, frames, stacks, categories, timestep
164173

165174

166175
def generate_memory_html(
167176
snapshot: dict,
168177
device: int = 0,
169178
title: str = "Memory Timeline",
170179
) -> str:
171-
timeline, alloc_polys, stacks, categories, max_ts = process_snapshot(snapshot, device)
180+
timeline, alloc_polys, frames, stacks, categories, max_ts = process_snapshot(
181+
snapshot, device
182+
)
172183
hwm = max((p["h"] for p in timeline), default=0)
173184
hwm_timestep = next((i for i, p in enumerate(timeline) if p["a"] == hwm), 0)
174185

@@ -193,6 +204,7 @@ def generate_memory_html(
193204
_MEMORY_VIZ_TEMPLATE.replace("__TITLE__", title)
194205
.replace("__TIMELINE__", json.dumps(timeline))
195206
.replace("__ALLOCS__", json.dumps(alloc_polys))
207+
.replace("__FRAMES__", json.dumps(frames))
196208
.replace("__STACKS__", json.dumps(stacks))
197209
.replace("__CATEGORIES__", json.dumps(cat_indices))
198210
.replace("__META__", json.dumps(meta))
@@ -683,10 +695,16 @@ def generate_memory_html(
683695
<script>
684696
const TIMELINE = __TIMELINE__;
685697
const ALLOCS = __ALLOCS__;
698+
const FRAMES = __FRAMES__;
686699
const STACKS = __STACKS__;
687700
const CATEGORIES = __CATEGORIES__;
688701
const META = __META__;
689702
703+
function resolveStack(stackIdx) {
704+
const indices = STACKS[stackIdx] || [];
705+
return indices.map(i => FRAMES[i]);
706+
}
707+
690708
function formatBytes(b) {
691709
if (Math.abs(b) >= 1024**3) return (b / 1024**3).toFixed(2) + ' GiB';
692710
if (Math.abs(b) >= 1024**2) return (b / 1024**2).toFixed(1) + ' MiB';
@@ -743,7 +761,7 @@ def generate_memory_html(
743761
}
744762
745763
function bestFrame(stackIdx) {
746-
const stack = STACKS[stackIdx] || [];
764+
const stack = resolveStack(stackIdx);
747765
for (const f of stack) {
748766
if (classifyFrame(f) === 'user') return f;
749767
}
@@ -782,7 +800,7 @@ def generate_memory_html(
782800
function renderStack(stackIdx, label) {
783801
lastStackIdx = stackIdx;
784802
lastStackLabel = label;
785-
const stack = STACKS[stackIdx] || [];
803+
const stack = resolveStack(stackIdx);
786804
detailStats.textContent = label;
787805
if (!stack.length) {
788806
detailBody.innerHTML = '<div class="empty-detail">No frames recorded</div>';
@@ -1025,7 +1043,7 @@ def generate_memory_html(
10251043
el.classed('dimmed', false).classed('highlighted', false);
10261044
return;
10271045
}
1028-
const stack = STACKS[d.si] || [];
1046+
const stack = resolveStack(d.si) || [];
10291047
const match = stack.some(f => matcher.test(f));
10301048
el.classed('dimmed', !match).classed('highlighted', match);
10311049
});

0 commit comments

Comments
 (0)