@atolljs/devtools 0.1.7 → 0.1.8
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 +408 -71
- package/app/api.js +68 -0
- package/app/index.html +161 -2
- package/app/main.js +877 -127
- package/app/panels/actions.js +324 -0
- package/app/panels/audit-rules.js +728 -0
- package/app/panels/audits.js +438 -0
- package/app/panels/inspect-island.js +351 -0
- package/app/panels/memory-values.js +287 -0
- package/app/panels/performance.js +304 -0
- package/app/panels/reactivity.js +271 -0
- package/app/panels/recorder.js +344 -0
- package/app/panels/recording-format.js +166 -0
- package/app/panels/route.d.ts +44 -0
- package/app/panels/route.js +264 -0
- package/app/panels/shell.js +789 -0
- package/app/panels/toast.js +38 -0
- package/dist/bin.js +704 -5
- package/dist/chunk-57NGHQUL.js +344 -0
- package/dist/{chunk-4N3NBFL3.js → chunk-AGT6VNZI.js} +179 -217
- package/dist/chunk-WIL2P7HG.js +661 -0
- package/dist/client.d.ts +7 -0
- package/dist/control.d.ts +4 -0
- package/dist/index.js +2 -1
- package/dist/node.js +2 -1
- package/dist/nodeWs.d.ts +1 -0
- package/dist/otel.d.ts +35 -0
- package/dist/otel.js +87 -0
- package/dist/otelMap.d.ts +232 -0
- package/dist/otelTransport.d.ts +69 -0
- package/dist/overlay.d.ts +16 -2
- package/dist/probes.d.ts +7 -0
- package/dist/protocol.d.ts +46 -4
- package/dist/server.d.ts +19 -0
- package/dist/server.js +61 -3
- package/package.json +6 -2
- package/src/bin.ts +27 -2
- package/src/broadcast.ts +6 -0
- package/src/client.ts +26 -2
- package/src/control.ts +51 -0
- package/src/nodeWs.ts +7 -1
- package/src/otel.ts +140 -0
- package/src/otelMap.ts +661 -0
- package/src/otelTransport.ts +286 -0
- package/src/overlay.ts +153 -27
- package/src/probes.ts +155 -0
- package/src/protocol.ts +52 -4
- package/src/server.ts +83 -3
package/README.md
CHANGED
|
@@ -1,57 +1,19 @@
|
|
|
1
1
|
# @atolljs/devtools
|
|
2
2
|
|
|
3
3
|
Devtools transport + analytics dashboard for AtollJS instrumentation events:
|
|
4
|
-
pool lifecycle, task timings, worker crashes/respawns,
|
|
5
|
-
writes
|
|
6
|
-
and
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
4
|
+
pool lifecycle, task timings and message cost, worker crashes/respawns,
|
|
5
|
+
shared-memory field writes and live values, island op traffic, props and
|
|
6
|
+
DOM, main-thread jank, reactivity, and SDK logs. Events stream from the app
|
|
7
|
+
to a dashboard (same-origin BroadcastChannel, or the standalone server), and
|
|
8
|
+
the dashboard can send commands back: kill a worker, inject faults, edit
|
|
9
|
+
island props, set shared-memory watchpoints.
|
|
10
|
+
|
|
11
|
+
The dashboard is a dependency-free static app in `app/`. Its views are
|
|
12
|
+
described in the [Views reference](#views-reference) below.
|
|
51
13
|
|
|
52
14
|
Sessions are pages/processes; a disconnected session is retained (bounded)
|
|
53
15
|
as an **ended** session so its history stays attributed for post-mortem
|
|
54
|
-
analysis
|
|
16
|
+
analysis: its rows are grayed and excluded from live KPIs. Hover an ended
|
|
55
17
|
session in the sidebar and click **×** to dismiss it: the server drops it
|
|
56
18
|
from the session list and purges its replayed batches, so it stays gone for
|
|
57
19
|
current and future viewers. Live sessions can't be dismissed.
|
|
@@ -60,10 +22,13 @@ current and future viewers. Live sessions can't be dismissed.
|
|
|
60
22
|
|
|
61
23
|
| Import | Purpose |
|
|
62
24
|
|---|---|
|
|
63
|
-
| `@atolljs/devtools` | `connectDevtools({ transport?, url?, session?, flushMs?, bufferCap?, replayBatches?, network?, memory? })
|
|
64
|
-
| `@atolljs/devtools
|
|
65
|
-
| `@atolljs/devtools
|
|
66
|
-
|
|
|
25
|
+
| `@atolljs/devtools` | `connectDevtools({ transport?, url?, session?, flushMs?, bufferCap?, replayBatches?, network?, memory?, jank? })`: 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 in dev, and emitted into `vite build` output whenever the app bundles devtools), WebSocket to the aggregate server everywhere else (`url` or `transport: 'websocket'` opts in; Node uses this path). `session` is `{ name?, hint?, framework? }`. `network` (default on) wraps `fetch` to emit `net:fetch` per request; `memory` (default on) samples JS heap into `runtime:memory`; `jank` (default on) installs the main-thread jank probe (`runtime:longframe`, `runtime:frames`). |
|
|
26
|
+
| `@atolljs/devtools` | `initDevtools({ enabled?, overlay?, ...connectOptions })`: the URL-gated one-liner (`?__atoll_devtools`), connects and mounts the overlay. |
|
|
27
|
+
| `@atolljs/devtools` | `mountDevtoolsOverlay({ src?, startOpen?, width?, height?, position?, persist?, hotkey? })`: the floating flyout (draggable, resizable). `persist` (default `true`, or a localStorage key, default `'atoll-devtools:overlay'`) remembers position, size and open state; `hotkey` (default `'Alt+Shift+D'`, `false` disables) toggles it. |
|
|
28
|
+
| `@atolljs/devtools/node` | Node entry: `initDevtools`/`connectDevtools` without the browser-only pieces (WebSocket-only, env-gated, no overlay). Use this import in Node apps. |
|
|
29
|
+
| `@atolljs/devtools/server` | `createDevtoolsServer({ port?, host?, appDir?, replayBatches?, closedTtlMs?, sweepMs? })`: Node http + dependency-free WebSocket server on `127.0.0.1` (default port 4780). |
|
|
30
|
+
| `@atolljs/devtools/otel` | `exportOtel({ endpoint?, headers?, serviceName?, resource?, signals?, flushIntervalMs?, maxBatch?, maxQueue?, maxSeries?, onError?, network?, memory?, jank? })`: dependency-free OTLP/HTTP JSON exporter (traces, metrics, logs) to any OpenTelemetry backend; returns `{ flush(), close() }`. Install before pools spawn. See [OpenTelemetry export](#opentelemetry-export). |
|
|
31
|
+
| `atoll-devtools` bin | Boots the server and prints the dashboard URL. `atoll devtools` resolves it through the CLI too. `--otlp <url>` also forwards every session to an OTLP endpoint. |
|
|
67
32
|
|
|
68
33
|
## Usage
|
|
69
34
|
|
|
@@ -71,18 +36,19 @@ current and future viewers. Live sessions can't be dismissed.
|
|
|
71
36
|
npx atoll-devtools # or: atoll devtools [--port 4780]
|
|
72
37
|
```
|
|
73
38
|
|
|
74
|
-
The quickest wire-up in an existing app is the scaffolder
|
|
39
|
+
The quickest wire-up in an existing app is the scaffolder:
|
|
75
40
|
`atoll add devtools` writes the right init module for your host (browser,
|
|
76
41
|
Node, or the Angular aggregate-server variant) and installs the package.
|
|
77
42
|
Or by hand:
|
|
78
43
|
|
|
79
44
|
```ts
|
|
80
|
-
// in the app
|
|
45
|
+
// in the app, one line. No-op unless the URL carries ?__atoll_devtools:
|
|
81
46
|
// then the BroadcastChannel sink installs and the overlay flyout mounts.
|
|
82
47
|
// Call before pools spawn: worker-side events forward only if the sink
|
|
83
48
|
// existed at INIT time.
|
|
84
49
|
import { initDevtools } from '@atolljs/devtools';
|
|
85
|
-
|
|
50
|
+
// `framework` (optional) puts the shell's mark on the map's main-thread hub.
|
|
51
|
+
initDevtools({ session: { name: 'my-app', framework: 'react' } });
|
|
86
52
|
|
|
87
53
|
// my-app/?__atoll_devtools → devtools on
|
|
88
54
|
// my-app/ → zero cost, no sink installed
|
|
@@ -93,8 +59,8 @@ initDevtools({ session: { name: 'my-app' } });
|
|
|
93
59
|
|
|
94
60
|
## Node apps (Express, NestJS, …)
|
|
95
61
|
|
|
96
|
-
Node takes the WebSocket path automatically
|
|
97
|
-
BroadcastChannel
|
|
62
|
+
Node takes the WebSocket path automatically (no `window`, so no
|
|
63
|
+
BroadcastChannel), pointed at the standalone server, which is also the
|
|
98
64
|
dashboard:
|
|
99
65
|
|
|
100
66
|
```bash
|
|
@@ -102,14 +68,14 @@ npx atoll-devtools # http://127.0.0.1:4780
|
|
|
102
68
|
```
|
|
103
69
|
|
|
104
70
|
```ts
|
|
105
|
-
// src/devtools.ts
|
|
71
|
+
// src/devtools.ts: a dedicated module, imported FIRST from the entry.
|
|
106
72
|
// No-op unless ATOLL_DEVTOOLS is set in the environment.
|
|
107
73
|
import { initDevtools } from '@atolljs/devtools/node';
|
|
108
74
|
initDevtools({ session: { name: 'my-api' } });
|
|
109
75
|
```
|
|
110
76
|
|
|
111
77
|
```ts
|
|
112
|
-
// main.ts
|
|
78
|
+
// main.ts: the import order matters. ESM evaluates imports before the
|
|
113
79
|
// body, and pools usually spawn inside a sibling module or NestFactory.
|
|
114
80
|
import './devtools';
|
|
115
81
|
```
|
|
@@ -118,31 +84,399 @@ import './devtools';
|
|
|
118
84
|
ATOLL_DEVTOOLS=1 npm run start
|
|
119
85
|
```
|
|
120
86
|
|
|
121
|
-
- Works on any Node
|
|
87
|
+
- Works on any Node: the client uses the global `WebSocket` on ≥ 22 and
|
|
122
88
|
falls back to a bundled dependency-free client (`src/nodeWs.ts`) below it.
|
|
123
|
-
- Inbound HTTP is not instrumented
|
|
89
|
+
- Inbound HTTP is not instrumented: `net:fetch` covers the `fetch()` calls
|
|
124
90
|
your app *makes*, on the main thread and inside workers.
|
|
125
|
-
- `runtime:memory` comes from `process.memoryUsage()` in Node
|
|
91
|
+
- `runtime:memory` comes from `process.memoryUsage()` in Node: per-worker
|
|
126
92
|
heap columns fill for `worker_threads` pools.
|
|
127
|
-
- No overlay, no `/__atoll
|
|
128
|
-
|
|
129
|
-
same server.
|
|
93
|
+
- No overlay, no `/__atoll/`, no jank probe (it is browser main thread
|
|
94
|
+
only): Node sessions appear on the aggregate dashboard as
|
|
95
|
+
`runtime: 'node'` alongside any browser apps pointed at the same server.
|
|
130
96
|
- See `examples/express` and `examples/nestjs` for the wiring.
|
|
131
97
|
|
|
98
|
+
## Views reference
|
|
99
|
+
|
|
100
|
+
The nav runs Dashboard, Tasks, Performance, Network, Memory, Reactivity,
|
|
101
|
+
Audits, Log. Each view opens with a one-line intro (dismissible; "Show view
|
|
102
|
+
intros again" in the palette brings them back). Views scope to the session
|
|
103
|
+
selected in the sidebar, or every session when none is.
|
|
104
|
+
|
|
105
|
+
### Dashboard
|
|
106
|
+
|
|
107
|
+
The **app map** sits above the sub-tabs: one world-space canvas holding
|
|
108
|
+
every live session as an atoll cluster. The main thread is at the center
|
|
109
|
+
(carrying the shell's framework mark when `session.framework` is set), pool
|
|
110
|
+
sockets (P) on the inner orbit, workers on the outer orbit. A worker
|
|
111
|
+
hosting one island is drawn *as* that island, as its framework mark
|
|
112
|
+
(React, Vue, Svelte, Angular, Solid, or vanilla JS, per the `framework` tag
|
|
113
|
+
the mounter declares or the renderer the worker reports). A worker shared by
|
|
114
|
+
several islands is a ring with a W badge and one node per instance inside. A
|
|
115
|
+
plain worker, or an untagged lone island, is a W. Nested sub-workers hang
|
|
116
|
+
off the instance (or worker) that spawned them on a violet link. Wheel to
|
|
117
|
+
zoom, drag to pan, double-click to refit and back out to every session;
|
|
118
|
+
click a node to inspect it, click a session disc to drill into that
|
|
119
|
+
session. Fed by `pool:init`, `worker:spawn`, `island:mount`.
|
|
120
|
+
|
|
121
|
+
- **Overview**: KPI cards (live pools, tasks settled, error rate, run p95
|
|
122
|
+
and max, field writes, islands, workers), the per-second **task
|
|
123
|
+
throughput** chart (last 60s, with latest, peak, average, total and
|
|
124
|
+
failures), and an **Attention needed** feed of worker respawns, worker
|
|
125
|
+
errors, and failed task settles.
|
|
126
|
+
- **Pools / Workers / Islands**: the entity tables. Pools: size, memory,
|
|
127
|
+
worker count, tasks, failures. Workers: tasks, failures, respawns, errors,
|
|
128
|
+
heap, hosted islands, last task, live/down. Islands: app, framework, pid,
|
|
129
|
+
host `poolId#slot` cross-link, round-trips, op batches, ops, replay ms,
|
|
130
|
+
events, live/unmounted. Click a row to open its inspector.
|
|
131
|
+
- **Worker** (inspector, appears on a worker click): KPIs, the worker's own
|
|
132
|
+
task lane (click a segment to drill Tasks to that task id), its last 150
|
|
133
|
+
tasks, a live heap chart, its last 50 fetches, and its error log. With a
|
|
134
|
+
live session that exposes the commands, the inspector also carries the
|
|
135
|
+
**Kill worker** button and the **Chaos** form (see
|
|
136
|
+
[Live controls](#live-controls)).
|
|
137
|
+
- **Island** (inspector, appears on an island click): KPIs (round-trips,
|
|
138
|
+
avg/max round-trip ms, ops applied, replay ms, emits, host heap, failed
|
|
139
|
+
calls), an op-traffic chart colored by call, per-call aggregates, the
|
|
140
|
+
event-channel breakdown, and the last 150 round-trips. Three tabs sit
|
|
141
|
+
above the charts:
|
|
142
|
+
- **Elements**: the island's real DOM as a collapsible tree, fetched with
|
|
143
|
+
`island.tree` (up to 4000 nodes per fetch; Refresh, or auto-refresh every
|
|
144
|
+
2s). Hover a node to outline it in the app page (`island.highlight`,
|
|
145
|
+
with its tag and size); click it for its attributes and text. A nested
|
|
146
|
+
island (`parent~app@N`) shows the top-level island's tree, which holds
|
|
147
|
+
its replayed DOM.
|
|
148
|
+
- **Props**: the `island:props` history (mount plus every `updateProps`,
|
|
149
|
+
last 100), each entry diffed against the previous one by top-level key.
|
|
150
|
+
- **Events**: the `island:event` log (last 500) with payload previews and
|
|
151
|
+
a name filter.
|
|
152
|
+
|
|
153
|
+
Island controls (**Edit props**, **Push** / **Poll** mode) sit in the
|
|
154
|
+
inspector header.
|
|
155
|
+
- **Map** (flyout only): in the `?mini=1` layout the app map becomes its own
|
|
156
|
+
default sub-tab and fills the view.
|
|
157
|
+
|
|
158
|
+
### Tasks
|
|
159
|
+
|
|
160
|
+
A waterfall of every pool task call: one swimlane per worker slot, queue
|
|
161
|
+
wait (gray) then run (colored by outcome), with hover tooltips. Below it,
|
|
162
|
+
per-task aggregates (calls, failures, wait avg, run avg, run p95, run max;
|
|
163
|
+
click a row to drill the waterfall and tables to that task) and the latest
|
|
164
|
+
200 calls. Fed by `task:enqueue` / `task:dispatch` / `task:settle`.
|
|
165
|
+
|
|
166
|
+
### Performance
|
|
167
|
+
|
|
168
|
+
Main-thread responsiveness and message cost, all on a rolling 60s window.
|
|
169
|
+
|
|
170
|
+
- **Main thread**: KPIs (long frames, total blocking time, fps now, min fps,
|
|
171
|
+
dropped frames, worst frame) and a timeline of long frames (bar height =
|
|
172
|
+
duration, red when blocking ≥ 100ms) overlaid with one fps line per
|
|
173
|
+
session. Fed by the [jank probe](#jank-probe-and-user-timing).
|
|
174
|
+
- **Worst long frames**: the longest retained frames with their top scripts
|
|
175
|
+
from Long Animation Frame attribution (the `longtask` fallback has none).
|
|
176
|
+
- **Message cost**: per task id, the estimated clone size of args posted
|
|
177
|
+
(avg, max) and results returned (avg, max); per island, the op-batch size
|
|
178
|
+
(avg, max, total). Fed by `task:dispatch.argBytes`,
|
|
179
|
+
`task:settle.resultBytes`, `island:ops.bytes`.
|
|
180
|
+
|
|
181
|
+
The nav badge counts long frames in the last 10s.
|
|
182
|
+
|
|
183
|
+
### Network
|
|
184
|
+
|
|
185
|
+
- **Traffic**: rolling 60s charts (tasks/s, avg run ms, errors/s, in-flight
|
|
186
|
+
calls, island ops/s, island round-trips/s, island events/s, fetches/s) and
|
|
187
|
+
a **fetch log** of real `fetch()` traffic from the main thread and workers
|
|
188
|
+
(latest 150, attributed to `main` or `poolId#slot`).
|
|
189
|
+
- **Request** (click a fetch row): request/response headers, capped body
|
|
190
|
+
previews, sizes (transfer/encoded/decoded), and a Resource Timing stage
|
|
191
|
+
breakdown (stalled, dns, tcp, tls, request+waiting, download).
|
|
192
|
+
Cross-origin responses without `Timing-Allow-Origin` show total time only.
|
|
193
|
+
|
|
194
|
+
### Memory
|
|
195
|
+
|
|
196
|
+
- **Values**: live shared-memory field values for one session, read with
|
|
197
|
+
`memory.read` about twice a second while the tab is open; changed cells
|
|
198
|
+
flash. **Snapshot** remembers the current values and a diff column
|
|
199
|
+
compares against them. **Watchpoints** and the watch-hit log sit below
|
|
200
|
+
(see [Watchpoints](#shared-memory-watchpoints)). Needs one live session:
|
|
201
|
+
select it in the sidebar when several are live.
|
|
202
|
+
- **Shared-memory writes**: field-writes/s and top-fields/s charts plus the
|
|
203
|
+
per-field table (writes, writes/sec, version, last writer `poolId#slot`
|
|
204
|
+
when worker-forwarded, rate sparkline). Fed by `memory:write`.
|
|
205
|
+
- **JS heap**: a per-session heap line and a per-execution-context table
|
|
206
|
+
from `performance.measureUserAgentSpecificMemory` (worker heaps identified
|
|
207
|
+
by worker script URL). Fed by `runtime:memory`.
|
|
208
|
+
|
|
209
|
+
### Reactivity
|
|
210
|
+
|
|
211
|
+
The cross-thread dependency graph from `reactive:node` events, laned by
|
|
212
|
+
owner: a **shared memory** lane for field sources, then the main thread, then
|
|
213
|
+
one lane per worker slot. Edges run dependency to dependent. Node kinds map
|
|
214
|
+
onto the SDK's reactivity:
|
|
215
|
+
|
|
216
|
+
| Kind | What it is |
|
|
217
|
+
|---|---|
|
|
218
|
+
| source | a shared-memory field, id `mem:<path>`, the same node on every thread |
|
|
219
|
+
| bridge | a version-counter watcher (`Atomics.waitAsync`, or the 50ms poll fallback): the part that crosses threads |
|
|
220
|
+
| derived | a selector slice of a `watch` / `observe` |
|
|
221
|
+
| effect | a `watch` callback, or an `observe` observable's subscriber fan-out |
|
|
222
|
+
|
|
223
|
+
Click a node for its id, lane, owner, dependencies and dependents. Disposed
|
|
224
|
+
nodes fade (toggle **show disposed**). Nodes are emitted only for
|
|
225
|
+
subscriptions created while devtools is enabled; **Load snapshot** fetches
|
|
226
|
+
the main thread's live nodes (`reactive.nodes`) when the dashboard opened
|
|
227
|
+
after the creation events left the replay tail.
|
|
228
|
+
|
|
229
|
+
### Audits
|
|
230
|
+
|
|
231
|
+
Live recommendations: pure rules re-run once a second over the selected
|
|
232
|
+
sessions' recent data, rendered as cards with a severity, the evidence, a
|
|
233
|
+
fix, a docs link, and entity chips that open the matching inspector. Filter
|
|
234
|
+
by severity; **mute rule** hides a rule's findings (persisted in
|
|
235
|
+
localStorage under `atoll.devtools.audits.muted`). The "What each rule
|
|
236
|
+
needs" table lists every rule's input events and whether that data has been
|
|
237
|
+
seen. The nav badge counts unmuted errors and warnings.
|
|
238
|
+
|
|
239
|
+
| Rule | Fires when | Severity |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `coi-islands` | islands are mounted on a page reporting `crossOriginIsolated = false` | warn |
|
|
242
|
+
| `large-args` / `large-results` | a task's args / results p95 or average exceeds 64KB (3+ calls in 60s) | warn |
|
|
243
|
+
| `island-batch-bytes` | an island's op-batch p95 exceeds 128KB (3+ batches in 60s) | warn |
|
|
244
|
+
| `island-slow-replay` | an island's replay p95 exceeds 16ms (3+ batches in 60s) | warn |
|
|
245
|
+
| `pool-saturated` | queue-wait p95 over 50ms and over run p95 (20+ dispatches), or a backlog of 20+ that grew by 10+ in 10s | warn |
|
|
246
|
+
| `pool-underused` | a pool of 2+ workers below 5% busy over 30s+ of observation, no backlog | info |
|
|
247
|
+
| `pool-oversized` | `poolSize` exceeds the session's `hardwareConcurrency` | warn |
|
|
248
|
+
| `main-jank` | total blocking time of visible-page frames in 60s reaches 300ms (error at 1000ms) | warn / error |
|
|
249
|
+
| `background-work` | script time in hidden-page long frames reaches 1000ms in 60s | info |
|
|
250
|
+
| `low-fps` | average fps below 45 over 10s (3+ samples) | warn |
|
|
251
|
+
| `task-errors` | a task's error + crashed rate reaches 10% (error at 50%), 5+ settles | warn / error |
|
|
252
|
+
| `task-timeouts` | a task's timeout rate reaches 5%, 5+ settles | warn |
|
|
253
|
+
| `queue-full` | any `queue-full` settle in 60s | warn |
|
|
254
|
+
| `crash-loop` | one slot respawned 3+ times in 60s | error |
|
|
255
|
+
| `heap-pressure` | heap / limit reaches 80% (error at 95%), worker or main thread | warn / error |
|
|
256
|
+
| `devtools-late` | task events for a pool whose `pool:init` was never seen | warn |
|
|
257
|
+
| `memory-write-storm` | one field averages 1000+ writes/s over 5s | warn |
|
|
258
|
+
| `fetch-failing` | requests to a method + path failed (network error or status ≥ 400) in 60s | info |
|
|
259
|
+
| `fetch-slow` | p95 to response headers over 2000ms (3+ requests) | info |
|
|
260
|
+
| `island-untagged` | mounted islands with no framework tag and no reported renderer | info |
|
|
261
|
+
|
|
262
|
+
Thresholds live as exported constants in `app/panels/audit-rules.js`.
|
|
263
|
+
Late-joining dashboards: a pool whose `pool:init` already left the app's
|
|
264
|
+
replay tail looks like `devtools-late`, and replayed history is stamped on
|
|
265
|
+
arrival, so the whole tail counts as "the last few seconds" for rate rules
|
|
266
|
+
until it ages out.
|
|
267
|
+
|
|
268
|
+
### Log
|
|
269
|
+
|
|
270
|
+
The raw event stream, one line per event (last 2000 lines). Filter by text,
|
|
271
|
+
by session (when several are live), and by group chips: one per event-type
|
|
272
|
+
prefix (`task`, `island`, `memory`, …) plus one per `log` level. Click a
|
|
273
|
+
chip to toggle it, shift- or alt-click to solo it. **pause** holds incoming
|
|
274
|
+
lines until resume; click a line to expand its full payload. Timestamps are
|
|
275
|
+
the dashboard's wall clock at receipt, with the event's per-thread elapsed
|
|
276
|
+
time alongside.
|
|
277
|
+
|
|
278
|
+
`log` lines are SDK log entries (`src/log.ts`) mirrored into the stream:
|
|
279
|
+
they follow the emitting thread's `setLogLevel` (default `info`), carry a
|
|
280
|
+
preview of the entry's data, and include worker logs once that worker's
|
|
281
|
+
devtools forwarding is on. A worker's logs from before its INIT handshake,
|
|
282
|
+
or from a worker spawned before devtools was enabled, never arrive.
|
|
283
|
+
|
|
284
|
+
## Live controls
|
|
285
|
+
|
|
286
|
+
The dashboard sends commands to a live app over the same transport
|
|
287
|
+
(`control` frames; see [docs/devtools.md](https://github.com/jwhenry3/atolljs/blob/main/docs/devtools.md)).
|
|
288
|
+
A control is enabled only when the session is live, not a replayed
|
|
289
|
+
recording, and the app advertises the command; otherwise its tooltip says
|
|
290
|
+
why.
|
|
291
|
+
|
|
292
|
+
- **Kill worker** (worker inspector): `worker.kill` runs the runner's real
|
|
293
|
+
crash path with the message `killed from devtools`: in-flight calls reject
|
|
294
|
+
with `WorkerCrashedError` and the worker respawns when the runner allows
|
|
295
|
+
it. Asks for confirmation first.
|
|
296
|
+
- **Chaos** (worker inspector, applies to the whole runner): `pool.chaos`
|
|
297
|
+
with a delay before each call is posted (ms), a fail rate (calls rejected
|
|
298
|
+
with `chaos: injected failure`, settle outcome `error`, never reaching the
|
|
299
|
+
worker), and a timeout rate (calls failed through the real timeout path,
|
|
300
|
+
outcome `timeout`; the call still runs and its late reply is discarded).
|
|
301
|
+
Rates are percentages in the form; fail + timeout can't exceed 100%.
|
|
302
|
+
**Clear** removes it. The palette's "Chaos: clear all" clears every runner
|
|
303
|
+
in every live session.
|
|
304
|
+
- **Edit props** (island inspector): loads the island's current props as
|
|
305
|
+
JSON (`island.props`), and **Apply** (Ctrl+Enter) sends
|
|
306
|
+
`island.updateProps`. Callbacks show as `"[fn]"`; leaving a placeholder in
|
|
307
|
+
place keeps the original callback wired.
|
|
308
|
+
- **Push / Poll** (island inspector): `island.setMode` switches the
|
|
309
|
+
island's transport mode.
|
|
310
|
+
|
|
311
|
+
Pools and islands created inside a worker (sub-pools, nested islands) are
|
|
312
|
+
not controllable: their handles live in that worker, which has no
|
|
313
|
+
transport. A nested island's DOM still shows up in its top-level island's
|
|
314
|
+
Elements tree.
|
|
315
|
+
|
|
316
|
+
## Shared-memory watchpoints
|
|
317
|
+
|
|
318
|
+
In **Memory › Values**, pick a field and a rule, then **Add watch**. Rules:
|
|
319
|
+
`change`, `> n`, `< n`, `>= n`, `<= n`, `== v`, `!= v`, where `v` is JSON
|
|
320
|
+
when it parses (`== "down"`, `== true`, `== 3`) and a bare string otherwise.
|
|
321
|
+
Numeric comparisons only match number/bigint fields; list fields support
|
|
322
|
+
`change` only. Each match emits a `memory:watch-hit` event (path, version,
|
|
323
|
+
value preview, rule), listed in the watch-hit log with per-watch hit
|
|
324
|
+
counts. Setting a watch takes the current value as the baseline (not a
|
|
325
|
+
hit). Writes on the main thread are checked synchronously; writes from
|
|
326
|
+
workers are checked when the main thread observes the version bump, so a
|
|
327
|
+
burst between two observations coalesces to the latest value.
|
|
328
|
+
|
|
329
|
+
## Jank probe and User Timing
|
|
330
|
+
|
|
331
|
+
`connectDevtools({ jank })` (default on) installs a browser main-thread
|
|
332
|
+
probe:
|
|
333
|
+
|
|
334
|
+
- `runtime:longframe`: Long Animation Frame entries where supported
|
|
335
|
+
(`blockingMs` plus the five longest scripts), `longtask` entries
|
|
336
|
+
otherwise. Frames that report no blocking and under 50ms of script time
|
|
337
|
+
are skipped as throttled renders. While the page is hidden, frame length
|
|
338
|
+
is throttling rather than work, so only frames with at least 100ms of
|
|
339
|
+
script (or a `longtask` of 100ms or more) are reported, flagged
|
|
340
|
+
`hidden: true`. Performance draws them gray and counts them as
|
|
341
|
+
"background work"; they stay out of the foreground jank KPIs, and Audits
|
|
342
|
+
reports them under `background-work` instead of `main-jank`.
|
|
343
|
+
- `runtime:frames`: a requestAnimationFrame sampler, one event per second
|
|
344
|
+
with fps and the frames that took over about twice the display interval.
|
|
345
|
+
A gap over a second (hidden tab, debugger pause) restarts the window.
|
|
346
|
+
|
|
347
|
+
The flyout dashboard is a same-origin iframe sharing the app's main thread,
|
|
348
|
+
so its own rendering counts as app jank. Use the full-page dashboard (its
|
|
349
|
+
own tab) when measuring.
|
|
350
|
+
|
|
351
|
+
While devtools is enabled the SDK also writes User Timing measures, so the
|
|
352
|
+
browser's Performance panel shows atoll work next to the app's frames. In
|
|
353
|
+
Chrome they land in an `atoll` track group:
|
|
354
|
+
|
|
355
|
+
| Measure | Where | Track |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| `atoll task <taskId>` | main thread, dispatch to reply | the runner id (`pool-1`, `search-w`) |
|
|
358
|
+
| `atoll run <taskId>` | inside the worker, task execution | `tasks` |
|
|
359
|
+
| `atoll replay <instance>` | main thread, one island op-batch replay | `islands` |
|
|
360
|
+
|
|
361
|
+
Each measure is cleared from the timeline buffer right after it is
|
|
362
|
+
recorded (the Performance panel has already captured it), so
|
|
363
|
+
`performance.getEntriesByType('measure')` won't accumulate them.
|
|
364
|
+
|
|
365
|
+
## Message cost
|
|
366
|
+
|
|
367
|
+
`task:dispatch.argBytes`, `task:settle.resultBytes`, and `island:ops.bytes`
|
|
368
|
+
are estimates from `estimateCloneBytes` (strings 2 bytes per char, numbers 8,
|
|
369
|
+
typed arrays and buffers by `byteLength`, plus small per-key overhead; the
|
|
370
|
+
walk stops after 5000 nodes), a "is this message big?" signal, not an exact
|
|
371
|
+
count. `resultBytes` is set only on `ok` settles; errored, timed-out,
|
|
372
|
+
aborted, and crashed calls carry none.
|
|
373
|
+
|
|
374
|
+
## Recording and replay
|
|
375
|
+
|
|
376
|
+
Every batch the dashboard receives lands in a rolling capture (the last
|
|
377
|
+
~50,000 events). The toolbar adds:
|
|
378
|
+
|
|
379
|
+
- **● Rec / ■ Stop**: an explicit recording (stops itself at 500,000
|
|
380
|
+
events).
|
|
381
|
+
- **Export**: downloads the recording when there is one, else the rolling
|
|
382
|
+
capture, as `atoll-devtools-<session-or-all>-<yyyymmdd-hhmmss>.json`.
|
|
383
|
+
With a session selected, only that session is exported.
|
|
384
|
+
- **Import**: opens a recording (or drop the file on the window). Replay
|
|
385
|
+
pauses the live transport, resets the dashboard, and re-ingests the file
|
|
386
|
+
through the normal ingest path, so every view rebuilds as it did live. A
|
|
387
|
+
banner offers Play/Pause, speed (1x, 4x, 16x, max), a scrubber (seeking
|
|
388
|
+
rebuilds up to that point), and **Back to live**. Replayed sessions are
|
|
389
|
+
named with a `⏺ ` prefix and are closed, so no control command can fire.
|
|
390
|
+
|
|
391
|
+
The file format is `{ format: 'atoll-devtools-recording', version: 1,
|
|
392
|
+
exportedAt, sessions, frames: [{ t, sessionId, events }] }`, with `t` the
|
|
393
|
+
dashboard's receive clock (replay only uses differences).
|
|
394
|
+
|
|
395
|
+
## Keyboard, palette, deep links
|
|
396
|
+
|
|
397
|
+
| Key | Action |
|
|
398
|
+
|---|---|
|
|
399
|
+
| Ctrl+K / ⌘K | Command palette: views, subviews, islands, workers, pools, sessions, commands |
|
|
400
|
+
| `g` then a letter | Go to a view: `d` Dashboard, `t` Tasks, `p` Performance, `n` Network, `m` Memory, `r` Reactivity, `a` Audits, `l` Log |
|
|
401
|
+
| `[` / `]` | Previous / next sub-tab |
|
|
402
|
+
| `/` | Focus the current view's search or filter box |
|
|
403
|
+
| `s` | Show / hide the sessions sidebar (full page, aggregate mode) |
|
|
404
|
+
| `?` | Shortcuts and what each view is for |
|
|
405
|
+
| Esc | Close the palette, help, or a menu |
|
|
406
|
+
| Alt+Shift+D | Show / hide the flyout, from the app page or inside the flyout |
|
|
407
|
+
|
|
408
|
+
The address hash tracks where you are:
|
|
409
|
+
`#/<view>[/<sub>][?session=…&island=…&worker=…]`. Share it, reload it, or
|
|
410
|
+
use Back / Forward on the full page; a routed island or worker opens once it
|
|
411
|
+
appears (matching by key in a reloaded session too). Inside the flyout the
|
|
412
|
+
hash is updated with `replaceState` only, so devtools navigation never adds
|
|
413
|
+
entries to the host page's history. The last view is remembered per layout
|
|
414
|
+
(flyout and full page). The flyout's **full page ↗** link opens the same
|
|
415
|
+
route; the palette's "Copy link to this view" copies it. A first-run tip
|
|
416
|
+
points at the palette.
|
|
417
|
+
|
|
418
|
+
## Chrome DevTools extension
|
|
419
|
+
|
|
420
|
+
The same dashboard also runs as an **atoll** panel inside Chrome DevTools,
|
|
421
|
+
for the inspected tab, with no flyout and no second tab:
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
npm run build:extension # → packages/devtools-extension/dist
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Open chrome://extensions, enable Developer mode, choose "Load unpacked" and
|
|
428
|
+
pick `packages/devtools-extension/dist`. The app still needs devtools on
|
|
429
|
+
(`initDevtools()` with `?__atoll_devtools`, or `connectDevtools()`). Frames
|
|
430
|
+
are pushed, not polled: a content script that stays passive until the panel
|
|
431
|
+
attaches joins the page's `atoll-devtools` channel and relays through the
|
|
432
|
+
extension's service worker. Because that script is declared for all URLs,
|
|
433
|
+
Chrome warns "Read and change all your data on all websites" at install;
|
|
434
|
+
tabs opened before install need a reload. Details and limitations are in
|
|
435
|
+
[packages/devtools-extension/README.md](https://github.com/jwhenry3/atolljs/blob/main/packages/devtools-extension/README.md).
|
|
436
|
+
|
|
437
|
+
## OpenTelemetry export
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import { exportOtel } from '@atolljs/devtools/otel';
|
|
441
|
+
|
|
442
|
+
// Before any pool spawns, like every devtools sink.
|
|
443
|
+
const otel = exportOtel({ endpoint: 'http://localhost:4318', serviceName: 'my-app' });
|
|
444
|
+
// ...
|
|
445
|
+
await otel.close(); // flushes; never rejects
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Task calls, island round trips and fetches become spans; pool, runtime and
|
|
449
|
+
shared-memory activity become cumulative metrics; `log` events and worker
|
|
450
|
+
crashes become log records. It runs alongside `connectDevtools()` or on its
|
|
451
|
+
own. To forward everything the aggregate server receives instead:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
npx atoll-devtools --otlp http://localhost:4318 --otlp-headers x-api-key=abc --otlp-service atoll
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Mapping, clock handling and retry behavior are in
|
|
458
|
+
[docs/devtools.md](https://github.com/jwhenry3/atolljs/blob/main/docs/devtools.md#opentelemetry-export-atolljsdevtoolsotel).
|
|
459
|
+
|
|
460
|
+
## How events get there
|
|
461
|
+
|
|
132
462
|
- Worker-side events reach the sink automatically: pools pass a
|
|
133
463
|
`devtools` flag in INIT, workers forward `ATOLL_DEVTOOLS` messages back
|
|
134
464
|
over the task channel, and the pool stamps each with
|
|
135
|
-
`worker: {poolId, slot}
|
|
136
|
-
|
|
137
|
-
was installed before the spawn.
|
|
465
|
+
`worker: {poolId, slot}`, so `memory:write`, `net:fetch`, worker heap
|
|
466
|
+
samples, and worker logs attribute to a specific worker. Only active when
|
|
467
|
+
a sink was installed before the spawn.
|
|
138
468
|
- `net:fetch` covers `fetch()` on the main thread (via the probe
|
|
139
469
|
`connectDevtools` installs) and inside workers (probed when forwarding
|
|
140
470
|
is enabled). XHR is not covered.
|
|
141
471
|
- `runtime:memory` uses Chrome's `performance.measureUserAgentSpecificMemory`
|
|
142
|
-
on the main thread (cluster breakdown
|
|
472
|
+
on the main thread (cluster breakdown: every worker context listed by
|
|
143
473
|
its script URL), `performance.memory` where available, and
|
|
144
|
-
`process.memoryUsage()` on Node (heapUsed + RSS
|
|
474
|
+
`process.memoryUsage()` on Node (heapUsed + RSS, also inside
|
|
145
475
|
`worker_threads`, so per-worker heap columns populate for Node pools).
|
|
476
|
+
- Commands register lazily, only while devtools is enabled: `pool.*` and
|
|
477
|
+
`worker.kill` with the first runner, `island.*` with the first mounted
|
|
478
|
+
island, `memory.*` with the first contract bind, `reactive.nodes` with the
|
|
479
|
+
first reactive node.
|
|
146
480
|
- Zero cost when unused: `emitDevtools` is one branch without a sink, and
|
|
147
481
|
`connectDevtools()` is the only thing that installs one.
|
|
148
482
|
- Events buffered while the socket connects are capped (50k, oldest
|
|
@@ -153,4 +487,7 @@ ATOLL_DEVTOOLS=1 npm run start
|
|
|
153
487
|
JSON text frames. Apps → server on `/events`: `{type:'hello', session}`
|
|
154
488
|
then `{type:'batch', events[]}`. Dashboards → server on `/view`:
|
|
155
489
|
`{type:'sessions'}` then `{type:'batch', session, events}` fan-out, with a
|
|
156
|
-
bounded tail replayed to late joiners.
|
|
490
|
+
bounded tail replayed to late joiners. Commands travel as
|
|
491
|
+
`{type:'control', sessionId, id, cmd, args}` and come back as
|
|
492
|
+
`{type:'control-result', sessionId, id, ok, result | error}`. See
|
|
493
|
+
`src/protocol.ts`.
|
package/app/api.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Panel plugin API for the devtools dashboard.
|
|
2
|
+
//
|
|
3
|
+
// main.js owns the event reducer, the core views, and the transport; every
|
|
4
|
+
// feature panel lives in app/panels/<name>.js, exports `setup(api)`, and is
|
|
5
|
+
// listed in PANELS in main.js. Panels never edit main.js state shapes —
|
|
6
|
+
// they keep their own state, fed by the hooks below.
|
|
7
|
+
//
|
|
8
|
+
// Rendering contract: hooks.render / inspector hooks run on EVERY render()
|
|
9
|
+
// (rAF-batched, many times a second under load). A panel that owns inputs
|
|
10
|
+
// (editors, filters) must not rebuild them each call: build once, then
|
|
11
|
+
// update text/values in place, or re-render only when its own key changes.
|
|
12
|
+
|
|
13
|
+
export const api = {
|
|
14
|
+
// ── hooks (panels push callbacks; main.js calls them) ───────────────────
|
|
15
|
+
hooks: {
|
|
16
|
+
/** (session, event) for every ingested event, after the core reducer. */
|
|
17
|
+
event: [],
|
|
18
|
+
/** (session, events) once per ingested batch — the recorder taps this. */
|
|
19
|
+
batch: [],
|
|
20
|
+
/** (liveSessionIds: Set) after the session list changes. */
|
|
21
|
+
reconcile: [],
|
|
22
|
+
/** () at the end of every render(). */
|
|
23
|
+
render: [],
|
|
24
|
+
/** () once a second (charts that roll with time). */
|
|
25
|
+
tick: [],
|
|
26
|
+
/** () after reset() clears all dashboard state. */
|
|
27
|
+
reset: [],
|
|
28
|
+
/** (islandKey, island, { extra, actions }) while the island inspector shows. */
|
|
29
|
+
inspectIsland: [],
|
|
30
|
+
/** (workerKey, worker, { actions }) while the worker inspector shows. */
|
|
31
|
+
inspectWorker: [],
|
|
32
|
+
},
|
|
33
|
+
onEvent(fn) { this.hooks.event.push(fn); },
|
|
34
|
+
onBatch(fn) { this.hooks.batch.push(fn); },
|
|
35
|
+
onReconcile(fn) { this.hooks.reconcile.push(fn); },
|
|
36
|
+
onRender(fn) { this.hooks.render.push(fn); },
|
|
37
|
+
onTick(fn) { this.hooks.tick.push(fn); },
|
|
38
|
+
onReset(fn) { this.hooks.reset.push(fn); },
|
|
39
|
+
onInspectIsland(fn) { this.hooks.inspectIsland.push(fn); },
|
|
40
|
+
onInspectWorker(fn) { this.hooks.inspectWorker.push(fn); },
|
|
41
|
+
|
|
42
|
+
// ── command palette registry (the palette UI reads this) ────────────────
|
|
43
|
+
/** [{ id, title, group, hint?, keys?, run() }] */
|
|
44
|
+
palette: [],
|
|
45
|
+
/** Add (or replace by id) a palette entry. */
|
|
46
|
+
addPaletteItem(item) {
|
|
47
|
+
const i = this.palette.findIndex((p) => p.id === item.id);
|
|
48
|
+
if (i >= 0) this.palette[i] = item; else this.palette.push(item);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// ── filled in by main.js before panels' setup() runs ────────────────────
|
|
52
|
+
// state, $, esc, fmt, fmtBytes, fmtInt, key, sessOf, inSel, withGroups,
|
|
53
|
+
// kpi(value, label, cls?), sparkline, drawLine, drawMultiLine, fwIconImg,
|
|
54
|
+
// render(), scheduleRender(),
|
|
55
|
+
// addView({ id, label, order?, html, badge?, title? }) → section element,
|
|
56
|
+
// addSubview(viewId, { id, label, html, order? }) → div element,
|
|
57
|
+
// openView(viewId), openSub(viewId, subId),
|
|
58
|
+
// control(sessionId, cmd, args?, timeoutMs?) → Promise<result>,
|
|
59
|
+
// hasCommand(sessionId, cmd) → boolean (cached 'devtools.commands'),
|
|
60
|
+
// liveSessions() → SessionInfo[], selectedOrOnlyLive() → sessionId | null,
|
|
61
|
+
// ingest(session, events), reset(), reannounce(), isMini,
|
|
62
|
+
// livePaused (true drops live transport frames: replay sets it),
|
|
63
|
+
// addToolbarButton({ id, label, title?, order?, onClick }),
|
|
64
|
+
// onNavigate(fn) (after openView / subtab changes), setViewIntro(viewId, html),
|
|
65
|
+
// viewIntros, toast(msg, kind?, ms?),
|
|
66
|
+
// transport ('broadcast' | 'extension' | 'ws': 'extension' is the Chrome
|
|
67
|
+
// DevTools panel, broadcast semantics relayed from the inspected page).
|
|
68
|
+
};
|