blob: e725ae1606024adb76caf6863107ca60c3a52499 [file] [log] [blame]
'use strict';
import * as d3 from "https://cdn.skypack.dev/d3@5";
import {axisLeft} from "https://cdn.skypack.dev/d3-axis@1";
import {scaleLinear} from "https://cdn.skypack.dev/d3-scale@1";
import {zoom, zoomIdentity} from "https://cdn.skypack.dev/d3-zoom@1";
import {brushX} from "https://cdn.skypack.dev/d3-brush@1";
const schemeTableau10 = [
'#4e79a7',
'#f28e2c',
'#e15759',
'#76b7b2',
'#59a14f',
'#edc949',
'#af7aa1',
'#ff9da7',
'#9c755f',
'#bab0ab',
];
function version_space() {
const version = {};
return (addr, increment) => {
if (!(addr in version)) {
version[addr] = 0;
}
const r = version[addr];
if (increment) {
version[addr]++;
}
return r;
};
}
function Segment(addr, size, stream, frames, version) {
return {addr, size, stream, version, frames};
}
function Block(addr, size, requested_size, frames, free_requested, version) {
return {addr, size, requested_size, frames, free_requested, version};
}
function EventSelector(outer, events, stack_info, memory_view) {
const events_div = outer
.append('div')
.attr(
'style',
'grid-column: 1; grid-row: 1; overflow: auto; font-family: monospace',
);
const events_selection = events_div
.selectAll('pre')
.data(events)
.enter()
.append('pre')
.text(e => formatEvent(e))
.attr('style', '');
let selected_event_idx = null;
const es = {
select(idx) {
if (selected_event_idx !== null) {
const selected_event = d3.select(
events_div.node().children[selected_event_idx],
);
selected_event.attr('style', '');
}
if (idx !== null) {
const div = d3.select(events_div.node().children[idx]);
div.attr('style', `background-color: ${schemeTableau10[5]}`);
const [reserved, allocated] = memory_view.draw(idx);
const enter = () => eventStack(div.datum(), allocated, reserved);
stack_info.highlight(enter);
div.node().scrollIntoViewIfNeeded(false);
} else {
memory_view.draw(0);
}
selected_event_idx = idx;
},
};
d3.select('body').on('keydown', _e => {
const key = d3.event.key;
const actions = {ArrowDown: 1, ArrowUp: -1};
if (selected_event_idx !== null && key in actions) {
const new_idx = selected_event_idx + actions[key];
es.select(Math.max(0, Math.min(new_idx, events.length - 1)));
d3.event.preventDefault();
}
});
stack_info.register(
events_selection,
t => eventStack(t.datum()),
_t => {},
d => es.select(d.datum().idx),
);
return es;
}
function formatSize(num) {
const orig = num;
// https://stackoverflow.com/questions/1094841/get-human-readable-version-of-file-size
const units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi'];
for (const unit of units) {
if (Math.abs(num) < 1024.0) {
return `${num.toFixed(1)}${unit}B (${orig} bytes)`;
}
num /= 1024.0;
}
return `${num.toFixed(1)}YiB`;
}
function formatAddr(event) {
const prefix = event.action.startsWith('segment') ? 's' : 'b';
return `${prefix}${event.addr.toString(16)}_${event.version}`;
}
function formatEvent(event) {
const stream =
event.stream === null ? '' : `\n (stream ${event.stream})`;
switch (event.action) {
case 'oom':
return `OOM (requested ${formatSize(event.size)}, CUDA has ${formatSize(
event.device_free,
)} memory free)${stream}`;
case 'snapshot':
return 'snapshot';
default:
return `${event.action.padEnd(14)} ${formatAddr(event).padEnd(
18,
)} ${formatSize(event.size)}${stream}`;
}
}
function eventStack(e, allocated, reserved) {
let event = formatEvent(e);
if (reserved !== undefined) {
event = `(${formatSize(allocated)} allocated / ${formatSize(
reserved,
)} reserved)\n${event}`;
}
return event + '\n' + format_frames(e.frames);
}
function hashCode(num) {
const numStr = num.toString();
let hash = 0;
for (let i = 0; i < numStr.length; i++) {
const charCode = numStr.charCodeAt(i);
hash = (hash << 5) - hash + charCode;
hash = hash & hash; // Convert to 32-bit integer
}
return hash;
}
function addStroke(d) {
d.attr('stroke', 'red')
.attr('stroke-width', '2')
.attr('vector-effect', 'non-scaling-stroke');
}
function removeStroke(d) {
d.attr('stroke', '');
}
function calculate_fragmentation(blocks, sorted_segments) {
const sorted_blocks = Object.values(blocks).sort((a, b) => a.addr - b.addr);
let block_i = 0;
let total_size = 0;
let sum_squared_free = 0;
for (const seg of sorted_segments) {
let addr = seg.addr;
total_size += seg.size;
while (
block_i < sorted_blocks.length &&
sorted_blocks[block_i].addr < seg.addr + seg.size
) {
const block = sorted_blocks[block_i];
if (block.addr > addr) {
sum_squared_free += (block.addr - addr) ** 2;
}
addr = block.addr + block.size;
block_i += 1;
}
if (addr < seg.addr + seg.size) {
sum_squared_free += (seg.addr + seg.size - addr) ** 2;
}
}
console.log(sum_squared_free / total_size ** 2);
}
function MemoryView(outer, stack_info, snapshot, device) {
const svg = outer
.append('svg')
.attr('style', 'grid-column: 2; grid-row: 1; width: 100%; height: 100%;')
.attr('viewBox', '0 0 200 100')
.attr('preserveAspectRatio', 'xMinYMin meet');
const g = svg.append('g');
const seg_zoom = zoom();
seg_zoom.on('zoom', () => {
g.attr('transform', d3.event.transform);
});
svg.call(seg_zoom);
const sorted_segments = [];
const block_map = {};
for (const seg of snapshot.segments) {
if (seg.device !== device) {
continue;
}
sorted_segments.push(
Segment(
seg.address,
seg.total_size,
seg.stream,
seg.frames || [],
seg.version,
),
);
for (const b of seg.blocks) {
if (b.state !== 'active_pending_free' && b.state !== 'active_allocated') {
continue;
}
block_map[b.addr] = Block(
b.addr,
b.size,
b.requested_size,
b.frames,
b.state === 'active_pending_free',
b.version,
);
}
}
sorted_segments.sort((x, y) => x.addr - y.addr);
function simulate_memory(idx) {
// create a copy of segments because we edit size properties below
const l_segments = sorted_segments.map(x => {
return {...x};
});
const l_block_map = {...block_map};
function map_segment(merge, seg) {
let idx = l_segments.findIndex(e => e.addr > seg.addr);
if (!merge) {
l_segments.splice(idx, 0, seg);
return;
}
if (idx === -1) {
idx = l_segments.length;
}
l_segments.splice(idx, 0, seg);
if (idx + 1 < l_segments.length) {
const next = l_segments[idx + 1];
if (seg.addr + seg.size === next.addr && seg.stream === next.stream) {
seg.size += next.size;
l_segments.splice(idx + 1, 1);
}
}
if (idx > 0) {
const prev = l_segments[idx - 1];
if (prev.addr + prev.size === seg.addr && prev.stream === seg.stream) {
prev.size += seg.size;
l_segments.splice(idx, 1);
}
}
}
function unmap_segment(merge, seg) {
if (!merge) {
l_segments.splice(
l_segments.findIndex(x => x.addr === seg.addr),
1,
);
return;
}
const seg_end = seg.addr + seg.size;
const idx = l_segments.findIndex(
e => e.addr <= seg.addr && seg_end <= e.addr + e.size,
);
const existing = l_segments[idx];
const existing_end = existing.addr + existing.size;
if (existing.addr === seg.addr) {
existing.addr += seg.size;
existing.size -= seg.size;
if (existing.size === 0) {
l_segments.splice(idx, 1);
}
} else if (existing_end === seg_end) {
existing.size -= seg.size;
} else {
existing.size = seg.addr - existing.addr;
seg.addr = seg_end;
seg.size = existing_end - seg_end;
l_segments.splice(idx + 1, 0, seg);
}
}
const events = snapshot.device_traces[device];
for (let i = events.length - 1; i > idx; i--) {
const event = events[i];
switch (event.action) {
case 'free':
l_block_map[event.addr] = Block(
event.addr,
event.size,
event.size,
event.frames,
false,
event.version,
);
break;
case 'free_requested':
l_block_map[event.addr].free_requested = false;
break;
case 'free_completed':
l_block_map[event.addr] = Block(
event.addr,
event.size,
event.size,
event.frames,
true,
event.version,
);
break;
case 'alloc':
delete l_block_map[event.addr];
break;
case 'segment_free':
case 'segment_unmap':
map_segment(
event.action === 'segment_unmap',
Segment(
event.addr,
event.size,
event.stream,
event.frames,
event.version,
),
);
break;
case 'segment_alloc':
case 'segment_map':
unmap_segment(
event.action === 'segment_map',
Segment(
event.addr,
event.size,
event.stream,
event.frames,
event.version,
),
);
break;
case 'oom':
break;
default:
break;
}
}
const new_blocks = Object.values(l_block_map);
return [l_segments, new_blocks];
}
return {
draw(idx) {
const [segments_unsorted, blocks] = simulate_memory(idx);
g.selectAll('g').remove();
const segment_d = g.append('g');
const block_g = g.append('g');
const block_r = g.append('g');
segment_d.selectAll('rect').remove();
block_g.selectAll('rect').remove();
block_r.selectAll('rect').remove();
const segments = [...segments_unsorted].sort((x, y) =>
x.size === y.size ? x.addr - y.addr : x.size - y.size,
);
const segments_by_addr = [...segments].sort((x, y) => x.addr - y.addr);
const max_size = segments.length === 0 ? 0 : segments.at(-1).size;
const xScale = scaleLinear().domain([0, max_size]).range([0, 200]);
const padding = xScale.invert(1);
let cur_row = 0;
let cur_row_size = 0;
for (const seg of segments) {
seg.occupied = 0;
seg.internal_free = 0;
if (cur_row_size + seg.size > max_size) {
cur_row_size = 0;
cur_row += 1;
}
seg.offset = cur_row_size;
seg.row = cur_row;
cur_row_size += seg.size + padding;
}
const num_rows = cur_row + 1;
const yScale = scaleLinear().domain([0, num_rows]).range([0, 100]);
const segments_selection = segment_d
.selectAll('rect')
.data(segments)
.enter()
.append('rect')
.attr('x', x => xScale(x.offset))
.attr('y', x => yScale(x.row))
.attr('width', x => xScale(x.size))
.attr('height', yScale(4 / 5))
.attr('stroke', 'black')
.attr('stroke-width', '1')
.attr('vector-effect', 'non-scaling-stroke')
.attr('fill', 'white');
stack_info.register(
segments_selection,
d => {
addStroke(d);
const t = d.datum();
const free = t.size - t.occupied;
let internal = '';
if (t.internal_free > 0) {
internal = ` (${(t.internal_free / free) * 100}% internal)`;
}
return (
`s${t.addr.toString(16)}_${t.version}: segment ${formatSize(
t.size,
)} allocated, ` +
`${formatSize(free)} free${internal} (stream ${
t.stream
})\n${format_frames(t.frames)}`
);
},
d => {
d.attr('stroke', 'black')
.attr('stroke-width', '1')
.attr('vector-effect', 'non-scaling-stroke');
},
);
function find_segment(addr) {
let left = 0;
let right = segments_by_addr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (addr < segments_by_addr[mid].addr) {
right = mid - 1;
} else if (
addr >=
segments_by_addr[mid].addr + segments_by_addr[mid].size
) {
left = mid + 1;
} else {
return segments_by_addr[mid];
}
}
return null;
}
for (const b of blocks) {
b.segment = find_segment(b.addr);
b.segment.occupied += b.requested_size;
b.segment.internal_free += b.size - b.requested_size;
}
const block_selection = block_g
.selectAll('rect')
.data(blocks)
.enter()
.append('rect')
.attr('x', x => xScale(x.segment.offset + (x.addr - x.segment.addr)))
.attr('y', x => yScale(x.segment.row))
.attr('width', x => xScale(x.requested_size))
.attr('height', yScale(4 / 5))
.attr('fill', (x, _i) =>
x.free_requested
? 'red'
: schemeTableau10[
Math.abs(hashCode(x.addr)) % schemeTableau10.length
],
);
stack_info.register(
block_selection,
d => {
addStroke(d);
const t = d.datum();
let requested = '';
if (t.free_requested) {
requested = ' (block freed but waiting due to record_stream)';
}
return (
`b${t.addr.toString(16)}_${t.version} ` +
`${formatSize(t.requested_size)} allocation${requested} (stream ${
t.segment.stream
})\n` +
format_frames(t.frames)
);
},
removeStroke,
);
const free_selection = block_r
.selectAll('rect')
.data(blocks)
.enter()
.append('rect')
.attr('x', x =>
xScale(
x.segment.offset + (x.addr - x.segment.addr) + x.requested_size,
),
)
.attr('y', x => yScale(x.segment.row))
.attr('width', x => xScale(x.size - x.requested_size))
.attr('height', yScale(4 / 5))
.attr('fill', (_x, _i) => 'red');
stack_info.register(
free_selection,
d => {
addStroke(d);
const t = d.datum();
return (
`Free space lost due to rounding ${formatSize(
t.size - t.requested_size,
)}` +
` (stream ${t.segment.stream})\n` +
format_frames(t.frames)
);
},
removeStroke,
);
const reserved = segments.reduce((x, y) => x + y.size, 0);
const allocated = blocks.reduce((x, y) => x + y.requested_size, 0);
return [reserved, allocated];
},
};
}
function StackInfo(outer) {
const stack_trace = outer
.append('pre')
.attr('style', 'grid-column: 1 / 3; grid-row: 2; overflow: auto');
let selected = {
enter: () => {
stack_trace.text('');
},
leave: () => {},
};
return {
register(dom, enter, leave = _e => {}, select = _e => {}) {
dom
.on('mouseover', _e => {
selected.leave();
stack_trace.text(enter(d3.select(d3.event.target)));
})
.on('mousedown', _e => {
const obj = d3.select(d3.event.target);
selected = {
enter: () => stack_trace.text(enter(obj)),
leave: () => leave(obj),
};
select(obj);
})
.on('mouseleave', _e => {
leave(d3.select(d3.event.target));
selected.enter();
});
},
highlight(enter, leave = () => {}) {
selected = {enter: () => stack_trace.text(enter()), leave};
selected.enter();
},
};
}
function create_segment_view(dst, snapshot, device) {
const outer = dst
.append('div')
.attr(
'style',
'display: grid; grid-template-columns: 1fr 2fr; grid-template-rows: 2fr 1fr; height: 100%; gap: 10px',
);
const events = snapshot.device_traces[device];
const stack_info = StackInfo(outer);
const memory_view = MemoryView(outer, stack_info, snapshot, device);
const event_selector = EventSelector(outer, events, stack_info, memory_view);
window.requestAnimationFrame(function () {
event_selector.select(events.length > 0 ? events.length - 1 : null);
});
}
function annotate_snapshot(snapshot) {
snapshot.segment_version = version_space();
snapshot.block_version = version_space();
snapshot.categories = [];
const empty_list = [];
let next_stream = 1;
const stream_names = {0: 0};
function stream_name(s) {
if (!(s in stream_names)) {
stream_names[s] = next_stream++;
}
return stream_names[s];
}
const new_traces = [];
for (const device_trace of snapshot.device_traces) {
const new_trace = [];
new_traces.push(new_trace);
for (const t of device_trace) {
if (!('frames' in t)) {
t.frames = empty_list;
}
// set unique version for each time an address is used
// so that ctrl-f can be used to search for the beginning
// and end of allocations and segments
t.stream = stream_name(t.stream);
switch (t.action) {
case 'free_completed':
t.version = snapshot.block_version(t.addr, true);
if (new_trace.length > 0) {
// elide free_requested/free_completed into a single event
const prev = new_trace.at(-1);
if (prev.action === 'free_requested' && prev.addr === t.addr) {
prev.action = 'free';
continue;
}
}
break;
case 'free_requested':
case 'alloc':
t.version = snapshot.block_version(t.addr, false);
break;
case 'segment_free':
case 'segment_unmap':
t.version = snapshot.segment_version(t.addr, true);
break;
case 'segment_alloc':
case 'segment_map':
t.version = snapshot.segment_version(t.addr, false);
break;
default:
break;
}
if ('category' in t && !snapshot.categories.includes(t.category)) {
snapshot.categories.push(t.category);
}
t.idx = new_trace.length;
new_trace.push(t);
}
}
snapshot.device_traces = new_traces;
// if every event was on the default stream, we elide stream printing
if (next_stream == 1) {
for (const device_trace of snapshot.device_traces) {
for (const t of device_trace) {
t.stream = null;
}
}
}
for (const seg of snapshot.segments) {
seg.stream = stream_name(seg.stream);
seg.version = snapshot.segment_version(seg.address, false);
let addr = seg.address;
for (const b of seg.blocks) {
b.addr = addr;
if (!('frames' in b)) {
// legacy format where 'requested_size' may be missing
// and frames might be in history rather than directly on block
if ('history' in b) {
b.frames = b.history[0].frames || empty_list;
b.requested_size = b.requested_size || b.history[0].real_size;
} else {
b.frames = empty_list;
b.requested_size = b.requested_size || b.size;
}
}
b.version = snapshot.block_version(b.addr, false);
addr += b.size;
}
}
if (
snapshot.categories.length > 0 &&
!snapshot.categories.includes('unknown')
) {
snapshot.categores.push('unknown');
}
}
function elideRepeats(frames) {
const result = [];
const length = frames.length;
for (let i = 0; i < length; ) {
let j = i + 1;
const f = frames[i];
while (j < length && f === frames[j]) {
j++;
}
switch (j - i) {
case 1:
result.push(f);
break;
case 2:
result.push(f, f);
break;
default:
result.push(f, `<repeats ${j - i - 1} times>`);
break;
}
i = j;
}
return result;
}
function frameFilter({name, filename}) {
const omitFunctions = [
'unwind::unwind',
'CapturedTraceback::gather',
'gather_with_cpp',
'_start',
'__libc_start_main',
'PyEval_',
'PyObject_',
'PyFunction_',
];
const omitFilenames = [
'core/boxing',
'/Register',
'/Redispatch',
'pythonrun.c',
'Modules/main.c',
'Objects/call.c',
'Objects/methodobject.c',
'pycore_ceval.h',
'ceval.c',
'cpython/abstract.h',
];
for (const of of omitFunctions) {
if (name.includes(of)) {
return false;
}
}
for (const of of omitFilenames) {
if (filename.includes(of)) {
return false;
}
}
return true;
}
function format_frames(frames) {
if (frames.length === 0) {
return (
`This block has no frames. Potential causes:\n` +
`1) This block was allocated before _record_memory_history was enabled.\n` +
`2) The context or stacks passed to _record_memory_history does not include this block. Consider changing context to 'state', 'alloc', or 'all', or changing stacks to 'all'.\n` +
`3) This event occurred during backward, which has no python frames, and memory history did not include C++ frames. Use stacks='all' to record both C++ and python frames.`
);
}
const frame_strings = frames
.filter(frameFilter)
.map(f => `${f.filename}:${f.line}:${f.name}`);
return elideRepeats(frame_strings).join('\n');
}
function process_alloc_data(snapshot, device, plot_segments, max_entries) {
const elements = [];
const initially_allocated = [];
const actions = [];
const addr_to_alloc = {};
const alloc = plot_segments ? 'segment_alloc' : 'alloc';
const [free, free_completed] = plot_segments
? ['segment_free', 'segment_free']
: ['free', 'free_completed'];
for (const e of snapshot.device_traces[device]) {
switch (e.action) {
case alloc:
elements.push(e);
addr_to_alloc[e.addr] = elements.length - 1;
actions.push(elements.length - 1);
break;
case free:
case free_completed:
if (e.addr in addr_to_alloc) {
actions.push(addr_to_alloc[e.addr]);
delete addr_to_alloc[e.addr];
} else {
elements.push(e);
initially_allocated.push(elements.length - 1);
actions.push(elements.length - 1);
}
break;
default:
break;
}
}
for (const seg of snapshot.segments) {
if (seg.device !== device) {
continue;
}
if (plot_segments) {
if (!(seg.address in addr_to_alloc)) {
const element = {
action: 'alloc',
addr: seg.address,
size: seg.total_size,
frames: [],
stream: seg.stream,
version: seg.version,
};
elements.push(element);
initially_allocated.push(elements.length - 1);
}
} else {
for (const b of seg.blocks) {
if (b.state === 'active_allocated' && !(b.addr in addr_to_alloc)) {
const element = {
action: 'alloc',
addr: b.addr,
size: b.requested_size,
frames: b.frames,
stream: seg.stream,
version: b.version,
};
elements.push(element);
initially_allocated.push(elements.length - 1);
}
}
}
}
initially_allocated.reverse();
// if there are no actions, the graph will be blank,
// but if there are existing allocations we do not want to hide them
// by having just one allocate action it will show a flat graph with all segments
if (actions.length === 0 && initially_allocated.length > 0) {
actions.push(initially_allocated.pop());
}
const current = [];
const current_data = [];
const data = [];
let max_size = 0;
let total_mem = 0;
let total_summarized_mem = 0;
let timestep = 0;
const max_at_time = [];
const summarized_mem = {
elem: 'summarized',
timesteps: [],
offsets: [total_mem],
size: [],
color: 0,
};
const summarized_elems = {};
function advance(n) {
summarized_mem.timesteps.push(timestep);
summarized_mem.offsets.push(total_mem);
summarized_mem.size.push(total_summarized_mem);
timestep += n;
for (let i = 0; i < n; i++) {
max_at_time.push(total_mem + total_summarized_mem);
}
}
const sizes = elements
.map((x, i) => [x.size, i])
.sort(([x, _xi], [y, _yi]) => y - x);
const draw_elem = {};
for (const [_s, e] of sizes.slice(0, max_entries)) {
draw_elem[e] = true;
}
function add_allocation(elem) {
const element_obj = elements[elem];
const size = element_obj.size;
current.push(elem);
let color = elem;
if (snapshot.categories.length > 0) {
color = snapshot.categories.indexOf(element_obj.category || 'unknown');
}
const e = {
elem,
timesteps: [timestep],
offsets: [total_mem],
size,
color,
};
current_data.push(e);
data.push(e);
total_mem += size;
element_obj.max_allocated_mem = total_mem + total_summarized_mem;
}
for (const elem of initially_allocated) {
if (elem in draw_elem) {
add_allocation(elem);
} else {
total_summarized_mem += elements[elem].size;
summarized_elems[elem] = true;
}
}
for (const elem of actions) {
const size = elements[elem].size;
if (!(elem in draw_elem)) {
if (elem in summarized_elems) {
advance(1);
total_summarized_mem -= size;
summarized_elems[elem] = null;
} else {
total_summarized_mem += size;
summarized_elems[elem] = true;
advance(1);
}
continue;
}
const idx = current.findLastIndex(x => x === elem);
// first time we see an action we add it
// second time we remove it
if (idx === -1) {
add_allocation(elem);
advance(1);
} else {
advance(1);
const removed = current_data[idx];
removed.timesteps.push(timestep);
removed.offsets.push(removed.offsets.at(-1));
current.splice(idx, 1);
current_data.splice(idx, 1);
if (idx < current.length) {
for (let j = idx; j < current.length; j++) {
const e = current_data[j];
e.timesteps.push(timestep);
e.offsets.push(e.offsets.at(-1));
e.timesteps.push(timestep + 3);
e.offsets.push(e.offsets.at(-1) - size);
}
advance(3);
}
total_mem -= size;
}
max_size = Math.max(total_mem + total_summarized_mem, max_size);
}
for (const elem of current_data) {
elem.timesteps.push(timestep);
elem.offsets.push(elem.offsets.at(-1));
}
data.push(summarized_mem);
return {
max_size,
allocations_over_time: data,
max_at_time,
summarized_mem,
elements_length: elements.length,
context_for_id: id => {
const elem = elements[id];
let text = `Addr: ${formatAddr(elem)}`;
text = `${text}, Size: ${formatSize(elem.size)} allocation`;
text = `${text}, Total memory used after allocation: ${formatSize(
elem.max_allocated_mem,
)}`;
if (elem.stream !== null) {
text = `${text}, stream ${elem.stream}`;
}
if (!elem.action.includes('alloc')) {
text = `${text}\nalloc not recorded, stack trace for free:`;
}
text = `${text}\n${format_frames(elem.frames)}`;
return text;
},
};
}
function MemoryPlot(
svg,
data,
left_pad,
width,
height,
colors = schemeTableau10,
) {
function format_points(d) {
const size = d.size;
const xs = d.timesteps.map(t => xscale(t));
const bottom = d.offsets.map(t => yscale(t));
const m = Array.isArray(size)
? (t, i) => yscale(t + size[i])
: t => yscale(t + size);
const top = d.offsets.map(m);
const p0 = xs.map((x, i) => `${x},${bottom[i]}`);
const p1 = xs.map((x, i) => `${x},${top[i]}`).reverse();
return `${p0.join(' ')} ${p1.join(' ')}`;
}
const max_timestep = data.max_at_time.length;
const max_size = data.max_size;
const plot_width = width - left_pad;
const plot_height = height;
const yscale = scaleLinear().domain([0, max_size]).range([plot_height, 0]);
const yaxis = axisLeft(yscale).tickFormat(d3.format('.3s'));
const xscale = scaleLinear().domain([0, max_timestep]).range([0, plot_width]);
const plot_coordinate_space = svg
.append('g')
.attr('transform', `translate(${left_pad}, ${0})`);
const plot_outer = plot_coordinate_space.append('g');
function view_rect(a) {
return a
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', plot_width)
.attr('height', plot_height)
.attr('fill', 'white');
}
view_rect(plot_outer);
const cp = svg.append('clipPath').attr('id', 'clip');
view_rect(cp);
plot_outer.attr('clip-path', 'url(#clip)');
const zoom_group = plot_outer.append('g');
const scrub_group = zoom_group.append('g');
const plot = scrub_group
.selectAll('polygon')
.data(data.allocations_over_time)
.enter()
.append('polygon')
.attr('points', format_points)
.attr('fill', d => colors[d.color % colors.length]);
const axis = plot_coordinate_space.append('g').call(yaxis);
function handleZoom() {
const t = d3.event.transform;
zoom_group.attr('transform', t);
axis.call(yaxis.scale(d3.event.transform.rescaleY(yscale)));
}
const thezoom = zoom().on('zoom', handleZoom);
plot_outer.call(thezoom);
return {
select_window: (stepbegin, stepend, max) => {
const begin = xscale(stepbegin);
const size = xscale(stepend) - xscale(stepbegin);
const scale = plot_width / size;
const translate = -begin;
const yscale = max_size / max;
scrub_group.attr(
'transform',
`scale(${scale / yscale}, 1) translate(${translate}, 0)`,
);
plot_outer.call(
thezoom.transform,
zoomIdentity
.scale(yscale)
.translate(0, -(plot_height - plot_height / yscale)),
);
},
set_delegate: delegate => {
plot
.on('mouseover', function (_e, _d) {
delegate.set_selected(d3.select(this));
})
.on('mousedown', function (_e, _d) {
delegate.default_selected = d3.select(this);
})
.on('mouseleave', function (_e, _d) {
delegate.set_selected(delegate.default_selected);
});
},
};
}
function ContextViewer(text, data) {
let current_selected = null;
return {
default_selected: null,
set_selected: d => {
if (current_selected !== null) {
current_selected.attr('stroke', null).attr('stroke-width', null);
}
if (d === null) {
text.text('');
} else {
const dd = d.datum();
if (dd.elem === 'summarized') {
text.html(
'Small tensors that were not plotted to cutdown on render time.\n' +
'Use detail slider to see smaller allocations.',
);
} else {
text.text(`${dd.elem} ${data.context_for_id(dd.elem)}`);
}
d.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('vector-effect', 'non-scaling-stroke');
}
current_selected = d;
},
};
}
function MiniMap(mini_svg, plot, data, left_pad, width, height = 70) {
const max_at_time = data.max_at_time;
const plot_width = width - left_pad;
const yscale = scaleLinear().domain([0, data.max_size]).range([height, 0]);
const minixscale = scaleLinear()
.domain([0, max_at_time.length])
.range([left_pad, width]);
const mini_points = [
[max_at_time.length, 0],
[0, 0],
];
for (const [i, m] of max_at_time.entries()) {
const [_lastx, lasty] = mini_points[mini_points.length - 1];
if (m !== lasty) {
mini_points.push([i, lasty]);
mini_points.push([i, m]);
} else if (i === max_at_time.length - 1) {
mini_points.push([i, m]);
}
}
let points = mini_points.map(([t, o]) => `${minixscale(t)}, ${yscale(o)}`);
points = points.join(' ');
mini_svg
.append('polygon')
.attr('points', points)
.attr('fill', schemeTableau10[0]);
const xscale = scaleLinear()
.domain([0, max_at_time.length])
.range([0, plot_width]);
const brush = brushX();
brush.extent([
[left_pad, 0],
[width, height],
]);
brush.on('brush', function () {
const [begin, end] = d3.event.selection.map(x => x - left_pad);
const stepbegin = Math.floor(xscale.invert(begin));
const stepend = Math.floor(xscale.invert(end));
let max = 0;
for (let i = stepbegin; i < stepend; i++) {
max = Math.max(max, max_at_time[i]);
}
plot.select_window(stepbegin, stepend, max);
});
mini_svg.call(brush);
return {};
}
function Legend(plot_svg, categories) {
const xstart = 100;
const ystart = 5;
plot_svg
.append('g')
.selectAll('rect')
.data(categories)
.enter()
.append('rect')
.attr('x', (c, i) => xstart)
.attr('y', (c, i) => ystart + i * 15)
.attr('width', 10)
.attr('height', 10)
.attr('fill', (c, i) => schemeTableau10[i % schemeTableau10.length]);
plot_svg
.append('g')
.selectAll('text')
.data(categories)
.enter()
.append('text')
.attr('x', (c, i) => xstart + 20)
.attr('y', (c, i) => ystart + i * 15 + 8)
.attr('font-family', 'helvetica')
.attr('font-size', 10)
.text(c => c);
return {};
}
function create_trace_view(
dst,
snapshot,
device,
plot_segments = false,
max_entries = 15000,
) {
const left_pad = 70;
const data = process_alloc_data(snapshot, device, plot_segments, max_entries);
dst.selectAll('svg').remove();
dst.selectAll('div').remove();
const d = dst.append('div');
d.append('input')
.attr('type', 'range')
.attr('min', 0)
.attr('max', data.elements_length)
.attr('value', max_entries)
.on('change', function () {
create_trace_view(dst, snapshot, device, plot_segments, this.value);
});
d.append('label').text('Detail');
const grid_container = dst
.append('div')
.attr(
'style',
'display: grid; grid-template-columns: 1fr; grid-template-rows: 10fr 1fr 8fr; height: 100%; gap: 10px',
);
const plot_svg = grid_container
.append('svg')
.attr('display', 'block')
.attr('viewBox', '0 0 1024 576')
.attr('preserveAspectRatio', 'none')
.attr('style', 'grid-column: 1; grid-row: 1; width: 100%; height: 100%;');
const plot = MemoryPlot(plot_svg, data, left_pad, 1024, 576);
if (snapshot.categories.length !== 0) {
Legend(plot_svg.append('g'), snapshot.categories);
}
const mini_svg = grid_container
.append('svg')
.attr('display', 'block')
.attr('viewBox', '0 0 1024 60')
.attr('preserveAspectRatio', 'none')
.attr('style', 'grid-column: 1; grid-row: 2; width: 100%; height: 100%;');
MiniMap(mini_svg, plot, data, left_pad, 1024);
const context_div = grid_container
.append('div')
.attr(
'style',
'grid-column: 1; grid-row: 3; width: 100%; height: 100%; overflow: auto;',
);
const delegate = ContextViewer(context_div.append('pre').text('none'), data);
plot.set_delegate(delegate);
}
function unpickle(buffer) {
const bytebuffer = new Uint8Array(buffer);
const decoder = new TextDecoder();
const stack = [];
const marks = [];
const memo = [];
let offset = 0;
let memo_id = 0;
const APPENDS = 'e'.charCodeAt(0);
const BINGET = 'h'.charCodeAt(0);
const BININT = 'J'.charCodeAt(0);
const BININT1 = 'K'.charCodeAt(0);
const BININT2 = 'M'.charCodeAt(0);
const EMPTY_DICT = '}'.charCodeAt(0);
const EMPTY_LIST = ']'.charCodeAt(0);
const FRAME = 0x95;
const LONG1 = 0x8a;
const LONG_BINGET = 'j'.charCodeAt(0);
const MARK = '('.charCodeAt(0);
const MEMOIZE = 0x94;
const PROTO = 0x80;
const SETITEMS = 'u'.charCodeAt(0);
const SHORT_BINUNICODE = 0x8c;
const STOP = '.'.charCodeAt(0);
const TUPLE2 = 0x86;
const APPEND = 'a'.charCodeAt(0);
const NEWFALSE = 0x89;
const BINPUT = 'q'.charCodeAt(0);
const BINUNICODE = 'X'.charCodeAt(0);
const EMPTY_TUPLE = ')'.charCodeAt(0);
const NEWTRUE = 0x88;
const NONE = 'N'.charCodeAt(0);
const BINFLOAT = 'G'.charCodeAt(0);
const TUPLE = 't'.charCodeAt(0);
const TUPLE1 = 0x85;
const TUPLE3 = 0x87;
// untested
const LONG_BINPUT = 'r'.charCodeAt(0);
const LIST = 'l'.charCodeAt(0);
const DICT = 'd'.charCodeAt(0);
const SETITEM = 's'.charCodeAt(0);
const scratch_buffer = new ArrayBuffer(8);
const scratch_bytes = new Uint8Array(scratch_buffer);
const big = new BigInt64Array(scratch_buffer);
const float64 = new Float64Array(scratch_buffer);
function read_uint4() {
const n =
bytebuffer[offset] +
bytebuffer[offset + 1] * 256 +
bytebuffer[offset + 2] * 65536 +
bytebuffer[offset + 3] * 16777216;
offset += 4;
return n;
}
function setitems(d, mark) {
for (let i = mark; i < stack.length; i += 2) {
d[stack[i]] = stack[i + 1];
}
stack.splice(mark, Infinity);
}
while (true) {
const opcode = bytebuffer[offset++];
switch (opcode) {
case PROTO:
{
const version = bytebuffer[offset++];
if (version < 2 || version > 4) {
throw new Error(`Unhandled version ${version}`);
}
}
break;
case APPEND:
{
const v = stack.pop();
stack.at(-1).push(v);
}
break;
case APPENDS:
{
const mark = marks.pop();
const arr = stack[mark - 1];
arr.push(...stack.splice(mark, Infinity));
}
break;
case LIST:
case TUPLE:
{
const mark = marks.pop();
stack.push([...stack.splice(mark, Infinity)]);
}
break;
case NEWFALSE:
stack.push(false);
break;
case NEWTRUE:
stack.push(true);
break;
case NONE:
stack.push(null);
break;
case BINGET:
stack.push(memo[bytebuffer[offset++]]);
break;
case BININT:
{
let i32 = read_uint4();
if (i32 > 0x7fffffff) {
i32 -= 0x100000000;
}
stack.push(i32);
}
break;
case BININT1:
stack.push(bytebuffer[offset++]);
break;
case BININT2:
{
const v = bytebuffer[offset] + bytebuffer[offset + 1] * 256;
stack.push(v);
offset += 2;
}
break;
case EMPTY_DICT:
stack.push({});
break;
case EMPTY_LIST:
stack.push([]);
break;
case FRAME:
offset += 8;
break;
case LONG1:
{
const s = bytebuffer[offset++];
if (s <= 8) {
for (let i = 0; i < s; i++) {
scratch_bytes[i] = bytebuffer[offset++];
}
const fill = scratch_bytes[s - 1] >= 128 ? 0xff : 0x0;
for (let i = s; i < 8; i++) {
scratch_bytes[i] = fill;
}
stack.push(Number(big[0]));
} else { // BigInt
let scratch_bytes_unbounded = [];
for (let i = 0; i < s; i++) {
scratch_bytes_unbounded.push(bytebuffer[offset++]);
}
// BigInt can only convert from unsigned hex, thus we need to
// convert from twos-complement if negative
const negative = scratch_bytes_unbounded[s - 1] >= 128;
if (negative) {
// implements scratch_bytes_unbounded = ~scratch_bytes_unbounded + 1
// byte-by-byte.
let carry = 1;
for (let i = 0; i < s; i++) {
const twos_complement = (0xff ^ scratch_bytes_unbounded[i]) + carry;
carry = twos_complement > 0xff ? 1 : 0;
scratch_bytes_unbounded[i] = 0xff & twos_complement;
}
}
const hex_str = Array.from(scratch_bytes_unbounded.reverse(), byte => {
return byte.toString(16).padStart(2, '0');
}).join('');
const big_int = negative ? -BigInt(`0x${hex_str}`) : BigInt(`0x${hex_str}`);
stack.push(big_int);
}
}
break;
case LONG_BINGET:
{
const idx = read_uint4();
stack.push(memo[idx]);
}
break;
case MARK:
marks.push(stack.length);
break;
case MEMOIZE:
memo[memo_id++] = stack.at(-1);
break;
case BINPUT:
memo[bytebuffer[offset++]] = stack.at(-1);
break;
case LONG_BINPUT:
memo[read_uint4()] = stack.at(-1);
break;
case SETITEMS:
{
const mark = marks.pop();
const d = stack[mark - 1];
setitems(d, mark);
}
break;
case SETITEM: {
const v = stack.pop();
const k = stack.pop();
stack.at(-1)[k] = v;
break;
}
case DICT:
{
const mark = marks.pop();
const d = {};
setitems(d, mark);
stack.push(d);
}
break;
case SHORT_BINUNICODE:
{
const n = bytebuffer[offset++];
stack.push(decoder.decode(new Uint8Array(buffer, offset, n)));
offset += n;
}
break;
case BINUNICODE:
{
const n = read_uint4();
stack.push(decoder.decode(new Uint8Array(buffer, offset, n)));
offset += n;
}
break;
case STOP:
return stack.pop();
case EMPTY_TUPLE:
stack.push([]);
break;
case TUPLE1:
stack.push([stack.pop()]);
break;
case TUPLE2:
stack.push(stack.splice(-2, Infinity));
break;
case TUPLE3:
stack.push(stack.splice(-3, Infinity));
break;
case BINFLOAT:
for (let i = 7; i >= 0; i--) {
// stored in big-endian order
scratch_bytes[i] = bytebuffer[offset++];
}
stack.push(float64[0]);
break;
default:
throw new Error(`UNKNOWN OPCODE: ${opcode}`);
}
}
}
function decode_base64(input) {
function decode_char(i, shift) {
const nChr = input.charCodeAt(i);
const r =
nChr > 64 && nChr < 91
? nChr - 65
: nChr > 96 && nChr < 123
? nChr - 71
: nChr > 47 && nChr < 58
? nChr + 4
: nChr === 43
? 62
: nChr === 47
? 63
: 0;
return r << shift;
}
const output = new Uint8Array((input.length / 4) * 3);
for (let i = 0, j = 0; i < input.length; i += 4, j += 3) {
const u24 =
decode_char(i, 18) +
decode_char(i + 1, 12) +
decode_char(i + 2, 6) +
decode_char(i + 3);
output[j] = u24 >> 16;
output[j + 1] = (u24 >> 8) & 0xff;
output[j + 2] = u24 & 0xff;
}
return output.buffer;
}
const kinds = {
'Active Memory Timeline': create_trace_view,
'Allocator State History': create_segment_view,
'Active Cached Segment Timeline': (dst, snapshot, device) =>
create_trace_view(dst, snapshot, device, true),
};
const snapshot_cache = {};
const snapshot_to_loader = {};
const snapshot_to_url = {};
const selection_to_div = {};
const style = `
pre {
margin: 0px;
}
html, body {
height: 100%;
overflow: clip;
}`;
const head = d3.select('head');
head.append('style').text(style);
const body = d3.select('body');
const snapshot_select = body.append('select');
const view = body.append('select');
for (const x in kinds) {
view.append('option').text(x);
}
const gpu = body.append('select');
function unpickle_and_annotate(data) {
data = unpickle(data);
console.log(data);
annotate_snapshot(data);
return data;
}
function snapshot_change(f) {
const view_value = view.node().value;
let device = Number(gpu.node().value);
const snapshot = snapshot_cache[f];
gpu.selectAll('option').remove();
const has_segments = {};
for (const s of snapshot.segments) {
has_segments[s.device] = true;
}
let device_valid = false;
for (const [i, trace] of snapshot.device_traces.entries()) {
if (trace.length > 0 || i in has_segments) {
gpu.append('option').text(i);
if (i === device) {
device_valid = true;
gpu.node().selectedIndex = gpu.node().children.length - 1;
}
}
}
if (!device_valid) {
device = Number(gpu.node().value);
}
const key = [f, view_value, device];
if (!(key in selection_to_div)) {
selection_to_div[key] = d3.select('body').append('div');
kinds[view_value](selection_to_div[key], snapshot, device);
}
const selected_div = selection_to_div[key];
selected_div.attr('style', 'display: float; height: 100%');
}
function selected_change() {
for (const d of Object.values(selection_to_div)) {
d.attr('style', 'display: none; height: 100%');
}
const f = snapshot_select.node().value;
if (f === '') {
return;
}
if (!(f in snapshot_cache)) {
snapshot_to_loader[f](f);
} else {
snapshot_change(f);
}
}
snapshot_select.on('change', selected_change);
view.on('change', selected_change);
gpu.on('change', selected_change);
body.on('dragover', e => {
event.preventDefault();
});
body.on('drop', () => {
console.log(event.dataTransfer.files);
Array.from(event.dataTransfer.files).forEach(file => {
add_snapshot(file.name, unique_name => {
const reader = new FileReader();
reader.onload = e => {
finished_loading(unique_name, e.target.result);
};
reader.readAsArrayBuffer(file);
});
});
event.preventDefault();
snapshot_select.node().selectedIndex =
snapshot_select.node().options.length - 1;
selected_change();
});
selection_to_div[''] = body
.append('div')
.text(
'Drag and drop a file to load a local snapshot. No data from the snapshot is uploaded.',
);
let next_unique_n = 1;
function add_snapshot(name, loader) {
if (name in snapshot_to_loader) {
name = `${name} (${next_unique_n++})`;
}
snapshot_select.append('option').text(name);
snapshot_to_loader[name] = loader;
}
function finished_loading(name, data) {
snapshot_cache[name] = unpickle_and_annotate(data);
snapshot_change(name);
}
export function add_remote_files(files) {
files.forEach(f =>
add_snapshot(f.name, unique_name => {
console.log('fetching', f.url);
fetch(f.url)
.then(x => x.arrayBuffer())
.then(data => finished_loading(unique_name, data));
}),
);
if (files.length > 0) {
selected_change();
}
}
export function add_local_files(files, view_value) {
view.node().value = view_value;
files.forEach(f =>
add_snapshot(f.name, unique_name => {
finished_loading(unique_name, decode_base64(f.base64));
}),
);
if (files.length > 0) {
selected_change();
}
}