| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Source VProf Viewer</title>
- <style>
- /* Layout only. A visual design pass will replace this. */
- * { box-sizing: border-box; }
- body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
- font-size: 13px; margin: 0; color: #111; background: #fafafa; }
- header { padding: 8px 12px; background: #1a202c; color: #fff; }
- header h1 { font-size: 16px; margin: 0 0 6px 0; }
- header .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
- header label { font-size: 12px; }
- main { padding: 12px; display: flex; flex-direction: column; gap: 12px; }
- section { background: #fff; border: 1px solid #ddd; padding: 8px; }
- h2 { font-size: 14px; margin: 0 0 6px 0; }
- h3 { font-size: 13px; margin: 10px 0 4px 0; }
- table { border-collapse: collapse; width: 100%; }
- th, td { border: 1px solid #ddd; padding: 2px 6px; text-align: left;
- font-size: 12px; vertical-align: top; }
- th { background: #f0f0f0; cursor: pointer; white-space: nowrap; }
- th.nosort { cursor: default; }
- td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
- .muted { color: #666; }
- .warn { background: #fff5e6; border: 1px solid #e0a800; padding: 6px 8px; }
- .notice { background: #e8f0fe; border: 1px solid #6b9bd2; padding: 6px 8px; }
- .summary-grid { display: flex; flex-wrap: wrap; gap: 12px 24px; }
- .summary-grid div { min-width: 120px; }
- .summary-grid .k { color: #666; font-size: 11px; }
- .summary-grid .v { font-size: 14px; font-weight: 600; }
- .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
- .controls input[type=number] { width: 90px; }
- #timeline-wrap { width: 100%; }
- #timeline { display: block; width: 100%; height: 120px; background: #fff;
- border: 1px solid #ddd; cursor: crosshair; }
- .grid2 { display: grid; grid-template-columns: minmax(0,1fr) 360px; gap: 12px; }
- @media (max-width: 900px) { .grid2 { grid-template-columns: 1fr; } }
- .toggle { cursor: pointer; user-select: none; display: inline-block; width: 12px; }
- .tree-name { white-space: nowrap; }
- .bar { display: inline-block; height: 8px; background: #6b9bd2; vertical-align: middle; }
- .progress { height: 6px; background: #ddd; width: 200px; }
- .progress > div { height: 100%; background: #2b6cb0; width: 0%; }
- .scroll { max-height: 520px; overflow: auto; }
- .hidden { display: none; }
- footer { padding: 8px 12px; color: #666; font-size: 11px; }
- </style>
- </head>
- <body>
- <header>
- <h1>Source VProf Viewer</h1>
- <div class="row">
- <label>Choose a .vprof file: <input type="file" id="file"></label>
- <button id="open-picker" class="hidden">Open with file picker…</button>
- <button id="reopen-last" class="hidden">Reopen last recording</button>
- <span id="drop-hint" class="muted">or drag & drop it onto this page</span>
- <div id="progress-wrap" class="hidden">
- <div class="progress"><div id="progress-bar"></div></div>
- <span id="status" class="muted"></span>
- </div>
- </div>
- </header>
- <main>
- <section id="error-box" class="warn hidden"></section>
- <section id="summary-section" class="hidden">
- <h2>Recording summary</h2>
- <div id="notices"></div>
- <div id="summary" class="summary-grid"></div>
- </section>
- <section id="timeline-section" class="hidden">
- <h2>Frame timeline</h2>
- <div class="controls">
- <button id="range-all">All frames</button>
- <label>From frame <input type="number" id="range-start" min="0" step="1"></label>
- <label>to <input type="number" id="range-end" min="0" step="1"></label>
- <button id="range-apply">Apply range</button>
- <button id="frame-prev">◀ prev</button>
- <button id="frame-next">next ▶</button>
- <span id="frame-label" class="muted"></span>
- <span id="range-status" class="muted"></span>
- </div>
- <div id="timeline-wrap"><canvas id="timeline"></canvas></div>
- <div id="timeline-caption" class="muted"></div>
- </section>
- <div class="grid2">
- <section id="tree-section" class="hidden">
- <h2>Call tree</h2>
- <div class="controls">
- <label>Filter <input type="search" id="filter" placeholder="name contains..."></label>
- <button id="expand-all">Expand all</button>
- <button id="collapse-all">Collapse all</button>
- <span id="tree-count" class="muted"></span>
- </div>
- <div class="scroll">
- <table id="tree-table">
- <thead><tr id="tree-head"></tr></thead>
- <tbody id="tree-body"></tbody>
- </table>
- </div>
- </section>
- <div>
- <section id="budget-section" class="hidden">
- <h2>Budget groups</h2>
- <div class="scroll">
- <table id="budget-table">
- <thead><tr><th class="nosort">Group</th><th class="nosort num">Calls</th>
- <th class="nosort num">Total</th><th class="nosort num">%</th></tr></thead>
- <tbody id="budget-body"></tbody>
- </table>
- </div>
- </section>
- <section id="top-section" class="hidden">
- <h2>Top nodes</h2>
- <div class="scroll">
- <table id="top-table">
- <thead><tr><th class="nosort">Scope</th><th class="nosort num">Total</th>
- <th class="nosort num">Self</th><th class="nosort num">Calls</th></tr></thead>
- <tbody id="top-body"></tbody>
- </table>
- </div>
- </section>
- </div>
- </div>
- </main>
- <footer>
- VProf binary format documentation: see <code>VPROF_FORMAT.md</code> in the repository.
- </footer>
- <!-- ===================== Parsing worker (inline, self-contained) ===================== -->
- <script id="worker-src" type="text/js-worker">
- "use strict";
- // The 31 budget groups registered by CVProfile::CVProfile(), in fixed ID order.
- var DEFAULT_BUDGET_GROUPS = [
- ["Unaccounted", 6],
- ["World Rendering", 1],
- ["Displacement_Rendering", 1],
- ["Game", 6],
- ["Player", 6],
- ["NPCs", 6],
- ["Server Animation", 6],
- ["Client_Animation", 1],
- ["Physics", 6],
- ["Static_Prop_Rendering", 1],
- ["Other_Model_Rendering", 1],
- ["Fast Path Model Rendering", 1],
- ["Light_Cache", 1],
- ["Brush_Model_Rendering", 1],
- ["Shadow_Rendering", 1],
- ["Detail_Prop_Rendering", 1],
- ["Particle/Effect_Rendering", 1],
- ["Ropes", 1],
- ["Dynamic_Light_Rendering", 1],
- ["Networking", 6],
- ["Sound", 6],
- ["VGUI", 6],
- ["FileSystem", 6],
- ["Prediction", 1],
- ["Interpolation", 1],
- ["Swap_Buffers", 1],
- ["Occlusion", 1],
- ["Overlays", 1],
- ["Tools", 5],
- ["Texture_Cache", 1],
- ["Replay", 2]
- ];
- var TOKEN_START_FRAME = 0;
- var TOKEN_ADD_NODE = 1;
- var TOKEN_ADD_BUDGET_GROUP = 2;
- var TOKEN_TIMINGS = 3;
- var TOKEN_END_OF_FRAME = 4;
- var TOKEN_FILE_FINISHED = 5;
- // Retained after a parse so range queries can re-parse only a byte range
- // without keeping every sample in memory.
- var g_recording = null;
- self.onmessage = function (e) {
- var msg = e.data;
- if (!msg) return;
- if (msg.cmd === "parse") {
- try {
- var out = parseVprof(msg.buffer, msg.opts || {});
- self.postMessage({ type: "done", result: out.result }, out.transfer);
- } catch (err) {
- self.postMessage({ type: "error", message: (err && err.message) || String(err) });
- }
- } else if (msg.cmd === "query") {
- var t0 = (self.performance && self.performance.now) ? self.performance.now() : Date.now();
- try {
- var agg = parseRange(msg.buffer, msg.startFrame, msg.endFrame);
- var t1 = (self.performance && self.performance.now) ? self.performance.now() : Date.now();
- self.postMessage({ type: "range", requestId: msg.requestId, startFrame: msg.startFrame, endFrame: msg.endFrame, aggregates: agg, ms: t1 - t0 });
- } catch (err) {
- self.postMessage({ type: "rangeError", requestId: msg.requestId, message: (err && err.message) || String(err) });
- }
- }
- };
- function parseVprof(buffer, opts) {
- var t0 = (self.performance && self.performance.now) ? self.performance.now() : Date.now();
- var dv = new DataView(buffer);
- var totalBytes = buffer.byteLength;
- var pos = 0;
- var decoder = new TextDecoder("utf-8");
- function u8() { return dv.getUint8(pos++); }
- function u16() { var v = dv.getUint16(pos, true); pos += 2; return v; }
- function u32() { var v = dv.getUint32(pos, true); pos += 4; return v; }
- function i32() { var v = dv.getInt32(pos, true); pos += 4; return v; }
- function cstr() {
- var start = pos;
- while (dv.getUint8(pos) !== 0) pos++;
- var s = decoder.decode(new Uint8Array(buffer, start, pos - start));
- pos++;
- return s;
- }
- if (totalBytes < 8) throw new Error("File is too small to be a .vprof recording.");
- var version = i32();
- var rootNodeId = i32();
- // ---- Budget groups (seed the 31 defaults so sparse recordings still resolve) ----
- var budgetGroups = [];
- var groupIdByName = Object.create(null);
- for (var i = 0; i < DEFAULT_BUDGET_GROUPS.length; i++) {
- var g = DEFAULT_BUDGET_GROUPS[i];
- budgetGroups.push({ id: i, name: g[0], flags: g[1], isDefault: true });
- groupIdByName[g[0]] = i;
- }
- function addBudgetGroup(name, flags) {
- var id = groupIdByName[name];
- if (id !== undefined) { budgetGroups[id].flags = flags; return id; }
- id = budgetGroups.length;
- budgetGroups.push({ id: id, name: name, flags: flags, isDefault: false });
- groupIdByName[name] = id;
- return id;
- }
- // ---- Nodes (sparse array indexed by unique node id) ----
- var nodesById = [];
- // Frame index in which each node was first declared. Because
- // CVProfNode::GetSubNode() only ever *prepends*, the children order at any
- // frame is the final order restricted to nodes whose addedFrame <= frame.
- // That lets a range be parsed without replaying structure from the start.
- var nodesAddedFrame = [];
- var frameCount = 0;
- function makeNode(id, name, parentId, budgetGroupId) {
- var n = { id: id, name: name, parentId: parentId, budgetGroupId: budgetGroupId, children: [] };
- nodesById[id] = n;
- nodesAddedFrame[id] = frameCount;
- return n;
- }
- makeNode(rootNodeId, "Root", null, 0);
- // ---- Aggregates (sparse array indexed by unique node id) ----
- var agg = [];
- function aggFor(id) {
- var a = agg[id];
- if (!a) { a = { calls: 0, us: 0, maxUs: 0, frames: 0 }; agg[id] = a; }
- return a;
- }
- // ---- Per-frame storage budget ----
- var maxSamples = (opts.maxSamples && opts.maxSamples > 0) ? opts.maxSamples : 4000000;
- var allowSamples = opts.noSamples !== true;
- var storing = allowSamples;
- var overflow = false;
- var totalSamples = 0;
- var samples = [];
- var frameTicks = [];
- var frameTotals = [];
- var frameCalls = [];
- var addedNodesByFrame = [];
- // Byte offset of each frame's Token_StartFrame and of the byte just past its
- // Token_EndOfFrame. A [start,end] frame range maps to one contiguous
- // file.slice(frameStartOffsets[start], frameEndOffsets[end]).
- var frameStartOffsets = [];
- var frameEndOffsets = [];
- var anyTime = false;
- var anyCalls = false;
- // Outer variables reused by readTimings (avoids allocating a closure per frame)
- var curSamples = null;
- var frameTotalUs = 0;
- var frameTotalCalls = 0;
- function readTimings(nodeId) {
- var callsToken = u8();
- var calls;
- if (callsToken === 255) calls = u16(); else calls = callsToken;
- var timeUnits = u16();
- var us;
- if (timeUnits === 0xFFFF) us = u32() * 4; else us = timeUnits * 4;
- if (calls > 0) anyCalls = true;
- if (us > 0) anyTime = true;
- totalSamples++;
- if (storing) {
- curSamples.ids.push(nodeId);
- curSamples.calls.push(calls);
- curSamples.us.push(us);
- if (totalSamples > maxSamples) {
- // Too large to keep every frame; fall back to whole-recording aggregate.
- storing = false;
- overflow = true;
- samples.length = 0;
- curSamples = null;
- }
- }
- frameTotalUs += us;
- frameTotalCalls += calls;
- var a = aggFor(nodeId);
- a.calls += calls;
- a.us += us;
- if (us > a.maxUs) a.maxUs = us;
- a.frames++;
- var kids = nodesById[nodeId].children;
- for (var k = 0; k < kids.length; k++) readTimings(kids[k]);
- }
- var progressAt = 0;
- var totalFramesApprox = -1;
- while (pos < totalBytes) {
- var frameStart = pos;
- var token = u8();
- if (token === TOKEN_FILE_FINISHED) break;
- if (token !== TOKEN_START_FRAME) {
- throw new Error("Expected start-of-frame token at offset " + (pos - 1) + ", got " + token + ".");
- }
- var tick = i32();
- frameTicks.push(tick);
- frameTotalUs = 0;
- frameTotalCalls = 0;
- curSamples = storing ? { ids: [], calls: [], us: [] } : null;
- var added = [];
- while (true) {
- var inner = u8();
- if (inner === TOKEN_END_OF_FRAME) break;
- if (inner === TOKEN_ADD_BUDGET_GROUP) {
- var bgName = cstr();
- var bgFlags = i32();
- addBudgetGroup(bgName, bgFlags);
- } else if (inner === TOKEN_ADD_NODE) {
- var parentId = i32();
- var nodeName = cstr();
- var bgId = i32();
- var nodeId = i32();
- var parent = nodesById[parentId];
- if (!parent) throw new Error("AddNode references unknown parent " + parentId + " at offset " + (pos - 4) + ".");
- if (nodesById[nodeId]) throw new Error("AddNode re-declares node " + nodeId + " at offset " + (pos - 4) + ".");
- makeNode(nodeId, nodeName, parentId, bgId);
- parent.children.unshift(nodeId); // CVProfNode::GetSubNode() prepends
- added.push(nodeId);
- } else if (inner === TOKEN_TIMINGS) {
- readTimings(rootNodeId);
- } else {
- throw new Error("Unexpected token " + inner + " inside frame " + frameCount + " at offset " + (pos - 1) + ".");
- }
- }
- if (storing && curSamples) samples.push(curSamples);
- frameTotals.push(frameTotalUs);
- frameCalls.push(frameTotalCalls);
- addedNodesByFrame.push(added);
- frameStartOffsets.push(frameStart);
- frameEndOffsets.push(pos);
- frameCount++;
- if (frameCount - progressAt >= 500) {
- progressAt = frameCount;
- self.postMessage({ type: "progress", frames: frameCount, pos: pos, total: totalBytes });
- }
- }
- // ---- Compact and collect transferables ----
- var transfer = [];
- var frameTicksOut = Int32Array.from(frameTicks);
- var frameTotalsOut = Float64Array.from(frameTotals);
- var frameCallsOut = Uint32Array.from(frameCalls);
- var frameStartOffsetsOut = Float64Array.from(frameStartOffsets);
- var frameEndOffsetsOut = Float64Array.from(frameEndOffsets);
- transfer.push(frameTicksOut.buffer, frameTotalsOut.buffer, frameCallsOut.buffer,
- frameStartOffsetsOut.buffer, frameEndOffsetsOut.buffer);
- var addedOut = [];
- for (var f = 0; f < addedNodesByFrame.length; f++) {
- var arr = Int32Array.from(addedNodesByFrame[f]);
- transfer.push(arr.buffer);
- addedOut.push(arr);
- }
- var samplesOut = null;
- if (storing) {
- samplesOut = [];
- for (var s = 0; s < samples.length; s++) {
- var fr = samples[s];
- var ids = Int32Array.from(fr.ids);
- var callsArr = Uint32Array.from(fr.calls);
- var usArr = Float64Array.from(fr.us);
- transfer.push(ids.buffer, callsArr.buffer, usArr.buffer);
- samplesOut.push({ ids: ids, calls: callsArr, us: usArr });
- }
- }
- var t1 = (self.performance && self.performance.now) ? self.performance.now() : Date.now();
- // Keep the parsed tree around so later range queries can walk it.
- g_recording = {
- nodes: nodesById,
- nodesAddedFrame: nodesAddedFrame,
- rootNodeId: rootNodeId
- };
- return {
- result: {
- version: version,
- rootNodeId: rootNodeId,
- budgetGroups: budgetGroups,
- nodes: nodesById,
- aggregates: agg,
- frameCount: frameCount,
- frameTicks: frameTicksOut,
- frameTotals: frameTotalsOut,
- frameCalls: frameCallsOut,
- frameStartOffsets: frameStartOffsetsOut,
- frameEndOffsets: frameEndOffsetsOut,
- addedNodesByFrame: addedOut,
- samples: samplesOut,
- samplesStored: storing,
- samplesOverflow: overflow,
- totalSamples: totalSamples,
- anyTime: anyTime,
- anyCalls: anyCalls,
- parseMs: t1 - t0
- },
- transfer: transfer
- };
- }
- // Re-parse only the frames in a contiguous byte range and aggregate per node.
- // `buffer` must start on the Token_StartFrame of `startFrame` and end just past
- // the Token_EndOfFrame of `endFrame` (i.e. file.slice(frameStartOffsets[start],
- // frameEndOffsets[end])).
- function parseRange(buffer, startFrame, endFrame) {
- if (!g_recording) throw new Error("No recording loaded for a range query.");
- var rec = g_recording;
- var dv = new DataView(buffer);
- var totalBytes = buffer.byteLength;
- var pos = 0;
- var decoder = new TextDecoder("utf-8");
- function u8() { return dv.getUint8(pos++); }
- function u16() { var v = dv.getUint16(pos, true); pos += 2; return v; }
- function u32() { var v = dv.getUint32(pos, true); pos += 4; return v; }
- function i32() { var v = dv.getInt32(pos, true); pos += 4; return v; }
- function cstr() {
- var start = pos;
- while (dv.getUint8(pos) !== 0) pos++;
- var s = decoder.decode(new Uint8Array(buffer, start, pos - start));
- pos++;
- return s;
- }
- var agg = [];
- function aggFor(id) {
- var a = agg[id];
- if (!a) { a = { calls: 0, us: 0, maxUs: 0, frames: 0 }; agg[id] = a; }
- return a;
- }
- function readTimings(nodeId, frameIndex) {
- var callsToken = u8();
- var calls;
- if (callsToken === 255) calls = u16(); else calls = callsToken;
- var timeUnits = u16();
- var us;
- if (timeUnits === 0xFFFF) us = u32() * 4; else us = timeUnits * 4;
- var a = aggFor(nodeId);
- a.calls += calls;
- a.us += us;
- if (us > a.maxUs) a.maxUs = us;
- a.frames++;
- // Only descend into children that existed at this frame; the final child
- // order already reflects the prepend-only evolution of the tree.
- var kids = rec.nodes[nodeId].children;
- for (var k = 0; k < kids.length; k++) {
- if (rec.nodesAddedFrame[kids[k]] <= frameIndex) readTimings(kids[k], frameIndex);
- }
- }
- var frameIndex = startFrame;
- while (pos < totalBytes) {
- var token = u8();
- if (token === TOKEN_FILE_FINISHED) break;
- if (token !== TOKEN_START_FRAME) {
- throw new Error("Range parse: expected start-of-frame at offset " + (pos - 1) + ", got " + token + ".");
- }
- i32(); // tick
- while (true) {
- var inner = u8();
- if (inner === TOKEN_END_OF_FRAME) break;
- if (inner === TOKEN_ADD_BUDGET_GROUP) {
- cstr(); i32();
- } else if (inner === TOKEN_ADD_NODE) {
- i32(); cstr(); i32(); i32();
- } else if (inner === TOKEN_TIMINGS) {
- readTimings(rec.rootNodeId, frameIndex);
- } else {
- throw new Error("Range parse: unexpected token " + inner + " at offset " + (pos - 1) + ".");
- }
- }
- frameIndex++;
- if (frameIndex > endFrame + 1) break;
- }
- return agg;
- }
- </script>
- <!-- ===================== UI ===================== -->
- <script>
- "use strict";
- /* ------------------------------- helpers ------------------------------- */
- function $(id) { return document.getElementById(id); }
- function esc(s) {
- return String(s).replace(/[&<>"']/g, function (c) {
- return ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c];
- });
- }
- function fmtUs(us) {
- if (!us) return "0";
- var ms = us / 1000;
- if (Math.abs(ms) >= 1000) return (ms / 1000).toFixed(3) + " s";
- if (Math.abs(ms) >= 1) return ms.toFixed(3) + " ms";
- return us.toFixed(1) + " \u00b5s";
- }
- function fmtInt(n) {
- return (n === undefined || n === null) ? "0" : Number(n).toLocaleString();
- }
- function fmtBytes(n) {
- if (n < 1024) return n + " B";
- if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KiB";
- if (n < 1024 * 1024 * 1024) return (n / 1048576).toFixed(1) + " MiB";
- return (n / 1073741824).toFixed(2) + " GiB";
- }
- function flagsDecoded(flags) {
- var out = [];
- if (flags & 1) out.push("CLIENT");
- if (flags & 2) out.push("SERVER");
- if (flags & 4) out.push("OTHER");
- if (flags & 32768) out.push("HIDDEN");
- return out;
- }
- /* ------------------------------- state ------------------------------- */
- var state = null;
- /* ------------------------------- loading ------------------------------- */
- var worker = null;
- var lastFile = null;
- function setError(msg) {
- var box = $("error-box");
- if (!msg) { box.classList.add("hidden"); box.textContent = ""; return; }
- box.classList.remove("hidden");
- box.textContent = msg;
- }
- function startParse(file, handle) {
- lastFile = file;
- if (handle) rememberHandle(handle);
- setError("");
- $("progress-wrap").classList.remove("hidden");
- $("progress-bar").style.width = "0%";
- $("status").textContent = "Reading " + file.name + " (" + fmtBytes(file.size) + ")...";
- file.arrayBuffer().then(function (buf) {
- if (worker) { worker.terminate(); worker = null; }
- var src = $("worker-src").textContent;
- var url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
- worker = new Worker(url);
- worker.onmessage = onWorkerMessage;
- worker.onerror = function (e) {
- setError("Worker error: " + (e.message || "unknown"));
- };
- $("status").textContent = "Parsing...";
- worker.postMessage({ cmd: "parse", buffer: buf, opts: { maxSamples: 4000000 } }, [buf]);
- }).catch(function (err) {
- setError("Could not read file: " + err);
- });
- }
- function onWorkerMessage(e) {
- var msg = e.data;
- if (msg.type === "progress") {
- var pct = msg.total ? Math.floor(100 * msg.pos / msg.total) : 0;
- $("progress-bar").style.width = pct + "%";
- $("status").textContent = "Parsing... " + fmtInt(msg.frames) + " frames (" + pct + "%)";
- } else if (msg.type === "error") {
- setError("Parse error: " + msg.message);
- $("status").textContent = "Failed.";
- } else if (msg.type === "range") {
- var s = state;
- if (!s || msg.requestId !== s.requestId) return;
- s.agg = msg.aggregates;
- s.rangeCache.set(msg.startFrame + ":" + msg.endFrame, msg.aggregates);
- trimRangeCache(s.rangeCache);
- s.rangePending = false;
- renderAll();
- updateRangeStatus("Frames " + msg.startFrame + "\u2013" + msg.endFrame + " parsed in " + Math.round(msg.ms) + " ms");
- } else if (msg.type === "rangeError") {
- var s2 = state;
- if (!s2 || msg.requestId !== s2.requestId) return;
- s2.rangePending = false;
- renderAll();
- updateRangeStatus("Range parse failed: " + msg.message);
- } else if (msg.type === "done") {
- $("progress-bar").style.width = "100%";
- $("status").textContent = "Done.";
- $("progress-wrap").classList.add("hidden");
- buildState(msg.result, lastFile);
- renderAll();
- }
- }
- /* --------------------------- file system access --------------------------- */
- // Persist the FileSystemFileHandle (when the browser supports the File System
- // Access API) so a large recording can be reopened next time and its byte range
- // re-read on demand for granular frame queries. Falls back silently to the
- // plain <input type=file> File object, which is re-readable within a session.
- var HANDLE_DB = "vprof-viewer";
- var HANDLE_STORE = "handles";
- var HANDLE_KEY = "last";
- function idbOpen() {
- return new Promise(function (resolve, reject) {
- if (!window.indexedDB) return reject(new Error("IndexedDB unavailable"));
- var req = indexedDB.open(HANDLE_DB, 1);
- req.onupgradeneeded = function () {
- if (!req.result.objectStoreNames.contains(HANDLE_STORE)) req.result.createObjectStore(HANDLE_STORE);
- };
- req.onsuccess = function () { resolve(req.result); };
- req.onerror = function () { reject(req.error); };
- });
- }
- function idbSet(key, value) {
- return idbOpen().then(function (db) {
- return new Promise(function (resolve, reject) {
- var tx = db.transaction(HANDLE_STORE, "readwrite");
- tx.objectStore(HANDLE_STORE).put(value, key);
- tx.oncomplete = function () { resolve(); };
- tx.onerror = function () { reject(tx.error); };
- });
- });
- }
- function idbGet(key) {
- return idbOpen().then(function (db) {
- return new Promise(function (resolve, reject) {
- var req = db.transaction(HANDLE_STORE, "readonly").objectStore(HANDLE_STORE).get(key);
- req.onsuccess = function () { resolve(req.result); };
- req.onerror = function () { reject(req.error); };
- });
- });
- }
- function rememberHandle(handle) {
- if (!handle || !handle.getFile) return;
- handle.getFile().then(function (file) {
- idbSet(HANDLE_KEY, { handle: handle, name: file.name, size: file.size, lastModified: file.lastModified });
- }).catch(function () {});
- }
- function showReopenButton(entry) {
- var btn = $("reopen-last");
- btn.textContent = "Reopen " + entry.name;
- btn.classList.remove("hidden");
- btn.onclick = function () {
- var permission = entry.handle.queryPermission
- ? entry.handle.queryPermission({ mode: "read" })
- : Promise.resolve("granted");
- permission.then(function (status) {
- if (status !== "granted" && entry.handle.requestPermission) {
- return entry.handle.requestPermission({ mode: "read" });
- }
- return status;
- }).then(function (status) {
- if (status !== "granted") throw new Error("read permission denied");
- return entry.handle.getFile();
- }).then(function (file) {
- startParse(file, entry.handle);
- }).catch(function (err) {
- setError("Could not reopen recording: " + err);
- });
- };
- }
- /* ------------------------------- state build ------------------------------- */
- function buildState(result, file) {
- var nodeById = result.nodes;
- var nodes = [];
- for (var i = 0; i < nodeById.length; i++) if (nodeById[i]) nodes.push(nodeById[i]);
- var groupName = Object.create(null);
- for (var g = 0; g < result.budgetGroups.length; g++) groupName[result.budgetGroups[g].id] = result.budgetGroups[g].name;
- // Collapse everything except the root unless the tree is small.
- var collapsed = new Set();
- for (var k = 0; k < nodes.length; k++) collapsed.add(nodes[k].id);
- collapsed.delete(result.rootNodeId);
- if (nodes.length <= 200) {
- // small tree: expand fully
- collapsed.clear();
- }
- state = {
- r: result,
- file: file,
- nodeById: nodeById,
- nodes: nodes,
- groupName: groupName,
- maxId: nodeById.length,
- frameCount: result.frameCount,
- rangeStart: 0,
- rangeEnd: Math.max(0, result.frameCount - 1),
- metricTime: result.anyTime,
- sortKey: result.anyTime ? "total" : "name",
- sortDir: result.anyTime ? -1 : 1,
- collapsed: collapsed,
- filter: "",
- agg: result.aggregates,
- self: new Float64Array(nodeById.length),
- rootTotal: 0,
- percentBase: 1,
- rangeCache: new Map(), // "start:end" -> aggregate (bounded)
- requestId: 0, // newest outstanding range request
- rangePending: false
- };
- updateRangeInputs();
- }
- /* ------------------------------- aggregation ------------------------------- */
- // Aggregate per-node timings from the per-frame samples retained for small
- // captures.
- function computeFromSamples(start, end) {
- var s = state;
- var agg = [];
- function get(id) {
- var a = agg[id];
- if (!a) { a = { calls: 0, us: 0, maxUs: 0, frames: 0 }; agg[id] = a; }
- return a;
- }
- var endClamped = Math.min(end, s.frameCount - 1);
- for (var f = Math.max(0, start); f <= endClamped; f++) {
- var fr = s.r.samples[f];
- if (!fr) continue;
- var ids = fr.ids, calls = fr.calls, us = fr.us;
- for (var i = 0; i < ids.length; i++) {
- var id = ids[i];
- var a = get(id);
- a.calls += calls[i];
- a.us += us[i];
- if (us[i] > a.maxUs) a.maxUs = us[i];
- a.frames++;
- }
- }
- return agg;
- }
- // Ranges can be served from retained samples or by re-parsing the original
- // file's byte range on demand (the frame offset index makes that seekable).
- function canQueryRanges() {
- var s = state;
- return !!(s && (s.r.samples || (s.file && s.r.frameStartOffsets && worker)));
- }
- function trimRangeCache(cache) {
- while (cache.size > 32) {
- var first = cache.keys().next().value;
- cache.delete(first);
- }
- }
- function updateRangeStatus(msg) {
- var el = $("range-status");
- if (el) el.textContent = msg || "";
- }
- // Compute state.agg for the current range: synchronously when cheap, otherwise
- // ask the worker to parse just the selected byte span.
- function refreshAggregate() {
- var s = state;
- var a = s.rangeStart, b = s.rangeEnd;
- if (a <= 0 && b >= s.frameCount - 1) {
- s.agg = s.r.aggregates;
- s.rangePending = false;
- renderAll();
- return;
- }
- var key = a + ":" + b;
- var cached = s.rangeCache.get(key);
- if (cached) {
- s.agg = cached;
- s.rangePending = false;
- renderAll();
- return;
- }
- if (s.r.samples) {
- s.agg = computeFromSamples(a, b);
- s.rangeCache.set(key, s.agg);
- trimRangeCache(s.rangeCache);
- s.rangePending = false;
- renderAll();
- return;
- }
- if (!s.file || !s.r.frameStartOffsets || !worker) {
- // No way to re-read the source; fall back to the whole-recording aggregate.
- s.agg = s.r.aggregates;
- s.rangePending = false;
- renderAll();
- return;
- }
- var requestId = ++s.requestId;
- s.rangePending = true;
- updateRangeStatus("Loading frames " + a + "\u2013" + b + "\u2026");
- drawTimeline();
- var startOff = s.r.frameStartOffsets[a];
- var endOff = s.r.frameEndOffsets[b];
- s.file.slice(startOff, endOff).arrayBuffer().then(function (buf) {
- if (!state || state.requestId !== requestId) return;
- worker.postMessage({ cmd: "query", buffer: buf, startFrame: a, endFrame: b, requestId: requestId }, [buf]);
- }).catch(function (err) {
- if (!state || state.requestId !== requestId) return;
- state.rangePending = false;
- updateRangeStatus("Range read failed: " + err);
- renderAll();
- });
- }
- function recompute() {
- var s = state;
- var agg = s.agg || s.r.aggregates;
- s.agg = agg;
- var self = new Float64Array(s.maxId);
- for (var id = 0; id < agg.length; id++) if (agg[id]) self[id] = agg[id].us;
- for (var id2 = 0; id2 < agg.length; id2++) {
- if (!agg[id2]) continue;
- var n = s.nodeById[id2];
- if (!n) continue;
- var kids = n.children;
- for (var c = 0; c < kids.length; c++) if (agg[kids[c]]) self[id2] -= agg[kids[c]].us;
- }
- s.self = self;
- var rootAgg = agg[s.r.rootNodeId];
- s.rootTotal = rootAgg ? rootAgg.us : 0;
- if (s.metricTime) {
- s.percentBase = s.rootTotal || 1;
- } else {
- s.percentBase = (rootAgg && rootAgg.calls) || 1;
- }
- }
- /* ------------------------------- rendering ------------------------------- */
- function renderAll() {
- recompute();
- renderSummary();
- drawTimeline();
- renderTree();
- renderBudgets();
- renderTop();
- ["summary-section", "timeline-section", "tree-section", "budget-section", "top-section"]
- .forEach(function (id) { $(id).classList.remove("hidden"); });
- // Range scoping works with retained samples or by re-parsing the source file
- // on demand, so only disable it when neither is available.
- var rangeDisabled = !canQueryRanges();
- ["range-start", "range-end", "range-apply", "frame-prev", "frame-next"].forEach(function (id) {
- $(id).disabled = rangeDisabled;
- });
- if (!state.rangePending) updateRangeStatus("");
- }
- function renderSummary() {
- var s = state, r = s.r;
- var notices = $("notices");
- notices.innerHTML = "";
- if (!r.anyTime) {
- var d = document.createElement("div");
- d.className = "warn";
- d.innerHTML = "<strong>No timing data in this recording.</strong> Every sampled " +
- "microsecond value is zero, so time/self/percentage columns are not meaningful. " +
- "This is common for captures made by the engine's snapshot path, which clears the " +
- "current frame's counters before writing them. The call tree, call counts and " +
- "structure below are still shown.";
- notices.appendChild(d);
- }
- if (r.samplesOverflow) {
- var d2 = document.createElement("div");
- d2.className = "notice";
- d2.textContent = "This recording is very large (" + fmtInt(r.totalSamples) +
- " samples), so per-frame samples were not kept in memory. Frame ranges are " +
- "still available: the viewer re-parses only the selected byte range from the " +
- "source file on demand.";
- notices.appendChild(d2);
- }
- var rows = [
- ["File", s.file ? s.file.name : "(unknown)"],
- ["Size", s.file ? fmtBytes(s.file.size) : "-"],
- ["Format version", r.version],
- ["Frames", fmtInt(r.frameCount)],
- ["Range", s.rangeStart + "\u2013" + s.rangeEnd + " (" + fmtInt(s.rangeEnd - s.rangeStart + 1) + " frames)"],
- ["Nodes", fmtInt(s.nodes.length)],
- ["Budget groups", fmtInt(r.budgetGroups.length)],
- ["Samples", fmtInt(r.totalSamples)],
- ["Total time", fmtUs(Math.round(s.rootTotal))],
- ["Parse time", Math.round(r.parseMs) + " ms"]
- ];
- $("summary").innerHTML = rows.map(function (row) {
- return '<div><div class="k">' + esc(row[0]) + '</div><div class="v">' + esc(row[1]) + "</div></div>";
- }).join("");
- }
- /* ---- timeline ---- */
- var drag = null;
- function useCallsForTimeline() {
- return !state.r.anyTime;
- }
- function timelineData() {
- return useCallsForTimeline() ? state.r.frameCalls : state.r.frameTotals;
- }
- function frameAt(clientX) {
- var c = $("timeline");
- var rect = c.getBoundingClientRect();
- var x = clientX - rect.left;
- var f = Math.floor((x / rect.width) * state.frameCount);
- if (f < 0) f = 0;
- if (f > state.frameCount - 1) f = state.frameCount - 1;
- return f;
- }
- function drawTimeline() {
- var c = $("timeline");
- var wrap = c.parentElement;
- var w = wrap.clientWidth || 600;
- var h = 120;
- var dpr = window.devicePixelRatio || 1;
- c.width = Math.round(w * dpr);
- c.height = Math.round(h * dpr);
- c.style.width = w + "px";
- c.style.height = h + "px";
- var ctx = c.getContext("2d");
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
- ctx.clearRect(0, 0, w, h);
- var s = state;
- var data = timelineData();
- var max = 0;
- for (var i = 0; i < s.frameCount; i++) if (data[i] > max) max = data[i];
- var selA = s.rangeStart, selB = s.rangeEnd;
- if (drag && drag.active) {
- selA = Math.min(drag.start, drag.end);
- selB = Math.max(drag.start, drag.end);
- }
- var padTop = 4, padBottom = 16;
- var plotH = h - padTop - padBottom;
- var bw = s.frameCount ? (w / s.frameCount) : w;
- for (var f = 0; f < s.frameCount; f++) {
- var v = data[f];
- var bh = max ? (v / max) * plotH : 0;
- var x = f * bw;
- var inSel = (f >= selA && f <= selB);
- ctx.fillStyle = inSel ? "#2b6cb0" : "#b9c6d6";
- ctx.fillRect(x, padTop + plotH - bh, Math.max(1, bw - 0.25), bh);
- }
- // selection background edges
- ctx.strokeStyle = "#e0a800";
- ctx.lineWidth = 1;
- if (selA > 0) { var xa = selA * bw; ctx.beginPath(); ctx.moveTo(xa, padTop); ctx.lineTo(xa, padTop + plotH); ctx.stroke(); }
- if (selB < s.frameCount - 1) { var xb = (selB + 1) * bw; ctx.beginPath(); ctx.moveTo(xb, padTop); ctx.lineTo(xb, padTop + plotH); ctx.stroke(); }
- ctx.fillStyle = "#333";
- ctx.font = "11px system-ui, sans-serif";
- var unit = useCallsForTimeline() ? "calls" : "time";
- ctx.fillText("max " + (useCallsForTimeline() ? fmtInt(max) : fmtUs(max)) + " per frame (" + unit + ")", 4, h - 4);
- ctx.fillText("frame 0", 4, h - 4 - 0);
- var lastLabel = "frame " + (s.frameCount - 1);
- ctx.fillText(lastLabel, Math.max(4, w - ctx.measureText(lastLabel).width - 4), h - 4);
- $("timeline-caption").textContent = useCallsForTimeline()
- ? "Showing per-frame total call counts (no timing data available). Drag to select a frame range."
- : "Showing per-frame total microseconds. Drag to select a frame range.";
- $("frame-label").textContent = "selected frames " + selA + "-" + selB;
- }
- function applyRange(a, b) {
- var s = state;
- a = Math.max(0, Math.min(a, s.frameCount - 1));
- b = Math.max(0, Math.min(b, s.frameCount - 1));
- if (a > b) { var t = a; a = b; b = t; }
- s.rangeStart = a; s.rangeEnd = b;
- updateRangeInputs();
- refreshAggregate();
- }
- function updateRangeInputs() {
- $("range-start").value = state.rangeStart;
- $("range-end").value = state.rangeEnd;
- }
- /* ---- tree ---- */
- function compareIds(aId, bId) {
- var s = state;
- if (s.sortKey === "name") {
- return s.nodeById[aId].name.localeCompare(s.nodeById[bId].name) * s.sortDir;
- }
- var a = s.agg[aId] || { calls: 0, us: 0, maxUs: 0, frames: 0 };
- var b = s.agg[bId] || { calls: 0, us: 0, maxUs: 0, frames: 0 };
- var av, bv;
- switch (s.sortKey) {
- case "calls": av = a.calls; bv = b.calls; break;
- case "self": av = s.self[aId] || 0; bv = s.self[bId] || 0; break;
- case "avg": av = a.frames ? a.us / a.frames : 0; bv = b.frames ? b.us / b.frames : 0; break;
- case "max": av = a.maxUs; bv = b.maxUs; break;
- default: av = a.us; bv = b.us;
- }
- if (av === bv) return s.nodeById[aId].name.localeCompare(s.nodeById[bId].name);
- return (av < bv ? -1 : 1) * s.sortDir;
- }
- function computeMatchSet(text) {
- if (!text) return null;
- var s = state;
- var t = text.toLowerCase();
- var set = new Set();
- for (var i = 0; i < s.nodes.length; i++) {
- var n = s.nodes[i];
- if (n.name.toLowerCase().indexOf(t) !== -1) {
- set.add(n.id);
- var p = n.parentId;
- while (p !== null && p !== undefined) { set.add(p); p = s.nodeById[p].parentId; }
- }
- }
- return set;
- }
- function renderTree() {
- var s = state;
- var matchSet = computeMatchSet(s.filter);
- var orderCache = Object.create(null);
- function order(id) {
- var c = orderCache[id];
- if (c) return c;
- c = s.nodeById[id].children.slice();
- c.sort(compareIds);
- orderCache[id] = c;
- return c;
- }
- var rows = [];
- function visit(id, depth) {
- var n = s.nodeById[id];
- if (!n) return;
- if (matchSet && !matchSet.has(id)) return;
- var a = s.agg[id] || { calls: 0, us: 0, maxUs: 0, frames: 0 };
- var totalUs = a.us;
- var selfUs = s.self[id] || 0;
- var pct = s.percentBase ? ((s.metricTime ? totalUs : a.calls) / s.percentBase) * 100 : 0;
- rows.push({ id: id, depth: depth, n: n, a: a, totalUs: totalUs, selfUs: selfUs, pct: pct });
- var collapsed = !matchSet && s.collapsed.has(id);
- if (!collapsed) { order(id).forEach(function (c) { visit(c, depth + 1); }); }
- }
- visit(s.r.rootNodeId, 0);
- // Header
- var cols = [
- { key: "name", label: "Scope", num: false },
- { key: "calls", label: "Calls", num: true },
- { key: "total", label: "Total", num: true },
- { key: "self", label: "Self", num: true },
- { key: "avg", label: "Avg", num: true },
- { key: "max", label: "Max", num: true },
- { key: "_pct", label: "%", num: true }
- ];
- $("tree-head").innerHTML = cols.map(function (c) {
- var sortable = c.key !== "_pct";
- var marker = "";
- if (sortable && s.sortKey === c.key) marker = s.sortDir < 0 ? " \u25bc" : " \u25b2";
- return '<th class="' + (sortable ? "" : "nosort") + (c.num ? " num" : "") +
- '" data-key="' + (sortable ? c.key : "") + '">' + esc(c.label) + marker + "</th>";
- }).join("");
- Array.prototype.forEach.call($("tree-head").children, function (th) {
- var key = th.getAttribute("data-key");
- if (!key) return;
- th.onclick = function () {
- if (state.sortKey === key) state.sortDir = -state.sortDir;
- else { state.sortKey = key; state.sortDir = key === "name" ? 1 : -1; }
- renderTree();
- };
- });
- var maxMetric = 0;
- rows.forEach(function (r) { maxMetric = Math.max(maxMetric, s.metricTime ? r.totalUs : r.a.calls); });
- var html = rows.map(function (r) {
- var hasKids = r.n.children.length > 0;
- var collapsed = !matchSet && s.collapsed.has(r.id);
- var toggle = hasKids ? (collapsed ? "\u25b6" : "\u25bc") : "\u00a0";
- var indent = "padding-left:" + (4 + r.depth * 14) + "px";
- var metric = s.metricTime ? r.totalUs : r.a.calls;
- var barW = maxMetric ? Math.round(60 * metric / maxMetric) : 0;
- var avg = r.a.frames ? r.a.us / r.a.frames : 0;
- var groupName = s.groupName[r.n.budgetGroupId] || ("group " + r.n.budgetGroupId);
- return '<tr data-id="' + r.id + '">' +
- '<td class="tree-name" style="' + indent + '"><span class="toggle">' + toggle + "</span>" +
- esc(r.n.name) + ' <span class="muted">[' + esc(groupName) + "]</span>" +
- '<div class="bar" style="width:' + barW + 'px"></div></td>' +
- '<td class="num">' + fmtInt(r.a.calls) + "</td>" +
- '<td class="num">' + (s.metricTime ? fmtUs(r.totalUs) : "-") + "</td>" +
- '<td class="num">' + (s.metricTime ? fmtUs(r.selfUs) : "-") + "</td>" +
- '<td class="num">' + (s.metricTime && r.a.frames ? fmtUs(avg) : "-") + "</td>" +
- '<td class="num">' + (s.metricTime ? fmtUs(r.a.maxUs) : "-") + "</td>" +
- '<td class="num">' + (s.metricTime ? r.pct.toFixed(2) + "%" : "-") + "</td>" +
- "</tr>";
- }).join("");
- $("tree-body").innerHTML = html;
- $("tree-count").textContent = rows.length + " rows shown";
- Array.prototype.forEach.call($("tree-body").querySelectorAll("tr"), function (tr) {
- tr.onclick = function (ev) {
- var id = Number(tr.getAttribute("data-id"));
- var n = s.nodeById[id];
- if (!n || !n.children.length) return;
- if (s.collapsed.has(id)) s.collapsed.delete(id); else s.collapsed.add(id);
- renderTree();
- };
- });
- }
- /* ---- budget groups ---- */
- function renderBudgets() {
- var s = state;
- var perGroup = Object.create(null);
- for (var id = 0; id < s.agg.length; id++) {
- var a = s.agg[id];
- if (!a) continue;
- var n = s.nodeById[id];
- if (!n) continue;
- var gid = n.budgetGroupId;
- var e = perGroup[gid];
- if (!e) e = perGroup[gid] = { us: 0, calls: 0 };
- e.us += a.us; e.calls += a.calls;
- }
- var rows = [];
- for (var gi = 0; gi < s.r.budgetGroups.length; gi++) {
- var g = s.r.budgetGroups[gi];
- var e2 = perGroup[g.id];
- if (!e2 && !g.isDefault) continue;
- rows.push({ id: g.id, name: g.name, us: e2 ? e2.us : 0, calls: e2 ? e2.calls : 0 });
- }
- var metric = s.metricTime ? "us" : "calls";
- rows.sort(function (a, b) { return b[metric] - a[metric]; });
- $("budget-body").innerHTML = rows.map(function (r) {
- var pct = s.percentBase ? (r[metric] / s.percentBase) * 100 : 0;
- return "<tr><td>" + esc(r.name) + "</td><td class='num'>" + fmtInt(r.calls) + "</td>" +
- "<td class='num'>" + (s.metricTime ? fmtUs(r.us) : "-") + "</td>" +
- "<td class='num'>" + (s.metricTime ? pct.toFixed(2) + "%" : "-") + "</td></tr>";
- }).join("");
- }
- /* ---- top nodes ---- */
- function renderTop() {
- var s = state;
- var rows = [];
- for (var id = 0; id < s.agg.length; id++) {
- var a = s.agg[id];
- if (!a) continue;
- var n = s.nodeById[id];
- if (!n) continue;
- rows.push({ id: id, name: n.name, us: a.us, self: s.self[id] || 0, calls: a.calls });
- }
- if (s.metricTime) rows.sort(function (a, b) { return b.us - a.us; });
- else rows.sort(function (a, b) { return b.calls - a.calls; });
- rows = rows.slice(0, 100);
- $("top-body").innerHTML = rows.map(function (r) {
- return "<tr><td>" + esc(r.name) + "</td>" +
- "<td class='num'>" + (s.metricTime ? fmtUs(r.us) : "-") + "</td>" +
- "<td class='num'>" + (s.metricTime ? fmtUs(r.self) : "-") + "</td>" +
- "<td class='num'>" + fmtInt(r.calls) + "</td></tr>";
- }).join("");
- }
- /* ------------------------------- events ------------------------------- */
- $("file").addEventListener("change", function (e) {
- var f = e.target.files && e.target.files[0];
- if (f) startParse(f);
- });
- document.addEventListener("dragover", function (e) { e.preventDefault(); });
- document.addEventListener("drop", function (e) {
- e.preventDefault();
- var f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
- if (f) startParse(f);
- });
- // File System Access API: use it when available so the source handle can be
- // persisted and reused for on-demand range reads.
- (function initFileSystemAccess() {
- if (window.showOpenFilePicker) {
- var picker = $("open-picker");
- picker.classList.remove("hidden");
- picker.onclick = function () {
- window.showOpenFilePicker({
- multiple: false,
- types: [{ description: "VProf recording", accept: { "application/octet-stream": [".vprof"] } }]
- }).then(function (handles) {
- return handles[0].getFile().then(function (file) { startParse(file, handles[0]); });
- }).catch(function (err) {
- if (err && err.name === "AbortError") return;
- setError("Could not open file: " + err);
- });
- };
- }
- idbGet(HANDLE_KEY).then(function (entry) {
- if (entry && entry.handle) showReopenButton(entry);
- }).catch(function () {});
- })();
- $("range-all").onclick = function () { applyRange(0, state.frameCount - 1); };
- $("range-apply").onclick = function () {
- applyRange(Number($("range-start").value), Number($("range-end").value));
- };
- $("frame-prev").onclick = function () {
- var f = Math.max(0, state.rangeStart - 1); applyRange(f, f);
- };
- $("frame-next").onclick = function () {
- var f = Math.min(state.frameCount - 1, state.rangeEnd + 1); applyRange(f, f);
- };
- $("expand-all").onclick = function () { state.collapsed.clear(); renderTree(); };
- $("collapse-all").onclick = function () {
- state.collapsed.clear();
- for (var i = 0; i < state.nodes.length; i++) state.collapsed.add(state.nodes[i].id);
- state.collapsed.delete(state.r.rootNodeId);
- renderTree();
- };
- var filterTimer = null;
- $("filter").addEventListener("input", function (e) {
- clearTimeout(filterTimer);
- filterTimer = setTimeout(function () { state.filter = e.target.value.trim(); renderTree(); }, 120);
- });
- /* timeline drag selection */
- (function () {
- var c = $("timeline");
- c.addEventListener("mousedown", function (e) {
- if (!state || !canQueryRanges()) return;
- e.preventDefault();
- drag = { active: true, start: frameAt(e.clientX), end: frameAt(e.clientX) };
- drawTimeline();
- });
- window.addEventListener("mousemove", function (e) {
- if (!drag || !drag.active) return;
- drag.end = frameAt(e.clientX);
- drawTimeline();
- });
- window.addEventListener("mouseup", function (e) {
- if (!drag || !drag.active) return;
- drag.end = frameAt(e.clientX);
- var a = drag.start, b = drag.end;
- drag = null;
- applyRange(a, b);
- });
- window.addEventListener("resize", function () { if (state) drawTimeline(); });
- })();
- </script>
- </body>
- </html>
|