1
0

index.html 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Source VProf Viewer</title>
  7. <style>
  8. /* Layout only. A visual design pass will replace this. */
  9. * { box-sizing: border-box; }
  10. body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  11. font-size: 13px; margin: 0; color: #111; background: #fafafa; }
  12. header { padding: 8px 12px; background: #1a202c; color: #fff; }
  13. header h1 { font-size: 16px; margin: 0 0 6px 0; }
  14. header .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  15. header label { font-size: 12px; }
  16. main { padding: 12px; display: flex; flex-direction: column; gap: 12px; }
  17. section { background: #fff; border: 1px solid #ddd; padding: 8px; }
  18. h2 { font-size: 14px; margin: 0 0 6px 0; }
  19. h3 { font-size: 13px; margin: 10px 0 4px 0; }
  20. table { border-collapse: collapse; width: 100%; }
  21. th, td { border: 1px solid #ddd; padding: 2px 6px; text-align: left;
  22. font-size: 12px; vertical-align: top; }
  23. th { background: #f0f0f0; cursor: pointer; white-space: nowrap; }
  24. th.nosort { cursor: default; }
  25. td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
  26. .muted { color: #666; }
  27. .warn { background: #fff5e6; border: 1px solid #e0a800; padding: 6px 8px; }
  28. .notice { background: #e8f0fe; border: 1px solid #6b9bd2; padding: 6px 8px; }
  29. .summary-grid { display: flex; flex-wrap: wrap; gap: 12px 24px; }
  30. .summary-grid div { min-width: 120px; }
  31. .summary-grid .k { color: #666; font-size: 11px; }
  32. .summary-grid .v { font-size: 14px; font-weight: 600; }
  33. .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  34. .controls input[type=number] { width: 90px; }
  35. #timeline-wrap { width: 100%; }
  36. #timeline { display: block; width: 100%; height: 120px; background: #fff;
  37. border: 1px solid #ddd; cursor: crosshair; }
  38. .grid2 { display: grid; grid-template-columns: minmax(0,1fr) 360px; gap: 12px; }
  39. @media (max-width: 900px) { .grid2 { grid-template-columns: 1fr; } }
  40. .toggle { cursor: pointer; user-select: none; display: inline-block; width: 12px; }
  41. .tree-name { white-space: nowrap; }
  42. .bar { display: inline-block; height: 8px; background: #6b9bd2; vertical-align: middle; }
  43. .progress { height: 6px; background: #ddd; width: 200px; }
  44. .progress > div { height: 100%; background: #2b6cb0; width: 0%; }
  45. .scroll { max-height: 520px; overflow: auto; }
  46. .hidden { display: none; }
  47. footer { padding: 8px 12px; color: #666; font-size: 11px; }
  48. </style>
  49. </head>
  50. <body>
  51. <header>
  52. <h1>Source VProf Viewer</h1>
  53. <div class="row">
  54. <label>Choose a .vprof file: <input type="file" id="file"></label>
  55. <button id="open-picker" class="hidden">Open with file picker&hellip;</button>
  56. <button id="reopen-last" class="hidden">Reopen last recording</button>
  57. <span id="drop-hint" class="muted">or drag &amp; drop it onto this page</span>
  58. <div id="progress-wrap" class="hidden">
  59. <div class="progress"><div id="progress-bar"></div></div>
  60. <span id="status" class="muted"></span>
  61. </div>
  62. </div>
  63. </header>
  64. <main>
  65. <section id="error-box" class="warn hidden"></section>
  66. <section id="summary-section" class="hidden">
  67. <h2>Recording summary</h2>
  68. <div id="notices"></div>
  69. <div id="summary" class="summary-grid"></div>
  70. </section>
  71. <section id="timeline-section" class="hidden">
  72. <h2>Frame timeline</h2>
  73. <div class="controls">
  74. <button id="range-all">All frames</button>
  75. <label>From frame <input type="number" id="range-start" min="0" step="1"></label>
  76. <label>to <input type="number" id="range-end" min="0" step="1"></label>
  77. <button id="range-apply">Apply range</button>
  78. <button id="frame-prev">&#9664; prev</button>
  79. <button id="frame-next">next &#9654;</button>
  80. <span id="frame-label" class="muted"></span>
  81. <span id="range-status" class="muted"></span>
  82. </div>
  83. <div id="timeline-wrap"><canvas id="timeline"></canvas></div>
  84. <div id="timeline-caption" class="muted"></div>
  85. </section>
  86. <div class="grid2">
  87. <section id="tree-section" class="hidden">
  88. <h2>Call tree</h2>
  89. <div class="controls">
  90. <label>Filter <input type="search" id="filter" placeholder="name contains..."></label>
  91. <button id="expand-all">Expand all</button>
  92. <button id="collapse-all">Collapse all</button>
  93. <span id="tree-count" class="muted"></span>
  94. </div>
  95. <div class="scroll">
  96. <table id="tree-table">
  97. <thead><tr id="tree-head"></tr></thead>
  98. <tbody id="tree-body"></tbody>
  99. </table>
  100. </div>
  101. </section>
  102. <div>
  103. <section id="budget-section" class="hidden">
  104. <h2>Budget groups</h2>
  105. <div class="scroll">
  106. <table id="budget-table">
  107. <thead><tr><th class="nosort">Group</th><th class="nosort num">Calls</th>
  108. <th class="nosort num">Total</th><th class="nosort num">%</th></tr></thead>
  109. <tbody id="budget-body"></tbody>
  110. </table>
  111. </div>
  112. </section>
  113. <section id="top-section" class="hidden">
  114. <h2>Top nodes</h2>
  115. <div class="scroll">
  116. <table id="top-table">
  117. <thead><tr><th class="nosort">Scope</th><th class="nosort num">Total</th>
  118. <th class="nosort num">Self</th><th class="nosort num">Calls</th></tr></thead>
  119. <tbody id="top-body"></tbody>
  120. </table>
  121. </div>
  122. </section>
  123. </div>
  124. </div>
  125. </main>
  126. <footer>
  127. VProf binary format documentation: see <code>VPROF_FORMAT.md</code> in the repository.
  128. </footer>
  129. <!-- ===================== Parsing worker (inline, self-contained) ===================== -->
  130. <script id="worker-src" type="text/js-worker">
  131. "use strict";
  132. // The 31 budget groups registered by CVProfile::CVProfile(), in fixed ID order.
  133. var DEFAULT_BUDGET_GROUPS = [
  134. ["Unaccounted", 6],
  135. ["World Rendering", 1],
  136. ["Displacement_Rendering", 1],
  137. ["Game", 6],
  138. ["Player", 6],
  139. ["NPCs", 6],
  140. ["Server Animation", 6],
  141. ["Client_Animation", 1],
  142. ["Physics", 6],
  143. ["Static_Prop_Rendering", 1],
  144. ["Other_Model_Rendering", 1],
  145. ["Fast Path Model Rendering", 1],
  146. ["Light_Cache", 1],
  147. ["Brush_Model_Rendering", 1],
  148. ["Shadow_Rendering", 1],
  149. ["Detail_Prop_Rendering", 1],
  150. ["Particle/Effect_Rendering", 1],
  151. ["Ropes", 1],
  152. ["Dynamic_Light_Rendering", 1],
  153. ["Networking", 6],
  154. ["Sound", 6],
  155. ["VGUI", 6],
  156. ["FileSystem", 6],
  157. ["Prediction", 1],
  158. ["Interpolation", 1],
  159. ["Swap_Buffers", 1],
  160. ["Occlusion", 1],
  161. ["Overlays", 1],
  162. ["Tools", 5],
  163. ["Texture_Cache", 1],
  164. ["Replay", 2]
  165. ];
  166. var TOKEN_START_FRAME = 0;
  167. var TOKEN_ADD_NODE = 1;
  168. var TOKEN_ADD_BUDGET_GROUP = 2;
  169. var TOKEN_TIMINGS = 3;
  170. var TOKEN_END_OF_FRAME = 4;
  171. var TOKEN_FILE_FINISHED = 5;
  172. // Retained after a parse so range queries can re-parse only a byte range
  173. // without keeping every sample in memory.
  174. var g_recording = null;
  175. self.onmessage = function (e) {
  176. var msg = e.data;
  177. if (!msg) return;
  178. if (msg.cmd === "parse") {
  179. try {
  180. var out = parseVprof(msg.buffer, msg.opts || {});
  181. self.postMessage({ type: "done", result: out.result }, out.transfer);
  182. } catch (err) {
  183. self.postMessage({ type: "error", message: (err && err.message) || String(err) });
  184. }
  185. } else if (msg.cmd === "query") {
  186. var t0 = (self.performance && self.performance.now) ? self.performance.now() : Date.now();
  187. try {
  188. var agg = parseRange(msg.buffer, msg.startFrame, msg.endFrame);
  189. var t1 = (self.performance && self.performance.now) ? self.performance.now() : Date.now();
  190. self.postMessage({ type: "range", requestId: msg.requestId, startFrame: msg.startFrame, endFrame: msg.endFrame, aggregates: agg, ms: t1 - t0 });
  191. } catch (err) {
  192. self.postMessage({ type: "rangeError", requestId: msg.requestId, message: (err && err.message) || String(err) });
  193. }
  194. }
  195. };
  196. function parseVprof(buffer, opts) {
  197. var t0 = (self.performance && self.performance.now) ? self.performance.now() : Date.now();
  198. var dv = new DataView(buffer);
  199. var totalBytes = buffer.byteLength;
  200. var pos = 0;
  201. var decoder = new TextDecoder("utf-8");
  202. function u8() { return dv.getUint8(pos++); }
  203. function u16() { var v = dv.getUint16(pos, true); pos += 2; return v; }
  204. function u32() { var v = dv.getUint32(pos, true); pos += 4; return v; }
  205. function i32() { var v = dv.getInt32(pos, true); pos += 4; return v; }
  206. function cstr() {
  207. var start = pos;
  208. while (dv.getUint8(pos) !== 0) pos++;
  209. var s = decoder.decode(new Uint8Array(buffer, start, pos - start));
  210. pos++;
  211. return s;
  212. }
  213. if (totalBytes < 8) throw new Error("File is too small to be a .vprof recording.");
  214. var version = i32();
  215. var rootNodeId = i32();
  216. // ---- Budget groups (seed the 31 defaults so sparse recordings still resolve) ----
  217. var budgetGroups = [];
  218. var groupIdByName = Object.create(null);
  219. for (var i = 0; i < DEFAULT_BUDGET_GROUPS.length; i++) {
  220. var g = DEFAULT_BUDGET_GROUPS[i];
  221. budgetGroups.push({ id: i, name: g[0], flags: g[1], isDefault: true });
  222. groupIdByName[g[0]] = i;
  223. }
  224. function addBudgetGroup(name, flags) {
  225. var id = groupIdByName[name];
  226. if (id !== undefined) { budgetGroups[id].flags = flags; return id; }
  227. id = budgetGroups.length;
  228. budgetGroups.push({ id: id, name: name, flags: flags, isDefault: false });
  229. groupIdByName[name] = id;
  230. return id;
  231. }
  232. // ---- Nodes (sparse array indexed by unique node id) ----
  233. var nodesById = [];
  234. // Frame index in which each node was first declared. Because
  235. // CVProfNode::GetSubNode() only ever *prepends*, the children order at any
  236. // frame is the final order restricted to nodes whose addedFrame <= frame.
  237. // That lets a range be parsed without replaying structure from the start.
  238. var nodesAddedFrame = [];
  239. var frameCount = 0;
  240. function makeNode(id, name, parentId, budgetGroupId) {
  241. var n = { id: id, name: name, parentId: parentId, budgetGroupId: budgetGroupId, children: [] };
  242. nodesById[id] = n;
  243. nodesAddedFrame[id] = frameCount;
  244. return n;
  245. }
  246. makeNode(rootNodeId, "Root", null, 0);
  247. // ---- Aggregates (sparse array indexed by unique node id) ----
  248. var agg = [];
  249. function aggFor(id) {
  250. var a = agg[id];
  251. if (!a) { a = { calls: 0, us: 0, maxUs: 0, frames: 0 }; agg[id] = a; }
  252. return a;
  253. }
  254. // ---- Per-frame storage budget ----
  255. var maxSamples = (opts.maxSamples && opts.maxSamples > 0) ? opts.maxSamples : 4000000;
  256. var allowSamples = opts.noSamples !== true;
  257. var storing = allowSamples;
  258. var overflow = false;
  259. var totalSamples = 0;
  260. var samples = [];
  261. var frameTicks = [];
  262. var frameTotals = [];
  263. var frameCalls = [];
  264. var addedNodesByFrame = [];
  265. // Byte offset of each frame's Token_StartFrame and of the byte just past its
  266. // Token_EndOfFrame. A [start,end] frame range maps to one contiguous
  267. // file.slice(frameStartOffsets[start], frameEndOffsets[end]).
  268. var frameStartOffsets = [];
  269. var frameEndOffsets = [];
  270. var anyTime = false;
  271. var anyCalls = false;
  272. // Outer variables reused by readTimings (avoids allocating a closure per frame)
  273. var curSamples = null;
  274. var frameTotalUs = 0;
  275. var frameTotalCalls = 0;
  276. function readTimings(nodeId) {
  277. var callsToken = u8();
  278. var calls;
  279. if (callsToken === 255) calls = u16(); else calls = callsToken;
  280. var timeUnits = u16();
  281. var us;
  282. if (timeUnits === 0xFFFF) us = u32() * 4; else us = timeUnits * 4;
  283. if (calls > 0) anyCalls = true;
  284. if (us > 0) anyTime = true;
  285. totalSamples++;
  286. if (storing) {
  287. curSamples.ids.push(nodeId);
  288. curSamples.calls.push(calls);
  289. curSamples.us.push(us);
  290. if (totalSamples > maxSamples) {
  291. // Too large to keep every frame; fall back to whole-recording aggregate.
  292. storing = false;
  293. overflow = true;
  294. samples.length = 0;
  295. curSamples = null;
  296. }
  297. }
  298. frameTotalUs += us;
  299. frameTotalCalls += calls;
  300. var a = aggFor(nodeId);
  301. a.calls += calls;
  302. a.us += us;
  303. if (us > a.maxUs) a.maxUs = us;
  304. a.frames++;
  305. var kids = nodesById[nodeId].children;
  306. for (var k = 0; k < kids.length; k++) readTimings(kids[k]);
  307. }
  308. var progressAt = 0;
  309. var totalFramesApprox = -1;
  310. while (pos < totalBytes) {
  311. var frameStart = pos;
  312. var token = u8();
  313. if (token === TOKEN_FILE_FINISHED) break;
  314. if (token !== TOKEN_START_FRAME) {
  315. throw new Error("Expected start-of-frame token at offset " + (pos - 1) + ", got " + token + ".");
  316. }
  317. var tick = i32();
  318. frameTicks.push(tick);
  319. frameTotalUs = 0;
  320. frameTotalCalls = 0;
  321. curSamples = storing ? { ids: [], calls: [], us: [] } : null;
  322. var added = [];
  323. while (true) {
  324. var inner = u8();
  325. if (inner === TOKEN_END_OF_FRAME) break;
  326. if (inner === TOKEN_ADD_BUDGET_GROUP) {
  327. var bgName = cstr();
  328. var bgFlags = i32();
  329. addBudgetGroup(bgName, bgFlags);
  330. } else if (inner === TOKEN_ADD_NODE) {
  331. var parentId = i32();
  332. var nodeName = cstr();
  333. var bgId = i32();
  334. var nodeId = i32();
  335. var parent = nodesById[parentId];
  336. if (!parent) throw new Error("AddNode references unknown parent " + parentId + " at offset " + (pos - 4) + ".");
  337. if (nodesById[nodeId]) throw new Error("AddNode re-declares node " + nodeId + " at offset " + (pos - 4) + ".");
  338. makeNode(nodeId, nodeName, parentId, bgId);
  339. parent.children.unshift(nodeId); // CVProfNode::GetSubNode() prepends
  340. added.push(nodeId);
  341. } else if (inner === TOKEN_TIMINGS) {
  342. readTimings(rootNodeId);
  343. } else {
  344. throw new Error("Unexpected token " + inner + " inside frame " + frameCount + " at offset " + (pos - 1) + ".");
  345. }
  346. }
  347. if (storing && curSamples) samples.push(curSamples);
  348. frameTotals.push(frameTotalUs);
  349. frameCalls.push(frameTotalCalls);
  350. addedNodesByFrame.push(added);
  351. frameStartOffsets.push(frameStart);
  352. frameEndOffsets.push(pos);
  353. frameCount++;
  354. if (frameCount - progressAt >= 500) {
  355. progressAt = frameCount;
  356. self.postMessage({ type: "progress", frames: frameCount, pos: pos, total: totalBytes });
  357. }
  358. }
  359. // ---- Compact and collect transferables ----
  360. var transfer = [];
  361. var frameTicksOut = Int32Array.from(frameTicks);
  362. var frameTotalsOut = Float64Array.from(frameTotals);
  363. var frameCallsOut = Uint32Array.from(frameCalls);
  364. var frameStartOffsetsOut = Float64Array.from(frameStartOffsets);
  365. var frameEndOffsetsOut = Float64Array.from(frameEndOffsets);
  366. transfer.push(frameTicksOut.buffer, frameTotalsOut.buffer, frameCallsOut.buffer,
  367. frameStartOffsetsOut.buffer, frameEndOffsetsOut.buffer);
  368. var addedOut = [];
  369. for (var f = 0; f < addedNodesByFrame.length; f++) {
  370. var arr = Int32Array.from(addedNodesByFrame[f]);
  371. transfer.push(arr.buffer);
  372. addedOut.push(arr);
  373. }
  374. var samplesOut = null;
  375. if (storing) {
  376. samplesOut = [];
  377. for (var s = 0; s < samples.length; s++) {
  378. var fr = samples[s];
  379. var ids = Int32Array.from(fr.ids);
  380. var callsArr = Uint32Array.from(fr.calls);
  381. var usArr = Float64Array.from(fr.us);
  382. transfer.push(ids.buffer, callsArr.buffer, usArr.buffer);
  383. samplesOut.push({ ids: ids, calls: callsArr, us: usArr });
  384. }
  385. }
  386. var t1 = (self.performance && self.performance.now) ? self.performance.now() : Date.now();
  387. // Keep the parsed tree around so later range queries can walk it.
  388. g_recording = {
  389. nodes: nodesById,
  390. nodesAddedFrame: nodesAddedFrame,
  391. rootNodeId: rootNodeId
  392. };
  393. return {
  394. result: {
  395. version: version,
  396. rootNodeId: rootNodeId,
  397. budgetGroups: budgetGroups,
  398. nodes: nodesById,
  399. aggregates: agg,
  400. frameCount: frameCount,
  401. frameTicks: frameTicksOut,
  402. frameTotals: frameTotalsOut,
  403. frameCalls: frameCallsOut,
  404. frameStartOffsets: frameStartOffsetsOut,
  405. frameEndOffsets: frameEndOffsetsOut,
  406. addedNodesByFrame: addedOut,
  407. samples: samplesOut,
  408. samplesStored: storing,
  409. samplesOverflow: overflow,
  410. totalSamples: totalSamples,
  411. anyTime: anyTime,
  412. anyCalls: anyCalls,
  413. parseMs: t1 - t0
  414. },
  415. transfer: transfer
  416. };
  417. }
  418. // Re-parse only the frames in a contiguous byte range and aggregate per node.
  419. // `buffer` must start on the Token_StartFrame of `startFrame` and end just past
  420. // the Token_EndOfFrame of `endFrame` (i.e. file.slice(frameStartOffsets[start],
  421. // frameEndOffsets[end])).
  422. function parseRange(buffer, startFrame, endFrame) {
  423. if (!g_recording) throw new Error("No recording loaded for a range query.");
  424. var rec = g_recording;
  425. var dv = new DataView(buffer);
  426. var totalBytes = buffer.byteLength;
  427. var pos = 0;
  428. var decoder = new TextDecoder("utf-8");
  429. function u8() { return dv.getUint8(pos++); }
  430. function u16() { var v = dv.getUint16(pos, true); pos += 2; return v; }
  431. function u32() { var v = dv.getUint32(pos, true); pos += 4; return v; }
  432. function i32() { var v = dv.getInt32(pos, true); pos += 4; return v; }
  433. function cstr() {
  434. var start = pos;
  435. while (dv.getUint8(pos) !== 0) pos++;
  436. var s = decoder.decode(new Uint8Array(buffer, start, pos - start));
  437. pos++;
  438. return s;
  439. }
  440. var agg = [];
  441. function aggFor(id) {
  442. var a = agg[id];
  443. if (!a) { a = { calls: 0, us: 0, maxUs: 0, frames: 0 }; agg[id] = a; }
  444. return a;
  445. }
  446. function readTimings(nodeId, frameIndex) {
  447. var callsToken = u8();
  448. var calls;
  449. if (callsToken === 255) calls = u16(); else calls = callsToken;
  450. var timeUnits = u16();
  451. var us;
  452. if (timeUnits === 0xFFFF) us = u32() * 4; else us = timeUnits * 4;
  453. var a = aggFor(nodeId);
  454. a.calls += calls;
  455. a.us += us;
  456. if (us > a.maxUs) a.maxUs = us;
  457. a.frames++;
  458. // Only descend into children that existed at this frame; the final child
  459. // order already reflects the prepend-only evolution of the tree.
  460. var kids = rec.nodes[nodeId].children;
  461. for (var k = 0; k < kids.length; k++) {
  462. if (rec.nodesAddedFrame[kids[k]] <= frameIndex) readTimings(kids[k], frameIndex);
  463. }
  464. }
  465. var frameIndex = startFrame;
  466. while (pos < totalBytes) {
  467. var token = u8();
  468. if (token === TOKEN_FILE_FINISHED) break;
  469. if (token !== TOKEN_START_FRAME) {
  470. throw new Error("Range parse: expected start-of-frame at offset " + (pos - 1) + ", got " + token + ".");
  471. }
  472. i32(); // tick
  473. while (true) {
  474. var inner = u8();
  475. if (inner === TOKEN_END_OF_FRAME) break;
  476. if (inner === TOKEN_ADD_BUDGET_GROUP) {
  477. cstr(); i32();
  478. } else if (inner === TOKEN_ADD_NODE) {
  479. i32(); cstr(); i32(); i32();
  480. } else if (inner === TOKEN_TIMINGS) {
  481. readTimings(rec.rootNodeId, frameIndex);
  482. } else {
  483. throw new Error("Range parse: unexpected token " + inner + " at offset " + (pos - 1) + ".");
  484. }
  485. }
  486. frameIndex++;
  487. if (frameIndex > endFrame + 1) break;
  488. }
  489. return agg;
  490. }
  491. </script>
  492. <!-- ===================== UI ===================== -->
  493. <script>
  494. "use strict";
  495. /* ------------------------------- helpers ------------------------------- */
  496. function $(id) { return document.getElementById(id); }
  497. function esc(s) {
  498. return String(s).replace(/[&<>"']/g, function (c) {
  499. return ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c];
  500. });
  501. }
  502. function fmtUs(us) {
  503. if (!us) return "0";
  504. var ms = us / 1000;
  505. if (Math.abs(ms) >= 1000) return (ms / 1000).toFixed(3) + " s";
  506. if (Math.abs(ms) >= 1) return ms.toFixed(3) + " ms";
  507. return us.toFixed(1) + " \u00b5s";
  508. }
  509. function fmtInt(n) {
  510. return (n === undefined || n === null) ? "0" : Number(n).toLocaleString();
  511. }
  512. function fmtBytes(n) {
  513. if (n < 1024) return n + " B";
  514. if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KiB";
  515. if (n < 1024 * 1024 * 1024) return (n / 1048576).toFixed(1) + " MiB";
  516. return (n / 1073741824).toFixed(2) + " GiB";
  517. }
  518. function flagsDecoded(flags) {
  519. var out = [];
  520. if (flags & 1) out.push("CLIENT");
  521. if (flags & 2) out.push("SERVER");
  522. if (flags & 4) out.push("OTHER");
  523. if (flags & 32768) out.push("HIDDEN");
  524. return out;
  525. }
  526. /* ------------------------------- state ------------------------------- */
  527. var state = null;
  528. /* ------------------------------- loading ------------------------------- */
  529. var worker = null;
  530. var lastFile = null;
  531. function setError(msg) {
  532. var box = $("error-box");
  533. if (!msg) { box.classList.add("hidden"); box.textContent = ""; return; }
  534. box.classList.remove("hidden");
  535. box.textContent = msg;
  536. }
  537. function startParse(file, handle) {
  538. lastFile = file;
  539. if (handle) rememberHandle(handle);
  540. setError("");
  541. $("progress-wrap").classList.remove("hidden");
  542. $("progress-bar").style.width = "0%";
  543. $("status").textContent = "Reading " + file.name + " (" + fmtBytes(file.size) + ")...";
  544. file.arrayBuffer().then(function (buf) {
  545. if (worker) { worker.terminate(); worker = null; }
  546. var src = $("worker-src").textContent;
  547. var url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
  548. worker = new Worker(url);
  549. worker.onmessage = onWorkerMessage;
  550. worker.onerror = function (e) {
  551. setError("Worker error: " + (e.message || "unknown"));
  552. };
  553. $("status").textContent = "Parsing...";
  554. worker.postMessage({ cmd: "parse", buffer: buf, opts: { maxSamples: 4000000 } }, [buf]);
  555. }).catch(function (err) {
  556. setError("Could not read file: " + err);
  557. });
  558. }
  559. function onWorkerMessage(e) {
  560. var msg = e.data;
  561. if (msg.type === "progress") {
  562. var pct = msg.total ? Math.floor(100 * msg.pos / msg.total) : 0;
  563. $("progress-bar").style.width = pct + "%";
  564. $("status").textContent = "Parsing... " + fmtInt(msg.frames) + " frames (" + pct + "%)";
  565. } else if (msg.type === "error") {
  566. setError("Parse error: " + msg.message);
  567. $("status").textContent = "Failed.";
  568. } else if (msg.type === "range") {
  569. var s = state;
  570. if (!s || msg.requestId !== s.requestId) return;
  571. s.agg = msg.aggregates;
  572. s.rangeCache.set(msg.startFrame + ":" + msg.endFrame, msg.aggregates);
  573. trimRangeCache(s.rangeCache);
  574. s.rangePending = false;
  575. renderAll();
  576. updateRangeStatus("Frames " + msg.startFrame + "\u2013" + msg.endFrame + " parsed in " + Math.round(msg.ms) + " ms");
  577. } else if (msg.type === "rangeError") {
  578. var s2 = state;
  579. if (!s2 || msg.requestId !== s2.requestId) return;
  580. s2.rangePending = false;
  581. renderAll();
  582. updateRangeStatus("Range parse failed: " + msg.message);
  583. } else if (msg.type === "done") {
  584. $("progress-bar").style.width = "100%";
  585. $("status").textContent = "Done.";
  586. $("progress-wrap").classList.add("hidden");
  587. buildState(msg.result, lastFile);
  588. renderAll();
  589. }
  590. }
  591. /* --------------------------- file system access --------------------------- */
  592. // Persist the FileSystemFileHandle (when the browser supports the File System
  593. // Access API) so a large recording can be reopened next time and its byte range
  594. // re-read on demand for granular frame queries. Falls back silently to the
  595. // plain <input type=file> File object, which is re-readable within a session.
  596. var HANDLE_DB = "vprof-viewer";
  597. var HANDLE_STORE = "handles";
  598. var HANDLE_KEY = "last";
  599. function idbOpen() {
  600. return new Promise(function (resolve, reject) {
  601. if (!window.indexedDB) return reject(new Error("IndexedDB unavailable"));
  602. var req = indexedDB.open(HANDLE_DB, 1);
  603. req.onupgradeneeded = function () {
  604. if (!req.result.objectStoreNames.contains(HANDLE_STORE)) req.result.createObjectStore(HANDLE_STORE);
  605. };
  606. req.onsuccess = function () { resolve(req.result); };
  607. req.onerror = function () { reject(req.error); };
  608. });
  609. }
  610. function idbSet(key, value) {
  611. return idbOpen().then(function (db) {
  612. return new Promise(function (resolve, reject) {
  613. var tx = db.transaction(HANDLE_STORE, "readwrite");
  614. tx.objectStore(HANDLE_STORE).put(value, key);
  615. tx.oncomplete = function () { resolve(); };
  616. tx.onerror = function () { reject(tx.error); };
  617. });
  618. });
  619. }
  620. function idbGet(key) {
  621. return idbOpen().then(function (db) {
  622. return new Promise(function (resolve, reject) {
  623. var req = db.transaction(HANDLE_STORE, "readonly").objectStore(HANDLE_STORE).get(key);
  624. req.onsuccess = function () { resolve(req.result); };
  625. req.onerror = function () { reject(req.error); };
  626. });
  627. });
  628. }
  629. function rememberHandle(handle) {
  630. if (!handle || !handle.getFile) return;
  631. handle.getFile().then(function (file) {
  632. idbSet(HANDLE_KEY, { handle: handle, name: file.name, size: file.size, lastModified: file.lastModified });
  633. }).catch(function () {});
  634. }
  635. function showReopenButton(entry) {
  636. var btn = $("reopen-last");
  637. btn.textContent = "Reopen " + entry.name;
  638. btn.classList.remove("hidden");
  639. btn.onclick = function () {
  640. var permission = entry.handle.queryPermission
  641. ? entry.handle.queryPermission({ mode: "read" })
  642. : Promise.resolve("granted");
  643. permission.then(function (status) {
  644. if (status !== "granted" && entry.handle.requestPermission) {
  645. return entry.handle.requestPermission({ mode: "read" });
  646. }
  647. return status;
  648. }).then(function (status) {
  649. if (status !== "granted") throw new Error("read permission denied");
  650. return entry.handle.getFile();
  651. }).then(function (file) {
  652. startParse(file, entry.handle);
  653. }).catch(function (err) {
  654. setError("Could not reopen recording: " + err);
  655. });
  656. };
  657. }
  658. /* ------------------------------- state build ------------------------------- */
  659. function buildState(result, file) {
  660. var nodeById = result.nodes;
  661. var nodes = [];
  662. for (var i = 0; i < nodeById.length; i++) if (nodeById[i]) nodes.push(nodeById[i]);
  663. var groupName = Object.create(null);
  664. for (var g = 0; g < result.budgetGroups.length; g++) groupName[result.budgetGroups[g].id] = result.budgetGroups[g].name;
  665. // Collapse everything except the root unless the tree is small.
  666. var collapsed = new Set();
  667. for (var k = 0; k < nodes.length; k++) collapsed.add(nodes[k].id);
  668. collapsed.delete(result.rootNodeId);
  669. if (nodes.length <= 200) {
  670. // small tree: expand fully
  671. collapsed.clear();
  672. }
  673. state = {
  674. r: result,
  675. file: file,
  676. nodeById: nodeById,
  677. nodes: nodes,
  678. groupName: groupName,
  679. maxId: nodeById.length,
  680. frameCount: result.frameCount,
  681. rangeStart: 0,
  682. rangeEnd: Math.max(0, result.frameCount - 1),
  683. metricTime: result.anyTime,
  684. sortKey: result.anyTime ? "total" : "name",
  685. sortDir: result.anyTime ? -1 : 1,
  686. collapsed: collapsed,
  687. filter: "",
  688. agg: result.aggregates,
  689. self: new Float64Array(nodeById.length),
  690. rootTotal: 0,
  691. percentBase: 1,
  692. rangeCache: new Map(), // "start:end" -> aggregate (bounded)
  693. requestId: 0, // newest outstanding range request
  694. rangePending: false
  695. };
  696. updateRangeInputs();
  697. }
  698. /* ------------------------------- aggregation ------------------------------- */
  699. // Aggregate per-node timings from the per-frame samples retained for small
  700. // captures.
  701. function computeFromSamples(start, end) {
  702. var s = state;
  703. var agg = [];
  704. function get(id) {
  705. var a = agg[id];
  706. if (!a) { a = { calls: 0, us: 0, maxUs: 0, frames: 0 }; agg[id] = a; }
  707. return a;
  708. }
  709. var endClamped = Math.min(end, s.frameCount - 1);
  710. for (var f = Math.max(0, start); f <= endClamped; f++) {
  711. var fr = s.r.samples[f];
  712. if (!fr) continue;
  713. var ids = fr.ids, calls = fr.calls, us = fr.us;
  714. for (var i = 0; i < ids.length; i++) {
  715. var id = ids[i];
  716. var a = get(id);
  717. a.calls += calls[i];
  718. a.us += us[i];
  719. if (us[i] > a.maxUs) a.maxUs = us[i];
  720. a.frames++;
  721. }
  722. }
  723. return agg;
  724. }
  725. // Ranges can be served from retained samples or by re-parsing the original
  726. // file's byte range on demand (the frame offset index makes that seekable).
  727. function canQueryRanges() {
  728. var s = state;
  729. return !!(s && (s.r.samples || (s.file && s.r.frameStartOffsets && worker)));
  730. }
  731. function trimRangeCache(cache) {
  732. while (cache.size > 32) {
  733. var first = cache.keys().next().value;
  734. cache.delete(first);
  735. }
  736. }
  737. function updateRangeStatus(msg) {
  738. var el = $("range-status");
  739. if (el) el.textContent = msg || "";
  740. }
  741. // Compute state.agg for the current range: synchronously when cheap, otherwise
  742. // ask the worker to parse just the selected byte span.
  743. function refreshAggregate() {
  744. var s = state;
  745. var a = s.rangeStart, b = s.rangeEnd;
  746. if (a <= 0 && b >= s.frameCount - 1) {
  747. s.agg = s.r.aggregates;
  748. s.rangePending = false;
  749. renderAll();
  750. return;
  751. }
  752. var key = a + ":" + b;
  753. var cached = s.rangeCache.get(key);
  754. if (cached) {
  755. s.agg = cached;
  756. s.rangePending = false;
  757. renderAll();
  758. return;
  759. }
  760. if (s.r.samples) {
  761. s.agg = computeFromSamples(a, b);
  762. s.rangeCache.set(key, s.agg);
  763. trimRangeCache(s.rangeCache);
  764. s.rangePending = false;
  765. renderAll();
  766. return;
  767. }
  768. if (!s.file || !s.r.frameStartOffsets || !worker) {
  769. // No way to re-read the source; fall back to the whole-recording aggregate.
  770. s.agg = s.r.aggregates;
  771. s.rangePending = false;
  772. renderAll();
  773. return;
  774. }
  775. var requestId = ++s.requestId;
  776. s.rangePending = true;
  777. updateRangeStatus("Loading frames " + a + "\u2013" + b + "\u2026");
  778. drawTimeline();
  779. var startOff = s.r.frameStartOffsets[a];
  780. var endOff = s.r.frameEndOffsets[b];
  781. s.file.slice(startOff, endOff).arrayBuffer().then(function (buf) {
  782. if (!state || state.requestId !== requestId) return;
  783. worker.postMessage({ cmd: "query", buffer: buf, startFrame: a, endFrame: b, requestId: requestId }, [buf]);
  784. }).catch(function (err) {
  785. if (!state || state.requestId !== requestId) return;
  786. state.rangePending = false;
  787. updateRangeStatus("Range read failed: " + err);
  788. renderAll();
  789. });
  790. }
  791. function recompute() {
  792. var s = state;
  793. var agg = s.agg || s.r.aggregates;
  794. s.agg = agg;
  795. var self = new Float64Array(s.maxId);
  796. for (var id = 0; id < agg.length; id++) if (agg[id]) self[id] = agg[id].us;
  797. for (var id2 = 0; id2 < agg.length; id2++) {
  798. if (!agg[id2]) continue;
  799. var n = s.nodeById[id2];
  800. if (!n) continue;
  801. var kids = n.children;
  802. for (var c = 0; c < kids.length; c++) if (agg[kids[c]]) self[id2] -= agg[kids[c]].us;
  803. }
  804. s.self = self;
  805. var rootAgg = agg[s.r.rootNodeId];
  806. s.rootTotal = rootAgg ? rootAgg.us : 0;
  807. if (s.metricTime) {
  808. s.percentBase = s.rootTotal || 1;
  809. } else {
  810. s.percentBase = (rootAgg && rootAgg.calls) || 1;
  811. }
  812. }
  813. /* ------------------------------- rendering ------------------------------- */
  814. function renderAll() {
  815. recompute();
  816. renderSummary();
  817. drawTimeline();
  818. renderTree();
  819. renderBudgets();
  820. renderTop();
  821. ["summary-section", "timeline-section", "tree-section", "budget-section", "top-section"]
  822. .forEach(function (id) { $(id).classList.remove("hidden"); });
  823. // Range scoping works with retained samples or by re-parsing the source file
  824. // on demand, so only disable it when neither is available.
  825. var rangeDisabled = !canQueryRanges();
  826. ["range-start", "range-end", "range-apply", "frame-prev", "frame-next"].forEach(function (id) {
  827. $(id).disabled = rangeDisabled;
  828. });
  829. if (!state.rangePending) updateRangeStatus("");
  830. }
  831. function renderSummary() {
  832. var s = state, r = s.r;
  833. var notices = $("notices");
  834. notices.innerHTML = "";
  835. if (!r.anyTime) {
  836. var d = document.createElement("div");
  837. d.className = "warn";
  838. d.innerHTML = "<strong>No timing data in this recording.</strong> Every sampled " +
  839. "microsecond value is zero, so time/self/percentage columns are not meaningful. " +
  840. "This is common for captures made by the engine's snapshot path, which clears the " +
  841. "current frame's counters before writing them. The call tree, call counts and " +
  842. "structure below are still shown.";
  843. notices.appendChild(d);
  844. }
  845. if (r.samplesOverflow) {
  846. var d2 = document.createElement("div");
  847. d2.className = "notice";
  848. d2.textContent = "This recording is very large (" + fmtInt(r.totalSamples) +
  849. " samples), so per-frame samples were not kept in memory. Frame ranges are " +
  850. "still available: the viewer re-parses only the selected byte range from the " +
  851. "source file on demand.";
  852. notices.appendChild(d2);
  853. }
  854. var rows = [
  855. ["File", s.file ? s.file.name : "(unknown)"],
  856. ["Size", s.file ? fmtBytes(s.file.size) : "-"],
  857. ["Format version", r.version],
  858. ["Frames", fmtInt(r.frameCount)],
  859. ["Range", s.rangeStart + "\u2013" + s.rangeEnd + " (" + fmtInt(s.rangeEnd - s.rangeStart + 1) + " frames)"],
  860. ["Nodes", fmtInt(s.nodes.length)],
  861. ["Budget groups", fmtInt(r.budgetGroups.length)],
  862. ["Samples", fmtInt(r.totalSamples)],
  863. ["Total time", fmtUs(Math.round(s.rootTotal))],
  864. ["Parse time", Math.round(r.parseMs) + " ms"]
  865. ];
  866. $("summary").innerHTML = rows.map(function (row) {
  867. return '<div><div class="k">' + esc(row[0]) + '</div><div class="v">' + esc(row[1]) + "</div></div>";
  868. }).join("");
  869. }
  870. /* ---- timeline ---- */
  871. var drag = null;
  872. function useCallsForTimeline() {
  873. return !state.r.anyTime;
  874. }
  875. function timelineData() {
  876. return useCallsForTimeline() ? state.r.frameCalls : state.r.frameTotals;
  877. }
  878. function frameAt(clientX) {
  879. var c = $("timeline");
  880. var rect = c.getBoundingClientRect();
  881. var x = clientX - rect.left;
  882. var f = Math.floor((x / rect.width) * state.frameCount);
  883. if (f < 0) f = 0;
  884. if (f > state.frameCount - 1) f = state.frameCount - 1;
  885. return f;
  886. }
  887. function drawTimeline() {
  888. var c = $("timeline");
  889. var wrap = c.parentElement;
  890. var w = wrap.clientWidth || 600;
  891. var h = 120;
  892. var dpr = window.devicePixelRatio || 1;
  893. c.width = Math.round(w * dpr);
  894. c.height = Math.round(h * dpr);
  895. c.style.width = w + "px";
  896. c.style.height = h + "px";
  897. var ctx = c.getContext("2d");
  898. ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  899. ctx.clearRect(0, 0, w, h);
  900. var s = state;
  901. var data = timelineData();
  902. var max = 0;
  903. for (var i = 0; i < s.frameCount; i++) if (data[i] > max) max = data[i];
  904. var selA = s.rangeStart, selB = s.rangeEnd;
  905. if (drag && drag.active) {
  906. selA = Math.min(drag.start, drag.end);
  907. selB = Math.max(drag.start, drag.end);
  908. }
  909. var padTop = 4, padBottom = 16;
  910. var plotH = h - padTop - padBottom;
  911. var bw = s.frameCount ? (w / s.frameCount) : w;
  912. for (var f = 0; f < s.frameCount; f++) {
  913. var v = data[f];
  914. var bh = max ? (v / max) * plotH : 0;
  915. var x = f * bw;
  916. var inSel = (f >= selA && f <= selB);
  917. ctx.fillStyle = inSel ? "#2b6cb0" : "#b9c6d6";
  918. ctx.fillRect(x, padTop + plotH - bh, Math.max(1, bw - 0.25), bh);
  919. }
  920. // selection background edges
  921. ctx.strokeStyle = "#e0a800";
  922. ctx.lineWidth = 1;
  923. if (selA > 0) { var xa = selA * bw; ctx.beginPath(); ctx.moveTo(xa, padTop); ctx.lineTo(xa, padTop + plotH); ctx.stroke(); }
  924. if (selB < s.frameCount - 1) { var xb = (selB + 1) * bw; ctx.beginPath(); ctx.moveTo(xb, padTop); ctx.lineTo(xb, padTop + plotH); ctx.stroke(); }
  925. ctx.fillStyle = "#333";
  926. ctx.font = "11px system-ui, sans-serif";
  927. var unit = useCallsForTimeline() ? "calls" : "time";
  928. ctx.fillText("max " + (useCallsForTimeline() ? fmtInt(max) : fmtUs(max)) + " per frame (" + unit + ")", 4, h - 4);
  929. ctx.fillText("frame 0", 4, h - 4 - 0);
  930. var lastLabel = "frame " + (s.frameCount - 1);
  931. ctx.fillText(lastLabel, Math.max(4, w - ctx.measureText(lastLabel).width - 4), h - 4);
  932. $("timeline-caption").textContent = useCallsForTimeline()
  933. ? "Showing per-frame total call counts (no timing data available). Drag to select a frame range."
  934. : "Showing per-frame total microseconds. Drag to select a frame range.";
  935. $("frame-label").textContent = "selected frames " + selA + "-" + selB;
  936. }
  937. function applyRange(a, b) {
  938. var s = state;
  939. a = Math.max(0, Math.min(a, s.frameCount - 1));
  940. b = Math.max(0, Math.min(b, s.frameCount - 1));
  941. if (a > b) { var t = a; a = b; b = t; }
  942. s.rangeStart = a; s.rangeEnd = b;
  943. updateRangeInputs();
  944. refreshAggregate();
  945. }
  946. function updateRangeInputs() {
  947. $("range-start").value = state.rangeStart;
  948. $("range-end").value = state.rangeEnd;
  949. }
  950. /* ---- tree ---- */
  951. function compareIds(aId, bId) {
  952. var s = state;
  953. if (s.sortKey === "name") {
  954. return s.nodeById[aId].name.localeCompare(s.nodeById[bId].name) * s.sortDir;
  955. }
  956. var a = s.agg[aId] || { calls: 0, us: 0, maxUs: 0, frames: 0 };
  957. var b = s.agg[bId] || { calls: 0, us: 0, maxUs: 0, frames: 0 };
  958. var av, bv;
  959. switch (s.sortKey) {
  960. case "calls": av = a.calls; bv = b.calls; break;
  961. case "self": av = s.self[aId] || 0; bv = s.self[bId] || 0; break;
  962. case "avg": av = a.frames ? a.us / a.frames : 0; bv = b.frames ? b.us / b.frames : 0; break;
  963. case "max": av = a.maxUs; bv = b.maxUs; break;
  964. default: av = a.us; bv = b.us;
  965. }
  966. if (av === bv) return s.nodeById[aId].name.localeCompare(s.nodeById[bId].name);
  967. return (av < bv ? -1 : 1) * s.sortDir;
  968. }
  969. function computeMatchSet(text) {
  970. if (!text) return null;
  971. var s = state;
  972. var t = text.toLowerCase();
  973. var set = new Set();
  974. for (var i = 0; i < s.nodes.length; i++) {
  975. var n = s.nodes[i];
  976. if (n.name.toLowerCase().indexOf(t) !== -1) {
  977. set.add(n.id);
  978. var p = n.parentId;
  979. while (p !== null && p !== undefined) { set.add(p); p = s.nodeById[p].parentId; }
  980. }
  981. }
  982. return set;
  983. }
  984. function renderTree() {
  985. var s = state;
  986. var matchSet = computeMatchSet(s.filter);
  987. var orderCache = Object.create(null);
  988. function order(id) {
  989. var c = orderCache[id];
  990. if (c) return c;
  991. c = s.nodeById[id].children.slice();
  992. c.sort(compareIds);
  993. orderCache[id] = c;
  994. return c;
  995. }
  996. var rows = [];
  997. function visit(id, depth) {
  998. var n = s.nodeById[id];
  999. if (!n) return;
  1000. if (matchSet && !matchSet.has(id)) return;
  1001. var a = s.agg[id] || { calls: 0, us: 0, maxUs: 0, frames: 0 };
  1002. var totalUs = a.us;
  1003. var selfUs = s.self[id] || 0;
  1004. var pct = s.percentBase ? ((s.metricTime ? totalUs : a.calls) / s.percentBase) * 100 : 0;
  1005. rows.push({ id: id, depth: depth, n: n, a: a, totalUs: totalUs, selfUs: selfUs, pct: pct });
  1006. var collapsed = !matchSet && s.collapsed.has(id);
  1007. if (!collapsed) { order(id).forEach(function (c) { visit(c, depth + 1); }); }
  1008. }
  1009. visit(s.r.rootNodeId, 0);
  1010. // Header
  1011. var cols = [
  1012. { key: "name", label: "Scope", num: false },
  1013. { key: "calls", label: "Calls", num: true },
  1014. { key: "total", label: "Total", num: true },
  1015. { key: "self", label: "Self", num: true },
  1016. { key: "avg", label: "Avg", num: true },
  1017. { key: "max", label: "Max", num: true },
  1018. { key: "_pct", label: "%", num: true }
  1019. ];
  1020. $("tree-head").innerHTML = cols.map(function (c) {
  1021. var sortable = c.key !== "_pct";
  1022. var marker = "";
  1023. if (sortable && s.sortKey === c.key) marker = s.sortDir < 0 ? " \u25bc" : " \u25b2";
  1024. return '<th class="' + (sortable ? "" : "nosort") + (c.num ? " num" : "") +
  1025. '" data-key="' + (sortable ? c.key : "") + '">' + esc(c.label) + marker + "</th>";
  1026. }).join("");
  1027. Array.prototype.forEach.call($("tree-head").children, function (th) {
  1028. var key = th.getAttribute("data-key");
  1029. if (!key) return;
  1030. th.onclick = function () {
  1031. if (state.sortKey === key) state.sortDir = -state.sortDir;
  1032. else { state.sortKey = key; state.sortDir = key === "name" ? 1 : -1; }
  1033. renderTree();
  1034. };
  1035. });
  1036. var maxMetric = 0;
  1037. rows.forEach(function (r) { maxMetric = Math.max(maxMetric, s.metricTime ? r.totalUs : r.a.calls); });
  1038. var html = rows.map(function (r) {
  1039. var hasKids = r.n.children.length > 0;
  1040. var collapsed = !matchSet && s.collapsed.has(r.id);
  1041. var toggle = hasKids ? (collapsed ? "\u25b6" : "\u25bc") : "\u00a0";
  1042. var indent = "padding-left:" + (4 + r.depth * 14) + "px";
  1043. var metric = s.metricTime ? r.totalUs : r.a.calls;
  1044. var barW = maxMetric ? Math.round(60 * metric / maxMetric) : 0;
  1045. var avg = r.a.frames ? r.a.us / r.a.frames : 0;
  1046. var groupName = s.groupName[r.n.budgetGroupId] || ("group " + r.n.budgetGroupId);
  1047. return '<tr data-id="' + r.id + '">' +
  1048. '<td class="tree-name" style="' + indent + '"><span class="toggle">' + toggle + "</span>" +
  1049. esc(r.n.name) + ' <span class="muted">[' + esc(groupName) + "]</span>" +
  1050. '<div class="bar" style="width:' + barW + 'px"></div></td>' +
  1051. '<td class="num">' + fmtInt(r.a.calls) + "</td>" +
  1052. '<td class="num">' + (s.metricTime ? fmtUs(r.totalUs) : "-") + "</td>" +
  1053. '<td class="num">' + (s.metricTime ? fmtUs(r.selfUs) : "-") + "</td>" +
  1054. '<td class="num">' + (s.metricTime && r.a.frames ? fmtUs(avg) : "-") + "</td>" +
  1055. '<td class="num">' + (s.metricTime ? fmtUs(r.a.maxUs) : "-") + "</td>" +
  1056. '<td class="num">' + (s.metricTime ? r.pct.toFixed(2) + "%" : "-") + "</td>" +
  1057. "</tr>";
  1058. }).join("");
  1059. $("tree-body").innerHTML = html;
  1060. $("tree-count").textContent = rows.length + " rows shown";
  1061. Array.prototype.forEach.call($("tree-body").querySelectorAll("tr"), function (tr) {
  1062. tr.onclick = function (ev) {
  1063. var id = Number(tr.getAttribute("data-id"));
  1064. var n = s.nodeById[id];
  1065. if (!n || !n.children.length) return;
  1066. if (s.collapsed.has(id)) s.collapsed.delete(id); else s.collapsed.add(id);
  1067. renderTree();
  1068. };
  1069. });
  1070. }
  1071. /* ---- budget groups ---- */
  1072. function renderBudgets() {
  1073. var s = state;
  1074. var perGroup = Object.create(null);
  1075. for (var id = 0; id < s.agg.length; id++) {
  1076. var a = s.agg[id];
  1077. if (!a) continue;
  1078. var n = s.nodeById[id];
  1079. if (!n) continue;
  1080. var gid = n.budgetGroupId;
  1081. var e = perGroup[gid];
  1082. if (!e) e = perGroup[gid] = { us: 0, calls: 0 };
  1083. e.us += a.us; e.calls += a.calls;
  1084. }
  1085. var rows = [];
  1086. for (var gi = 0; gi < s.r.budgetGroups.length; gi++) {
  1087. var g = s.r.budgetGroups[gi];
  1088. var e2 = perGroup[g.id];
  1089. if (!e2 && !g.isDefault) continue;
  1090. rows.push({ id: g.id, name: g.name, us: e2 ? e2.us : 0, calls: e2 ? e2.calls : 0 });
  1091. }
  1092. var metric = s.metricTime ? "us" : "calls";
  1093. rows.sort(function (a, b) { return b[metric] - a[metric]; });
  1094. $("budget-body").innerHTML = rows.map(function (r) {
  1095. var pct = s.percentBase ? (r[metric] / s.percentBase) * 100 : 0;
  1096. return "<tr><td>" + esc(r.name) + "</td><td class='num'>" + fmtInt(r.calls) + "</td>" +
  1097. "<td class='num'>" + (s.metricTime ? fmtUs(r.us) : "-") + "</td>" +
  1098. "<td class='num'>" + (s.metricTime ? pct.toFixed(2) + "%" : "-") + "</td></tr>";
  1099. }).join("");
  1100. }
  1101. /* ---- top nodes ---- */
  1102. function renderTop() {
  1103. var s = state;
  1104. var rows = [];
  1105. for (var id = 0; id < s.agg.length; id++) {
  1106. var a = s.agg[id];
  1107. if (!a) continue;
  1108. var n = s.nodeById[id];
  1109. if (!n) continue;
  1110. rows.push({ id: id, name: n.name, us: a.us, self: s.self[id] || 0, calls: a.calls });
  1111. }
  1112. if (s.metricTime) rows.sort(function (a, b) { return b.us - a.us; });
  1113. else rows.sort(function (a, b) { return b.calls - a.calls; });
  1114. rows = rows.slice(0, 100);
  1115. $("top-body").innerHTML = rows.map(function (r) {
  1116. return "<tr><td>" + esc(r.name) + "</td>" +
  1117. "<td class='num'>" + (s.metricTime ? fmtUs(r.us) : "-") + "</td>" +
  1118. "<td class='num'>" + (s.metricTime ? fmtUs(r.self) : "-") + "</td>" +
  1119. "<td class='num'>" + fmtInt(r.calls) + "</td></tr>";
  1120. }).join("");
  1121. }
  1122. /* ------------------------------- events ------------------------------- */
  1123. $("file").addEventListener("change", function (e) {
  1124. var f = e.target.files && e.target.files[0];
  1125. if (f) startParse(f);
  1126. });
  1127. document.addEventListener("dragover", function (e) { e.preventDefault(); });
  1128. document.addEventListener("drop", function (e) {
  1129. e.preventDefault();
  1130. var f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
  1131. if (f) startParse(f);
  1132. });
  1133. // File System Access API: use it when available so the source handle can be
  1134. // persisted and reused for on-demand range reads.
  1135. (function initFileSystemAccess() {
  1136. if (window.showOpenFilePicker) {
  1137. var picker = $("open-picker");
  1138. picker.classList.remove("hidden");
  1139. picker.onclick = function () {
  1140. window.showOpenFilePicker({
  1141. multiple: false,
  1142. types: [{ description: "VProf recording", accept: { "application/octet-stream": [".vprof"] } }]
  1143. }).then(function (handles) {
  1144. return handles[0].getFile().then(function (file) { startParse(file, handles[0]); });
  1145. }).catch(function (err) {
  1146. if (err && err.name === "AbortError") return;
  1147. setError("Could not open file: " + err);
  1148. });
  1149. };
  1150. }
  1151. idbGet(HANDLE_KEY).then(function (entry) {
  1152. if (entry && entry.handle) showReopenButton(entry);
  1153. }).catch(function () {});
  1154. })();
  1155. $("range-all").onclick = function () { applyRange(0, state.frameCount - 1); };
  1156. $("range-apply").onclick = function () {
  1157. applyRange(Number($("range-start").value), Number($("range-end").value));
  1158. };
  1159. $("frame-prev").onclick = function () {
  1160. var f = Math.max(0, state.rangeStart - 1); applyRange(f, f);
  1161. };
  1162. $("frame-next").onclick = function () {
  1163. var f = Math.min(state.frameCount - 1, state.rangeEnd + 1); applyRange(f, f);
  1164. };
  1165. $("expand-all").onclick = function () { state.collapsed.clear(); renderTree(); };
  1166. $("collapse-all").onclick = function () {
  1167. state.collapsed.clear();
  1168. for (var i = 0; i < state.nodes.length; i++) state.collapsed.add(state.nodes[i].id);
  1169. state.collapsed.delete(state.r.rootNodeId);
  1170. renderTree();
  1171. };
  1172. var filterTimer = null;
  1173. $("filter").addEventListener("input", function (e) {
  1174. clearTimeout(filterTimer);
  1175. filterTimer = setTimeout(function () { state.filter = e.target.value.trim(); renderTree(); }, 120);
  1176. });
  1177. /* timeline drag selection */
  1178. (function () {
  1179. var c = $("timeline");
  1180. c.addEventListener("mousedown", function (e) {
  1181. if (!state || !canQueryRanges()) return;
  1182. e.preventDefault();
  1183. drag = { active: true, start: frameAt(e.clientX), end: frameAt(e.clientX) };
  1184. drawTimeline();
  1185. });
  1186. window.addEventListener("mousemove", function (e) {
  1187. if (!drag || !drag.active) return;
  1188. drag.end = frameAt(e.clientX);
  1189. drawTimeline();
  1190. });
  1191. window.addEventListener("mouseup", function (e) {
  1192. if (!drag || !drag.active) return;
  1193. drag.end = frameAt(e.clientX);
  1194. var a = drag.start, b = drag.end;
  1195. drag = null;
  1196. applyRange(a, b);
  1197. });
  1198. window.addEventListener("resize", function () { if (state) drawTimeline(); });
  1199. })();
  1200. </script>
  1201. </body>
  1202. </html>