@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 +156 -0
- package/app/index.html +388 -0
- package/app/main.js +1823 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +313 -0
- package/dist/broadcast.d.ts +3 -0
- package/dist/chunk-4N3NBFL3.js +474 -0
- package/dist/client.d.ts +34 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/init.d.ts +28 -0
- package/dist/node.d.ts +18 -0
- package/dist/node.js +12 -0
- package/dist/nodeWs.d.ts +9 -0
- package/dist/overlay.d.ts +31 -0
- package/dist/protocol.d.ts +83 -0
- package/dist/server.d.ts +23 -0
- package/dist/server.js +296 -0
- package/dist/ws.d.ts +43 -0
- package/package.json +49 -0
- package/src/bin.ts +23 -0
- package/src/broadcast.ts +97 -0
- package/src/client.ts +178 -0
- package/src/index.ts +11 -0
- package/src/init.ts +50 -0
- package/src/node.ts +18 -0
- package/src/nodeWs.ts +118 -0
- package/src/overlay.ts +165 -0
- package/src/protocol.ts +69 -0
- package/src/server.ts +218 -0
- package/src/ws.ts +206 -0
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>
|