# Processes Window — Implementation Plan ## Goal A `sovereign://processes` internal page that lets the user pinpoint which open tab is consuming CPU and dig into *what that page is doing*. Primary use case: diagnosing `~/lt/client`, which currently burns CPU across many tabs. ## The core technical reality The codebase deliberately **shares one WebProcess across tabs** — every new tab/webview is created with `webkit_web_view_new_with_related_view()` (see [`tab_manager.c`](../src/tab_manager.c:135) and the call sites at lines 1106, 1228, 2657, 3602, 3624). This was done to avoid a `std::optional` assertion crash in WebKitGTK. Consequence: process-level CPU% from `/proc` tells you which *WebProcess* is hot, but **not which tab within it**. If 10 `~/lt/client` tabs share one WebProcess at 90% CPU, `/proc` alone cannot attribute the load. The plan therefore uses **two layers**: 1. **Layer 1 — Process view** (from `/proc`): main, WebKitWebProcess(es) with their hosted tabs listed, WebKitNetworkProcess, Tor, FIPS. Catches the single-tab-per-process case and shows overall footprint. 2. **Layer 2 — Per-tab page probe** (injected JS in every webview): this is what actually pinpoints the offending tab *within* a shared WebProcess and tells you what it is doing. ## Architecture ```mermaid flowchart LR subgraph BrowserProcess[sovereign_browser main PID] ProcInfo[process_info.c reads /proc] Bridge[nostr_bridge.c sovereign:// routes] AgentServer[agent_server / MCP] end subgraph WebProcess[WebKitWebProcess pid N - shared by many tabs] ProbeA[perf-probe.js in tab A] ProbeB[perf-probe.js in tab B] ProbeC[perf-probe.js in tab C] end ProbeA -->|sovereign://processes/probe-report POST| Bridge ProbeB -->|sovereign://processes/probe-report POST| Bridge ProbeC -->|sovereign://processes/probe-report POST| Bridge ProcInfo -->|enumerates /proc and webkit_web_view_get_process_id| Bridge Bridge -->|JSON| UI[www/processes.js renders tables] Bridge -->|JSON| AgentServer ``` ## Layer 1 — Process view (`src/process_info.{c,h}`) ### Process discovery | Process | How found | |---|---| | Main (`sovereign_browser`) | `getpid()` | | WebKitWebProcess (≥1) | For each tab, `webkit_web_view_get_process_id(tab->webview)` returns the WebProcess PID. Group tabs by PID. | | WebKitNetworkProcess | Scan `/proc/*/comm` for `WebKitNetworkProcess` whose PPID is the main PID (read `/proc//stat` field 4). | | WebKitGPUProcess / StorageProcess | Same PPID scan, if present. | | Tor (managed) | `net_service_get_status(NET_SERVICE_TOR)->pid` when `ownership == OWNERSHIP_MANAGED`. | | FIPS (managed) | `net_service_get_status(NET_SERVICE_FIPS)->pid` when `ownership == OWNERSHIP_MANAGED`. | ### Per-process fields (all from `/proc`, no external deps) | Field | Source | |---|---| | pid | — | | name | `/proc//comm` | | cmdline | `/proc//cmdline` (NUL-split, space-joined) | | state | `/proc//stat` field 3 (R/S/D/Z/T) | | ppid | `/proc//stat` field 4 | | cpu_percent | delta of (`utime`+`stime`, fields 14+15) between two reads, divided by elapsed wall time × `sysconf(_SC_CLK_TCK)` | | rss_kb | `/proc//status` → `VmRSS` | | pss_kb | `/proc//smaps_rollup` → `Pss` (fallback to RSS if unavailable) | | uss_kb | `/proc//smaps_rollup` → `Private_Clean` + `Private_Dirty` | | vmpeak_kb | `/proc//status` → `VmPeak` | | vmswap_kb | `/proc//status` → `VmSwap` | | threads | count of entries in `/proc//task/` | | uptime_sec | `time(NULL) - (btime + starttime/clk_tck)`; btime from `/proc/stat` | | io_read_kb | `/proc//io` → `read_bytes` / 1024 | | io_write_kb | `/proc//io` → `write_bytes` / 1024 | | fd_count | count of entries in `/proc//fd/` | | ownership | `main` / `webkit-renderer` / `webkit-network` / `webkit-gpu` / `tor` / `fips` | | service_state | for Tor/FIPS: from `net_service_t.state` (READY/BOOTSTRAPPING/…) | | hosted_tabs | for renderers: array of `{index, title, url}` from `tab_manager` | ### CPU% computation Keep a static `GHashTable` between calls. Each call: 1. Read current `utime+stime` and `time(NULL)`. 2. `cpu% = (cur - prev) / clk_tck / (now_wall - prev_wall) * 100`. 3. Store current as prev. Caller polls `sovereign://processes/list` every 1s; first call returns 0% (no baseline), second call onward is accurate. Same approach as `top`/`htop`. ### API ```c /* src/process_info.h */ cJSON *process_info_get_processes_json(void); /* Layer 1 table */ cJSON *process_info_get_tabs_json(void); /* tab→WebProcess mapping + last probe */ cJSON *process_info_get_tab_probe_json(int tab_index); /* drill-down for one tab */ void process_info_record_probe(int tab_index, cJSON *probe); /* called from probe-report route */ ``` ## Layer 2 — Per-tab page probe (`www/js/perf-probe.js`) Injected into every webview via `WebKitUserContentManager` (same pattern as the existing `window.nostr` injection in [`nostr_inject.c`](../src/nostr_inject.c:1)). Runs before page scripts where possible (using `WebKitUserContentInjectedFramesAllFrames` and `WebKitUserScriptInjectAtDocumentStart`). ### Signals collected | Signal | Source | What it tells you | |---|---|---| | **long_tasks** | `PerformanceObserver({entryTypes:['longtask']})` | Every main-thread task >50ms with duration + attribution. *Direct answer to "which tab is hogging CPU".* | | **cpu_busy_percent** | sum of long-task durations in last 1s window | True per-tab CPU-busy %. | | **fps** | `requestAnimationFrame` cadence over 1s | Constant repainting / animation loops. | | **timer_count** | patched `setTimeout`/`setInterval` (count active) | Runaway polling loops (common in dashboards like `~/lt/client`). | | **timer_top** | top 5 intervals by frequency in last 1s | Which polling endpoints/scripts. | | **net_requests** | `PerformanceObserver({entryTypes:['resource']})` | Fetch/XHR/WebSocket storms, polling endpoints. | | **net_in_flight** | patched `fetch`/`XMLHttpRequest`/`WebSocket` | Current open requests. | | **heap_used_mb** | `performance.memory.usedJSHeapSize` (if exposed) | Memory leaks, growing arrays. | | **event_listener_count** | patched `addEventListener` | Leaking listeners. | | **worker_count** | patched `Worker` constructor | Background CPU not visible as long tasks. | | **dom_node_count** | `document.getElementsByTagName('*').length` | Page bloat. | | **long_task_timeline** | ring buffer of last 30s of long tasks | Drill-down chart. | | **long_task_top_sources** | aggregate by `entry.name` / script URL | Which script is responsible. | ### Reporting The probe batches a report every 1s and POSTs it to `sovereign://processes/probe-report?tab_index=N` with JSON body. The route handler in `nostr_bridge.c` calls `process_info_record_probe(N, body)`, which stores the latest probe per tab index in a static array (sized to `tab_manager_count()`). The probe must NOT run on `sovereign://` internal pages (settings, fips, processes itself, agents) — skip injection when the URL scheme is `sovereign://`. This avoids self-noise and recursion. ### Probe payload shape ```json { "tab_index": 3, "cpu_busy_percent": 42.5, "fps": 12, "timer_count": 38, "timer_top": [{"code":"pollStatus","interval_ms":250,"count":4}], "net_in_flight": 3, "net_requests_last_sec": 8, "net_top_endpoints": [{"url":"wss://.../events","count":4}], "heap_used_mb": 124.3, "event_listener_count": 217, "worker_count": 2, "dom_node_count": 4821, "long_tasks_last_sec": [{"duration_ms":180,"name":"setTimeout","attribution":"app.js:420"}], "long_task_top_sources": [{"name":"app.js","total_ms":1240,"count":8}] } ``` ## Routes (added to `nostr_bridge.c`) Following the [`sovereign://fips`](../src/nostr_bridge.c:4227) pattern: | Route | Handler | Returns | |---|---|---| | `sovereign://processes` | `serve_embedded_file("processes.html")` | HTML | | `sovereign://processes.css` | `serve_embedded_file("processes.css")` | CSS | | `sovereign://processes.js` | `serve_embedded_file("processes.js")` | JS | | `sovereign://processes/list` | `process_info_get_processes_json()` | JSON, polled 1s | | `sovereign://processes/tabs` | `process_info_get_tabs_json()` | JSON, polled 1s | | `sovereign://processes/tab_probe?index=N` | `process_info_get_tab_probe_json(N)` | JSON, on drill-down | | `sovereign://processes/probe-report?tab_index=N` | `process_info_record_probe(N, body)` | 204 No Content | ## UI (`www/processes.{html,css,js}`) Follows the [`www/fips.*`](../www/fips.html:1) tabbed pattern. ### Tab 1 — Processes Sortable table, default sort by `cpu_percent` desc: | PID | Name | CPU% | RSS | PSS | Threads | Uptime | State | Tabs | |---|---|---|---|---|---|---|---|---| | 12345 | sovereign_browser | 2.1 | 180M | 160M | 12 | 1h23m | R | — | | 12367 | WebKitWebProcess | 88.4 | 820M | 740M | 8 | 1h22m | R | 3,7,9,12,15 | | 12368 | WebKitNetworkProcess | 1.2 | 90M | 80M | 4 | 1h22m | S | — | | 12400 | tor | 0.3 | 45M | 40M | 3 | 1h20m | S | — | Click a WebKitWebProcess row → expands inline to list its hosted tabs with their Layer-2 `cpu_busy_percent` and `fps` so you can see the hot tab even within a shared process. ### Tab 2 — Tabs (the primary diagnostic view) Sortable table, default sort by `cpu_busy_percent` desc: | # | Title | URL | CPU-busy% | FPS | Timers | Net | Heap | DOM | WPID | |---|---|---|---|---|---|---|---|---|---| | 9 | Client — Dashboard | https://client/... | 41.2 | 11 | 38 | 3 | 124M | 4821 | 12367 | | 7 | Client — Alerts | https://client/... | 0.8 | 60 | 2 | 0 | 22M | 410 | 12367 | | 3 | sovereign://settings | — | — | — | — | — | — | — | 12367 | Click a tab row → **drill-down panel** opens below with: - Long-task timeline (last 30s, sparkline) - Top long-task sources (script URLs + total ms) - Active timers list (code label, interval, count) - Recent network requests (URL, type, count) - Worker count, event listener count, heap trend - Action buttons: **Reload tab**, **Suspend tab** (replace with `about:blank` keeping URL), **Close tab** ### Styling Reuse `sovereign-base.css` and the card/table styles from [`www/fips.css`](../www/fips.css:1). Add a heat-bar column background (green→yellow→red by CPU%) for instant visual scan. ## Menu integration - Hamburger menu item **"Processes…"** in [`main.c`](../src/main.c:325) next to "FIPS Mesh…", navigates active tab to `sovereign://processes` (or opens a new tab if none active) — same pattern as `open_fips_cb` at line 332. - Keyboard shortcut `Ctrl+Shift+Esc` (matches Windows Task Manager convention) registered in [`shortcuts.c`](../src/shortcuts.c:78) as `open_processes`. ## MCP tools (so an agent can self-diagnose) Add to [`agent_tools.c`](../src/agent_tools.c:1) / [`agent_mcp.c`](../src/agent_mcp.c:1): | Tool | Args | Returns | |---|---|---| | `processes.list` | none | Layer 1 JSON (all PIDs + fields) | | `processes.tabs` | none | Layer 2 JSON (per-tab probes) | | `processes.tab_probe` | `{tab_index}` | drill-down JSON for one tab | This lets you ask the agent "find the tab burning CPU in `~/lt/client` and tell me what it's doing" — the agent calls `processes.tabs`, finds the high `cpu_busy_percent` tab, calls `processes.tab_probe` for the drill-down, and reports the offending script/timer/endpoint. ## Build / packaging - Add `src/process_info.c` to `Makefile` sources. - Add `www/processes.{html,css,js}` and `www/js/perf-probe.js` to [`embed_web_files.sh`](../embed_web_files.sh:1) so they are embedded into the binary via `serve_embedded_file()`. - Register the perf-probe user script in the shared `WebKitWebContext`'s `WebKitUserContentManager` at startup (in `main.c` near where `nostr_inject` is wired), with a URL filter that excludes `sovereign://*`. ## Verification 1. `make` 2. `./browser.sh restart --login-method generate` 3. Open `sovereign://processes` — verify main + WebKit processes listed with sane CPU%/RSS. 4. Open several tabs of a CPU-heavy page (e.g. [`tests/local-site/media.html`](../tests/local-site/media.html:1) or a synthetic `` page) — verify the Tabs view ranks them by `cpu_busy_percent` and the drill-down shows long-task sources. 5. Open `~/lt/client` across multiple tabs — verify the hot tab is identifiable and the drill-down shows which script/timer/endpoint is responsible. 6. Via MCP: call `processes.tabs` and `processes.tab_probe` and verify JSON shape matches the contract above. ## Out of scope (future work) - Forcing process-per-tab via `WebKitWebsitePolicies` / `WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES` — would give true per-tab `/proc` attribution but breaks the existing `related_view` sharing workaround. Tracked separately. - Historical recording (save probe samples to SQLite for trend charts). - Per-tab network throttling / CPU limiting (would need WebKit API support that may not exist). - Killing/suspending a tab's WebProcess specifically (currently only per-tab reload/close, which is safe).