| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993 |
- <!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>
- <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>
- </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;
- self.onmessage = function (e) {
- var msg = e.data;
- if (!msg || msg.cmd !== "parse") return;
- 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) });
- }
- };
- 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 = [];
- function makeNode(id, name, parentId, budgetGroupId) {
- var n = { id: id, name: name, parentId: parentId, budgetGroupId: budgetGroupId, children: [] };
- nodesById[id] = n;
- 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 = [];
- 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 frameCount = 0;
- var progressAt = 0;
- var totalFramesApprox = -1;
- while (pos < totalBytes) {
- 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);
- 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);
- transfer.push(frameTicksOut.buffer, frameTotalsOut.buffer, frameCallsOut.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();
- return {
- result: {
- version: version,
- rootNodeId: rootNodeId,
- budgetGroups: budgetGroups,
- nodes: nodesById,
- aggregates: agg,
- frameCount: frameCount,
- frameTicks: frameTicksOut,
- frameTotals: frameTotalsOut,
- frameCalls: frameCallsOut,
- addedNodesByFrame: addedOut,
- samples: samplesOut,
- samplesStored: storing,
- samplesOverflow: overflow,
- totalSamples: totalSamples,
- anyTime: anyTime,
- anyCalls: anyCalls,
- parseMs: t1 - t0
- },
- transfer: transfer
- };
- }
- </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) {
- lastFile = file;
- 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 === "done") {
- $("progress-bar").style.width = "100%";
- $("status").textContent = "Done.";
- $("progress-wrap").classList.add("hidden");
- buildState(msg.result, lastFile);
- renderAll();
- }
- }
- /* ------------------------------- 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
- };
- updateRangeInputs();
- }
- /* ------------------------------- aggregation ------------------------------- */
- function getAggregate(start, end) {
- var s = state;
- if (!s.r.samples || (start <= 0 && end >= s.frameCount - 1)) return s.r.aggregates;
- 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;
- }
- function recompute() {
- var s = state;
- var agg = getAggregate(s.rangeStart, s.rangeEnd);
- 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 needs per-frame samples; disable it when they were dropped.
- var rangeDisabled = !state.r.samples;
- ["range-start", "range-end", "range-apply", "frame-prev", "frame-next"].forEach(function (id) {
- $(id).disabled = rangeDisabled;
- });
- }
- 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 retained. Statistics cover the whole " +
- "recording and frame-range selection is disabled.";
- 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)],
- ["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();
- renderAll();
- }
- 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);
- });
- $("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 || !state.r.samples) 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>
|