<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tokio Trace Viewer</title>
<link rel="stylesheet" href="flamegraph.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
monospace;
background: #1a1a2e;
color: #e0e0e0;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
#drop-zone {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-height: 0;
box-sizing: border-box;
border: 3px dashed #444;
border-radius: 12px;
margin: 20px;
font-size: 1.2em;
color: #888;
cursor: pointer;
transition: all 0.2s;
}
#drop-zone:hover,
#drop-zone.dragover {
border-color: #6c63ff;
color: #6c63ff;
background: rgba(108, 99, 255, 0.05);
}
#drop-zone input {
display: none;
}
#drag-overlay {
display: none;
position: fixed;
inset: 20px;
z-index: 300;
pointer-events: none;
align-items: center;
justify-content: center;
background: rgba(26, 26, 46, 0.75);
border: 3px dashed #6c63ff;
border-radius: 12px;
backdrop-filter: blur(2px);
}
#drag-overlay .drag-overlay-msg {
color: #6c63ff;
font-size: 1.4em;
font-weight: 600;
text-align: center;
}
#drag-overlay .drag-overlay-msg small {
display: block;
font-size: 0.65em;
font-weight: 400;
color: #8a8ac0;
margin-top: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-spinner {
width: 32px; height: 32px;
border: 3px solid #333;
border-top-color: #6c63ff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
.loading-progress {
font-size: 0.85em;
color: #888;
margin-top: 8px;
}
#viewer {
display: none;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
#viewer-body {
display: flex;
flex-direction: row;
flex: 1;
min-height: 0;
overflow: hidden;
}
#stack-sidebar {
width: 400px;
min-width: 200px;
max-width: 70vw;
background: #1a1a2e;
display: none;
flex-direction: column;
flex-shrink: 0;
font-size: 0.8em;
line-height: 1.6;
z-index: 5;
position: relative;
}
#stack-sidebar .sidebar-resize {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
cursor: col-resize;
background: #6c63ff;
z-index: 10;
transition: background 0.15s;
}
#stack-sidebar .sidebar-resize:hover,
#stack-sidebar .sidebar-resize.active {
background: #8a82ff;
}
#stack-sidebar .sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #16213e;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
#stack-sidebar .sidebar-title {
color: #6c63ff;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#stack-sidebar .sidebar-close {
cursor: pointer;
color: #888;
font-size: 1.2em;
padding: 0 4px;
flex-shrink: 0;
}
#stack-sidebar .sidebar-close:hover {
color: #fff;
}
#stack-sidebar .sidebar-body {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
font-family: monospace;
font-size: 0.9em;
}
#stack-sidebar .sidebar-tabs {
display: flex;
gap: 0;
padding: 0 12px;
background: #16213e;
flex-shrink: 0;
border-bottom: 1px solid #333;
}
#stack-sidebar .sidebar-tabs .tab {
padding: 6px 14px;
font-size: 0.85em;
cursor: pointer;
color: #888;
border-bottom: 2px solid transparent;
transition: color 0.15s, border-color 0.15s;
}
#stack-sidebar .sidebar-tabs .tab:hover {
color: #ccc;
}
#stack-sidebar .sidebar-tabs .tab.active {
color: #fff;
border-bottom-color: #6c63ff;
}
#toolbar {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 16px;
background: #16213e;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
#toolbar .toolbar-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 12px;
min-height: 36px;
}
#toolbar .tb-info {
display: flex;
flex-direction: column;
flex-shrink: 1;
min-width: 240px;
max-width: 500px;
overflow: hidden;
line-height: 1.3;
}
#toolbar .tb-meta {
font-size: 0.8em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#toolbar .tb-meta .key { color: #888; }
#toolbar .tb-meta .val { color: #7ec8e3; }
#toolbar .filename {
font-weight: 600;
color: #6c63ff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.85em;
}
#toolbar .filename::after {
content: " v15";
font-size: 0.7em;
color: #888;
}
#toolbar .stats {
color: #888;
font-size: 0.8em;
white-space: nowrap;
}
#toolbar button {
background: #2a2a4a;
border: 1px solid #444;
color: #ccc;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
}
#toolbar button:hover {
background: #3a3a5a;
}
#toolbar button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
#toolbar select {
background: #2a2a4a;
border: 1px solid #444;
color: #ccc;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85em;
}
#tooltip {
display: none;
position: fixed;
background: #222244;
border: 1px solid #555;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.8em;
pointer-events: none;
z-index: 100;
max-width: 320px;
line-height: 1.5;
}
#tooltip .label {
color: #888;
}
#tooltip .value {
color: #fff;
font-weight: 600;
}
#main-area {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
#viewport-controls {
position: absolute;
right: 16px;
bottom: 16px;
z-index: 50;
display: flex;
flex-direction: column;
background: rgba(22, 33, 62, 0.92);
border: 1px solid #444;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
}
#viewport-controls button {
background: none;
border: none;
border-bottom: 1px solid #333;
color: #ccc;
width: 34px;
height: 34px;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
#viewport-controls button:last-child {
border-bottom: none;
}
#viewport-controls button:hover {
background: #2a3a5a;
color: #fff;
}
#viewport-controls .vc-sep {
height: 1px;
background: #555;
margin: 0;
}
#timeline-header {
height: 30px;
background: #16213e;
border-bottom: 1px solid #333;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
#lanes-wrapper {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
position: relative;
}
#lanes-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
#task-detail {
display: none;
height: 160px;
background: #16213e;
border-top: 1px solid #333;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
#span-panel {
display: none;
height: 120px;
background: #1a1a2e;
border-top: 1px solid #333;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
#span-panel .chart-label {
position: absolute;
top: 4px;
left: 8px;
font-size: 0.75em;
color: #888;
z-index: 2;
width: 184px;
max-height: 112px;
overflow-y: auto;
overflow-x: hidden;
}
#span-panel .chart-label .kv-row {
display: flex;
align-items: baseline;
gap: 2px;
margin-top: 2px;
line-height: 1.3;
}
#span-panel .chart-label .kv-key {
color: #888;
font-size: 0.9em;
white-space: nowrap;
}
#span-panel .chart-label .kv-val {
color: #ccc;
font-size: 0.9em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
#span-panel .chart-label .kv-copy {
background: none;
border: 1px solid #555;
color: #aaa;
font-size: 0.8em;
padding: 0 3px;
cursor: pointer;
border-radius: 2px;
flex-shrink: 0;
}
#span-panel .chart-label .kv-copy:hover {
background: #333;
color: #fff;
}
#span-panel canvas {
width: 100%;
height: 100%;
display: block;
}
#task-detail .chart-label {
position: absolute;
top: 4px;
left: 8px;
font-size: 0.75em;
color: #888;
z-index: 2;
}
#queue-chart {
height: 120px;
background: #16213e;
border-top: 1px solid #333;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
#queue-chart .chart-label {
position: absolute;
top: 4px;
left: 8px;
font-size: 0.75em;
color: #888;
z-index: 2;
}
#info-panel {
position: absolute;
top: 4px;
right: 12px;
font-size: 0.75em;
color: #ccc;
z-index: 2;
text-align: right;
line-height: 1.6;
}
#info-panel .ip-label { color: #888; }
#info-panel .ip-value { color: #fff; font-weight: 600; }
.lane {
height: 60px;
border-bottom: 1px solid #222;
position: relative;
display: flex;
}
.lane-label {
width: 100px;
flex-shrink: 0;
display: flex;
align-items: center;
padding-left: 12px;
font-size: 0.8em;
color: #aaa;
background: #16213e;
border-right: 1px solid #333;
z-index: 1;
}
.lane-content {
flex: 1;
position: relative;
overflow: hidden;
}
canvas {
display: block;
}
#selection-overlay {
position: absolute;
background: rgba(66, 133, 244, 0.15);
border: 1px solid rgba(66, 133, 244, 0.6);
pointer-events: none;
z-index: 9;
display: none;
}
#flamegraph-panel {
display: none;
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toast-wiggle {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-4px); }
40% { transform: translateX(4px); }
60% { transform: translateX(-3px); }
80% { transform: translateX(2px); }
}
.toast {
z-index: 60;
padding: 4px 12px;
border-radius: 6px;
font-size: 0.78em;
pointer-events: auto;
animation: toast-in 0.2s ease-out;
white-space: nowrap;
}
.toast-info {
background: #1e2050;
border: 1px solid rgba(108, 99, 255, 0.7);
color: #d0ccff;
}
.toast-warn {
background: #2a1e00;
border: 1px solid rgba(255, 152, 0, 0.7);
color: #ffd180;
}
.toast-error {
background: #2a0a0a;
border: 1px solid rgba(255, 68, 68, 0.7);
color: #ff8a8a;
}
a.uninstrumented-badge {
background: #3a1a1a;
border: 1px solid rgba(255, 68, 68, 0.5);
padding: 1px 4px;
border-radius: 2px;
font-size: 0.85em;
color: #ff8a8a;
text-decoration: none;
display: inline-block;
vertical-align: text-bottom;
cursor: pointer;
}
a.uninstrumented-badge:hover {
text-decoration: underline;
}
#help-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
z-index: 200;
background: rgba(0,0,0,0.6);
justify-content: center;
align-items: center;
}
#help-overlay.visible { display: flex; }
#help-content {
background: #1a1a2e;
border: 1px solid #444;
border-radius: 8px;
padding: 24px 32px;
max-width: 520px;
color: #ccc;
font-size: 0.9em;
line-height: 1.6;
}
#help-content h3 { color: #eee; margin: 16px 0 8px; font-size: 1em; }
#help-content h3:first-child { margin-top: 0; }
#help-content table { border-collapse: collapse; width: 100%; }
#help-content td { padding: 3px 8px; vertical-align: top; }
#help-content td:first-child { white-space: nowrap; color: #a0a0ff; font-family: monospace; }
#help-content .close-hint { text-align: center; color: #666; font-size: 0.85em; margin-top: 12px; }
#main-area:focus-visible, #queue-chart:focus-visible, #btn-info:focus-visible {
outline: 2px solid #6c63ff;
outline-offset: -2px;
}
</style>
</head>
<body>
<div id="drag-overlay">
<div class="drag-overlay-msg">
Drop trace file to load
<small>Replaces the current trace</small>
</div>
</div>
<div id="drop-zone">
<div>
<div style="font-size: 2em; margin-bottom: 12px">📊</div>
<div>
Drop a <code>.bin</code> or <code>.bin.gz</code> trace file here or click to open
</div>
<div style="font-size: 0.8em; margin-top: 8px; color: #666">
Expects D9TF binary format
</div>
<div style="margin-top: 12px">
<a href="#" id="load-demo" style="color: #6c63ff; font-size: 0.9em">or load demo trace</a>
</div>
</div>
<input type="file" id="file-input" accept=".bin,.gz" />
</div>
<div id="viewer">
<div id="toolbar">
<div class="toolbar-row" id="toolbar-row-data">
<div class="tb-info">
<span class="filename" id="tb-filename"></span>
<span class="stats" id="tb-stats"></span>
</div>
<span style="flex: 1"></span>
<select id="poi-filter">
<option value="sched">Kernel Scheduling Delays</option>
<option value="long-poll">Long Polls (>1ms)</option>
<option value="cpu-sampled">Polls with CPU Samples</option>
<option value="wake-delay">Wake→Poll Delays (>100µs)</option>
<option value="uninstrumented">Uninstrumented Polls</option>
</select>
<label style="display:flex;align-items:center;gap:4px;font-size:0.85em;cursor:pointer">
<input type="checkbox" id="sort-by-worst" style="cursor:pointer" checked>
Worst first
</label>
<button id="btn-prev-poi">◀ Prev</button>
<button id="btn-next-poi">Next ▶</button>
<span id="poi-counter" style="font-size:0.85em;color:#888"></span>
<span style="width:1px;height:20px;background:#444;margin:0 8px"></span>
<button id="btn-sched-panel" style="background:#3a1a1a;border-color:#ff4444;color:#ff8a65">⚠ Blocking Calls</button>
<button id="btn-uninstrumented-info" style="display:none;background:#1e2050;border-color:#6c63ff;color:#d0ccff;white-space:nowrap" title="Some tasks lack wake tracking — click for details">ⓘ Blind spawns</button>
<button id="btn-set-range" title="Set time range filter (reloads with only events in range)">Set Range</button>
<button id="btn-clear-range" title="Clear time range filter" style="display:none">Clear Range</button>
<button id="btn-reset">New File</button>
</div>
<div class="toolbar-row" id="toolbar-row-view">
<span style="flex: 1"></span>
<button id="btn-time-mode" title="Toggle between relative (offset from trace start) and absolute timestamps">Time: Relative</button>
<button id="btn-tz-mode" title="Toggle between UTC and local timezone for absolute timestamps" style="display:none">TZ: UTC</button>
</div>
</div>
<div id="viewer-body">
<div id="main-area" tabindex="0" role="application" aria-label="Trace timeline">
<div id="selection-overlay"></div>
<canvas id="crosshair-overlay" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:10"></canvas>
<div id="timeline-header">
<canvas id="timeline-canvas"></canvas>
<div id="toast-container" style="position:absolute;top:0;bottom:0;left:0;display:flex;align-items:center;gap:8px;z-index:60;pointer-events:none"></div>
</div>
<div id="lanes-wrapper">
<div id="lanes-container"></div>
<div id="viewport-controls" aria-label="Viewport controls">
<button id="btn-zoom-in" title="Zoom in" aria-label="Zoom in">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><line x1="15.2" y1="15.2" x2="21" y2="21"/><line x1="7.5" y1="10.5" x2="13.5" y2="10.5"/><line x1="10.5" y1="7.5" x2="10.5" y2="13.5"/></svg>
</button>
<button id="btn-zoom-out" title="Zoom out" aria-label="Zoom out">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><line x1="15.2" y1="15.2" x2="21" y2="21"/><line x1="7.5" y1="10.5" x2="13.5" y2="10.5"/></svg>
</button>
<div class="vc-sep"></div>
<button id="btn-fit" title="Fit all" aria-label="Fit all">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 9 4 4 9 4"/><polyline points="20 9 20 4 15 4"/><polyline points="4 15 4 20 9 20"/><polyline points="20 15 20 20 15 20"/></svg>
</button>
</div>
</div>
<div id="span-legend" style="display:none;padding:4px 12px;background:#1a1a2e;border-top:1px solid #333;font-size:0.78em;color:#aaa">
<input id="span-filter" type="text" placeholder="Filter spans" style="background:#0f1629;border:1px solid #444;color:#ccc;padding:2px 6px;border-radius:3px;font-size:1em;width:160px;margin-right:2px">
<button id="btn-span-filter-clear" style="font-size:0.8em;display:none;margin-right:4px">✕</button>
<button id="btn-span-prev" style="font-size:0.9em" disabled>◀</button>
<button id="btn-span-next" style="font-size:0.9em" disabled>▶</button>
<span id="span-filter-count" style="margin:0 4px;color:#666"></span>
<select id="span-pct-filter" style="background:#0f1629;border:1px solid #444;color:#ccc;padding:2px 4px;border-radius:3px;font-size:0.95em;margin-right:4px">
<option value="0">All</option>
<option value="50">≥ P50</option>
<option value="90">≥ P90</option>
<option value="95">≥ P95</option>
<option value="99">≥ P99</option>
</select>
<button id="btn-span-clear-names" style="font-size:0.8em;display:none">✕ Clear</button>
<span id="span-legend-items"></span>
</div>
<div id="span-panel">
<div class="chart-label" id="span-panel-label">Spans</div>
<span id="span-panel-info" style="position:absolute;top:4px;right:12px;font-size:0.75em;color:#aaa;z-index:2;pointer-events:none"></span>
<canvas id="span-panel-canvas"></canvas>
</div>
<div id="task-detail">
<span class="chart-label" id="task-detail-label">Task Detail</span>
<span id="task-detail-status" style="position:absolute;top:4px;right:12px;font-size:0.75em;color:#aaa;z-index:2"></span>
<canvas id="task-detail-canvas"></canvas>
</div>
<div id="queue-chart" tabindex="0" aria-label="Queue depth chart">
<span class="chart-label"
>Queue Depth (global=blue, local max=orange)</span
>
<div id="info-panel"></div>
<canvas id="queue-canvas"></canvas>
</div>
<div id="flamegraph-panel">
<div id="fg-container" style="flex:1;display:flex;flex-direction:column;overflow:hidden"></div>
</div>
<div id="uninstrumented-popup" style="display:none;position:fixed;background:#1a1a2e;border:1px solid #6c63ff;border-radius:8px;padding:12px 16px;z-index:200;max-width:700px;max-height:400px;overflow-y:auto;font-size:0.82em;line-height:1.6;box-shadow:0 4px 20px rgba(0,0,0,0.5);font-family:monospace"></div>
</div>
<div id="stack-sidebar">
<div class="sidebar-resize" id="sidebar-resize-handle"></div>
<div class="sidebar-header">
<span class="sidebar-title" id="stack-sidebar-title"></span>
<span class="sidebar-close" id="stack-sidebar-close">×</span>
</div>
<div class="sidebar-tabs" id="sidebar-tabs" style="display:none">
<span class="tab" id="tab-poll-detail" data-tab="poll-detail" style="display:none">Poll Detail</span>
<span class="tab" id="tab-flamegraph" data-tab="flamegraph">Flamegraph</span>
<span class="tab" id="tab-blocking" data-tab="blocking">Blocking Calls</span>
</div>
<div class="sidebar-body" id="stack-sidebar-body"></div>
</div>
</div>
</div>
<div id="tooltip"></div>
<div id="help-overlay" role="dialog" aria-label="Keyboard and mouse shortcuts">
<div id="help-content">
<h3>🖱 Mouse</h3>
<table>
<tr><td>Scroll wheel</td><td>Scroll worker lanes vertically</td></tr>
<tr><td>⌘/Ctrl + scroll</td><td>Zoom in/out (centered on cursor)</td></tr>
<tr><td>Drag</td><td>Pan left/right</td></tr>
<tr><td>Click poll</td><td>Select task & show wake timeline</td></tr>
<tr><td>⇧ Shift+drag</td><td>Select region → flamegraph / blocking calls</td></tr>
<tr><td>⌥ Option+drag</td><td>Select region → zoom to fit</td></tr>
</table>
<h3>⌨ Keyboard</h3>
<table>
<tr><td>Tab</td><td>Navigate between UI regions</td></tr>
<tr><td>↑ / ↓</td><td>Zoom in / out</td></tr>
<tr><td>← / →</td><td>Pan left / right</td></tr>
<tr><td>Shift</td><td>Toggle region selection (analysis). Press once to start, ← → to extend, press again or Enter to confirm</td></tr>
<tr><td>Option / Alt</td><td>Toggle region selection (zoom). Press once to start, ← → to extend, press again or Enter to confirm</td></tr>
<tr><td>Esc</td><td>Cancel selection / close panels</td></tr>
<tr><td>?</td><td>Toggle this help</td></tr>
</table>
<div class="close-hint">Press Esc or click outside to close</div>
</div>
</div>
<div id="aria-live" aria-live="polite" style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden"></div>
<script src="decode.js"></script>
<script src="trace_parser.js"></script>
<script src="trace_analysis.js"></script>
<script src="format.js"></script>
<script src="flamegraph.js"></script>
<script src="panel_layout.js"></script>
<script>
const { EVENT_TYPES: ET, parseTrace, formatFrame, symbolizeChain, deduplicateSamples } = TraceParser;
const { buildWorkerSpans, attachCpuSamples, buildActiveTaskTimeline, computeSchedulingDelays, filterPointsOfInterest, buildSpanData, collectDescendants, selectSpanRenderSet, computeSpanLayout } = TraceAnalysis;
function esc(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
function renderFrame(frame) {
const f = formatFrame(frame);
if (f.docsUrl) return `<a href="${esc(f.docsUrl)}" target="_blank" style="color:inherit;text-decoration:underline dotted">${esc(f.text)}</a>`;
return esc(f.text);
}
const _toasts = {}; const _toastTimers = {}; const _persistentToasts = new Set(); function showToast(id, msg, type = "info", autoHideMs = 0, persistent = false) {
const container = document.getElementById("toast-container");
let el = _toasts[id];
if (el) {
el.style.animation = "none";
el.offsetHeight;
el.style.animation = "toast-wiggle 0.4s ease";
return;
}
el = document.createElement("div");
el.className = `toast toast-${type}`;
el.textContent = msg;
container.appendChild(el);
_toasts[id] = el;
if (persistent) _persistentToasts.add(id);
if (autoHideMs > 0) {
_toastTimers[id] = setTimeout(() => hideToast(id), autoHideMs);
}
}
function hideToast(id) {
const el = _toasts[id];
if (!el) return;
el.remove();
delete _toasts[id];
_persistentToasts.delete(id);
clearTimeout(_toastTimers[id]);
delete _toastTimers[id];
}
function clearToasts() {
for (const id of Object.keys(_toasts)) {
if (!_persistentToasts.has(id)) hideToast(id);
}
}
const ET_NAMES = [
"PollStart",
"PollEnd",
"WorkerPark",
"WorkerUnpark",
"QueueSample",
];
const ET_COLORS = { poll: "#4fc3f7", park: "#ff8a65" };
function pollColor(startNs, endNs) {
const durUs = (endNs - startNs) / 1e3;
if (durUs > 1000) return "#ff4444"; if (durUs > 100) return "#ff8a65"; if (durUs > 10) return "#4fc3f7"; return "#2a5a7a"; }
const SPAN_COLORS = [
"#7c4dff", "#00bfa5", "#ff6e40", "#448aff", "#c6ff00",
"#ea80fc", "#ff5252", "#64ffda", "#ffd740", "#b388ff",
"#69f0ae", "#ff80ab", "#40c4ff", "#ffab40", "#b2ff59",
"#8c9eff", "#f48fb1", "#80cbc4", "#ffe57f", "#ce93d8",
];
const _spanColorMap = new Map();
function spanColor(name) {
let color = _spanColorMap.get(name);
if (!color) {
color = SPAN_COLORS[_spanColorMap.size % SPAN_COLORS.length];
_spanColorMap.set(name, color);
}
return color;
}
function spanMatchesFilter(span) {
if (selectedSpanNames.size > 0 && !selectedSpanNames.has(span.spanName)) return false;
if (spanPctFilter > 0) {
const durs = getSpanDurations()[span.spanName];
if (!durs) return false;
const thresholdIdx = Math.floor(durs.length * spanPctFilter / 100);
const threshold = durs[thresholdIdx] || 0;
if ((span.end - span.start) < threshold) return false;
}
if (!spanFilter) return true;
const f = spanFilter.toLowerCase();
if (span.spanName.toLowerCase().includes(f)) return true;
if (span.fields && typeof span.fields === "object") {
for (const [k, v] of Object.entries(span.fields)) {
if (k.toLowerCase().includes(f) || String(v).toLowerCase().includes(f)) return true;
}
}
return false;
}
let _spanDurCache = null;
function getSpanDurations() {
if (_spanDurCache) return _spanDurCache;
_spanDurCache = {};
for (const s of allSpans) {
(_spanDurCache[s.spanName] ??= []).push(s.end - s.start);
}
for (const durs of Object.values(_spanDurCache)) durs.sort((a, b) => a - b);
return _spanDurCache;
}
function spanPercentileRank(span) {
const durs = getSpanDurations()[span.spanName];
if (!durs || durs.length === 0) return null;
const dur = span.end - span.start;
const rank = durs.filter(d => d <= dur).length;
return Math.round(rank / durs.length * 100);
}
let spanPctFilter = 0;
function pollColorDim(startNs, endNs) {
const durUs = (endNs - startNs) / 1e3;
if (durUs > 1000) return "#803030"; if (durUs > 100) return "#805540"; if (durUs > 10) return "#2a5a7a"; return "#1e3040"; }
let trace = null;
let workerIds = [];
let minTs = 0,
maxTs = 0,
durationNs = 0;
let useAbsoluteTime = false;
let useLocalTz = false;
function fmtDuration(ns) {
const prefix = "+";
const v = ns;
if (v >= 1e9) return prefix + (v / 1e9).toFixed(2) + "s";
if (v >= 1e6) return prefix + (v / 1e6).toFixed(2) + "ms";
if (v >= 1e3) return prefix + (v / 1e3).toFixed(1) + "µs";
return prefix + v.toFixed(0) + "ns";
}
function clockOffsetForTimestamp(ns) {
if (!trace) return null;
const anchors = trace.clockSyncAnchors || [];
if (anchors.length === 0) return trace.clockOffsetNs ?? null;
if (anchors.length === 1) {
const a = anchors[0];
return a.realtimeNs - a.monotonicNs;
}
let lo = 0, hi = anchors.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (anchors[mid].monotonicNs < ns) lo = mid + 1;
else hi = mid;
}
if (lo === 0) {
const a = anchors[0];
return a.realtimeNs - a.monotonicNs;
}
if (lo >= anchors.length) {
const a = anchors[anchors.length - 1];
return a.realtimeNs - a.monotonicNs;
}
const prev = anchors[lo - 1];
const next = anchors[lo];
const pick = Math.abs(ns - prev.monotonicNs) <= Math.abs(next.monotonicNs - ns)
? prev
: next;
return pick.realtimeNs - pick.monotonicNs;
}
function fmtWallClock(ns) {
const offsetNs = clockOffsetForTimestamp(ns);
if (offsetNs == null) {
return fmtDuration(ns - minTs);
}
const wallNs = ns + offsetNs;
const ms = wallNs / 1e6;
const d = new Date(ms);
const pad2 = n => String(n).padStart(2, "0");
if (useLocalTz) {
return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" +
pad2(d.getSeconds());
} else {
return pad2(d.getUTCHours()) + ":" + pad2(d.getUTCMinutes()) + ":" +
pad2(d.getUTCSeconds());
}
}
function fmtTs(ns) {
if (useAbsoluteTime) return fmtWallClock(ns);
return fmtDuration(ns - minTs);
}
let viewStart = 0,
viewEnd = 0;
let workerSpans = {};
let queueSamples = [];
let workerQueueSamples = {};
let maxLocalQueue = 1;
let pointsOfInterest = [];
let currentPoiIndex = -1;
let selectedTaskId = null;
let selectedSpanId = null; let selectedSpanIds = new Set(); let activeTaskSamples = [];
let taskFirstPoll = new Map();
let hoveredWakerTaskId = null;
let taskDetailWakeRegions = [];
let wakesByTask = {}; let wakesByWorker = {}; let schedDelays = []; let allSpans = []; let spansById = new Map(); let spanMeta = new Map(); let childrenByParent = new Map(); let spanMaxDepth = 0;
let spanFilter = ""; let filteredSpans = []; let filteredSpanIndex = -1;
let selectedSpanNames = new Set(); let unmatchedSpans = [];
let focusedSpanId = null; let laneCanvases = {};
const LABEL_W = 100;
const LANE_H = 60;
const dropZone = document.getElementById("drop-zone");
const dragOverlay = document.getElementById("drag-overlay");
const fileInput = document.getElementById("file-input");
const viewer = document.getElementById("viewer");
const taskDetailPanel = document.getElementById("task-detail");
const clearRangeBtn = document.getElementById("btn-clear-range");
const tooltip = document.getElementById("tooltip");
const timelineCanvas = document.getElementById("timeline-canvas");
const queueCanvas = document.getElementById("queue-canvas");
const lanesContainer = document.getElementById("lanes-container");
dropZone.addEventListener("click", () => fileInput.click());
let dragCounter = 0;
const isFileDrag = (e) => e.dataTransfer?.types.includes("Files");
const showDragFeedback = () => {
if (viewer.style.display === "flex") dragOverlay.style.display = "flex";
else dropZone.classList.add("dragover");
};
const hideDragFeedback = () => {
dragOverlay.style.display = "none";
dropZone.classList.remove("dragover");
};
document.addEventListener("dragenter", (e) => {
if (!isFileDrag(e)) return;
e.preventDefault();
if (++dragCounter === 1) showDragFeedback();
});
document.addEventListener("dragover", (e) => {
if (isFileDrag(e)) e.preventDefault(); });
document.addEventListener("dragleave", (e) => {
if (!isFileDrag(e)) return;
if (--dragCounter <= 0) {
dragCounter = 0;
hideDragFeedback();
}
});
document.addEventListener("drop", (e) => {
if (!isFileDrag(e)) return;
e.preventDefault();
dragCounter = 0;
hideDragFeedback();
const file = e.dataTransfer.files[0];
if (file) loadFile(file);
});
fileInput.addEventListener("change", (e) => {
if (e.target.files[0]) loadFile(e.target.files[0]);
});
document.getElementById("btn-reset").addEventListener("click", () => {
resetTraceState({ clearUrlRange: true });
currentTraceBuffer = null;
currentTraceName = null;
viewer.style.display = "none";
resetDropZone();
});
let currentTraceBuffer = null;
let currentTraceName = null;
function resetTraceState({ clearUrlRange } = {}) {
selectedTaskId = null;
hoveredWakerTaskId = null;
hideStackPopup();
taskDetailPanel.style.display = "none";
schedPanelTimeRange = null;
const shouldClear = clearUrlRange ?? (viewer.style.display === "flex");
if (shouldClear) {
updateUrlTimeRange(null, null);
clearRangeBtn.style.display = "none";
}
}
function enterLoadingView(label) {
viewer.style.display = "none";
dropZone.style.display = "flex";
showLoadingState(label);
}
async function parseAndShowTrace(buffer, name, opts = {}) {
try {
const fullOpts = {
...opts,
onParseProgress: ({ bytesRead, totalBytes, eventCount }) => {
const pct = ((bytesRead / totalBytes) * 100) | 0;
updateLoadProgress(`Parsing: ${pct}% · ${(eventCount / 1000) | 0}k events`);
},
};
updateLoadProgress("Decompressing…");
trace = await parseTrace(buffer, fullOpts);
updateLoadProgress(`Analyzing ${trace.events.length.toLocaleString()} events…`);
await new Promise((r) => setTimeout(r, 0));
processTrace();
currentTraceBuffer = buffer;
currentTraceName = name;
showViewer(name);
} catch (err) {
if (err.name === "AbortError") return;
alert("Error: " + err.message);
resetDropZone();
}
}
function loadFile(file) {
resetTraceState();
enterLoadingView(`Loading ${file.name}…`);
const reader = new FileReader();
reader.onload = (e) =>
parseAndShowTrace(e.target.result, file.name, getParseOptions());
reader.readAsArrayBuffer(file);
}
let loadAbortController = null;
function showLoadingState(label) {
loadAbortController = new AbortController();
dropZone.innerHTML = '';
const wrap = document.createElement("div");
wrap.style.textAlign = "center";
wrap.innerHTML = `<div class="loading-spinner"></div><div style="margin-bottom:8px">${esc(label)}</div><div class="loading-progress" id="load-progress"></div>`;
const btn = document.createElement("button");
btn.textContent = "← Back";
btn.style.cssText = "background:#2a2a4a;border:1px solid #444;color:#ccc;padding:6px 16px;border-radius:4px;cursor:pointer;font-size:0.9em;margin-top:12px";
btn.addEventListener("click", cancelLoad);
wrap.appendChild(btn);
wrap.insertAdjacentHTML("beforeend", '<div style="margin-top:8px;font-size:0.8em;color:#666">or press Escape</div>');
dropZone.appendChild(wrap);
}
function updateLoadProgress(text) {
const el = document.getElementById("load-progress");
if (el) el.textContent = text;
}
function cancelLoad() {
if (loadAbortController) { loadAbortController.abort(); loadAbortController = null; }
resetDropZone();
}
function resetDropZone() {
dropZone.innerHTML = `
<div>
<div style="font-size: 2em; margin-bottom: 12px">📊</div>
<div>Drop a <code>.bin</code> or <code>.bin.gz</code> trace file here or click to open</div>
<div style="font-size: 0.8em; margin-top: 8px; color: #666">Expects D9TF binary format</div>
<div style="margin-top: 12px">
<a href="#" id="load-demo" style="color: #6c63ff; font-size: 0.9em">or load demo trace</a>
</div>
</div>`;
dropZone.style.display = "flex";
document.getElementById("load-demo")?.addEventListener("click", onLoadDemo);
}
async function loadTraceFromUrl(url) {
try {
updateLoadProgress("Fetching…");
const response = await fetch(url, { signal: loadAbortController?.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const arrayBuffer = await response.arrayBuffer();
loadAbortController = null;
const name = urlParams.get('svc') || url.split('/').pop()?.split('?')[0] || 'trace.bin';
await parseAndShowTrace(arrayBuffer, name, getParseOptions());
} catch (err) {
if (err.name === 'AbortError') return;
alert("Error loading trace from URL: " + err.message);
resetDropZone();
}
}
async function reparseWithRange(startNs, endNs) {
if (!currentTraceBuffer) return;
updateUrlTimeRange(startNs, endNs);
resetTraceState({ clearUrlRange: false });
enterLoadingView(`Loading ${currentTraceName}…`);
const opts = {};
if (startNs != null) opts.startTime = startNs;
if (endNs != null) opts.endTime = endNs;
await parseAndShowTrace(currentTraceBuffer, currentTraceName, opts);
clearRangeBtn.style.display = (startNs != null || endNs != null) ? "" : "none";
}
const urlParams = new URLSearchParams(window.location.search);
const traceUrl = urlParams.get('trace');
function getParseOptions() {
const params = new URLSearchParams(window.location.search);
const opts = {};
const s = params.get('start');
const e = params.get('end');
if (s != null) opts.startTime = Number(s);
if (e != null) opts.endTime = Number(e);
return opts;
}
function updateUrlTimeRange(startNs, endNs) {
const params = new URLSearchParams(window.location.search);
if (startNs != null) params.set('start', Math.round(startNs).toString());
else params.delete('start');
if (endNs != null) params.set('end', Math.round(endNs).toString());
else params.delete('end');
const newUrl = window.location.pathname + '?' + params.toString();
window.history.replaceState(null, '', newUrl);
}
if (traceUrl) {
showLoadingState('Loading trace…');
loadTraceFromUrl(traceUrl);
}
function onLoadDemo(e) {
e.stopPropagation();
e.preventDefault();
showLoadingState('Loading demo trace…');
loadTraceFromUrl("demo-trace.bin");
}
document.getElementById("load-demo")?.addEventListener("click", onLoadDemo);
function processTrace() {
const evts = trace.events;
if (!evts.length) {
alert("No events in trace");
return;
}
const wSet = new Set();
evts.forEach((e) => {
if (e.eventType !== ET.QueueSample && e.eventType !== ET.WakeEvent) wSet.add(e.workerId);
});
workerIds = [...wSet].sort((a, b) => a - b);
minTs = evts[0].timestamp;
maxTs = evts[evts.length - 1].timestamp;
for (const e of evts) {
if (e.timestamp < minTs) minTs = e.timestamp;
if (e.timestamp > maxTs) maxTs = e.timestamp;
}
durationNs = maxTs - minTs || 1;
viewStart = minTs;
viewEnd = maxTs;
const spanResult = buildWorkerSpans(evts, workerIds, maxTs);
workerSpans = spanResult.workerSpans;
const perWorker = spanResult.perWorker;
queueSamples = spanResult.queueSamples;
for (const w of workerIds) {
const a = workerSpans[w].actives;
console.log(`Worker ${w}: ${a.length} active periods` +
(a.length > 0 ? `, first ratio=${a[0].ratio.toFixed(3)}` : ''));
}
console.log(`Found ${queueSamples.length} global queue samples`);
if (trace.cpuSamples.length > 0) {
const { pollsWithCpuSamples, pollsWithSchedSamples } = attachCpuSamples(trace.cpuSamples, workerSpans);
console.log(`CPU samples: ${trace.cpuSamples.length}, polls with cpu samples: ${pollsWithCpuSamples}, polls with sched events: ${pollsWithSchedSamples}`);
}
workerQueueSamples = spanResult.workerQueueSamples;
maxLocalQueue = spanResult.maxLocalQueue;
const atResult = buildActiveTaskTimeline(trace.taskSpawnTimes, trace.taskTerminateTimes);
activeTaskSamples = atResult.activeTaskSamples;
taskFirstPoll = atResult.taskFirstPoll;
wakesByTask = spanResult.wakesByTask;
wakesByWorker = spanResult.wakesByWorker;
schedDelays = computeSchedulingDelays(workerSpans, workerIds, wakesByTask);
console.log(`Found ${Object.keys(wakesByTask).length} tasks with wake events, ${schedDelays.length} scheduling delays`);
allSpans = [];
spansById = new Map();
spanMeta = new Map();
childrenByParent = new Map();
unmatchedSpans = [];
spanMaxDepth = 0;
_spanDurCache = null;
focusedSpanId = null;
selectedSpanId = null;
selectedSpanIds = new Set();
selectedTaskId = null;
if (trace.customEvents && trace.customEvents.length > 0) {
const spanResult2 = buildSpanData(trace.customEvents);
allSpans = spanResult2.allSpans;
spanMeta = spanResult2.spanMeta;
spanMaxDepth = spanResult2.maxDepth;
unmatchedSpans = spanResult2.unmatchedSpans || [];
childrenByParent = spanResult2.childrenByParent || new Map();
for (const s of allSpans) spansById.set(s.spanId, s);
const totalSegs = allSpans.reduce((s, sp) => s + sp.segments.length, 0);
console.log(`Span events: ${allSpans.length} spans (${totalSegs} segments), ${spanMeta.size} unique span IDs, maxDepth=${spanMaxDepth}, ${unmatchedSpans.length} unmatched`);
}
}
function showViewer(filename) {
dropZone.style.display = "none";
viewer.style.display = "flex";
const infoEl = document.querySelector('#toolbar .tb-info');
const svc = urlParams.get('svc');
const host = urlParams.get('host');
const fromTime = urlParams.get('from');
const toTime = urlParams.get('to');
const segs = urlParams.get('segs');
const durStr = formatHumanDuration(durationNs);
const truncNote = trace.truncated ? " (truncated)" : "";
const filterNote = trace.timeFiltered ? " (time-filtered)" : "";
const statsText = `${trace.events.length.toLocaleString()} events · ${workerIds.length} workers · ${durStr}${truncNote}${filterNote}`;
if (svc) {
let line1 = `<span class="key">svc:</span> <span class="val">${esc(svc)}</span>`;
let lines = [line1];
if (host) lines.push(`<span class="key">host:</span> <span class="val">${esc(host)}</span>`);
if (fromTime) {
let timeLine = `<span class="key">time:</span> <span class="val">${esc(fromTime)}`;
if (toTime && toTime !== fromTime) timeLine += ` → ${esc(toTime)}`;
timeLine += `</span>`;
lines.push(timeLine);
}
lines.push(`<span class="key">${statsText}</span>`);
infoEl.innerHTML = lines.map(l => `<div class="tb-meta">${l}</div>`).join('');
} else {
infoEl.innerHTML = `<span class="filename">${esc(filename)}</span><span class="stats">${statsText}</span>`;
}
buildLanes();
updatePointsOfInterest();
{
const legend = document.getElementById("span-legend");
const spanPanel = document.getElementById("span-panel");
const allNames = new Set();
for (const s of allSpans) allNames.add(s.spanName);
if (allNames.size > 0) {
function rebuildChips() {
const items = [...allNames].sort().map(name => {
const active = selectedSpanNames.has(name);
const bg = active ? spanColor(name) : "transparent";
const border = `1px solid ${spanColor(name)}`;
const color = active ? "#fff" : "#aaa";
return `<span class="span-chip" data-name="${esc(name)}" style="display:inline-block;padding:1px 6px;margin:0 2px;border-radius:3px;cursor:pointer;background:${bg};border:${border};color:${color};font-size:0.95em">${esc(name)}</span>`;
}).join("");
const clearBtn = document.getElementById("btn-span-clear-names");
clearBtn.style.display = selectedSpanNames.size > 0 ? "" : "none";
document.getElementById("span-legend-items").innerHTML = items;
for (const chip of document.querySelectorAll(".span-chip")) {
chip.addEventListener("click", () => {
const name = chip.dataset.name;
if (selectedSpanNames.has(name)) selectedSpanNames.delete(name);
else selectedSpanNames.add(name);
rebuildChips();
buildFilteredSpans();
renderAll();
});
}
}
rebuildChips();
if (unmatchedSpans.length > 0) {
let el = document.getElementById("span-unmatched-count");
if (!el) {
el = document.createElement("span");
el.id = "span-unmatched-count";
el.style.cssText = "margin-left:8px;color:#c66;font-size:0.9em";
document.getElementById("span-legend-items").after(el);
}
el.textContent = `⚠ ${unmatchedSpans.length} unmatched`;
el.title = "Spans with enter but no exit (trace ended mid-span or segment rotated)";
}
document.getElementById("btn-span-clear-names").addEventListener("click", () => {
selectedSpanNames.clear();
rebuildChips();
buildFilteredSpans();
renderAll();
});
legend.style.display = "block";
spanPanel.style.display = "block";
document.getElementById("span-filter").addEventListener("input", (e) => {
spanFilter = e.target.value;
document.getElementById("btn-span-filter-clear").style.display = spanFilter ? "" : "none";
buildFilteredSpans();
renderAll();
});
document.getElementById("btn-span-filter-clear").addEventListener("click", () => {
const input = document.getElementById("span-filter");
input.value = "";
input.dispatchEvent(new Event("input"));
});
document.getElementById("span-pct-filter").addEventListener("change", (e) => {
spanPctFilter = Number(e.target.value);
_spanDurCache = null; buildFilteredSpans();
renderAll();
});
function buildFilteredSpans() {
filteredSpans = [];
const hasFilter = spanFilter || selectedSpanNames.size > 0 || spanPctFilter > 0;
if (!hasFilter) {
filteredSpanIndex = -1;
document.getElementById("span-filter-count").textContent = "";
document.getElementById("btn-span-prev").disabled = true;
document.getElementById("btn-span-next").disabled = true;
return;
}
for (const s of allSpans) {
if (spanMatchesFilter(s)) filteredSpans.push(s);
}
filteredSpans.sort((a, b) => a.start - b.start);
filteredSpanIndex = -1;
const countEl = document.getElementById("span-filter-count");
const prevBtn = document.getElementById("btn-span-prev");
const nextBtn = document.getElementById("btn-span-next");
countEl.textContent = filteredSpans.length > 0 ? `${filteredSpans.length} matches` : "";
prevBtn.disabled = filteredSpans.length === 0;
nextBtn.disabled = filteredSpans.length === 0;
}
function jumpToFilteredSpan(index) {
if (filteredSpans.length === 0) return;
filteredSpanIndex = ((index % filteredSpans.length) + filteredSpans.length) % filteredSpans.length;
const s = filteredSpans[filteredSpanIndex];
const spanDur = s.end - s.start;
const viewDur = Math.max(spanDur * 10, 1e6);
viewStart = Math.max(minTs, s.start - viewDur * 0.3);
viewEnd = Math.min(maxTs, viewStart + viewDur);
const laneIdx = workerIds.indexOf(s.workerId);
if (laneIdx >= 0) lanesContainer.scrollTop = laneIdx * LANE_H;
selectedSpanId = s.spanId;
selectedSpanIds = new Set([s.spanId]);
if (s.parentSpanId != null) {
let pid = s.parentSpanId;
while (pid != null && !selectedSpanIds.has(pid)) {
selectedSpanIds.add(pid);
const parent = spanMeta.get(pid);
pid = parent?.parentSpanId ?? null;
}
}
selectedTaskId = null;
document.getElementById("span-filter-count").textContent =
`${filteredSpanIndex + 1}/${filteredSpans.length}`;
renderAll();
}
document.getElementById("btn-span-prev").addEventListener("click", () => {
jumpToFilteredSpan(filteredSpanIndex - 1);
});
document.getElementById("btn-span-next").addEventListener("click", () => {
jumpToFilteredSpan(filteredSpanIndex + 1);
});
}
}
let hasSchedEvents = false;
for (const w of workerIds) {
for (const p of workerSpans[w].polls) {
if (p.schedSamples && p.schedSamples.length > 0) { hasSchedEvents = true; break; }
}
if (hasSchedEvents) break;
}
document.getElementById("btn-sched-panel").style.display = hasSchedEvents ? "" : "none";
const uninstrumentedCount = trace.taskInstrumented
? [...trace.taskInstrumented.values()].filter(v => !v).length
: 0;
const btnUninst = document.getElementById("btn-uninstrumented-info");
btnUninst.style.display = uninstrumentedCount > 0 ? "" : "none";
if (uninstrumentedCount > 0) {
btnUninst.textContent = `ⓘ Uninstrumented (${uninstrumentedCount})`;
}
showToast("shift-drag-hint", "⇧ Shift+drag to select a region", "info", 0, true);
showToast("option-drag-hint", "⌥ Option+drag to zoom", "info", 0, true);
requestAnimationFrame(renderAll);
}
function updatePointsOfInterest() {
const filterType = document.getElementById("poi-filter").value;
const sortByWorst = document.getElementById("sort-by-worst").checked;
pointsOfInterest = filterPointsOfInterest(filterType, workerSpans, workerIds, schedDelays, { hasSchedWait: trace.hasSchedWait, sortByWorst, taskInstrumented: trace.taskInstrumented });
currentPoiIndex = -1;
updatePoiCounter();
}
function updatePoiCounter() {
const counter = document.getElementById("poi-counter");
const prevBtn = document.getElementById("btn-prev-poi");
const nextBtn = document.getElementById("btn-next-poi");
if (pointsOfInterest.length === 0) {
counter.textContent = "None found";
prevBtn.disabled = true;
nextBtn.disabled = true;
} else {
const idx = currentPoiIndex >= 0 ? currentPoiIndex + 1 : 0;
counter.textContent = `${idx}/${pointsOfInterest.length}`;
prevBtn.disabled = currentPoiIndex <= 0;
nextBtn.disabled = currentPoiIndex >= pointsOfInterest.length - 1;
}
}
function jumpToPoi(index) {
if (index < 0 || index >= pointsOfInterest.length) return;
currentPoiIndex = index;
const poi = pointsOfInterest[index];
const spanDur = poi.span.end - poi.span.start;
const viewDur = Math.max(spanDur * 5, 1e6); viewStart = Math.max(minTs, poi.time - viewDur * 0.3);
viewEnd = Math.min(maxTs, viewStart + viewDur);
if (poi.schedDelay) {
const sd = poi.schedDelay;
const totalDur = sd.poll.end - sd.wakeTime;
const padded = Math.max(totalDur * 3, 1e6);
viewStart = Math.max(minTs, sd.wakeTime - padded * 0.2);
viewEnd = Math.min(maxTs, viewStart + padded);
selectedTaskId = sd.taskId;
}
const laneIdx = workerIds.indexOf(poi.worker);
if (laneIdx >= 0) {
const scrollTop = laneIdx * LANE_H;
lanesContainer.scrollTop = scrollTop;
}
updatePoiCounter();
renderAll();
}
function buildLanes() {
lanesContainer.innerHTML = "";
laneCanvases = {};
for (const w of workerIds) {
const lane = document.createElement("div");
lane.className = "lane";
const label = document.createElement("div");
label.className = "lane-label";
label.textContent = `Worker ${w}`;
const content = document.createElement("div");
content.className = "lane-content";
const canvas = document.createElement("canvas");
canvas.id = `worker-${w}-canvas`;
content.appendChild(canvas);
lane.appendChild(label);
lane.appendChild(content);
lanesContainer.appendChild(lane);
laneCanvases[w] = canvas;
}
}
let mouseNs = null; let queueChartRafId = null;
function lowerBound(arr, val) {
let lo = 0, hi = arr.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid].t < val) lo = mid + 1;
else hi = mid - 1;
}
return lo;
}
function upperBound(arr, val) {
let lo = 0, hi = arr.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid].t <= val) lo = mid + 1;
else hi = mid - 1;
}
return lo;
}
function findNearest(arr, val) {
if (!arr.length) return null;
const i = lowerBound(arr, val);
const candidates = [];
if (i < arr.length) candidates.push(i);
if (i > 0) candidates.push(i - 1);
let best = null, bestDist = Infinity;
for (const idx of candidates) {
const d = Math.abs(arr[idx].t - val);
if (d < bestDist) { bestDist = d; best = arr[idx]; }
}
return best;
}
function findSpanAt(spans, ns) {
let lo = 0, hi = spans.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (spans[mid].end < ns) lo = mid + 1;
else if (spans[mid].start > ns) hi = mid - 1;
else return spans[mid];
}
return null;
}
function findFirstVisible(spans, viewStart) {
let lo = 0, hi = spans.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (spans[mid].end < viewStart) lo = mid + 1;
else hi = mid - 1;
}
return lo;
}
function renderAll() {
const rect = lanesContainer.getBoundingClientRect();
const drawW = rect.width - LABEL_W;
if (drawW <= 0) return;
const scrollbarW = lanesContainer.offsetWidth - lanesContainer.clientWidth;
window.visibleQueueRanges = {};
let maxVisibleQ = 1;
for (const w of workerIds) {
const samples = workerQueueSamples[w];
if (!samples || !samples.length) continue;
const start = Math.max(0, lowerBound(samples, viewStart) - 1);
const end = Math.min(samples.length - 1, upperBound(samples, viewEnd));
for (let i = start; i <= end; i++) {
if (samples[i].local > maxVisibleQ) maxVisibleQ = samples[i].local;
}
}
window.sharedVisibleMaxQ = maxVisibleQ;
renderTimeline(drawW - scrollbarW);
for (const w of workerIds) renderLane(w, drawW);
renderSpanPanel(scrollbarW);
renderTaskDetail(scrollbarW);
renderQueueChart(drawW, scrollbarW);
if (sidebarSelStart && sidebarSelEnd &&
document.getElementById("stack-sidebar").style.display === "flex") {
regionStartNs = sidebarSelStart;
regionEndNs = sidebarSelEnd;
const mainRect = document.getElementById("main-area").getBoundingClientRect();
selOverlay.style.top = "0px";
selOverlay.style.height = mainRect.height + "px";
selOverlay.style.display = "block";
updateSelOverlay();
}
}
function nsToX(ns, drawW) {
return ((ns - viewStart) / (viewEnd - viewStart)) * drawW;
}
const { makeTimePanelLayout } = window.PanelLayout;
function timePanelLayout(panel, scrollbarW) {
const base = makeTimePanelLayout(
panel.clientWidth, LABEL_W, scrollbarW, viewStart, viewEnd,
);
return {
...base,
resizeCanvas(canvas, height) {
const dpr = devicePixelRatio || 1;
canvas.width = base.pw * dpr;
canvas.height = height * dpr;
canvas.style.width = base.pw + "px";
canvas.style.height = height + "px";
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
return ctx;
},
};
}
function renderTimeline(drawW) {
const c = timelineCanvas;
const parent = c.parentElement;
const dpr = devicePixelRatio || 1;
c.width = parent.clientWidth * dpr;
c.height = 30 * dpr;
c.style.width = parent.clientWidth + "px";
c.style.height = "30px";
const ctx = c.getContext("2d");
ctx.scale(dpr, dpr);
const w = parent.clientWidth;
ctx.fillStyle = "#16213e";
ctx.fillRect(0, 0, w, 30);
const viewDur = viewEnd - viewStart;
const targetTicks = Math.max(4, Math.floor(drawW / 100));
const rawInterval = viewDur / targetTicks;
const niceIntervals = [
1e3, 5e3, 1e4, 5e4, 1e5, 5e5, 1e6, 5e6, 1e7, 5e7, 1e8, 5e8,
1e9, 5e9, 1e10,
];
let interval =
niceIntervals.find((i) => i >= rawInterval) || rawInterval;
ctx.fillStyle = "#888";
ctx.font = "10px monospace";
ctx.textAlign = "center";
ctx.strokeStyle = "#333";
const firstTick = Math.ceil(viewStart / interval) * interval;
for (let t = firstTick; t <= viewEnd; t += interval) {
const x = LABEL_W + nsToX(t, drawW);
ctx.beginPath();
ctx.moveTo(x, 20);
ctx.lineTo(x, 30);
ctx.stroke();
ctx.fillText(fmtTs(t), x, 16);
}
}
function renderLane(workerId, drawW) {
const c = laneCanvases[workerId];
if (!c) {
console.error(`No canvas for worker ${workerId}`);
return;
}
const parent = c.parentElement;
const dpr = devicePixelRatio || 1;
const pw = parent.clientWidth,
ph = LANE_H;
c.width = pw * dpr;
c.height = ph * dpr;
c.style.width = pw + "px";
c.style.height = ph + "px";
const ctx = c.getContext("2d");
ctx.scale(dpr, dpr);
const spans = workerSpans[workerId];
ctx.fillStyle = "#1a1e2a";
ctx.fillRect(0, 0, pw, ph);
if (trace.hasCpuTime) {
const startIdx = findFirstVisible(spans.actives, viewStart);
for (let i = startIdx; i < spans.actives.length; i++) {
const s = spans.actives[i];
if (s.start > viewEnd) break;
const x1 = Math.max(0, nsToX(s.start, pw));
const x2 = Math.min(pw, nsToX(s.end, pw));
const w = Math.max(x2 - x1, 1);
if (s.ratio >= 0.95) {
ctx.fillStyle = "#1a2a1a"; } else if (s.ratio >= 0.5) {
ctx.fillStyle = "#2a2a1a"; } else {
ctx.fillStyle = "#2a1a1a"; }
ctx.fillRect(x1, 0, w, ph);
}
}
{
const parkStart = findFirstVisible(spans.parks, viewStart);
for (let i = parkStart; i < spans.parks.length; i++) {
const s = spans.parks[i];
if (s.start > viewEnd) break;
const x1 = Math.max(0, nsToX(s.start, pw));
const x2 = Math.min(pw, nsToX(s.end, pw));
const w = Math.max(x2 - x1, 1);
if (trace.hasSchedWait && s.schedWait > 0) {
const wakeupShouldBe = s.end - s.schedWait;
if (wakeupShouldBe > s.start) {
const xSplit = Math.min(pw, nsToX(wakeupShouldBe, pw));
const normalW = Math.max(xSplit - x1, 0);
if (normalW > 0) {
ctx.fillStyle = "#2a1520";
ctx.fillRect(x1, 0, normalW, ph);
ctx.fillStyle = "#cc5533";
ctx.fillRect(x1, 0, normalW, 4);
}
const delayW = Math.max(x2 - xSplit, 0);
if (delayW > 0) {
ctx.fillStyle = "#ff0000";
ctx.fillRect(xSplit, 0, delayW, ph);
}
} else {
ctx.fillStyle = "#ff0000";
ctx.fillRect(x1, 0, w, ph);
}
} else {
ctx.fillStyle = "#2a1520";
ctx.fillRect(x1, 0, w, ph);
ctx.fillStyle = "#cc5533";
ctx.fillRect(x1, 0, w, 4);
}
}
}
const bandTop = 10,
bandH = 20;
const poiSpan = (currentPoiIndex >= 0) ? pointsOfInterest[currentPoiIndex].span : null;
const pollStart = findFirstVisible(spans.polls, viewStart);
const nsPerPx = (viewEnd - viewStart) / pw;
if (selectedTaskId) {
for (let i = pollStart; i < spans.polls.length; i++) {
const s = spans.polls[i];
if (s.start > viewEnd) break;
if (s.taskId === selectedTaskId) continue;
const x1 = Math.max(0, nsToX(s.start, pw));
const x2 = Math.min(pw, nsToX(s.end, pw));
ctx.fillStyle = pollColorDim(s.start, s.end);
ctx.fillRect(x1, bandTop, Math.max(x2 - x1, 1), bandH);
}
ctx.fillStyle = "#ffeb3b";
for (let i = pollStart; i < spans.polls.length; i++) {
const s = spans.polls[i];
if (s.start > viewEnd) break;
if (s.taskId !== selectedTaskId) continue;
const x1 = Math.max(0, nsToX(s.start, pw));
const x2 = Math.min(pw, nsToX(s.end, pw));
ctx.fillRect(x1, bandTop, Math.max(x2 - x1, 1), bandH);
}
} else if (nsPerPx > 5000) {
let prevPx = -2, prevColor = null, runStart = -1;
for (let i = pollStart; i < spans.polls.length; i++) {
const s = spans.polls[i];
if (s.start > viewEnd) break;
const x1 = Math.max(0, nsToX(s.start, pw)) | 0;
const x2 = Math.min(pw, nsToX(s.end, pw)) | 0;
const color = pollColor(s.start, s.end);
if (x1 <= prevPx + 1 && color === prevColor) {
prevPx = Math.max(prevPx, x2);
} else {
if (prevColor) {
ctx.fillStyle = prevColor;
ctx.fillRect(runStart, bandTop, Math.max(prevPx - runStart, 1), bandH);
}
runStart = x1;
prevPx = Math.max(x1, x2);
prevColor = color;
}
}
if (prevColor) {
ctx.fillStyle = prevColor;
ctx.fillRect(runStart, bandTop, Math.max(prevPx - runStart, 1), bandH);
}
} else {
for (let i = pollStart; i < spans.polls.length; i++) {
const s = spans.polls[i];
if (s.start > viewEnd) break;
const x1 = Math.max(0, nsToX(s.start, pw));
const x2 = Math.min(pw, nsToX(s.end, pw));
ctx.fillStyle = pollColor(s.start, s.end);
ctx.fillRect(x1, bandTop, Math.max(x2 - x1, 1), bandH);
}
}
if (poiSpan) {
for (let i = pollStart; i < spans.polls.length; i++) {
const s = spans.polls[i];
if (s.start > viewEnd) break;
if (s !== poiSpan) continue;
const x1 = Math.max(0, nsToX(s.start, pw));
const x2 = Math.min(pw, nsToX(s.end, pw));
const w = Math.max(x2 - x1, 5);
ctx.fillStyle = "#ff4444";
ctx.fillRect(x1, bandTop, w, bandH);
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 2;
ctx.strokeRect(x1, bandTop, w, bandH);
}
}
ctx.fillStyle = "#ce93d8";
{
const times = spans.cpuSampleTimes;
let lo = 0, hi = times.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (times[mid] < viewStart) lo = mid + 1;
else hi = mid - 1;
}
for (let i = lo; i < times.length; i++) {
if (times[i] > viewEnd) break;
const x = nsToX(times[i], pw);
ctx.fillRect(x, bandTop + bandH, 3, 4);
}
}
for (let i = pollStart; i < spans.polls.length; i++) {
const s = spans.polls[i];
if (s.start > viewEnd) break;
if (!s.schedSamples) continue;
for (const sample of s.schedSamples) {
if (sample.timestamp < viewStart || sample.timestamp > viewEnd) continue;
const x = nsToX(sample.timestamp, pw);
ctx.fillStyle = "#ff2222";
ctx.beginPath();
ctx.moveTo(x, bandTop + bandH + 1);
ctx.lineTo(x - 3, bandTop + bandH + 7);
ctx.lineTo(x + 3, bandTop + bandH + 7);
ctx.closePath();
ctx.fill();
}
}
if (hoveredWakerTaskId) {
ctx.fillStyle = "#ff8a65";
for (let i = pollStart; i < spans.polls.length; i++) {
const s = spans.polls[i];
if (s.start > viewEnd) break;
if (s.taskId !== hoveredWakerTaskId) continue;
const x1 = Math.max(0, nsToX(s.start, pw));
const x2 = Math.min(pw, nsToX(s.end, pw));
ctx.fillRect(x1, bandTop, Math.max(x2 - x1, 2), bandH);
}
}
if (selectedSpanIds.size > 0) {
for (const s of allSpans) {
if (!selectedSpanIds.has(s.spanId)) continue;
for (const seg of s.segments) {
if (seg.workerId !== workerId) continue;
const poll = findSpanAt(spans.polls, seg.start);
if (poll) {
const x1 = Math.max(0, nsToX(poll.start, pw));
const x2 = Math.min(pw, nsToX(poll.end, pw));
ctx.strokeStyle = "#ffeb3b";
ctx.lineWidth = 2;
ctx.strokeRect(x1, bandTop, Math.max(x2 - x1, 4), bandH);
}
}
}
}
if (selectedTaskId) {
const wakes = wakesByWorker[workerId];
if (wakes && wakes.length) {
ctx.fillStyle = "#66bb6a";
for (const w of wakes) {
if (w.timestamp < viewStart || w.timestamp > viewEnd) continue;
if (w.wokenTaskId !== selectedTaskId) continue;
const x = nsToX(w.timestamp, pw);
ctx.beginPath();
ctx.moveTo(x, 2);
ctx.lineTo(x - 3, 8);
ctx.lineTo(x + 3, 8);
ctx.closePath();
ctx.fill();
}
}
}
const samples = workerQueueSamples[workerId];
if (samples && samples.length) {
const iStart = Math.max(0, lowerBound(samples, viewStart) - 1);
const iEnd = Math.min(samples.length - 1, upperBound(samples, viewEnd));
if (!window.visibleQueueRanges) window.visibleQueueRanges = {};
window.visibleQueueRanges[workerId] = { iStart, iEnd };
const qTop = 34,
qH = ph - 38;
const laneMaxQ = window.sharedVisibleMaxQ || 1;
ctx.fillStyle = "#666";
ctx.font = "8px monospace";
ctx.textAlign = "left";
ctx.fillText("q:" + laneMaxQ, 2, qTop + 8);
ctx.beginPath();
let started = false;
let lastY = qTop + qH;
for (let i = iStart; i <= iEnd; i++) {
const x = Math.max(0, Math.min(pw, nsToX(samples[i].t, pw)));
const y = qTop + qH - (samples[i].local / laneMaxQ) * qH;
if (!started) {
ctx.moveTo(x, y);
started = true;
} else {
ctx.lineTo(x, lastY);
ctx.lineTo(x, y);
}
lastY = y;
}
ctx.lineTo(pw, lastY);
const colors = ["rgba(255,200,50,0.8)", "rgba(50,255,200,0.8)", "rgba(255,50,200,0.8)", "rgba(200,50,255,0.8)"];
ctx.strokeStyle = colors[workerId % colors.length];
ctx.lineWidth = 1.5;
ctx.stroke();
}
ctx.strokeStyle = "#333";
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(0, 33);
ctx.lineTo(pw, 33);
ctx.stroke();
}
let taskDetailHitRegions = [];
let spanPanelHits = [];
function renderSpanPanel(scrollbarW) {
const panel = document.getElementById("span-panel");
if (panel.style.display === "none") return;
const c = document.getElementById("span-panel-canvas");
const ph = 120;
const layout = timePanelLayout(panel, scrollbarW);
const { pw, drawW, nsToPanelX, nsToPanelXClamped } = layout;
const ctx = layout.resizeCanvas(c, ph);
ctx.clearRect(0, 0, pw, ph);
spanPanelHits = [];
if (drawW <= 0) return;
const visibleSpans = [];
for (const s of allSpans) {
if (s.end < viewStart || s.start > viewEnd) continue;
if (!spanMatchesFilter(s)) continue;
visibleSpans.push(s);
}
if (visibleSpans.length === 0) {
ctx.fillStyle = "#555";
ctx.font = "12px sans-serif";
ctx.fillText("No spans in view" + (spanFilter ? ` (filter: "${spanFilter}")` : ""), LABEL_W + 10, 60);
return;
}
const renderSet = selectSpanRenderSet({
allSpans: visibleSpans,
focusedSpanId,
childrenByParent,
});
if (renderSet.length === 0) {
ctx.fillStyle = "#555";
ctx.font = "12px sans-serif";
ctx.fillText("No root spans in view", LABEL_W + 10, 60);
return;
}
const CLUSTER_X = 6;
const BAR_H = 4;
const spanLayout = computeSpanLayout({
spans: renderSet,
viewStart,
viewEnd,
drawW,
panelH: ph,
clusterXPx: CLUSTER_X,
barH: BAR_H,
});
if (focusedSpanId != null) {
const focusBucket = spanLayout.buckets.find(b => b.representative.spanId === focusedSpanId);
if (focusBucket) {
const PARENT_Y = 4;
const CHILD_Y = 34;
focusBucket.y = PARENT_Y;
for (const b of spanLayout.buckets) {
if (b !== focusBucket) b.y = CHILD_Y;
}
}
}
for (const bucket of spanLayout.buckets) {
const rep = bucket.representative;
const x1 = nsToPanelXClamped(rep.start);
const x2 = nsToPanelXClamped(rep.end);
const y = bucket.y;
const isSelected = selectedSpanIds.has(rep.spanId);
const isFocused = rep.spanId === focusedSpanId;
const color = spanColor(rep.spanName);
const clusterSize = bucket.spans.length;
let h = bucket.h;
if (isFocused) {
h = Math.min(h * 6, ph / 3);
} else if (focusedSpanId != null) {
h = Math.max(h * 3, 12);
if (clusterSize > 1) h = Math.min(h + Math.log2(clusterSize) * 3, ph / 4);
} else if (clusterSize > 1) {
h = Math.min(h + Math.log2(clusterSize) * 3, ph / 4);
}
ctx.fillStyle = color;
ctx.globalAlpha = isSelected ? 0.4 : (selectedSpanIds.size > 0 ? 0.08 : 0.15);
ctx.fillRect(x1, y, Math.max(x2 - x1, 1), h);
ctx.globalAlpha = isSelected ? 1.0 : (selectedSpanIds.size > 0 ? 0.3 : 0.8);
for (const seg of rep.segments) {
if (seg.end < viewStart || seg.start > viewEnd) continue;
const sx1 = nsToPanelXClamped(seg.start);
const sx2 = nsToPanelXClamped(seg.end);
ctx.fillRect(sx1, y, Math.max(sx2 - sx1, 1), h);
}
spanPanelHits.push({ spans: bucket.spans, representative: rep, x1, x2: Math.max(x2, x1 + 4), y, h });
}
ctx.globalAlpha = 1.0;
if (spanLayout.minDur > 0 && spanLayout.maxDur > 0) {
const PAD_TOP = 2, BAR_H_TICK = 4;
const usableH = ph - PAD_TOP - 2 - BAR_H_TICK;
const minL = Math.log(spanLayout.minDur), maxL = Math.log(spanLayout.maxDur);
const range = maxL - minL || 1;
ctx.fillStyle = "#666";
ctx.font = "9px sans-serif";
ctx.textAlign = "left";
const ticks = [];
const decades = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9];
for (const d of decades) {
if (d >= spanLayout.minDur && d <= spanLayout.maxDur) ticks.push(d);
}
if (ticks.length < 2) { ticks.push(spanLayout.minDur); ticks.push(spanLayout.maxDur); }
for (const dur of ticks) {
const norm = (Math.log(dur) - minL) / range;
let y = PAD_TOP + (1 - norm) * usableH;
if (y < 2 || y > ph - 8) continue;
ctx.globalAlpha = 0.4;
ctx.fillRect(LABEL_W, y, drawW, 0.5); ctx.globalAlpha = 0.7;
ctx.fillText(formatHumanDuration(dur), LABEL_W + 2, y - 2);
}
}
const info = document.getElementById("span-panel-info");
const focusNote = focusedSpanId != null ? " (focused)" : "";
info.textContent = `${renderSet.length} spans · ${spanLayout.buckets.length} clusters${focusNote}`;
const label = document.getElementById("span-panel-label");
if (focusedSpanId != null) {
const meta = spanMeta.get(focusedSpanId);
const focusedSpan = allSpans.find(s => s.spanId === focusedSpanId);
const fields = focusedSpan?.fields || meta?.fields || {};
const name = meta?.spanName || focusedSpan?.spanName || "Span";
let html = `<div style="color:#ccc;margin-bottom:3px">${esc(name)}</div>`;
for (const [k, v] of Object.entries(fields)) {
const val = String(v);
html += `<div class="kv-row"><span class="kv-key">${esc(k)}:</span><span class="kv-val" title="${esc(val)}">${esc(val)}</span><button class="kv-copy" data-val="${esc(val)}">⎘</button></div>`;
}
label.innerHTML = html;
} else {
label.textContent = "Spans";
}
}
function findBucketAtXY(mx, my) {
for (let i = spanPanelHits.length - 1; i >= 0; i--) {
const h = spanPanelHits[i];
if (mx >= h.x1 && mx <= h.x2 && my >= h.y && my <= h.y + h.h) {
return h;
}
}
return null;
}
document.getElementById("span-panel-canvas").addEventListener("mousemove", (e) => {
const c = document.getElementById("span-panel-canvas");
const rect = c.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const bucket = findBucketAtXY(mx, my);
if (bucket) {
const spans = bucket.spans;
const rep = bucket.representative;
let html = "";
if (spans.length === 1) {
const dur = rep.end - rep.start;
const idle = dur - rep.activeNs;
const pct = spanPercentileRank(rep);
const pctStr = pct != null ? ` (P${pct})` : "";
const nSegs = rep.segments.length;
html = `<span class="label">Span:</span> <span class="value">${esc(rep.spanName)}</span> <span style="color:#888">${formatHumanDuration(dur)}${pctStr}</span>`;
html += `<br><span class="label">Active:</span> <span class="value">${formatHumanDuration(rep.activeNs)}</span>`;
if (idle > 0) html += ` · <span class="label">Idle:</span> <span class="value">${formatHumanDuration(idle)}</span>`;
if (nSegs > 1) html += ` · <span class="label">Polls:</span> <span class="value">${nSegs}</span>`;
const workers = [...new Set(rep.segments.map(s => s.workerId))].sort();
html += `<br><span class="label">Worker${workers.length > 1 ? "s" : ""}:</span> <span class="value">${workers.join(", ")}</span>`;
if (rep.fields && typeof rep.fields === "object") {
const entries = Object.entries(rep.fields);
if (entries.length > 0) {
html += `<br><span class="label">Fields:</span> <span class="value">${entries.map(([k, v]) => esc(k) + "=" + esc(String(v))).join(", ")}</span>`;
}
}
} else {
const nameCounts = {};
let minDur = Infinity, maxDur = 0;
for (const s of spans) {
nameCounts[s.spanName] = (nameCounts[s.spanName] || 0) + 1;
const d = s.end - s.start;
if (d < minDur) minDur = d;
if (d > maxDur) maxDur = d;
}
const nameList = Object.entries(nameCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([n, c]) => `${esc(n)}${c > 1 ? " ×" + c : ""}`)
.join(", ");
html = `<span class="label">Cluster:</span> <span class="value">${spans.length} spans</span>`;
html += `<br><span class="label">Names:</span> <span class="value">${nameList}</span>`;
html += `<br><span class="label">Duration:</span> <span class="value">${formatHumanDuration(minDur)} – ${formatHumanDuration(maxDur)}</span>`;
}
tooltip.innerHTML = html;
tooltip.style.display = "block";
tooltip.style.left = Math.min(e.clientX + 12, window.innerWidth - 340) + "px";
tooltip.style.top = (e.clientY + 16) + "px";
} else {
tooltip.style.display = "none";
}
});
document.getElementById("span-panel-canvas").addEventListener("mouseleave", () => {
tooltip.style.display = "none";
});
document.getElementById("span-panel-label").addEventListener("click", (e) => {
const btn = e.target.closest(".kv-copy");
if (btn) {
navigator.clipboard.writeText(btn.dataset.val);
btn.textContent = "✓";
setTimeout(() => { btn.textContent = "⎘"; }, 800);
}
});
document.getElementById("span-panel-canvas").addEventListener("click", (e) => {
const c = document.getElementById("span-panel-canvas");
const rect = c.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const bucket = findBucketAtXY(mx, my);
if (bucket) {
const hit = bucket.representative;
if (hit.spanId === focusedSpanId) {
focusedSpanId = null;
selectedSpanId = null;
selectedSpanIds = new Set();
selectedTaskId = null;
} else {
focusedSpanId = hit.spanId;
selectedSpanId = hit.spanId;
selectedSpanIds = new Set();
selectedSpanIds.add(hit.spanId);
let parentId = hit.parentSpanId;
while (parentId != null && !selectedSpanIds.has(parentId)) {
selectedSpanIds.add(parentId);
const parent = spanMeta.get(parentId);
parentId = parent?.parentSpanId ?? null;
}
const firstSeg = hit.segments[0];
if (firstSeg) {
const polls = workerSpans[firstSeg.workerId]?.polls;
if (polls) {
const poll = polls.find(p => p.start <= firstSeg.start && p.end >= firstSeg.start);
if (poll && poll.taskId) selectedTaskId = poll.taskId;
}
}
const durations = allSpans
.filter(s => s.spanName === hit.spanName)
.map(s => s.end - s.start)
.sort((a, b) => a - b);
const dur = hit.end - hit.start;
const rank = durations.filter(d => d <= dur).length;
const pct = (rank / durations.length * 100).toFixed(0);
const p50 = durations[Math.floor(durations.length * 0.5)];
const p99 = durations[Math.floor(durations.length * 0.99)];
document.getElementById("span-panel-info").textContent =
`${hit.spanName}: ${formatHumanDuration(dur)} (P${pct} of ${durations.length}) · P50=${formatHumanDuration(p50)} P99=${formatHumanDuration(p99)}`;
}
} else {
focusedSpanId = null;
selectedSpanId = null;
selectedSpanIds = new Set();
selectedTaskId = null;
}
renderAll();
});
function renderTaskDetail(scrollbarW) {
const panel = document.getElementById("task-detail");
const c = document.getElementById("task-detail-canvas");
if (!selectedTaskId) {
panel.style.display = "none";
return;
}
const polls = [];
for (const w of workerIds) {
for (const s of workerSpans[w].polls) {
if (s.taskId === selectedTaskId) polls.push(s);
}
}
polls.sort((a, b) => a.start - b.start);
if (polls.length < 1) { panel.style.display = "none"; return; }
const wakes = wakesByTask[selectedTaskId] || [];
panel.style.display = "block";
const loc = trace.spawnLocations.get(polls[0].spawnLocId);
const wakeCount = wakes.length;
const terminateTs = trace.taskTerminateTimes?.get(selectedTaskId);
const hasTerminate = terminateTs != null;
const spawnTs = trace.taskSpawnTimes?.get(selectedTaskId);
const isInstrumented = trace.taskInstrumented?.get(selectedTaskId) ?? true;
let lifecycleStr = "";
if (hasTerminate && spawnTs != null) {
const dur = terminateTs - spawnTs;
const durMs = dur / 1e6;
lifecycleStr = ` · lifetime ${formatHumanDuration(dur)}`;
}
const escapeHtml = (s) => String(s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
let labelHtml = `Task 0x${selectedTaskId.toString(16)}`;
if (loc) labelHtml += ` — ${escapeHtml(loc)}`;
labelHtml += ` · ${polls.length} polls`;
if (isInstrumented) {
labelHtml += ` · ${wakeCount} wakes`;
}
labelHtml += `${lifecycleStr}${hasTerminate ? " ✓" : ""}`;
if (!isInstrumented) {
labelHtml += ` · <a class="uninstrumented-badge" href="https://docs.rs/dial9-tokio-telemetry/latest/dial9_tokio_telemetry/telemetry/struct.TelemetryHandle.html#method.spawn" target="_blank" title="Task spawned via raw tokio::spawn() — click for docs on TelemetryHandle::spawn">no wake data ⓘ</a>`;
}
const taskHasDumps = trace.taskDumps && trace.taskDumps.has(selectedTaskId) && trace.taskDumps.get(selectedTaskId).length > 0;
if (taskHasDumps) {
const dumpCount = trace.taskDumps.get(selectedTaskId).length;
labelHtml += ` · <span id="btn-idle-flamegraph" style="cursor:pointer;color:#b388ff;text-decoration:underline;position:relative;z-index:10" title="Show time-weighted flamegraph of idle periods">🔥 idle flamegraph (${dumpCount})</span>`;
}
document.getElementById("task-detail-label").innerHTML = labelHtml;
if (taskHasDumps) {
document.getElementById("btn-idle-flamegraph").onclick = (e) => {
e.stopPropagation();
e.preventDefault();
try {
showIdleTimeFlamegraph();
} catch (err) {
console.error("showIdleTimeFlamegraph error:", err);
showToast("idle-fg-err", "Error: " + err.message, "error", 5000);
}
};
}
const parent = c.parentElement;
const dpr = devicePixelRatio || 1;
const pw = parent.clientWidth, ph = parent.clientHeight;
c.width = pw * dpr;
c.height = ph * dpr;
c.style.width = pw + "px";
c.style.height = ph + "px";
const ctx = c.getContext("2d");
ctx.scale(dpr, dpr);
ctx.fillStyle = "#16213e";
ctx.fillRect(0, 0, pw, ph);
const drawW = pw - LABEL_W - (scrollbarW || 0);
if (drawW <= 0) return;
function fmtDur(ns) {
return formatHumanDuration(ns);
}
function drawCrossHatch(x, y, w, h, { stroke = "rgba(140,120,255,0.35)", step = 8 } = {}) {
ctx.save();
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.clip();
ctx.strokeStyle = stroke;
ctx.lineWidth = 1;
ctx.setLineDash([]);
for (let hx = x - h; hx < x + w; hx += step) {
ctx.beginPath();
ctx.moveTo(hx, y + h);
ctx.lineTo(hx + h, y);
ctx.stroke();
}
ctx.restore();
}
const bandTop = 50, bandH = 30;
ctx.font = "9px monospace";
ctx.textAlign = "center";
const pollWakes = []; for (let pi = 0; pi < polls.length; pi++) {
const s = polls[pi];
let best = null;
if (wakes.length) {
let lo = 0, hi = wakes.length - 1, bi = -1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (wakes[mid].timestamp <= s.start) { bi = mid; lo = mid + 1; }
else hi = mid - 1;
}
if (bi >= 0) {
const w = wakes[bi];
let effectiveWake = w.timestamp;
for (let j = 0; j < pi; j++) {
if (w.timestamp >= polls[j].start && w.timestamp <= polls[j].end) {
effectiveWake = polls[j].end;
break;
}
}
const delay = s.start - effectiveWake;
if (delay >= 0 && delay < 1e9) best = { wake: w, effectiveWake };
}
}
pollWakes.push(best);
}
taskDetailWakeRegions = [];
taskDetailHitRegions = [];
for (let i = 0; i < polls.length; i++) {
const pollWake = pollWakes[i];
if (!pollWake) continue;
const { wake, effectiveWake } = pollWake;
const s = polls[i];
if (s.start < viewStart || effectiveWake > viewEnd) continue;
const delay = s.start - effectiveWake;
const x1 = LABEL_W + Math.max(0, nsToX(effectiveWake, drawW));
const x2 = LABEL_W + Math.min(drawW, nsToX(s.start, drawW));
const w = x2 - x1;
if (w < 1) continue;
const delayUs = delay / 1000;
if (delayUs > 1000) ctx.fillStyle = "rgba(255,50,50,0.3)";
else if (delayUs > 100) ctx.fillStyle = "rgba(255,150,50,0.3)";
else ctx.fillStyle = "rgba(100,200,100,0.15)";
ctx.fillRect(x1, bandTop, w, bandH);
ctx.strokeStyle = delayUs > 1000 ? "#ff4444" : delayUs > 100 ? "#ff8a65" : "#555";
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
ctx.strokeRect(x1, bandTop, w, bandH);
ctx.setLineDash([]);
if (w > 25) {
ctx.fillStyle = delayUs > 1000 ? "#ff4444" : delayUs > 100 ? "#ff8a65" : "#888";
ctx.fillText(fmtDur(delay), x1 + w / 2, bandTop - 6);
}
const wx = x1;
ctx.fillStyle = "#66bb6a";
ctx.beginPath();
ctx.moveTo(wx, bandTop + bandH + 2);
ctx.lineTo(wx - 4, bandTop + bandH + 9);
ctx.lineTo(wx + 4, bandTop + bandH + 9);
ctx.closePath();
ctx.fill();
taskDetailHitRegions.push({
x1, x2: x1 + w, y1: bandTop, y2: bandTop + bandH,
type: "scheduled",
detail: `Scheduled — waiting ${fmtDur(delay)} for worker to poll after wake`,
});
{
let label;
const wid = wake.wakerTaskId;
if (!wid || wid === 0 || (wid >= 1 && wid <= workerIds.length)) {
label = "io";
} else {
const wakerLoc = trace.taskSpawnLocs.get(wid);
const wakerLocStr = wakerLoc ? trace.spawnLocations.get(wakerLoc) : null;
label = wakerLocStr ? wakerLocStr.replace(/.*\//, '') : `task 0x${wid.toString(16)}`;
}
if (w > 40) {
const isHovered = hoveredWakerTaskId === wake.wakerTaskId;
ctx.fillStyle = isHovered ? "#fff" : "#66bb6a";
ctx.font = isHovered ? "bold 8px monospace" : "8px monospace";
ctx.textAlign = "left";
ctx.fillText("⬆ " + label, wx, bandTop + bandH + 20);
ctx.font = "9px monospace";
taskDetailWakeRegions.push({
x1, x2: x1 + w,
y1: bandTop + bandH, y2: bandTop + bandH + 24,
wakerTaskId: wake.wakerTaskId,
});
}
}
}
if (hasTerminate && spawnTs != null) {
const lifeStart = spawnTs;
const lifeEnd = terminateTs;
if (lifeStart < viewEnd && lifeEnd > viewStart) {
const lx1 = LABEL_W + Math.max(0, nsToX(lifeStart, drawW));
const lx2 = LABEL_W + Math.min(drawW, nsToX(lifeEnd, drawW));
ctx.fillStyle = "rgba(129,199,132,0.08)";
ctx.fillRect(lx1, bandTop - 2, lx2 - lx1, bandH + 4);
ctx.strokeStyle = "rgba(129,199,132,0.3)";
ctx.lineWidth = 1;
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(lx1, bandTop - 2); ctx.lineTo(lx2, bandTop - 2);
ctx.moveTo(lx1, bandTop + bandH + 2); ctx.lineTo(lx2, bandTop + bandH + 2);
ctx.stroke();
if (lifeStart >= viewStart) {
ctx.strokeStyle = "#81c784";
ctx.beginPath(); ctx.moveTo(lx1, bandTop - 6); ctx.lineTo(lx1, bandTop + bandH + 6); ctx.stroke();
ctx.fillStyle = "#81c784"; ctx.font = "8px monospace"; ctx.textAlign = "left";
ctx.fillText("spawn▸", lx1 + 2, bandTop - 8);
}
if (lifeEnd <= viewEnd) {
ctx.strokeStyle = "#81c784";
ctx.beginPath(); ctx.moveTo(lx2, bandTop - 6); ctx.lineTo(lx2, bandTop + bandH + 6); ctx.stroke();
ctx.fillStyle = "#81c784"; ctx.font = "8px monospace"; ctx.textAlign = "right";
ctx.fillText("◂done", lx2 - 2, bandTop - 8);
}
ctx.font = "9px monospace";
}
}
for (const s of polls) {
if (s.end < viewStart || s.start > viewEnd) continue;
const x1 = LABEL_W + Math.max(0, nsToX(s.start, drawW));
const x2 = LABEL_W + Math.min(drawW, nsToX(s.end, drawW));
const w = Math.max(x2 - x1, 1);
ctx.fillStyle = "#4fc3f7";
ctx.fillRect(x1, bandTop, w, bandH);
if (w > 35) {
ctx.fillStyle = "#000";
ctx.fillText(fmtDur(s.end - s.start), x1 + w / 2, bandTop + bandH / 2 + 3);
}
taskDetailHitRegions.push({
x1, x2, y1: bandTop, y2: bandTop + bandH,
type: "polling",
detail: `Polling — actively executing for ${fmtDur(s.end - s.start)}`,
});
}
const dumps = trace.taskDumps ? (trace.taskDumps.get(selectedTaskId) || []) : [];
let dumpIdx = 0; for (let i = 0; i < polls.length - 1; i++) {
const gapStart = polls[i].end;
const gapEnd = polls[i + 1].start;
if (gapEnd < viewStart || gapStart > viewEnd) continue;
const nextPw = pollWakes[i + 1];
const nextWakeTs = nextPw ? nextPw.effectiveWake : null;
if (nextWakeTs !== null && nextWakeTs <= gapStart) continue;
const x1 = LABEL_W + Math.max(0, nsToX(gapStart, drawW));
const wakeX = nextWakeTs !== null ? LABEL_W + Math.max(0, nsToX(nextWakeTs, drawW)) : LABEL_W + Math.min(drawW, nsToX(gapEnd, drawW));
const x2 = Math.min(wakeX, LABEL_W + Math.min(drawW, nsToX(gapEnd, drawW)));
const w = Math.max(x2 - x1, 0);
if (w < 1) continue;
const gapDumps = [];
const prevPollStart = i > 0 ? polls[i - 1].start : polls[i].start;
while (dumpIdx < dumps.length && dumps[dumpIdx].timestamp < prevPollStart) dumpIdx++;
for (let di = dumpIdx; di < dumps.length && dumps[di].timestamp <= polls[i].start; di++) {
gapDumps.push(dumps[di]);
}
const hasDump = gapDumps.length > 0;
ctx.fillStyle = hasDump ? "#2a2a5a" : "#2a2a4a";
ctx.fillRect(x1, bandTop, w, bandH);
if (hasDump) {
drawCrossHatch(x1, bandTop, w, bandH);
}
ctx.strokeStyle = hasDump ? "#7c6cff" : "#444";
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.strokeRect(x1, bandTop, w, bandH);
ctx.setLineDash([]);
const dur = (nextWakeTs !== null ? nextWakeTs : gapEnd) - gapStart;
ctx.fillStyle = dur > 1e6 ? "#ff8a65" : "#888";
if (w > 35) {
ctx.fillText(fmtDur(dur), x1 + w / 2, bandTop - 6);
}
taskDetailHitRegions.push({
x1, x2, y1: bandTop, y2: bandTop + bandH,
type: "idle",
detail: hasDump
? `Idle — waiting ${fmtDur(dur)} (click for async stack trace)`
: `Idle — waiting ${fmtDur(dur)} for waker (no wake received yet)`,
taskDumps: hasDump ? gapDumps : null,
});
}
ctx.fillStyle = "#aaa";
ctx.font = "9px monospace";
ctx.textAlign = "left";
ctx.fillText("Task", 12, bandTop + bandH / 2 + 4);
ctx.fillStyle = "#66bb6a";
ctx.fillText("▲ = wake", 12, bandTop + bandH + 18);
}
function renderQueueChart(drawW, scrollbarW) {
const c = queueCanvas;
const parent = c.parentElement;
const dpr = devicePixelRatio || 1;
const pw = parent.clientWidth,
ph = parent.clientHeight;
c.width = pw * dpr;
c.height = ph * dpr;
c.style.width = pw + "px";
c.style.height = ph + "px";
const ctx = c.getContext("2d");
ctx.scale(dpr, dpr);
ctx.fillStyle = "#16213e";
ctx.fillRect(0, 0, pw, ph);
if (!queueSamples.length && !activeTaskSamples.length) return;
const chartLeft = LABEL_W,
chartW = pw - LABEL_W - (scrollbarW || 0);
const chartTop = 24,
chartH = ph - 30;
if (chartW <= 0) return;
const numBuckets = Math.ceil(chartW);
const buckets = new Array(numBuckets);
for (let i = 0; i < numBuckets; i++)
buckets[i] = { maxGlobal: 0, maxLocal: 0, hasData: false };
const viewDur = viewEnd - viewStart;
{
const startIdx = Math.max(0, lowerBound(queueSamples, viewStart) - 1);
for (let i = startIdx; i < queueSamples.length; i++) {
const s = queueSamples[i];
if (s.t > viewEnd) break;
if (s.t < viewStart) continue;
const bi = Math.floor(
((s.t - viewStart) / viewDur) * (numBuckets - 1),
);
if (bi < 0 || bi >= numBuckets) continue;
const b = buckets[bi];
b.hasData = true;
if (s.global > b.maxGlobal) b.maxGlobal = s.global;
}
}
const allLocalSamples = [];
for (const w of workerIds) {
const wSamples = workerQueueSamples[w] || [];
if (wSamples.length === 0) continue;
const startIdx = Math.max(0, lowerBound(wSamples, viewStart) - 1);
for (let i = startIdx; i < wSamples.length; i++) {
const s = wSamples[i];
if (s.t > viewEnd) break;
if (s.t >= viewStart) {
allLocalSamples.push({ t: s.t, w, local: s.local });
}
}
}
allLocalSamples.sort((a, b) => a.t - b.t);
const workerState = {};
for (const w of workerIds) workerState[w] = 0;
let sampleIdx = 0;
for (let bi = 0; bi < numBuckets; bi++) {
const bucketEnd = viewStart + ((bi + 1) / numBuckets) * viewDur;
while (sampleIdx < allLocalSamples.length && allLocalSamples[sampleIdx].t < bucketEnd) {
const s = allLocalSamples[sampleIdx];
workerState[s.w] = s.local;
buckets[bi].hasData = true;
sampleIdx++;
}
let max = 0;
for (const w of workerIds) {
if (workerState[w] > max) max = workerState[w];
}
buckets[bi].maxLocal = max;
}
let maxQ = 1;
for (const b of buckets) {
if (b.maxGlobal > maxQ) maxQ = b.maxGlobal;
if (b.maxLocal > maxQ) maxQ = b.maxLocal;
}
ctx.fillStyle = "#666";
ctx.font = "10px monospace";
ctx.textAlign = "right";
ctx.fillText(maxQ.toString(), LABEL_W - 6, chartTop + 10);
ctx.fillText("0", LABEL_W - 6, chartTop + chartH);
ctx.beginPath();
ctx.moveTo(chartLeft, chartTop + chartH);
let lastY = chartTop + chartH;
for (let i = 0; i < numBuckets; i++) {
const x = chartLeft + i;
if (buckets[i].hasData) {
lastY =
chartTop +
chartH -
(buckets[i].maxGlobal / maxQ) * chartH;
}
ctx.lineTo(x, lastY);
}
ctx.lineTo(chartLeft + numBuckets - 1, chartTop + chartH);
ctx.closePath();
ctx.fillStyle = "rgba(79,195,247,0.3)";
ctx.fill();
ctx.strokeStyle = "#4fc3f7";
ctx.lineWidth = 1;
ctx.stroke();
ctx.beginPath();
lastY = chartTop + chartH;
let started = false;
for (let i = 0; i < numBuckets; i++) {
const x = chartLeft + i;
if (buckets[i].hasData) {
lastY =
chartTop +
chartH -
(buckets[i].maxLocal / maxQ) * chartH;
}
if (!started) {
ctx.moveTo(x, lastY);
started = true;
} else ctx.lineTo(x, lastY);
}
ctx.strokeStyle = "#ff8a65";
ctx.lineWidth = 1;
ctx.stroke();
if (activeTaskSamples.length > 0) {
let maxTasks = 1;
for (const s of activeTaskSamples) {
if (s.t >= viewStart && s.t <= viewEnd && s.count > maxTasks) maxTasks = s.count;
}
let countAtViewStart = 0;
for (const s of activeTaskSamples) {
if (s.t > viewStart) break;
countAtViewStart = s.count;
}
if (countAtViewStart > maxTasks) maxTasks = countAtViewStart;
ctx.fillStyle = "#81c784";
ctx.font = "10px monospace";
ctx.textAlign = "left";
ctx.fillText("tasks:" + maxTasks, chartLeft + chartW + 4, chartTop + 10);
ctx.beginPath();
let ty = chartTop + chartH - (countAtViewStart / maxTasks) * chartH;
ctx.moveTo(chartLeft, ty);
for (const s of activeTaskSamples) {
if (s.t < viewStart) continue;
if (s.t > viewEnd) break;
const x = chartLeft + ((s.t - viewStart) / viewDur) * chartW;
ctx.lineTo(x, ty);
ty = chartTop + chartH - (s.count / maxTasks) * chartH;
ctx.lineTo(x, ty);
}
ctx.lineTo(chartLeft + chartW, ty);
ctx.strokeStyle = "#81c784";
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
function updateInfoPanel() {
const panel = document.getElementById("info-panel");
if (mouseNs === null || mouseNs < viewStart || mouseNs > viewEnd) {
panel.innerHTML = "";
return;
}
const ns = mouseNs;
let globalQ = "-";
{
const s = findNearest(queueSamples, ns);
if (s) globalQ = s.global;
}
let maxLocal = 0;
for (const w of workerIds) {
const s = findNearest(workerQueueSamples[w] || [], ns);
if (s && s.local > maxLocal) maxLocal = s.local;
}
let taskCount = 0;
if (activeTaskSamples.length > 0) {
for (const s of activeTaskSamples) {
if (s.t > ns) break;
taskCount = s.count;
}
}
let html = `<span class="ip-label">Global Q:</span> <span class="ip-value">${globalQ}</span>`;
html += ` · <span class="ip-label">Local max:</span> <span class="ip-value">${maxLocal}</span>`;
if (activeTaskSamples.length > 0) {
html += ` · <span class="ip-label">Active tasks:</span> <span class="ip-value">${taskCount}</span>`;
}
panel.innerHTML = html;
}
function renderCrosshair() {
const overlay = document.getElementById("crosshair-overlay");
const mainArea = document.getElementById("main-area");
const dpr = devicePixelRatio || 1;
const w = mainArea.clientWidth, h = mainArea.clientHeight;
overlay.width = w * dpr;
overlay.height = h * dpr;
overlay.style.width = w + "px";
overlay.style.height = h + "px";
const ctx = overlay.getContext("2d");
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, w, h);
updateInfoPanel();
const lanesRect = lanesContainer.getBoundingClientRect();
const mainRect = mainArea.getBoundingClientRect();
const laneDrawW = lanesRect.width - LABEL_W;
const scrollbarW = lanesContainer.offsetWidth - lanesContainer.clientWidth;
const effectiveDrawW = laneDrawW - scrollbarW;
if (mouseNs !== null && mouseNs >= viewStart && mouseNs <= viewEnd) {
const x = (lanesRect.left - mainRect.left) + LABEL_W + nsToX(mouseNs, effectiveDrawW);
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
ctx.stroke();
ctx.setLineDash([]);
}
if (kbSelecting && kbCursorNs !== null && kbCursorNs >= viewStart && kbCursorNs <= viewEnd) {
const kx = (lanesRect.left - mainRect.left) + LABEL_W + nsToX(kbCursorNs, effectiveDrawW);
ctx.strokeStyle = "rgba(255,255,255,0.8)";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(kx, 0);
ctx.lineTo(kx, h);
ctx.stroke();
}
}
document.getElementById("poi-filter").addEventListener("change", updatePointsOfInterest);
document.getElementById("sort-by-worst").addEventListener("change", updatePointsOfInterest);
document.getElementById("btn-sched-panel").addEventListener("click", showSchedPanel);
document.getElementById("btn-uninstrumented-info").addEventListener("click", (e) => {
showUninstrumentedPopup(e.target);
});
document.getElementById("btn-prev-poi").addEventListener("click", () => {
if (currentPoiIndex > 0) jumpToPoi(currentPoiIndex - 1);
else if (currentPoiIndex < 0 && pointsOfInterest.length > 0) jumpToPoi(0);
});
document.getElementById("btn-next-poi").addEventListener("click", () => {
if (currentPoiIndex < pointsOfInterest.length - 1) jumpToPoi(currentPoiIndex + 1);
else if (currentPoiIndex < 0 && pointsOfInterest.length > 0) jumpToPoi(0);
});
document
.getElementById("btn-zoom-in")
.addEventListener("click", () => zoom(0.5));
document
.getElementById("btn-zoom-out")
.addEventListener("click", () => zoom(2));
document.getElementById("btn-fit").addEventListener("click", () => {
viewStart = minTs;
viewEnd = maxTs;
renderAll();
});
document
.getElementById("viewport-controls")
.addEventListener("click", (e) => e.stopPropagation());
document.getElementById("btn-set-range").addEventListener("click", () => {
reparseWithRange(Math.round(viewStart), Math.round(viewEnd));
});
clearRangeBtn.addEventListener("click", () => reparseWithRange(null, null));
if (urlParams.get('start') != null || urlParams.get('end') != null) {
clearRangeBtn.style.display = "";
}
document.getElementById("btn-time-mode").addEventListener("click", () => {
useAbsoluteTime = !useAbsoluteTime;
document.getElementById("btn-time-mode").textContent =
useAbsoluteTime ? "Time: Absolute" : "Time: Relative";
document.getElementById("btn-tz-mode").style.display =
useAbsoluteTime ? "" : "none";
renderAll();
});
document.getElementById("btn-tz-mode").addEventListener("click", () => {
useLocalTz = !useLocalTz;
document.getElementById("btn-tz-mode").textContent =
useLocalTz ? "TZ: Local" : "TZ: UTC";
renderAll();
});
function zoom(factor, centerFrac = 0.5) {
clearToasts();
const viewDur = viewEnd - viewStart;
const center = viewStart + viewDur * centerFrac;
const newDur = Math.max(viewDur * factor, 100); viewStart = Math.max(minTs, center - newDur * centerFrac);
viewEnd = Math.min(maxTs, center + newDur * (1 - centerFrac));
renderAll();
}
document.getElementById("main-area").addEventListener(
"wheel",
(e) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
const rect = lanesContainer.getBoundingClientRect();
const mouseX = e.clientX - rect.left - LABEL_W;
const drawW = rect.width - LABEL_W;
const frac = Math.max(0, Math.min(1, mouseX / drawW));
const factor = e.deltaY > 0 ? 1.3 : 1 / 1.3;
zoom(factor, frac);
}
},
{ passive: false },
);
lanesContainer.addEventListener("scroll", () => {
renderCrosshair();
});
let dragging = false,
dragStartX = 0,
dragViewStart = 0,
dragViewEnd = 0,
dragMoved = false;
let regionSelecting = false,
regionStartNs = 0,
regionEndNs = 0;
let altZooming = false;
let kbSelecting = false, kbSelMode = null, kbSelStartNs = null, kbCursorNs = null;
const selOverlay = document.getElementById("selection-overlay");
const SEL_COLORS = {
shift: { bg: "rgba(66, 133, 244, 0.15)", border: "rgba(66, 133, 244, 0.6)" },
alt: { bg: "rgba(0, 188, 180, 0.15)", border: "rgba(0, 188, 180, 0.6)" },
};
function setSelOverlayColor(mode) {
const c = SEL_COLORS[mode] || SEL_COLORS.shift;
selOverlay.style.background = c.bg;
selOverlay.style.borderColor = c.border;
}
document
.getElementById("main-area")
.addEventListener("mousedown", (e) => {
if (kbSelecting) cancelKbSelection();
if (e.shiftKey && !e.altKey && trace && trace.cpuSamples.length === 0) {
showToast("no-cpu-samples", "No CPU samples in trace — enable cpu-profiling for flamegraphs", "error", 4000);
return;
}
if ((e.shiftKey || e.altKey) && trace) {
regionSelecting = true;
altZooming = e.altKey && !e.shiftKey;
setSelOverlayColor(altZooming ? "alt" : "shift");
if (altZooming) hideToast("option-drag-hint");
else hideToast("shift-drag-hint");
dragMoved = false;
dragStartX = e.clientX;
const rect = lanesContainer.getBoundingClientRect();
const mouseX = e.clientX - rect.left - LABEL_W;
const drawW = rect.width - LABEL_W;
regionStartNs = viewStart + (Math.max(0, mouseX) / drawW) * (viewEnd - viewStart);
regionEndNs = regionStartNs;
const mainRect = document.getElementById("main-area").getBoundingClientRect();
selOverlay.style.top = "0px";
selOverlay.style.height = mainRect.height + "px";
selOverlay.style.display = "block";
updateSelOverlay(e.clientX);
document.body.style.cursor = "crosshair";
return;
}
dragging = true;
dragMoved = false;
dragStartX = e.clientX;
dragViewStart = viewStart;
dragViewEnd = viewEnd;
document.body.style.cursor = "grabbing";
});
function updateSelOverlay(clientX) {
const rect = lanesContainer.getBoundingClientRect();
const drawW = rect.width - LABEL_W;
const mainRect = document.getElementById("main-area").getBoundingClientRect();
const lanesLeft = rect.left - mainRect.left + LABEL_W;
const x1 = lanesLeft + nsToX(Math.min(regionStartNs, regionEndNs), drawW);
const x2 = lanesLeft + nsToX(Math.max(regionStartNs, regionEndNs), drawW);
selOverlay.style.left = x1 + "px";
selOverlay.style.width = Math.max(1, x2 - x1) + "px";
}
function updateKbSelOverlay() {
setSelOverlayColor(kbSelMode);
const rect = lanesContainer.getBoundingClientRect();
const drawW = rect.width - LABEL_W;
const mainRect = document.getElementById("main-area").getBoundingClientRect();
const lanesLeft = rect.left - mainRect.left + LABEL_W;
const s = Math.min(kbSelStartNs, kbCursorNs);
const e = Math.max(kbSelStartNs, kbCursorNs);
const x1 = lanesLeft + nsToX(s, drawW);
const x2 = lanesLeft + nsToX(e, drawW);
selOverlay.style.left = x1 + "px";
selOverlay.style.width = Math.max(1, x2 - x1) + "px";
selOverlay.style.top = "0px";
selOverlay.style.height = mainRect.height + "px";
selOverlay.style.display = "block";
}
function announce(msg) {
document.getElementById("aria-live").textContent = msg;
}
function cancelKbSelection() {
kbSelecting = false;
kbSelMode = null;
kbCursorNs = null;
kbSelStartNs = null;
selOverlay.style.display = "none";
renderCrosshair();
}
function completeKbSelection() {
const selStart = Math.min(kbSelStartNs, kbCursorNs);
const selEnd = Math.max(kbSelStartNs, kbCursorNs);
const mode = kbSelMode;
regionStartNs = selStart;
regionEndNs = selEnd;
kbSelecting = false;
kbSelMode = null;
kbCursorNs = null;
kbSelStartNs = null;
renderCrosshair();
if (selEnd - selStart < 100) { selOverlay.style.display = "none";
return;
}
if (mode === "alt") {
selOverlay.style.display = "none";
hideToast("option-drag-hint");
viewStart = Math.max(minTs, selStart);
viewEnd = Math.min(maxTs, selEnd);
renderAll();
announce("Zoomed to selected region");
} else {
hideToast("shift-drag-hint");
const hasSched = collectSchedSamples({ start: selStart, end: selEnd }).length > 0;
const hasCpu = trace.cpuSamples.some(s =>
s.timestamp >= selStart && s.timestamp <= selEnd && s.callchain.length > 0 && s.source !== 1
);
if (hasSched && !hasCpu) {
showSchedPanel(selStart, selEnd);
} else {
showFlamegraph(selStart, selEnd);
}
announce("Selected region opened");
}
}
window.addEventListener("mousemove", (e) => {
if (regionSelecting) {
if (Math.abs(e.clientX - dragStartX) > 3) dragMoved = true;
const rect = lanesContainer.getBoundingClientRect();
const drawW = rect.width - LABEL_W;
const mouseX = e.clientX - rect.left - LABEL_W;
regionEndNs = viewStart + (Math.max(0, Math.min(drawW, mouseX)) / drawW) * (viewEnd - viewStart);
updateSelOverlay(e.clientX);
return;
}
if (!dragging) return;
if (Math.abs(e.clientX - dragStartX) > 3) dragMoved = true;
const rect = lanesContainer.getBoundingClientRect();
const drawW = rect.width - LABEL_W;
const dx = e.clientX - dragStartX;
const nsPerPx = (dragViewEnd - dragViewStart) / drawW;
const shift = -dx * nsPerPx;
let newStart = dragViewStart + shift;
let newEnd = dragViewEnd + shift;
if (newStart < minTs) {
newEnd += minTs - newStart;
newStart = minTs;
}
if (newEnd > maxTs) {
newStart -= newEnd - maxTs;
newEnd = maxTs;
}
viewStart = Math.max(minTs, newStart);
viewEnd = Math.min(maxTs, newEnd);
renderAll();
});
window.addEventListener("mouseup", () => {
if (regionSelecting) {
regionSelecting = false;
document.body.style.cursor = "";
if (dragMoved) {
clearToasts();
const selStart = Math.min(regionStartNs, regionEndNs);
const selEnd = Math.max(regionStartNs, regionEndNs);
if (altZooming) {
hideToast("option-drag-hint");
selOverlay.style.display = "none";
viewStart = Math.max(minTs, selStart);
viewEnd = Math.min(maxTs, selEnd);
renderAll();
} else {
hideToast("shift-drag-hint");
const hasSched = collectSchedSamples({ start: selStart, end: selEnd }).length > 0;
const hasCpu = trace.cpuSamples.some(s =>
s.timestamp >= selStart && s.timestamp <= selEnd && s.callchain.length > 0 && s.source !== 1
);
if (hasSched && !hasCpu) {
showSchedPanel(selStart, selEnd);
} else {
showFlamegraph(selStart, selEnd);
}
}
} else {
selOverlay.style.display = "none";
}
altZooming = false;
return;
}
dragging = false;
document.body.style.cursor = "";
});
document.getElementById("main-area").addEventListener("click", (e) => {
if (dragMoved) return; clearToasts();
const lanesRect = lanesContainer.getBoundingClientRect();
if (e.clientY < lanesRect.top || e.clientY > lanesRect.bottom ||
e.clientX < lanesRect.left || e.clientX > lanesRect.right) return;
const mouseX = e.clientX - lanesRect.left - LABEL_W;
const mouseY = e.clientY - lanesRect.top;
const drawW = lanesRect.width - LABEL_W;
if (mouseX < 0 || mouseX > drawW) { selectedTaskId = null; renderAll(); return; }
const ns = viewStart + (mouseX / drawW) * (viewEnd - viewStart);
const laneIdx = Math.floor((mouseY + lanesContainer.scrollTop) / LANE_H);
if (laneIdx < 0 || laneIdx >= workerIds.length) { selectedTaskId = null; renderAll(); return; }
const w = workerIds[laneIdx];
const spans = workerSpans[w];
let found = null;
let foundSpan = null;
const hitPoll = findSpanAt(spans.polls, ns);
if (hitPoll) {
if (hitPoll.taskId) found = hitPoll.taskId;
foundSpan = hitPoll;
}
if (foundSpan && ((foundSpan.cpuSamples && foundSpan.cpuSamples.length > 0) ||
(foundSpan.schedSamples && foundSpan.schedSamples.length > 0))) {
showStackPopup(foundSpan, e.clientX, e.clientY);
} else {
hideStackPopup();
}
let containingSpan = null;
for (const s of allSpans) {
if (s.end < ns || s.start > ns) continue;
for (const seg of s.segments) {
if (seg.workerId === w && seg.start <= ns && seg.end >= ns) {
containingSpan = s;
break;
}
}
if (containingSpan) break;
}
let newFocusedSpanId = null;
const newSelectedSpanIds = new Set();
if (containingSpan) {
let cur = containingSpan;
let outermost = containingSpan;
newSelectedSpanIds.add(cur.spanId);
while (cur.parentSpanId != null) {
const parentSpan = spansById.get(cur.parentSpanId);
if (!parentSpan) break;
if (parentSpan.start <= ns && parentSpan.end >= ns) {
outermost = parentSpan;
newSelectedSpanIds.add(parentSpan.spanId);
cur = parentSpan;
} else {
break;
}
if (newSelectedSpanIds.size > 1024) break;
}
newFocusedSpanId = outermost.spanId;
}
const togglingOff = found && found === selectedTaskId;
if (togglingOff) {
selectedTaskId = null;
selectedSpanId = null;
selectedSpanIds = new Set();
focusedSpanId = null;
} else {
selectedTaskId = found || null;
selectedSpanId = newFocusedSpanId;
selectedSpanIds = newSelectedSpanIds;
focusedSpanId = newFocusedSpanId;
}
renderAll();
});
function showStackPopup(poll, x, y) {
sidebarSelStart = 0;
sidebarSelEnd = 0;
selOverlay.style.display = "none";
const sidebar = document.getElementById("stack-sidebar");
const title = document.getElementById("stack-sidebar-title");
const body = document.getElementById("stack-sidebar-body");
const durStr = formatHumanDuration(poll.end - poll.start);
const cpuCount = poll.cpuSamples ? poll.cpuSamples.length : 0;
const schedCount = poll.schedSamples ? poll.schedSamples.length : 0;
title.textContent = `Poll ${durStr}` +
(cpuCount ? ` · ${cpuCount} CPU sample${cpuCount > 1 ? "s" : ""}` : "") +
(schedCount ? ` · ${schedCount} sched event${schedCount > 1 ? "s" : ""}` : "");
let html = "";
if (poll.schedSamples && poll.schedSamples.length > 0) {
html += `<div style="color:#ff4444;font-weight:600;margin-bottom:6px">⚠ Blocking during poll (${schedCount} sched event${schedCount > 1 ? "s" : ""})</div>`;
const groups = deduplicateSamples(poll.schedSamples, trace.callframeSymbols);
for (const g of groups) {
const pct = ((g.count / schedCount) * 100).toFixed(0);
const barW = Math.max(2, (g.count / schedCount) * 200);
html += `<div style="margin-bottom:10px;border-left:3px solid #ff4444;padding-left:8px">`;
html += `<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;flex-wrap:wrap">`;
html += `<span style="color:#ff4444;font-weight:600;font-size:1.1em">${g.count}×</span>`;
html += `<span style="color:#fff;font-weight:600;word-break:break-all">${g.leaf}</span>`;
html += `<span style="color:#888">(${pct}%)</span>`;
html += `<span style="display:inline-block;height:8px;width:${barW}px;background:#ff4444;border-radius:2px;opacity:0.6"></span>`;
html += `</div>`;
const ctx = g.frames.slice(0, 3);
for (let i = 0; i < ctx.length; i++) {
html += `<div style="color:${i === 0 ? '#ff8a65' : '#777'};padding-left:${i * 8}px;font-size:0.9em;word-break:break-all">${renderFrame(ctx[i])}</div>`;
}
if (g.frames.length > 3) {
const id = 'sched-expand-' + Math.random().toString(36).slice(2);
html += `<div id="${id}-toggle" style="color:#6c63ff;cursor:pointer;font-size:0.85em;padding-left:16px" onclick="document.getElementById('${id}').style.display=document.getElementById('${id}').style.display==='none'?'block':'none';this.textContent=this.textContent.startsWith('▶')?'▼ collapse':'▶ ${g.frames.length - 3} more frames'">▼ collapse</div>`;
html += `<div id="${id}">`;
for (let i = 3; i < g.frames.length; i++) {
html += `<div style="color:#666;padding-left:24px;font-size:0.85em;word-break:break-all">${renderFrame(g.frames[i])}</div>`;
}
html += `</div>`;
}
html += `</div>`;
}
}
if (poll.cpuSamples && poll.cpuSamples.length > 0) {
if (schedCount > 0) {
html += `<div style="color:#ff8c00;font-weight:600;margin:8px 0 6px">CPU Profile (${cpuCount} sample${cpuCount > 1 ? "s" : ""})</div>`;
}
const groups = deduplicateSamples(poll.cpuSamples, trace.callframeSymbols);
for (const g of groups) {
const pct = ((g.count / cpuCount) * 100).toFixed(0);
html += `<div style="margin-bottom:8px;border-left:2px solid #333;padding-left:8px">`;
html += `<span style="color:#ff8c00">${g.count}×</span> `;
html += `<span style="color:#fff;word-break:break-all">${g.leaf}</span> `;
html += `<span style="color:#888">(${pct}%)</span>`;
const ctx = g.frames.slice(1, 3);
for (const f of ctx) {
html += `<div style="color:#777;padding-left:8px;font-size:0.9em;word-break:break-all">${renderFrame(f)}</div>`;
}
if (g.frames.length > 3) {
const id = 'cpu-expand-' + Math.random().toString(36).slice(2);
html += `<div id="${id}-toggle" style="color:#6c63ff;cursor:pointer;font-size:0.85em;padding-left:8px" onclick="document.getElementById('${id}').style.display=document.getElementById('${id}').style.display==='none'?'block':'none';this.textContent=this.textContent.startsWith('▶')?'▼ collapse':'▶ ${g.frames.length - 3} more frames'">▼ collapse</div>`;
html += `<div id="${id}">`;
for (let i = 3; i < g.frames.length; i++) {
html += `<div style="color:#666;padding-left:16px;font-size:0.85em;word-break:break-all">${renderFrame(g.frames[i])}</div>`;
}
html += `</div>`;
}
html += `</div>`;
}
}
body.innerHTML = html;
showSidebarTabs("poll-detail");
const wasHidden = sidebar.style.display !== "flex";
sidebar.style.display = "flex";
if (wasHidden && trace) requestAnimationFrame(renderAll);
}
let sidebarSelStart = 0, sidebarSelEnd = 0;
function hideStackPopup() {
if (fgActive) {
fgActive = false;
document.getElementById("flamegraph-panel").appendChild(fgContainer);
}
schedActive = false;
sidebarSelStart = 0;
sidebarSelEnd = 0;
const sidebar = document.getElementById("stack-sidebar");
const body = document.getElementById("stack-sidebar-body");
const wasVisible = sidebar.style.display === "flex";
sidebar.style.display = "none";
document.getElementById("sidebar-tabs").style.display = "none";
body.style.display = "";
body.style.flexDirection = "";
selOverlay.style.display = "none";
if (wasVisible && trace) requestAnimationFrame(renderAll);
}
document.getElementById("stack-sidebar-close").addEventListener("click", hideStackPopup);
function showUninstrumentedPopup(anchor) {
if (!trace || !trace.taskInstrumented) return;
const popup = document.getElementById("uninstrumented-popup");
if (popup.style.display !== "none") {
popup.style.display = "none";
return;
}
const groups = new Map();
let total = 0;
for (const [taskId, instrumented] of trace.taskInstrumented) {
if (instrumented) continue;
total++;
const loc = trace.taskSpawnLocs.get(taskId);
const locStr = loc ? (trace.spawnLocations.get(loc) || loc) : "(unknown)";
if (!groups.has(locStr)) groups.set(locStr, 0);
groups.set(locStr, groups.get(locStr) + 1);
}
const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
let html = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">`;
html += `<span style="color:#6c63ff;font-weight:600">${total} uninstrumented task(s) at ${sorted.length} site(s)</span>`;
html += `<span id="uninstrumented-popup-close" style="cursor:pointer;color:#888;padding:0 4px">×</span>`;
html += `</div>`;
html += `<div style="color:#888;margin-bottom:8px;font-family:sans-serif;font-size:0.95em">No wake data — use the filter <b>Uninstrumented Polls</b> to navigate. <a href="https://docs.rs/dial9-tokio-telemetry/latest/dial9_tokio_telemetry/telemetry/struct.TelemetryHandle.html#method.spawn" target="_blank" style="color:#6c63ff">Docs →</a></div>`;
for (const [loc, count] of sorted) {
const locEsc = loc.replace(/</g, "<");
const docsMatch = loc.match(/\/([a-z][a-z0-9_-]*)-(\d+\.\d+[^/]*)\/(.+\.rs)(?::(\d+))?/);
let locHtml;
if (docsMatch) {
const [, crate_, version, rawPath, line] = docsMatch;
const crateSrc = crate_.replace(/-/g, "_");
const path = rawPath.replace(/^src\//, "");
let url = `https://docs.rs/${crate_}/${version}/src/${crateSrc}/${path}.html`;
if (line) url += `#${line}`;
locHtml = `<a href="${url}" target="_blank" style="color:#ccc;text-decoration:underline dotted">${locEsc}</a>`;
} else {
locHtml = `<span style="color:#ccc">${locEsc}</span>`;
}
html += `<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:2px">`;
html += `<span style="color:#d0ccff;font-weight:600;min-width:30px;text-align:right">${count}×</span>`;
html += locHtml;
html += `</div>`;
}
popup.innerHTML = html;
const rect = anchor.getBoundingClientRect();
popup.style.top = (rect.bottom + 4) + "px";
popup.style.right = (window.innerWidth - rect.right) + "px";
popup.style.left = "auto";
popup.style.display = "block";
document.getElementById("uninstrumented-popup-close").addEventListener("click", () => {
popup.style.display = "none";
});
}
document.addEventListener("click", (e) => {
const popup = document.getElementById("uninstrumented-popup");
const btn = document.getElementById("btn-uninstrumented-info");
if (popup.style.display !== "none" && !popup.contains(e.target) && e.target !== btn) {
popup.style.display = "none";
}
});
function showSidebarTabs(activeTab) {
const tabs = document.getElementById("sidebar-tabs");
tabs.style.display = "flex";
const pollDetail = tabs.querySelector("#tab-poll-detail");
const flamegraph = tabs.querySelector("#tab-flamegraph");
const blocking = tabs.querySelector("#tab-blocking");
if (activeTab === "poll-detail") {
pollDetail.style.display = "";
flamegraph.style.display = "none";
blocking.style.display = "none";
} else {
pollDetail.style.display = "none";
flamegraph.style.display = "";
blocking.style.display = "";
}
pollDetail.classList.toggle("active", activeTab === "poll-detail");
flamegraph.classList.toggle("active", activeTab === "flamegraph");
blocking.classList.toggle("active", activeTab === "blocking");
}
document.getElementById("tab-flamegraph").addEventListener("click", () => {
if (fgActive) return; schedActive = false;
showFlamegraph(sidebarSelStart, sidebarSelEnd);
});
document.getElementById("tab-blocking").addEventListener("click", () => {
if (schedActive) return; if (fgActive) {
fgActive = false;
document.getElementById("flamegraph-panel").appendChild(fgContainer);
}
showSchedPanel(sidebarSelStart, sidebarSelEnd);
});
(function initSidebarResize() {
const handle = document.getElementById("sidebar-resize-handle");
const sidebar = document.getElementById("stack-sidebar");
let resizing = false;
let startX = 0;
let startW = 0;
let resizeRafId = 0;
handle.addEventListener("mousedown", (e) => {
e.preventDefault();
resizing = true;
startX = e.clientX;
startW = sidebar.offsetWidth;
handle.classList.add("active");
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
});
window.addEventListener("mousemove", (e) => {
if (!resizing) return;
const dx = startX - e.clientX; const newW = Math.min(
Math.max(200, startW + dx),
window.innerWidth * 0.7
);
sidebar.style.width = newW + "px";
if (trace && !resizeRafId) {
resizeRafId = requestAnimationFrame(() => {
resizeRafId = 0;
renderAll();
});
}
});
window.addEventListener("mouseup", () => {
if (!resizing) return;
resizing = false;
handle.classList.remove("active");
document.body.style.cursor = "";
document.body.style.userSelect = "";
if (fgActive) fgInstance.resize();
if (trace) requestAnimationFrame(renderAll);
});
})();
let schedPanelTimeRange = null; let schedActive = false;
function showSchedPanel(startNs, endNs) {
schedPanelTimeRange = (startNs != null && endNs != null) ? { start: startNs, end: endNs } : null;
const allSched = collectSchedSamples(schedPanelTimeRange);
if (!allSched.length) {
showToast("no-sched-events", "No scheduling events found" + (schedPanelTimeRange ? " in selected range" : " in trace"), "error", 4000);
return;
}
if (fgActive) {
fgActive = false;
document.getElementById("flamegraph-panel").appendChild(fgContainer);
}
schedActive = true;
const sidebar = document.getElementById("stack-sidebar");
const title = document.getElementById("stack-sidebar-title");
const body = document.getElementById("stack-sidebar-body");
body.style.display = "";
body.style.flexDirection = "";
if (startNs != null && endNs != null) {
sidebarSelStart = startNs;
sidebarSelEnd = endNs;
const durMs = ((endNs - startNs) / 1e6).toFixed(2);
title.textContent = `${durMs}ms selected`;
const hasCpu = trace.cpuSamples.some(s =>
s.timestamp >= startNs && s.timestamp <= endNs && s.callchain.length > 0 && s.source !== 1
);
if (hasCpu) {
showSidebarTabs("blocking");
} else {
document.getElementById("sidebar-tabs").style.display = "none";
}
} else {
title.textContent = `Blocking Calls \u00b7 ${allSched.length} events`;
document.getElementById("sidebar-tabs").style.display = "none";
}
renderSchedPanel(allSched);
clearToasts();
const wasHidden = sidebar.style.display !== "flex";
sidebar.style.display = "flex";
if (wasHidden && trace) requestAnimationFrame(renderAll);
}
function collectSchedSamples(range) {
const allSched = [];
for (const w of workerIds) {
for (const poll of workerSpans[w].polls) {
if (!poll.schedSamples) continue;
if (range && (poll.end < range.start || poll.start > range.end)) continue;
for (const s of poll.schedSamples) {
if (range && (s.timestamp < range.start || s.timestamp > range.end)) continue;
allSched.push({ sample: s, poll, worker: w });
}
}
}
return allSched;
}
function renderSchedPanel(allSched) {
const groupBy = schedActive
? (document.getElementById("sched-group-by-sb") || {}).value || "leaf"
: "leaf";
const body = document.getElementById("stack-sidebar-body");
const total = allSched.length;
const groups = new Map();
for (const { sample, poll, worker } of allSched) {
const frames = symbolizeChain(sample.callchain, trace.callframeSymbols);
const leafRaw = frames[0] ? frames[0].symbol : '(unknown)';
const leafDisplay = frames[0] ? renderFrame(frames[0]) : '(unknown)';
const key = groupBy === "leaf" ? leafRaw : frames.map(f => f.symbol).join('\0');
if (!groups.has(key)) {
groups.set(key, { leaf: leafDisplay, leafRaw, count: 0, stacks: new Map(), polls: [] });
}
const g = groups.get(key);
g.count++;
g.polls.push({ poll, worker, timestamp: sample.timestamp });
const fullKey = frames.map(f => f.symbol).join('\0');
if (!g.stacks.has(fullKey)) {
g.stacks.set(fullKey, { count: 0, frames });
}
g.stacks.get(fullKey).count++;
}
const sorted = [...groups.values()].sort((a, b) => b.count - a.count);
let html = `<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<select id="sched-group-by-sb" style="background:#2a2a4a;border:1px solid #444;color:#ccc;padding:3px 6px;border-radius:4px;font-size:0.9em">
<option value="leaf"${groupBy === "leaf" ? " selected" : ""}>Group by blocking call</option>
<option value="full"${groupBy === "full" ? " selected" : ""}>Group by full stack</option>
</select>
</div>`;
html += `<div style="margin-bottom:10px;color:#aaa;font-size:0.9em">
Blocking calls that caused the kernel to deschedule a tokio worker thread during a poll.
</div>`;
html += `<div style="margin-bottom:16px">`;
for (const g of sorted) {
const pct = ((g.count / total) * 100).toFixed(1);
const barW = Math.max(4, (g.count / total) * 100);
const lr = g.leafRaw;
const color = lr.includes("lock") || lr.includes("mutex") ? "#ff4444"
: lr.includes("epoll") || lr.includes("poll") ? "#4fc3f7"
: lr.includes("write") || lr.includes("read") || lr.includes("recv") || lr.includes("send") ? "#ff8a65"
: lr.includes("syscall") ? "#888"
: "#cc66ff";
html += `<div style="display:flex;align-items:center;gap:8px;margin-bottom:3px;cursor:pointer" class="sched-row" data-leaf="${encodeURIComponent(g.leafRaw)}">`;
html += `<span style="width:50px;text-align:right;color:${color};font-weight:600">${g.count}</span>`;
html += `<span style="display:inline-block;height:14px;width:${barW}%;background:${color};border-radius:2px;opacity:0.7"></span>`;
html += `<span style="color:#fff">${g.leaf}</span>`;
html += `<span style="color:#666">${pct}%</span>`;
html += `</div>`;
}
html += `</div>`;
for (const g of sorted) {
const pct = ((g.count / total) * 100).toFixed(1);
const color = g.leafRaw.includes("lock") || g.leafRaw.includes("mutex") ? "#ff4444" : "#ff8a65";
const gid = 'sg-' + Math.random().toString(36).slice(2);
html += `<div style="border:1px solid #333;border-radius:6px;margin-bottom:8px;overflow:hidden">`;
html += `<div style="padding:8px 12px;background:#16213e;display:flex;align-items:center;gap:8px;cursor:pointer" onclick="var d=document.getElementById('${gid}');d.style.display=d.style.display==='none'?'block':'none';this.querySelector('.sg-toggle').textContent=d.style.display==='none'?'▶ details':'▼ collapse'">`;
html += `<span style="color:${color};font-weight:700;font-size:1.1em">${g.count}×</span>`;
html += `<span style="color:#fff;font-weight:600">${g.leaf}</span>`;
html += `<span style="color:#888">(${pct}% of all sched events)</span>`;
html += `<span class="sg-toggle" style="color:#6c63ff;margin-left:auto;font-size:0.9em">▼ collapse</span>`;
html += `</div>`;
html += `<div id="${gid}" style="padding:8px 12px">`;
const subStacks = [...g.stacks.values()].sort((a, b) => b.count - a.count);
for (const sub of subStacks) {
const subPct = ((sub.count / g.count) * 100).toFixed(0);
html += `<div style="margin-bottom:10px;border-left:3px solid ${color};padding-left:10px">`;
html += `<div style="color:${color};margin-bottom:2px"><strong>${sub.count}×</strong> <span style="color:#888">(${subPct}% of group)</span></div>`;
for (let i = 0; i < sub.frames.length; i++) {
const isLeaf = i === 0;
const raw = sub.frames[i].symbol || '';
const isTokio = raw.includes("tokio::runtime");
const c = isLeaf ? "#fff" : isTokio ? "#555" : "#aaa";
const weight = isLeaf ? "font-weight:600" : "";
html += `<div style="color:${c};${weight};padding-left:${Math.min(i, 1) * 12}px">${renderFrame(sub.frames[i])}</div>`;
}
html += `</div>`;
}
const examples = g.polls.slice(0, 5);
html += `<div style="margin-top:6px;color:#6c63ff;font-size:0.9em">`;
html += `Example polls: `;
for (let i = 0; i < examples.length; i++) {
const ex = examples[i];
const durStr = formatHumanDuration(ex.poll.end - ex.poll.start);
html += `<span class="sched-jump" style="cursor:pointer;text-decoration:underline;margin-right:8px" data-time="${ex.poll.start}" data-end="${ex.poll.end}" data-worker="${ex.worker}">W${ex.worker} @${fmtTs(ex.poll.start)} (${durStr})</span>`;
}
if (g.polls.length > 5) html += `<span style="color:#888">… ${g.polls.length - 5} more</span>`;
html += `</div>`;
html += `</div></div>`;
}
body.innerHTML = html;
const groupByEl = document.getElementById("sched-group-by-sb");
if (groupByEl) {
groupByEl.addEventListener("change", () => {
const allSched = collectSchedSamples(schedPanelTimeRange);
renderSchedPanel(allSched);
});
}
body.querySelectorAll('.sched-jump').forEach(el => {
el.addEventListener('click', (e) => {
const t = parseFloat(el.dataset.time);
const end = parseFloat(el.dataset.end);
const w = parseInt(el.dataset.worker);
const dur = end - t;
const pad = Math.max(dur * 5, 1e6);
viewStart = Math.max(minTs, t - pad * 0.3);
viewEnd = Math.min(maxTs, viewStart + pad);
hideStackPopup();
const laneIdx = workerIds.indexOf(w);
if (laneIdx >= 0) lanesContainer.scrollTop = laneIdx * LANE_H;
renderAll();
});
});
}
document.addEventListener("keydown", (e) => {
if (e.key !== "Enter") return;
const el = document.activeElement;
if (el && el.getAttribute("role") === "button") { e.preventDefault(); el.click(); }
if (el && el.type === "checkbox") { e.preventDefault(); el.click(); }
});
window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && loadAbortController) {
e.preventDefault();
cancelLoad();
return;
}
if (!trace) return;
const viewDur = viewEnd - viewStart;
if (e.key === "Shift" || e.key === "Alt") {
e.preventDefault();
const mode = e.key === "Shift" ? "shift" : "alt";
if (!kbSelecting) {
if (sidebarSelStart && sidebarSelEnd) return;
kbSelecting = true;
kbSelMode = mode;
kbCursorNs = kbSelStartNs = (mouseNs !== null && mouseNs >= viewStart && mouseNs <= viewEnd) ? mouseNs : (viewStart + viewEnd) / 2;
updateKbSelOverlay();
renderCrosshair();
announce(mode === "shift" ? "Selection started — use ← → to extend, Shift or Enter to confirm" : "Zoom selection started — use ← → to extend, Alt or Enter to confirm");
if (mode === "shift") hideToast("shift-drag-hint");
else hideToast("option-drag-hint");
} else if (kbSelMode === mode) {
completeKbSelection();
}
return;
}
if (kbSelecting) {
if (e.key === "Enter") {
e.preventDefault();
completeKbSelection();
return;
}
if (e.key === "Escape") {
e.preventDefault();
cancelKbSelection();
announce("Selection cancelled");
return;
}
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
e.preventDefault();
const step = viewDur * 0.05;
kbCursorNs += e.key === "ArrowLeft" ? -step : step;
kbCursorNs = Math.max(minTs, Math.min(maxTs, kbCursorNs));
updateKbSelOverlay();
renderCrosshair();
return;
}
}
switch (e.key) {
case "Escape":
e.preventDefault();
if (document.getElementById("help-overlay").classList.contains("visible")) {
document.getElementById("help-overlay").classList.remove("visible");
break;
}
clearToasts();
if (document.getElementById("uninstrumented-popup").style.display !== "none") {
document.getElementById("uninstrumented-popup").style.display = "none";
} else if (document.getElementById("stack-sidebar").style.display === "flex") {
if (fgActive && fgInstance.handleEscape()) {
break;
}
hideStackPopup();
}
selectedTaskId = null;
document.getElementById("main-area").focus();
renderAll();
break;
case "ArrowUp":
e.preventDefault();
zoom(0.5);
break;
case "ArrowDown":
e.preventDefault();
zoom(2);
break;
case "ArrowLeft": {
e.preventDefault();
const shift = viewDur * 0.2;
viewStart = Math.max(minTs, viewStart - shift);
viewEnd = viewStart + viewDur;
if (viewEnd > maxTs) {
viewEnd = maxTs;
viewStart = viewEnd - viewDur;
}
renderAll();
break;
}
case "ArrowRight": {
e.preventDefault();
const shift = viewDur * 0.2;
viewEnd = Math.min(maxTs, viewEnd + shift);
viewStart = viewEnd - viewDur;
if (viewStart < minTs) {
viewStart = minTs;
viewEnd = viewStart + viewDur;
}
renderAll();
break;
}
case "?":
e.preventDefault();
if (kbSelecting) cancelKbSelection();
document.getElementById("help-overlay").classList.toggle("visible");
break;
}
});
function eventToTimelineNs(clientX) {
const rect = lanesContainer.getBoundingClientRect();
const scrollbarW = lanesContainer.offsetWidth - lanesContainer.clientWidth;
const mouseX = clientX - rect.left - LABEL_W;
const drawW = rect.width - LABEL_W - scrollbarW;
if (mouseX < 0 || mouseX > drawW) return null;
return viewStart + (mouseX / drawW) * (viewEnd - viewStart);
}
function scheduleCrosshairRedraw() {
if (!queueChartRafId) {
queueChartRafId = requestAnimationFrame(() => {
renderCrosshair();
queueChartRafId = null;
});
}
}
document
.getElementById("main-area")
.addEventListener("mousemove", (e) => {
if (dragging) {
tooltip.style.display = "none";
mouseNs = null;
return;
}
if (e.target.closest && e.target.closest("#span-panel")) {
mouseNs = eventToTimelineNs(e.clientX);
lanesContainer.style.cursor = "";
scheduleCrosshairRedraw();
return;
}
const rect = lanesContainer.getBoundingClientRect();
const mouseX = e.clientX - rect.left - LABEL_W;
const mouseY = e.clientY - rect.top;
const drawW = rect.width - LABEL_W;
if (mouseX < 0 || mouseX > drawW) {
tooltip.style.display = "none";
mouseNs = null;
lanesContainer.style.cursor = "";
return;
}
const ns =
viewStart + (mouseX / drawW) * (viewEnd - viewStart);
mouseNs = ns;
scheduleCrosshairRedraw();
const laneIdx = Math.floor(
(mouseY + lanesContainer.scrollTop) / LANE_H,
);
if (laneIdx < 0 || laneIdx >= workerIds.length) {
tooltip.style.display = "none";
lanesContainer.style.cursor = "";
return;
}
const w = workerIds[laneIdx];
const spans = workerSpans[w];
let state = "🟢 Active";
let stateDetail = "";
let schedInfo = "";
if (trace.hasCpuTime) {
const hitActive = findSpanAt(spans.actives, ns);
if (hitActive) {
const pct = (hitActive.ratio * 100).toFixed(1);
const icon = hitActive.ratio >= 0.95 ? "🟢" : hitActive.ratio >= 0.5 ? "🟡" : "🔴";
schedInfo = `<span class="label">Scheduling:</span> <span class="value">${icon} ${pct}% on-CPU</span><br>`;
}
}
{
const s = findSpanAt(spans.parks, ns);
if (s) {
const parkNs = s.end - s.start;
state = "💤 Parked";
stateDetail = ` for <span class="value">${formatHumanDuration(parkNs)}</span>`;
schedInfo = "";
if (trace.hasSchedWait && s.schedWait != null) {
const sw = s.schedWait;
const icon = sw < 100_000 ? "🟢" : sw < 1_000_000 ? "🟡" : "🔴";
stateDetail += `<br><span class="label">Kernel sched delay:</span> <span class="value">${icon} ${formatHumanDuration(sw)}</span>`;
}
}
}
let pollInfo = "";
let hasClickableStack = false;
{
const s = findSpanAt(spans.polls, ns);
if (s) {
const pollNs = s.end - s.start;
state = "⚡ Polling";
pollInfo = `<span class="label">Poll duration:</span> <span class="value">${formatHumanDuration(pollNs)}</span><br>`;
if (trace.hasTaskTracking && s.taskId) {
pollInfo += `<span class="label">Task ID:</span> <span class="value">0x${s.taskId.toString(16)}</span><br>`;
if (s.spawnLoc) {
pollInfo += `<span class="label">Spawned at:</span> <span class="value">${s.spawnLoc}</span><br>`;
}
}
if (s.cpuSamples && s.cpuSamples.length > 0) {
pollInfo += `<span class="label">CPU samples:</span> <span class="value">🔥 ${s.cpuSamples.length}</span> <span style="color:#666">(click to view)</span><br>`;
hasClickableStack = true;
}
if (s.schedSamples && s.schedSamples.length > 0) {
pollInfo += `<span class="label">Sched events:</span> <span class="value">⚠️ ${s.schedSamples.length} blocking</span> <span style="color:#666">(click to view)</span><br>`;
hasClickableStack = true;
}
{
let spanCount = 0;
const spanNames = {};
for (const sp of allSpans) {
if (sp.start > s.end) break;
if (sp.end < s.start) continue;
for (const seg of sp.segments) {
if (seg.workerId === w && seg.start >= s.start && seg.end <= s.end) {
spanCount++;
spanNames[sp.spanName] = (spanNames[sp.spanName] || 0) + 1;
}
}
}
if (spanCount > 0) {
const summary = Object.entries(spanNames).map(([n, c]) => `${n}×${c}`).join(", ");
pollInfo += `<span class="label">Spans:</span> <span class="value">${spanCount}</span> <span style="color:#666">(${summary})</span><br>`;
}
}
}
}
lanesContainer.style.cursor = hasClickableStack ? "pointer" : "";
const nearest = findNearest(workerQueueSamples[w] || [], ns);
const nearestGlobal = findNearest(queueSamples, ns);
let qInfo = "";
const globalStr = nearestGlobal != null ? nearestGlobal.global : "-";
const localStr = nearest != null ? nearest.local : "-";
const totalEst = (nearestGlobal != null ? nearestGlobal.global : 0) + (nearest != null ? nearest.local : 0);
qInfo = `<span class="label">Global Q:</span> <span class="value">${globalStr}</span> · <span class="label">Local Q:</span> <span class="value">${localStr}</span> · <span class="label">Est. Total:</span> <span class="value">${totalEst}</span>`;
let taskCountInfo = "";
if (activeTaskSamples.length > 0) {
let taskCount = 0;
for (const s of activeTaskSamples) {
if (s.t > ns) break;
taskCount = s.count;
}
taskCountInfo = `<br><span class="label">Active Tasks:</span> <span class="value">${taskCount}</span>`;
}
let spanInfo = "";
{
let hit = null;
for (const s of allSpans) {
if (s.end < ns || s.start > ns) continue;
for (const seg of s.segments) {
if (seg.workerId === w && seg.start <= ns && seg.end >= ns) { hit = s; break; }
}
if (hit) break;
}
if (hit) {
const durNs = hit.end - hit.start;
spanInfo = `<br><span class="label">Span:</span> <span class="value">${esc(hit.spanName)}</span> <span style="color:#888">${formatHumanDuration(durNs)}</span>`;
const fields = hit.fields;
if (fields && typeof fields === "object") {
const entries = Object.entries(fields);
if (entries.length > 0) {
spanInfo += `<br><span class="label">Fields:</span> <span class="value">${entries.map(([k, v]) => esc(k) + "=" + esc(v)).join(", ")}</span>`;
}
}
if (hit.parentSpanId != null) {
const parentSpan = allSpans.find(s => s.spanId === hit.parentSpanId && s.start <= hit.start && s.end >= hit.end);
if (parentSpan) {
spanInfo += `<br><span class="label">Parent:</span> <span class="value">${esc(parentSpan.spanName)}</span>`;
}
}
}
}
tooltip.innerHTML =
`<span class="label">Worker</span> <span class="value">${w}</span> · <span class="label">Time</span> <span class="value">${fmtTs(ns)}</span><br>` +
`<span class="label">State:</span> ${state}${stateDetail}<br>` +
schedInfo +
pollInfo +
qInfo +
taskCountInfo +
spanInfo;
tooltip.style.display = "block";
tooltip.style.left =
Math.min(e.clientX + 12, window.innerWidth - 340) +
"px";
tooltip.style.top = e.clientY + 130 + "px";
});
document
.getElementById("main-area")
.addEventListener("mouseleave", () => {
tooltip.style.display = "none";
mouseNs = null;
renderCrosshair();
});
document.getElementById("task-detail").addEventListener("mousemove", (e) => {
const c = document.getElementById("task-detail-canvas");
const rect = c.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
let found = null;
for (const r of taskDetailWakeRegions) {
if (mx >= r.x1 && mx <= r.x2 && my >= r.y1 && my <= r.y2) {
found = r.wakerTaskId;
break;
}
}
if (found !== hoveredWakerTaskId) {
hoveredWakerTaskId = found;
document.getElementById("task-detail").style.cursor = found ? "pointer" : "";
renderAll();
}
const statusEl = document.getElementById("task-detail-status");
let hit = null;
for (const r of taskDetailHitRegions) {
if (mx >= r.x1 && mx <= r.x2 && my >= r.y1 && my <= r.y2) {
hit = r;
break;
}
}
if (!found) {
document.getElementById("task-detail").style.cursor = (hit && hit.taskDumps) ? "pointer" : "";
}
const icon = hit ? (hit.type === "polling" ? "⚡" : hit.type === "scheduled" ? "⏳" : "💤") : "";
statusEl.textContent = hit ? `${icon} ${hit.detail}` : "";
});
document.getElementById("task-detail").addEventListener("mouseleave", () => {
document.getElementById("task-detail-status").textContent = "";
if (hoveredWakerTaskId) {
hoveredWakerTaskId = null;
renderAll();
}
});
document.getElementById("task-detail").addEventListener("click", (e) => {
const c = document.getElementById("task-detail-canvas");
const rect = c.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
for (const r of taskDetailWakeRegions) {
if (mx >= r.x1 && mx <= r.x2 && my >= r.y1 && my <= r.y2) {
selectedTaskId = r.wakerTaskId;
renderAll();
return;
}
}
for (const r of taskDetailHitRegions) {
if (mx >= r.x1 && mx <= r.x2 && my >= r.y1 && my <= r.y2 && r.taskDumps) {
try {
showTaskDumpStack(r.taskDumps);
} catch (err) {
console.error("showTaskDumpStack error:", err);
showToast("task-dump-err", "Error: " + err.message, "error", 5000);
}
return;
}
}
});
window.addEventListener("resize", () => {
if (trace) renderAll();
});
(function addLegend() {
const tb = document.getElementById("toolbar-row-view");
const leg = document.createElement("span");
leg.style.cssText =
"font-size:0.8em;display:flex;gap:12px;align-items:center;flex-wrap:wrap;";
leg.innerHTML = `
<span style="display:flex;align-items:center;gap:4px"><span style="width:12px;height:12px;background:#2a5a7a;display:inline-block;border-radius:2px"></span>Poll ≤10µs</span>
<span style="display:flex;align-items:center;gap:4px"><span style="width:12px;height:12px;background:#4fc3f7;display:inline-block;border-radius:2px"></span>>10µs</span>
<span style="display:flex;align-items:center;gap:4px"><span style="width:12px;height:12px;background:#ff8a65;display:inline-block;border-radius:2px"></span>>100µs</span>
<span style="display:flex;align-items:center;gap:4px"><span style="width:12px;height:12px;background:#ff4444;display:inline-block;border-radius:2px"></span>>1ms</span>
<span style="display:flex;align-items:center;gap:4px"><span style="width:12px;height:12px;background:#2a1520;border-top:3px solid #cc5533;display:inline-block;border-radius:2px"></span>Parked</span>
<span style="display:flex;align-items:center;gap:4px"><span style="width:12px;height:12px;background:#ff0000;display:inline-block;border-radius:2px"></span>Kernel Sched Delay</span>
<span style="display:flex;align-items:center;gap:4px"><span style="width:12px;height:4px;background:#ce93d8;display:inline-block"></span>CPU Sampled</span>
<span style="display:flex;align-items:center;gap:4px"><span style="color:#ff2222;font-size:10px">▲</span>Sched (blocking)</span>
<span style="display:flex;align-items:center;gap:4px"><span style="color:#66bb6a;font-size:10px">▼</span>Wake</span>
<span style="display:flex;align-items:center;gap:4px"><span style="width:12px;height:2px;background:rgba(255,200,50,0.7);display:inline-block"></span>Local Q</span>
<span style="display:flex;align-items:center;gap:4px"><span style="width:12px;height:2px;background:#81c784;display:inline-block"></span>Active Tasks</span>
`;
tb.insertBefore(leg, tb.firstChild);
const helpBtn = document.createElement("button");
helpBtn.id = "btn-info";
helpBtn.setAttribute("aria-label", "Help");
helpBtn.title = "Help";
helpBtn.style.cssText =
"margin-left:8px;background:none;border:1px solid #555;border-radius:4px;color:#aaa;cursor:pointer;padding:6px;line-height:1;display:inline-flex;align-items:center;justify-content:center";
helpBtn.innerHTML =
'<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:block"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 0 1 5 0c0 1.5-2.5 2-2.5 3.5"/><line x1="12" y1="16.5" x2="12" y2="16.6"/></svg>';
helpBtn.addEventListener("mouseenter", () => { helpBtn.style.color = "#fff"; helpBtn.style.borderColor = "#888"; });
helpBtn.addEventListener("mouseleave", () => { helpBtn.style.color = "#aaa"; helpBtn.style.borderColor = "#555"; });
tb.appendChild(helpBtn);
})();
(function initHelp() {
const overlay = document.getElementById("help-overlay");
document.getElementById("btn-info").addEventListener("click", () => {
overlay.classList.toggle("visible");
});
overlay.addEventListener("click", (e) => {
if (e.target === overlay) overlay.classList.remove("visible");
});
})();
let fgSelStart = 0, fgSelEnd = 0;
let fgActive = false;
const fgContainer = document.getElementById("fg-container");
const fgInstance = FlamegraphRenderer.createFlamegraph(fgContainer);
function renderFlamegraphInSidebar({ title, subtitle, samples }) {
fgActive = true;
schedActive = false;
const sidebar = document.getElementById("stack-sidebar");
const titleEl = document.getElementById("stack-sidebar-title");
const body = document.getElementById("stack-sidebar-body");
document.getElementById("sidebar-tabs").style.display = "none";
titleEl.textContent = title;
body.innerHTML = "";
body.style.display = "flex";
body.style.flexDirection = "column";
const actions = document.createElement("div");
actions.style.cssText = "display:flex;gap:8px;margin-bottom:6px;flex-shrink:0;align-items:center";
actions.innerHTML = `<span style="color:#888;font-size:0.85em">${subtitle}</span>`;
body.appendChild(actions);
fgContainer.style.flex = "1";
fgContainer.style.minHeight = "0";
body.appendChild(fgContainer);
const wasHidden = sidebar.style.display !== "flex";
sidebar.style.display = "flex";
if (wasHidden && trace) requestAnimationFrame(renderAll);
requestAnimationFrame(() => {
fgInstance.setData(samples, trace.callframeSymbols);
fgInstance.resize();
});
}
function showTaskDumpStack(dumps) {
const samples = dumps.map(d => ({ callchain: d.callchain, workerId: 0 }));
const s = dumps.length > 1 ? "s" : "";
renderFlamegraphInSidebar({
title: `Waiting on — ${dumps.length} capture${s}`,
subtitle: `${dumps.length} async stack capture${s}`,
samples,
});
}
function showIdleTimeFlamegraph() {
if (!selectedTaskId || !trace.taskDumps) return;
const dumps = trace.taskDumps.get(selectedTaskId);
if (!dumps || dumps.length === 0) return;
const polls = [];
for (const w of workerIds) {
for (const s of workerSpans[w].polls) {
if (s.taskId === selectedTaskId) polls.push(s);
}
}
polls.sort((a, b) => a.start - b.start);
const weightedSamples = [];
let di = 0;
for (let i = 0; i < polls.length - 1 && di < dumps.length; i++) {
const gapStart = polls[i].end;
const gapEnd = polls[i + 1].start;
const dur = gapEnd - gapStart;
while (di < dumps.length && dumps[di].timestamp <= gapStart) di++;
for (let j = di; j < dumps.length && dumps[j].timestamp <= gapEnd; j++) {
const weight = Math.max(1, Math.round(dur / 1000));
weightedSamples.push({ callchain: dumps[j].callchain, weight });
}
}
if (polls.length > 0) {
const lastEnd = polls[polls.length - 1].end;
const traceEnd = trace.maxTs || lastEnd;
while (di < dumps.length) {
const dur = traceEnd - lastEnd;
const weight = Math.max(1, Math.round(dur / 1000));
weightedSamples.push({ callchain: dumps[di].callchain, weight });
di++;
}
}
if (weightedSamples.length === 0) return;
const totalWeight = weightedSamples.reduce((s, x) => s + x.weight, 0);
const scale = totalWeight > 10000 ? 10000 / totalWeight : 1;
const expandedSamples = [];
for (const ws of weightedSamples) {
const count = Math.max(1, Math.round(ws.weight * scale));
for (let k = 0; k < count; k++) {
expandedSamples.push({ callchain: ws.callchain, workerId: 0 });
}
}
renderFlamegraphInSidebar({
title: `Idle time flamegraph — ${dumps.length} samples`,
subtitle: `${dumps.length} task dumps, time-weighted`,
samples: expandedSamples,
});
}
function showFlamegraph(selStart, selEnd) {
const samples = FlamegraphRenderer.filterCpuSamples(trace.cpuSamples, selStart, selEnd);
if (!samples.length) {
showToast("no-cpu-samples-region", "No CPU samples in selected region", "error", 4000);
return;
}
fgSelStart = selStart;
fgSelEnd = selEnd;
fgActive = true;
schedActive = false;
sidebarSelStart = selStart;
sidebarSelEnd = selEnd;
const durMs = ((selEnd - selStart) / 1e6).toFixed(2);
const sidebar = document.getElementById("stack-sidebar");
const title = document.getElementById("stack-sidebar-title");
const body = document.getElementById("stack-sidebar-body");
title.textContent = `${durMs}ms selected`;
const hasSched = collectSchedSamples({ start: selStart, end: selEnd }).length > 0;
if (hasSched) {
showSidebarTabs("flamegraph");
} else {
document.getElementById("sidebar-tabs").style.display = "none";
}
body.innerHTML = "";
body.style.display = "flex";
body.style.flexDirection = "column";
const actions = document.createElement("div");
actions.style.cssText = "display:flex;gap:8px;margin-bottom:6px;flex-shrink:0;align-items:center";
actions.innerHTML = `<span style="color:#888;font-size:0.85em">${samples.length} samples</span><span id="fg-popout-sb" tabindex="0" role="button" title="Open in new tab" style="cursor:pointer;color:#6c63ff;font-size:0.9em">↗ Pop Out</span>`;
body.appendChild(actions);
fgContainer.style.flex = "1";
fgContainer.style.minHeight = "0";
body.appendChild(fgContainer);
clearToasts();
const wasHidden = sidebar.style.display !== "flex";
sidebar.style.display = "flex";
if (wasHidden && trace) requestAnimationFrame(renderAll);
requestAnimationFrame(() => {
fgInstance.setData(samples, trace.callframeSymbols);
fgInstance.resize();
});
document.getElementById("fg-popout-sb").onclick = handleFgPopout;
}
function handleFgPopout() {
const params = new URLSearchParams(window.location.search);
let traceUrl = params.get("trace");
let isBlobUrl = false;
if (!traceUrl && currentTraceBuffer) {
const blob = new Blob([currentTraceBuffer]);
traceUrl = URL.createObjectURL(blob);
isBlobUrl = true;
}
if (!traceUrl) {
showToast("popout-no-url", "Pop-out requires a loaded trace", "error", 3000);
return;
}
let url = `flamegraph.html?trace=${encodeURIComponent(traceUrl)}&start=${Math.round(fgSelStart)}&end=${Math.round(fgSelEnd)}`;
const z = fgInstance.getZoomPath();
if (z.worker.length > 0) url += `&worker-zoom=${encodeURIComponent(z.worker.join("\t"))}`;
if (z.offworker.length > 0) url += `&offworker-zoom=${encodeURIComponent(z.offworker.join("\t"))}`;
window.open(url, "_blank");
if (isBlobUrl) {
showToast("popout-blob", "Pop-out linked to this tab \u2014 keep it open", "info", 5000);
}
}
let qcDragging = false, qcDragStartX = 0, qcDragStartNs = 0, qcDragEndNs = 0, qcDragMoved = false;
document.getElementById("queue-chart").addEventListener("mousedown", (e) => {
if (!trace || !activeTaskSamples.length) return;
qcDragging = true;
qcDragMoved = false;
qcDragStartX = e.clientX;
const rect = queueCanvas.getBoundingClientRect();
const scrollbarW = lanesContainer.offsetWidth - lanesContainer.clientWidth;
const chartW = rect.width - LABEL_W - scrollbarW;
const mx = e.clientX - rect.left - LABEL_W;
qcDragStartNs = viewStart + (Math.max(0, mx) / chartW) * (viewEnd - viewStart);
qcDragEndNs = qcDragStartNs;
const mainRect = document.getElementById("main-area").getBoundingClientRect();
selOverlay.style.top = "0px";
selOverlay.style.height = mainRect.height + "px";
selOverlay.style.display = "block";
updateSelOverlay(e.clientX);
document.body.style.cursor = "crosshair";
e.stopPropagation();
});
window.addEventListener("mousemove", (e) => {
if (!qcDragging) return;
if (Math.abs(e.clientX - qcDragStartX) > 3) qcDragMoved = true;
const rect = queueCanvas.getBoundingClientRect();
const scrollbarW = lanesContainer.offsetWidth - lanesContainer.clientWidth;
const chartW = rect.width - LABEL_W - scrollbarW;
const mx = e.clientX - rect.left - LABEL_W;
qcDragEndNs = viewStart + (Math.max(0, Math.min(chartW, mx)) / chartW) * (viewEnd - viewStart);
regionStartNs = qcDragStartNs;
regionEndNs = qcDragEndNs;
updateSelOverlay(e.clientX);
renderQueueChart(rect.width - LABEL_W, scrollbarW);
const c = queueCanvas;
const ctx = c.getContext("2d");
const dpr = devicePixelRatio || 1;
ctx.save();
ctx.scale(dpr, dpr);
const selStart = Math.min(qcDragStartNs, qcDragEndNs);
const selEnd = Math.max(qcDragStartNs, qcDragEndNs);
const x1 = LABEL_W + nsToX(selStart, chartW);
const x2 = LABEL_W + nsToX(selEnd, chartW);
ctx.fillStyle = "rgba(129,199,132,0.2)";
ctx.fillRect(x1, 0, x2 - x1, c.height / dpr);
ctx.strokeStyle = "rgba(129,199,132,0.6)";
ctx.lineWidth = 1;
ctx.strokeRect(x1, 0, x2 - x1, c.height / dpr);
ctx.restore();
});
window.addEventListener("mouseup", (e) => {
if (!qcDragging) return;
qcDragging = false;
selOverlay.style.display = "none";
document.body.style.cursor = "";
if (qcDragMoved) {
clearToasts();
const selStart = Math.min(qcDragStartNs, qcDragEndNs);
const selEnd = Math.max(qcDragStartNs, qcDragEndNs);
showSpawnedTasks(selStart, selEnd);
}
renderAll();
});
function showSpawnedTasks(selStart, selEnd) {
const tasks = [];
for (const [taskId, t] of taskFirstPoll) {
if (t >= selStart && t <= selEnd) {
const locId = trace.taskSpawnLocs.get(taskId);
const loc = locId != null ? (trace.spawnLocations.get(locId) || null) : null;
tasks.push({ taskId, firstPoll: t, loc });
}
}
if (!tasks.length) return;
const groups = new Map();
for (const t of tasks) {
const key = t.loc || "(unknown)";
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(t);
}
const sorted = [...groups.entries()].sort((a, b) => b[1].length - a[1].length);
const durMs = ((selEnd - selStart) / 1e6).toFixed(2);
const sidebar = document.getElementById("stack-sidebar");
const title = document.getElementById("stack-sidebar-title");
const body = document.getElementById("stack-sidebar-body");
title.textContent = `${tasks.length} tasks spawned in ${durMs}ms`;
let html = "";
for (const [loc, taskList] of sorted) {
const short = loc.replace(/.*\//, '');
html += `<div style="margin-bottom:10px;border-left:3px solid #81c784;padding-left:8px">`;
html += `<div style="color:#81c784;font-weight:600">${taskList.length}× <span style="color:#fff">${esc(short)}</span></div>`;
if (short !== loc) html += `<div style="color:#666;font-size:0.85em;word-break:break-all">${esc(loc)}</div>`;
const show = taskList.slice(0, 5);
for (const t of show) {
html += `<div style="margin-left:8px"><span class="spawned-task-link" style="color:#6c63ff;cursor:pointer;text-decoration:underline" data-task-id="${t.taskId}">0x${t.taskId.toString(16)}</span> <span style="color:#888">@ ${fmtTs(t.firstPoll)}</span></div>`;
}
if (taskList.length > 5) html += `<div style="color:#888;margin-left:8px;font-size:0.9em">... ${taskList.length - 5} more</div>`;
html += `</div>`;
}
body.innerHTML = html;
const wasHidden = sidebar.style.display !== "flex";
sidebar.style.display = "flex";
if (wasHidden && trace) requestAnimationFrame(renderAll);
body.querySelectorAll('.spawned-task-link').forEach(el => {
el.addEventListener('click', () => {
const tid = parseInt(el.dataset.taskId);
selectedTaskId = tid;
hideStackPopup();
renderAll();
});
});
}
window.addEventListener("resize", () => {
if (fgActive) {
fgInstance.resize();
}
});
</script>
</body>
</html>