@atolljs/devtools 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # @atolljs/devtools
2
+
3
+ Devtools transport + analytics dashboard for AtollJS instrumentation events:
4
+ pool lifecycle, task timings, worker crashes/respawns, shared-memory field
5
+ writes, and island op traffic — streamed from the app to a standalone server
6
+ and rendered by a bundled dashboard.
7
+
8
+ The dashboard is a multi-view analytics site (dependency-free static app in
9
+ `app/`):
10
+
11
+ - **Dashboard** — sub-tabs: *Overview* (the **app map** — one world-space
12
+ canvas holding every live session as an atoll cluster: main thread at
13
+ the center, pool sockets on the rim, workers on the mid orbit, islands
14
+ on the outer orbit rendered as framework marks — React ⚛, Vue V,
15
+ Svelte S, Angular A, Solid, vanilla JS, per the `framework` tag the
16
+ mounter declares. Wheel to zoom, drag to pan, double-click to refit;
17
+ zoom tiers apply clustering — sessions collapse to labeled discs, mid
18
+ zoom collapses islands into a green pool ring; click a node to inspect,
19
+ click a session disc/center to drill in) plus KPI cards, per-second
20
+ task-throughput chart, and an "attention needed" feed. *Pools*,
21
+ *Workers*, *Islands*: the entity tables — pools (size, memory, worker
22
+ count, tasks, failures), workers (tasks, failures, respawns, errors,
23
+ heap, hosted islands, last task, live/down), islands (app, framework,
24
+ pid, host `poolN#0` cross-link, round-trips, op batches, replay ms,
25
+ events, live/unmounted). *Worker* / *Island*: the inspector sub-tabs,
26
+ appearing on row or map-node click — worker inspector (own waterfall
27
+ lane, live heap chart, that worker's fetches, task history, error
28
+ log), island inspector (framework badge, op-traffic chart, per-call
29
+ aggregates, event-channel breakdown, round-trip table). Island and
30
+ worker inspectors are cross-linked — `island:mount` carries the
31
+ client's `poolId`.
32
+ - **Tasks** — a waterfall/flame view: one swimlane per pool worker slot,
33
+ queued→dispatched→settled segments colored by outcome, hover tooltips;
34
+ per-task aggregates (n, failures, wait/run avg, run p95/max) with
35
+ click-to-drill filtering of the waterfall and the per-call table.
36
+ - **Network** — two sub-tabs. *Traffic*: rolling 60s charts (tasks/s,
37
+ avg run ms, errors/s, in-flight calls, island ops/round-trips/events
38
+ per second, fetches/s) plus a **fetch log** of real `fetch()` traffic
39
+ from main thread and workers. *Request* (click a fetch row): request/
40
+ response headers, capped body previews, sizes (transfer/encoded/
41
+ decoded), and a Resource-Timing stage breakdown (stalled, dns, tcp,
42
+ tls, request+waiting, download). Cross-origin responses without
43
+ `Timing-Allow-Origin` show total time only.
44
+ - **Memory** — two sub-tabs. *Shared-memory writes*: field-writes/s and
45
+ top-fields/s charts plus the per-field table (writes, writes/sec,
46
+ version, last writer `poolId#slot` when worker-forwarded, rate
47
+ sparkline). *JS heap*: per-execution-context heap via
48
+ `performance.measureUserAgentSpecificMemory` — worker heaps identified
49
+ by worker script URL.
50
+ - **Log** — the raw event stream.
51
+
52
+ Sessions are pages/processes; a disconnected session is retained (bounded)
53
+ as an **ended** session so its history stays attributed for post-mortem
54
+ analysis — its rows are grayed and excluded from live KPIs. Hover an ended
55
+ session in the sidebar and click **×** to dismiss it: the server drops it
56
+ from the session list and purges its replayed batches, so it stays gone for
57
+ current and future viewers. Live sessions can't be dismissed.
58
+
59
+ ## Surface
60
+
61
+ | Import | Purpose |
62
+ |---|---|
63
+ | `@atolljs/devtools` | `connectDevtools({ transport?, url?, session?, flushMs?, bufferCap?, replayBatches?, network?, memory? })` — installs the core event sink and batches events to dashboards. Default `transport: 'auto'`: **BroadcastChannel** in a browser window (pure client — no backend; the dashboard is served on the app's own origin at `/__atoll/` by the vite plugin, and `mountDevtoolsOverlay()` embeds it in a floating flyout — draggable, resizable, `position` option), WebSocket to the aggregate server everywhere else (`url` or `transport: 'websocket'` opts in — Node uses this path). `network` (default on) wraps `fetch` to emit `net:fetch` per request; `memory` (default on) samples JS heap into `runtime:memory`. |
64
+ | `@atolljs/devtools/node` | Node entry — `initDevtools`/`connectDevtools` without the browser-only pieces (WebSocket-only, env-gated, no overlay). Use this import in Node apps. |
65
+ | `@atolljs/devtools/server` | `createDevtoolsServer({ port?, host?, appDir?, replayBatches? })` — Node http + dependency-free WebSocket server on `127.0.0.1` (default port 4780). |
66
+ | `atoll-devtools` bin | Boots the server and prints the dashboard URL. `atoll devtools` resolves it through the CLI too. |
67
+
68
+ ## Usage
69
+
70
+ ```bash
71
+ npx atoll-devtools # or: atoll devtools [--port 4780]
72
+ ```
73
+
74
+ The quickest wire-up in an existing app is the scaffolder —
75
+ `atoll add devtools` writes the right init module for your host (browser,
76
+ Node, or the Angular aggregate-server variant) and installs the package.
77
+ Or by hand:
78
+
79
+ ```ts
80
+ // in the app — one line. No-op unless the URL carries ?__atoll_devtools:
81
+ // then the BroadcastChannel sink installs and the overlay flyout mounts.
82
+ // Call before pools spawn: worker-side events forward only if the sink
83
+ // existed at INIT time.
84
+ import { initDevtools } from '@atolljs/devtools';
85
+ initDevtools({ session: { name: 'my-app' } });
86
+
87
+ // my-app/?__atoll_devtools → devtools on
88
+ // my-app/ → zero cost, no sink installed
89
+ //
90
+ // Always-on or programmatic control: `connectDevtools(opts)` ignores the
91
+ // param; `initDevtools({ enabled, overlay })` overrides the gate.
92
+ ```
93
+
94
+ ## Node apps (Express, NestJS, …)
95
+
96
+ Node takes the WebSocket path automatically — no `window`, so no
97
+ BroadcastChannel — pointed at the standalone server, which is also the
98
+ dashboard:
99
+
100
+ ```bash
101
+ npx atoll-devtools # http://127.0.0.1:4780
102
+ ```
103
+
104
+ ```ts
105
+ // src/devtools.ts — a dedicated module, imported FIRST from the entry.
106
+ // No-op unless ATOLL_DEVTOOLS is set in the environment.
107
+ import { initDevtools } from '@atolljs/devtools/node';
108
+ initDevtools({ session: { name: 'my-api' } });
109
+ ```
110
+
111
+ ```ts
112
+ // main.ts — the import order matters: ESM evaluates imports before the
113
+ // body, and pools usually spawn inside a sibling module or NestFactory.
114
+ import './devtools';
115
+ ```
116
+
117
+ ```bash
118
+ ATOLL_DEVTOOLS=1 npm run start
119
+ ```
120
+
121
+ - Works on any Node — the client uses the global `WebSocket` on ≥ 22 and
122
+ falls back to a bundled dependency-free client (`src/nodeWs.ts`) below it.
123
+ - Inbound HTTP is not instrumented — `net:fetch` covers the `fetch()` calls
124
+ your app *makes*, on the main thread and inside workers.
125
+ - `runtime:memory` comes from `process.memoryUsage()` in Node — per-worker
126
+ heap columns fill for `worker_threads` pools.
127
+ - No overlay, no `/__atoll/` — Node sessions appear on the aggregate
128
+ dashboard as `runtime: 'node'` alongside any browser apps pointed at the
129
+ same server.
130
+ - See `examples/express` and `examples/nestjs` for the wiring.
131
+
132
+ - Worker-side events reach the sink automatically: pools pass a
133
+ `devtools` flag in INIT, workers forward `ATOLL_DEVTOOLS` messages back
134
+ over the task channel, and the pool stamps each with
135
+ `worker: {poolId, slot}` — so `memory:write`, `net:fetch`, and worker
136
+ heap samples attribute to a specific worker. Only active when a sink
137
+ was installed before the spawn.
138
+ - `net:fetch` covers `fetch()` on the main thread (via the probe
139
+ `connectDevtools` installs) and inside workers (probed when forwarding
140
+ is enabled). XHR is not covered.
141
+ - `runtime:memory` uses Chrome's `performance.measureUserAgentSpecificMemory`
142
+ on the main thread (cluster breakdown — every worker context listed by
143
+ its script URL), `performance.memory` where available, and
144
+ `process.memoryUsage()` on Node (heapUsed + RSS — also inside
145
+ `worker_threads`, so per-worker heap columns populate for Node pools).
146
+ - Zero cost when unused: `emitDevtools` is one branch without a sink, and
147
+ `connectDevtools()` is the only thing that installs one.
148
+ - Events buffered while the socket connects are capped (50k, oldest
149
+ dropped); batches flush every 100ms.
150
+
151
+ ## Wire protocol
152
+
153
+ JSON text frames. Apps → server on `/events`: `{type:'hello', session}`
154
+ then `{type:'batch', events[]}`. Dashboards → server on `/view`:
155
+ `{type:'sessions'}` then `{type:'batch', session, events}` fan-out, with a
156
+ bounded tail replayed to late joiners. See `src/protocol.ts`.
package/app/index.html ADDED
@@ -0,0 +1,388 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>atoll devtools</title>
7
+ <style>
8
+ :root { color-scheme: dark; }
9
+ * { box-sizing: border-box; }
10
+ body {
11
+ margin: 0; font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
12
+ background: #0d1117; color: #c9d1d9;
13
+ }
14
+ header {
15
+ display: flex; align-items: baseline; gap: 12px; padding: 10px 16px;
16
+ border-bottom: 1px solid #21262d; position: sticky; top: 0; background: #0d1117; z-index: 5;
17
+ }
18
+ header h1 { font-size: 14px; margin: 0; color: #58a6ff; }
19
+ #conn { font-size: 11px; }
20
+ #conn.on { color: #3fb950; } #conn.off { color: #f85149; }
21
+ main { display: grid; grid-template-columns: 220px 1fr; min-height: calc(100vh - 42px); }
22
+ aside { border-right: 1px solid #21262d; padding: 10px; }
23
+ aside h2, section h2, .view h2 {
24
+ font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #8b949e; margin: 12px 0 6px;
25
+ }
26
+ .sess { padding: 6px 8px; border: 1px solid #21262d; border-radius: 6px; margin-bottom: 6px; cursor: pointer; position: relative; }
27
+ .sess.sel { border-color: #58a6ff; background: #161b22; }
28
+ .sess:hover { border-color: #30363d; }
29
+ .sess:hover .nm { text-decoration: underline; }
30
+ .sess .dismiss {
31
+ position: absolute; top: 3px; right: 6px; color: #8b949e; font-size: 12px;
32
+ line-height: 1; padding: 1px 4px; border-radius: 4px; display: none;
33
+ }
34
+ .sess:hover .dismiss { display: block; }
35
+ .sess .dismiss:hover { color: #f85149; background: #21262d; text-decoration: none; }
36
+ .sess .pin {
37
+ position: absolute; top: 3px; right: 22px; color: #8b949e; font-size: 10px;
38
+ line-height: 1.4; padding: 0 4px; border-radius: 4px; display: none;
39
+ }
40
+ .sess:hover .pin, .sess .pin.on { display: block; }
41
+ .sess .pin.on { color: #58a6ff; }
42
+ .sess .pin:hover { color: #58a6ff; background: #21262d; text-decoration: none; }
43
+ .sess .rt { color: #8b949e; font-size: 10px; }
44
+ .panels { padding: 10px 16px 30px; min-width: 0; display: flex; flex-direction: column; min-height: calc(100vh - 42px); box-sizing: border-box; }
45
+ #scope { display: flex; align-items: baseline; gap: 8px; margin: 0 0 8px; font-size: 14px; color: #c9d1d9; }
46
+ #scope .nm { font-weight: 600; }
47
+ #scope .meta { color: #8b949e; font-size: 11px; }
48
+ .panels > .view { flex: none; }
49
+ /* flex:1 fills the panels column; max-height keeps tall logs inside the
50
+ viewport (42px header + ~80px nav/padding) so #log scrolls internally. */
51
+ #view-log.on { display: flex; flex-direction: column; flex: 1; min-height: 0; max-height: calc(100vh - 122px); }
52
+ nav { display: flex; gap: 4px; margin: 4px 0 10px; }
53
+ /* main nav reads as folder tabs — the active tab shares the content
54
+ background and overlaps the baseline so it looks attached */
55
+ .panels > nav { gap: 0; border-bottom: 1px solid #21262d; margin-bottom: 12px; }
56
+ nav button {
57
+ background: none; border: 1px solid #21262d; color: #8b949e; border-radius: 6px;
58
+ padding: 4px 12px; cursor: pointer; font: inherit; font-size: 12px;
59
+ }
60
+ .panels > nav button {
61
+ border-color: transparent; border-bottom: none; border-radius: 6px 6px 0 0;
62
+ padding: 6px 14px; margin-bottom: -1px;
63
+ }
64
+ .panels > nav button:hover { color: #c9d1d9; }
65
+ .panels > nav button.on {
66
+ color: #c9d1d9; background: #0d1117; border-color: #21262d;
67
+ }
68
+ table { border-collapse: collapse; width: 100%; }
69
+ th, td { text-align: left; padding: 3px 8px; border-bottom: 1px solid #21262d; font-weight: normal; }
70
+ th { color: #8b949e; font-size: 11px; cursor: pointer; user-select: none; }
71
+ th:hover { text-decoration: underline; }
72
+ tbody tr[data-drill] { cursor: pointer; }
73
+ tbody tr[data-drill]:hover { background: #161b22; }
74
+ tbody tr[data-inspect], tbody tr[data-winspect], tbody tr[data-fetch] { cursor: pointer; }
75
+ tbody tr[data-inspect]:hover, tbody tr[data-winspect]:hover, tbody tr[data-fetch]:hover { background: #161b22; }
76
+ tbody tr[data-drill]:hover td:first-child,
77
+ tbody tr[data-inspect]:hover td:first-child,
78
+ tbody tr[data-fetch]:hover td.url,
79
+ tbody tr[data-winspect]:hover td:nth-child(2),
80
+ tbody tr[data-winspect]:hover td:nth-child(3) { text-decoration: underline; }
81
+ .ok { color: #3fb950; } .error, .crashed { color: #f85149; }
82
+ .aborted, .timeout, .queue-full { color: #d29922; }
83
+ .muted { color: #8b949e; }
84
+ tr.grp td {
85
+ background: #161b22; color: #8b949e; font-size: 10px;
86
+ text-transform: uppercase; letter-spacing: .06em; padding: 4px 8px;
87
+ }
88
+ .pill { display: inline-block; padding: 0 6px; border-radius: 8px; background: #21262d; font-size: 10px; margin-left: 6px; }
89
+ .kpis { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; }
90
+ .kpi { border: 1px solid #21262d; border-radius: 8px; padding: 8px 10px; }
91
+ .kpi .v { font-size: 20px; }
92
+ .kpi .l { color: #8b949e; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; }
93
+ .kpi.bad .v { color: #f85149; } .kpi.warn .v { color: #d29922; } .kpi.good .v { color: #3fb950; }
94
+ #alerts { border: 1px solid #21262d; border-radius: 8px; padding: 6px 10px; max-height: 150px; overflow-y: auto; }
95
+ #alerts div { padding: 2px 0; border-bottom: 1px dashed #21262d; }
96
+ #waterfall { width: 100%; height: 240px; display: block; border: 1px solid #21262d; border-radius: 8px; background: #010409; cursor: crosshair; }
97
+ #tip {
98
+ position: fixed; pointer-events: none; background: #161b22; border: 1px solid #30363d;
99
+ border-radius: 6px; padding: 6px 8px; font-size: 11px; display: none; z-index: 10; max-width: 320px;
100
+ }
101
+ .legend span { display: inline-block; margin-right: 12px; font-size: 11px; }
102
+ .legend i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 4px; }
103
+ #drill { display: none; margin: 6px 0; }
104
+ #drill.on { display: inline-block; }
105
+ #log { flex: 1; min-height: 0; overflow-y: auto; border: 1px solid #21262d; border-radius: 6px; padding: 6px 8px; }
106
+ #log div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
107
+ .logts { color: #484f58; font-size: 10px; }
108
+ canvas.spark { display: inline-block; vertical-align: middle; }
109
+ td.url { max-width: 380px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
110
+ .wlink { color: #58a6ff; cursor: pointer; text-decoration: none; } .wlink:hover { text-decoration: underline; }
111
+ nav button:hover, .subnav button:hover { text-decoration: underline; }
112
+ #drill { cursor: pointer; } #drill:hover { text-decoration: underline; }
113
+ .subnav { display: flex; gap: 4px; margin: 0 0 10px; }
114
+ .subnav button {
115
+ background: none; border: 1px solid #21262d; color: #8b949e; border-radius: 6px;
116
+ padding: 3px 10px; cursor: pointer; font: inherit; font-size: 11px;
117
+ }
118
+ .subnav button.on { color: #c9d1d9; border-color: #58a6ff; background: #161b22; }
119
+ .fwicon { width: 16px; height: 16px; vertical-align: middle; margin-top: -2px; }
120
+ .subview { display: none; } .subview.on { display: block; }
121
+ pre.body {
122
+ max-height: 220px; overflow: auto; white-space: pre-wrap; word-break: break-all;
123
+ border: 1px solid #21262d; border-radius: 6px; padding: 6px 8px; font-size: 11px;
124
+ background: #010409; margin: 0;
125
+ }
126
+ .tbar { display: flex; height: 18px; border-radius: 4px; overflow: hidden; border: 1px solid #21262d; }
127
+ .tbar div { min-width: 0; }
128
+ .tkv td { font-size: 11px; }
129
+ .tkv td:first-child { color: #8b949e; white-space: nowrap; width: 1%; }
130
+ canvas.topo {
131
+ width: 100%; height: 320px; display: block; margin-bottom: 10px;
132
+ border: 1px solid #21262d; border-radius: 8px; background: #010409; cursor: pointer;
133
+ }
134
+ .view { display: none; }
135
+ .view.on { display: block; }
136
+ .charts { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 10px; }
137
+ .chart-card { border: 1px solid #21262d; border-radius: 8px; padding: 8px 10px; }
138
+ .chart-card .lbl { display: flex; justify-content: space-between; align-items: baseline; font-size: 10px; color: #8b949e; text-transform: uppercase; letter-spacing: .05em; }
139
+ .chart-card .lbl b { font-size: 15px; }
140
+ .chart-card canvas { width: 100%; height: 64px; display: block; margin-top: 4px; }
141
+ .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
142
+ @media (max-width: 900px) { .grid2 { grid-template-columns: 1fr; } }
143
+
144
+ /* ── broadcast (pure-client) mode — sessions are this origin's own
145
+ instances; the selector/scope title earn their space back. ── */
146
+ body.local main { grid-template-columns: 1fr; }
147
+ body.local aside, body.local #scope { display: none; }
148
+
149
+ /* ── mini mode (?mini=1) — the overlay flyout: single instance, so no
150
+ session sidebar/scope, and the redundant session columns go too. ── */
151
+ body.mini { font-size: 12px; }
152
+ body.mini header { padding: 6px 12px; position: static; }
153
+ body.mini header h1 { font-size: 12px; }
154
+ body.mini main { grid-template-columns: 1fr; min-height: 0; }
155
+ body.mini aside, body.mini #scope { display: none; }
156
+ body.mini .panels { padding: 6px 10px 20px; min-height: 0; }
157
+ body.mini .subview#tv-map.on { display: flex; flex-direction: column; height: calc(100vh - 96px); }
158
+ body.mini #appmap { flex: 1; min-height: 0; height: auto !important; }
159
+ body.mini .kpis { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); }
160
+ body.mini .kpi { padding: 6px 8px; }
161
+ body.mini .kpi .v { font-size: 15px; }
162
+ body.mini table { font-size: 11px; }
163
+ body.mini th, body.mini td { padding: 3px 6px; }
164
+ body.mini h2, body.mini section h2, body.mini .view h2 { margin: 8px 0 4px; }
165
+ body.mini tr.grp { display: none; }
166
+ body.mini #workers th:first-child, body.mini #workers td:first-child,
167
+ body.mini #pools th:first-child, body.mini #pools td:first-child { display: none; }
168
+ body.mini .grid2 { grid-template-columns: 1fr; gap: 8px; }
169
+ body.mini nav button { padding: 3px 8px; font-size: 11px; }
170
+ </style>
171
+ </head>
172
+ <body>
173
+ <header>
174
+ <h1>atoll devtools</h1>
175
+ <span id="conn" class="off">connecting…</span>
176
+ <span class="muted" id="counts"></span>
177
+ </header>
178
+ <main>
179
+ <aside>
180
+ <h2>Sessions</h2>
181
+ <div id="sessions"><div class="muted">none</div></div>
182
+ </aside>
183
+ <div class="panels">
184
+ <div id="scope"></div>
185
+ <nav>
186
+ <button data-view="dashboard" class="on">Dashboard</button>
187
+ <button data-view="tasks">Tasks</button>
188
+ <button data-view="network">Network</button>
189
+ <button data-view="memory">Memory</button>
190
+ <button data-view="log">Log</button>
191
+ </nav>
192
+
193
+ <section class="view on" id="view-dashboard">
194
+ <h2>App map <span class="muted">— scroll to zoom · drag to pan · double-click to fit · click a node to inspect · click a session disc to drill in</span></h2>
195
+ <canvas id="appmap" class="topo" style="height:420px"></canvas>
196
+ <nav class="subnav" style="margin-top:10px">
197
+ <button class="on" data-sub="tv-dash">Overview</button>
198
+ <button data-sub="tv-pools">Pools</button>
199
+ <button data-sub="tv-workers">Workers</button>
200
+ <button data-sub="tv-islands">Islands</button>
201
+ <button data-sub="tv-worker" id="navTopoW" style="display:none">Worker</button>
202
+ <button data-sub="tv-island" id="navTopoI" style="display:none">Island</button>
203
+ </nav>
204
+ <div class="subview on" id="tv-dash">
205
+ <div class="kpis" id="kpis"></div>
206
+ <div class="grid2" style="margin-top:14px">
207
+ <div>
208
+ <h2>Task throughput <span class="muted">/s, last 60s</span></h2>
209
+ <canvas id="tput" width="560" height="80" style="width:100%"></canvas>
210
+ </div>
211
+ <div>
212
+ <h2>Attention needed</h2>
213
+ <div id="alerts"><div class="muted">nothing flagged</div></div>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ <div class="subview" id="tv-pools">
218
+ <h2>Pools</h2>
219
+ <table id="pools"><thead><tr>
220
+ <th>session</th><th>pool</th><th>size</th><th>memory</th><th>workers</th><th>tasks</th><th>failed</th>
221
+ </tr></thead><tbody></tbody></table>
222
+ </div>
223
+ <div class="subview" id="tv-workers">
224
+ <h2>Workers <span class="muted">— every pool slot; click a row to inspect</span></h2>
225
+ <table id="workers"><thead><tr>
226
+ <th>session</th><th>worker</th><th>tasks</th><th>failed</th><th>respawns</th><th>errors</th><th>heap</th><th>islands</th><th>last task</th><th>state</th>
227
+ </tr></thead><tbody></tbody></table>
228
+ </div>
229
+ <div class="subview" id="tv-islands">
230
+ <h2>Islands <span class="muted">— click a row to inspect</span></h2>
231
+ <table id="islands"><thead><tr>
232
+ <th>instance</th><th>app</th><th>fw</th><th>pid</th><th>host</th><th>round-trips</th><th>op batches</th><th>ops</th><th>replay ms</th><th>events</th><th>state</th>
233
+ </tr></thead><tbody></tbody></table>
234
+ </div>
235
+ <div class="subview" id="tv-worker">
236
+ <h2 id="wkTitle">Worker inspector</h2>
237
+ <div class="kpis" id="wkKpis"></div>
238
+ <h2 style="margin-top:14px">Task lane <span class="muted" id="wkrange"></span></h2>
239
+ <canvas id="wklane" style="width:100%;height:120px;display:block;border:1px solid #21262d;border-radius:8px;background:#010409;cursor:crosshair"></canvas>
240
+ <div class="grid2" style="margin-top:14px">
241
+ <div>
242
+ <h2>Tasks on this worker <span class="muted">(latest 150)</span></h2>
243
+ <table id="wkTasks"><thead><tr>
244
+ <th>call</th><th>task</th><th>wait ms</th><th>run ms</th><th>outcome</th><th>error</th>
245
+ </tr></thead><tbody></tbody></table>
246
+ </div>
247
+ <div>
248
+ <h2>JS heap <span class="muted">— live (2s samples)</span></h2>
249
+ <canvas id="wkheap" style="width:100%;height:80px;display:block;border:1px solid #21262d;border-radius:8px;background:#010409"></canvas>
250
+ <h2 style="margin-top:14px">Fetches from this worker <span class="muted">(latest 50)</span></h2>
251
+ <table id="wkFetch"><thead><tr>
252
+ <th>ms</th><th>status</th><th>method</th><th>url</th><th>bytes</th>
253
+ </tr></thead><tbody></tbody></table>
254
+ <h2 style="margin-top:14px">Worker errors</h2>
255
+ <div id="wkErrors" style="border:1px solid #21262d;border-radius:8px;padding:6px 10px;max-height:220px;overflow-y:auto"></div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ <div class="subview" id="tv-island">
260
+ <h2 id="inspTitle">Island inspector</h2>
261
+ <div class="kpis" id="inspKpis"></div>
262
+ <div class="grid2" style="margin-top:14px">
263
+ <div>
264
+ <h2>Op traffic <span class="muted">— batch size over time, colored by call</span></h2>
265
+ <canvas id="optraffic" style="width:100%;height:140px;display:block;border:1px solid #21262d;border-radius:8px;background:#010409"></canvas>
266
+ <div class="legend">
267
+ <span><i style="background:#58a6ff"></i>mount</span>
268
+ <span><i style="background:#3fb950"></i>dispatch</span>
269
+ <span><i style="background:#8b949e"></i>flush</span>
270
+ <span><i style="background:#d29922"></i>setSize</span>
271
+ <span><i style="background:#a371f7"></i>updateProps</span>
272
+ <span><i style="background:#f85149"></i>unmount</span>
273
+ </div>
274
+ </div>
275
+ <div>
276
+ <h2>By call</h2>
277
+ <table id="inspVia"><thead><tr>
278
+ <th>via</th><th>batches</th><th>ops</th><th>replay ms</th>
279
+ </tr></thead><tbody></tbody></table>
280
+ <h2 style="margin-top:14px">Event channel <span class="muted">(worker → shell emits)</span></h2>
281
+ <table id="inspEvents"><thead><tr><th>name</th><th>count</th></tr></thead><tbody></tbody></table>
282
+ </div>
283
+ </div>
284
+ <h2>Round-trips <span class="muted">— every main→worker call (latest 150)</span></h2>
285
+ <table id="inspTasks"><thead><tr>
286
+ <th>#</th><th>method</th><th>ms</th><th>ops back</th><th>result</th><th>error</th>
287
+ </tr></thead><tbody></tbody></table>
288
+ </div>
289
+ </section>
290
+
291
+ <section class="view" id="view-tasks">
292
+ <h2>Task waterfall <span class="muted">— wait (gray) → run (colored) per worker slot</span></h2>
293
+ <div class="legend">
294
+ <span><i style="background:#58a6ff"></i>ok</span>
295
+ <span><i style="background:#f85149"></i>error/crash</span>
296
+ <span><i style="background:#d29922"></i>abort/timeout</span>
297
+ <span><i style="background:#8b949e"></i>queued / in-flight</span>
298
+ <span class="muted" id="wfrange"></span>
299
+ <span id="drill" class="pill"></span>
300
+ </div>
301
+ <canvas id="waterfall"></canvas>
302
+ <div id="tip"></div>
303
+ <h2>By task <span class="muted">— click a row to drill into its calls</span></h2>
304
+ <table id="taskAgg"><thead><tr>
305
+ <th>task</th><th>calls</th><th>fail</th><th>wait avg</th><th>run avg</th><th>run p95</th><th>run max</th>
306
+ </tr></thead><tbody></tbody></table>
307
+ <h2>Calls <span class="muted">(latest 200)</span></h2>
308
+ <table id="tasks"><thead><tr>
309
+ <th>call</th><th>task</th><th>pool</th><th>slot</th><th>wait ms</th><th>run ms</th><th>outcome</th><th>error</th>
310
+ </tr></thead><tbody></tbody></table>
311
+ </section>
312
+
313
+ <section class="view" id="view-network">
314
+ <nav class="subnav">
315
+ <button class="on" data-sub="nv-traffic">Traffic</button>
316
+ <button data-sub="nv-req" id="navNetReq" style="display:none">Request</button>
317
+ </nav>
318
+ <div class="subview on" id="nv-traffic">
319
+ <h2>Network <span class="muted">— live channel traffic (per-second, last 60s)</span></h2>
320
+ <div class="charts" id="charts"></div>
321
+ <h2 style="margin-top:14px">Fetch log <span class="muted">— real HTTP traffic, latest 150 — click a row for detail</span></h2>
322
+ <table id="fetchlog"><thead><tr>
323
+ <th>session</th><th>ms</th><th>status</th><th>method</th><th>url</th><th>from</th><th>bytes</th>
324
+ </tr></thead><tbody></tbody></table>
325
+ </div>
326
+ <div class="subview" id="nv-req">
327
+ <div id="reqTitle" class="muted" style="margin-bottom:8px"></div>
328
+ <div class="kpis" id="reqKpis" style="margin-bottom:12px"></div>
329
+ <h2>Timing</h2>
330
+ <div id="reqTiming" style="margin-bottom:12px"></div>
331
+ <div class="grid2">
332
+ <div>
333
+ <h2>Request headers</h2>
334
+ <table id="reqReqH"><tbody></tbody></table>
335
+ <h2 style="margin-top:12px">Request body</h2>
336
+ <pre class="body" id="reqReqB"></pre>
337
+ </div>
338
+ <div>
339
+ <h2>Response headers</h2>
340
+ <table id="reqResH"><tbody></tbody></table>
341
+ <h2 style="margin-top:12px">Response body</h2>
342
+ <pre class="body" id="reqResB"></pre>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ </section>
347
+
348
+ <section class="view" id="view-memory">
349
+ <nav class="subnav">
350
+ <button class="on" data-sub="mv-fields">Shared-memory writes</button>
351
+ <button data-sub="mv-heap">JS heap</button>
352
+ </nav>
353
+ <div class="subview on" id="mv-fields">
354
+ <div class="grid2">
355
+ <div>
356
+ <h2>Field writes /s</h2>
357
+ <canvas id="memrate" style="width:100%;height:80px;display:block;border:1px solid #21262d;border-radius:8px;background:#010409"></canvas>
358
+ </div>
359
+ <div>
360
+ <h2>Top fields /s</h2>
361
+ <canvas id="memtop" style="width:100%;height:80px;display:block;border:1px solid #21262d;border-radius:8px;background:#010409"></canvas>
362
+ <div class="legend" id="memtopLegend"></div>
363
+ </div>
364
+ </div>
365
+ <table id="mem" style="margin-top:10px"><thead><tr>
366
+ <th>field</th><th>session</th><th>writes</th><th>writes/s</th><th>version</th><th>last writer</th><th>rate</th>
367
+ </tr></thead><tbody></tbody></table>
368
+ </div>
369
+ <div class="subview" id="mv-heap">
370
+ <h2>JS heap <span class="muted">— per-session total, live</span></h2>
371
+ <canvas id="heapline" style="width:100%;height:110px;display:block;border:1px solid #21262d;border-radius:8px;background:#010409"></canvas>
372
+ <div class="legend" id="heaplineLegend" style="margin-top:4px"></div>
373
+ <h2 style="margin-top:14px">JS heap by context <span class="muted">— measureUserAgentSpecificMemory attribution</span></h2>
374
+ <table id="heapctx"><thead><tr>
375
+ <th>session</th><th>context</th><th>heap</th>
376
+ </tr></thead><tbody></tbody></table>
377
+ </div>
378
+ </section>
379
+
380
+ <section class="view" id="view-log">
381
+ <h2>Event log</h2>
382
+ <div id="log"></div>
383
+ </section>
384
+ </div>
385
+ </main>
386
+ <script type="module" src="./main.js"></script>
387
+ </body>
388
+ </html>