|
| 1 | +# CDP Monitor |
| 2 | + |
| 3 | +The monitor is the browser-facing layer of the kernel browser logging pipeline. It connects to Chrome's DevTools endpoint, tracks all page sessions via CDP's `Target.setAutoAttach`, and converts raw CDP notifications into typed `events.Event` values for downstream consumers. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +`cdpmonitor` manages a Chrome DevTools Protocol (CDP) WebSocket connection to a running Chrome browser. It subscribes to CDP events across all attached tabs, translates them into structured `events.Event` values, and publishes them via a caller-supplied `PublishFunc`. It also derives synthetic events from sequences of CDP events and takes screenshots on significant page activity. |
| 8 | + |
| 9 | +Chrome can restart independently of the monitor. When that happens, `UpstreamProvider` pushes a new DevTools URL and the monitor reconnects automatically, emitting lifecycle events so consumers can track continuity. |
| 10 | + |
| 11 | +## Event taxonomy |
| 12 | + |
| 13 | +**CDP-derived** (1-to-1 with a CDP notification): `console_log`, `console_error`, `network_request`, `network_response`, `network_loading_failed`, `navigation`, `dom_content_loaded`, `page_load`, `layout_shift` |
| 14 | + |
| 15 | +**Computed** (inferred from sequences of CDP events): `network_idle` (fires when in-flight requests drop to zero), `layout_settled` (1 s after `page_load` with no intervening layout shifts), `navigation_settled` (fires once `dom_content_loaded`, `network_idle`, and `layout_settled` have all fired for the same navigation). |
| 16 | + |
| 17 | +**Interaction** (fired by `interaction.js` via `Runtime.bindingCalled`): `interaction_click`, `interaction_key`, `scroll_settled` |
| 18 | + |
| 19 | +**Monitor lifecycle** (emitted by the monitor itself, not by Chrome): `screenshot`, `monitor_disconnected`, `monitor_reconnected`, `monitor_reconnect_failed`, `monitor_init_failed` |
| 20 | + |
| 21 | +## Responsibilities |
| 22 | + |
| 23 | +| Concern | Where | |
| 24 | +| --- | --- | |
| 25 | +| WebSocket lifecycle (connect, read, reconnect) | `monitor.go` | |
| 26 | +| CDP domain setup per session | `domains.go` | |
| 27 | +| Event translation (CDP params to `events.Event`) | `handlers.go` | |
| 28 | +| Synthetic event state machines | `computed.go` | |
| 29 | +| Screenshot capture via ffmpeg | `screenshot.go` | |
| 30 | +| CDP protocol types | `cdp_proto.go`, `types.go` | |
| 31 | +| Interaction tracking injected into the page | `interaction.js` | |
| 32 | +| Body/MIME capture sizing and text truncation helpers | `util.go` | |
| 33 | + |
| 34 | +## Internals |
| 35 | + |
| 36 | +### Reconnect model |
| 37 | + |
| 38 | +`subscribeToUpstream` listens to `UpstreamProvider.Subscribe()` for new DevTools URLs. On each URL change (indicating Chrome restarted), `handleUpstreamRestart` tears down the existing connection, dials the new URL with capped-exponential backoff (250 ms → 500 ms → 1 s → 2 s, up to 10 attempts), then restarts `readLoop` and re-initializes all CDP sessions. `restartMu` serializes concurrent restart signals so rapid Chrome restarts do not produce overlapping reconnects. |
| 39 | + |
| 40 | +### Goroutines |
| 41 | + |
| 42 | +| Goroutine | Lifetime | Tracked by | |
| 43 | +| --- | --- | --- | |
| 44 | +| `readLoop` | one per WebSocket connection | `done` channel | |
| 45 | +| `subscribeToUpstream` | same as `lifecycleCtx` | `asyncWg` | |
| 46 | +| `sweepPendingRequests` | same as `lifecycleCtx` | `asyncWg` | |
| 47 | +| `initSession` | short-lived, one per connect or reconnect | `asyncWg` | |
| 48 | +| `attachExistingTargets` wrapper | short-lived, one per existing target on reconnect | `asyncWg` | |
| 49 | +| `enableDomains` + `injectScript` | short-lived, one per target attach | `asyncWg` | |
| 50 | +| `fetchResponseBody` | one per completed network request | `asyncWg` | |
| 51 | +| `captureScreenshot` | one per screenshot trigger | `asyncWg` | |
| 52 | + |
| 53 | +`Stop()` cancels `lifecycleCtx`, waits for `readLoop` via `done`, then waits for all other goroutines via `asyncWg` before closing the connection. |
| 54 | + |
| 55 | +### Lock ordering |
| 56 | + |
| 57 | +Locks must be acquired left to right. Never hold a lock on the left while acquiring one further right. |
| 58 | + |
| 59 | +``` |
| 60 | +restartMu -> lifeMu -> pendReqMu -> computed.mu -> pendMu -> sessionsMu |
| 61 | +``` |
| 62 | + |
| 63 | +`bindingRateMu` is independent of this ordering and is always acquired alone. |
| 64 | + |
| 65 | +| Lock | Protects | |
| 66 | +| --- | --- | |
| 67 | +| `restartMu` | Serializes `handleUpstreamRestart` to prevent overlapping reconnects from rapid Chrome restarts | |
| 68 | +| `lifeMu` | `conn`, `lifecycleCtx`, `cancel`, `done`, `readReady` -- all fields that change during Start / Stop / reconnect | |
| 69 | +| `pendReqMu` | `pendingRequests` (requestId -> `networkReqState`): in-flight network requests accumulating request/response metadata until `loadingFinished` | |
| 70 | +| `computed.mu` | All `computedState` fields: counters and timers for the `network_idle`, `layout_settled`, and `navigation_settled` state machines | |
| 71 | +| `pendMu` | `pending` (id -> reply channel): in-flight CDP commands waiting for a response from Chrome | |
| 72 | +| `sessionsMu` | `sessions` (sessionID -> `targetInfo`): the set of currently attached CDP targets (tabs, iframes, workers) | |
| 73 | +| `bindingRateMu` | `bindingLastSeen` (sessionID:eventType -> time): rate-limit state for `__kernelEvent` binding calls | |
| 74 | + |
| 75 | +Fields that need no mutex use `sync/atomic`: `nextID`, `mainSessionID`, `running`, `lastScreenshotAt`, `screenshotInFlight`. |
| 76 | + |
| 77 | +### WebSocket concurrency |
| 78 | + |
| 79 | +`coder/websocket` guarantees one concurrent `Read` and one concurrent `Write` are safe on the same connection. `readLoop` is the sole reader. All writes go through `send`, which calls `conn.Write` directly -- `conn.Write` is internally serialized by the library, so no external write mutex is needed. |
0 commit comments