# Sheet as a live model — sysmon `packages/sheet/examples/sysmon/prog.dft` recreates the sysmon app as a **sheet workbook**: instead of a Deftlet drawing ASCII graphs, one interval cell samples the system, boundary cells decide when the model is out of bounds, and every due tick records a **workbook snapshot**. The grid stays small and fixed-size — the snapshot timeline is the time-series store; the charts app draws the live ranges. This is also the sheet's real-world soak test: intervals, watches, deferred API writes, table spills, named ranges and their events, publish feeds, formats, snapshots and the headless daemon all run together, for hours, against live `/proc` and GC data. ## Setup ```sh mkdir -p sheets/sysmon.dfs cp /sheet/examples/sysmon/prog.dft sheets/sysmon.dfs/prog.dft ``` Open the sheet app and `/File Open…` → `sheets/sysmon` (or launch a workspace like `workspace-sysmon.dft`). The program file seeds the grid on load — an empty workbook becomes the model with no manual cell entry. - `tick (s)` (`B2`) — sampling interval; its watch re-applies the interval to the tick cell live. - `snapshot every (ticks)` (`B3`) — record a snapshot every N ticks (`0` disables). The snapshot timeline is the history. - `BOUNDS` (`A5:D13`) — per metric `min`, `max`, `max Δ/tick`, plus the two per-app rows below the process metrics: `app-heap` caps any single app thread's heap, `app-cpu` its CPU share. `Δ` with `-1` disables the growth check; `0` alerts on any increase (the stored-cell metric uses this as a leak detector). ## Layout One region per concern — no duplicated views: | Cell | Table | Rows | |------|-------|------| | `A16` | `SYSMON_NOW` | metric \| value \| min \| max \| Δ \| trend \| status (the tick anchor) | | `A26` | `SYSMON_APPS` | app \| heap KB \| peak KB \| cpu% \| gc \| freed \| next-gc | | `A37` | `SYSMON_ALERTS` | time \| app \| metric \| kind \| value \| bound (latest only) | Every table starts with a **header row** and the named range includes it. The charts app detects the leading all-string row, skips it as data, and uses it to label series: bar/pie categories read `rss·value`, `rss·min`, … instead of column letters, and stacked charts take their series names from it. Headerless ranges keep the old letter fallback. `A16` is the only interval cell (`=[table "SYSMON_NOW" [sysmon-tick $SYSMON_BOUNDS $B$3]] # every 2`) — the tick anchor IS the metrics table, so there is one metrics region instead of separate values/status views. Every other table is a plain formula that references `$A$16` (fully anchored, so `/Move` of any block cannot rewire it), and the tick reads the bounds block through the `SYSMON_BOUNDS` named range, which follows `/Move` too. `I17:I23` is a real-cell publish strip (one cell per metric) wired to `sheet/sysmon/cells` with publish names — spilled table members can't carry publish flags, so the strip mirrors SYSMON_NOW's value column as publishable cells (labelled at `I16`). Each table has a title cell one row above its anchor. Metrics: `rss`, `vmdata` (from `/proc/self/status`), `heap`, `peak`, `cycles` (`gc/stats`), `cpu` (process share of `/proc/stat`), and `cells` (`[len [sheet/cells]]` — the sheet's own stored-cell count). Per-app `gc`/`freed`/`next-gc` columns mirror each app runtime's `gc/stats` counters (collection cycles, cumulative KB freed, next-GC threshold KB) via `tui/app-threads`. ## History: snapshots, not cells Each tick evaluates the bounds and keeps a small in-memory window (for Δ/trend); every `B3`-th tick it calls `[sheet/save]`, recording the workbook state — metric cells, alert log, program source — in the snapshot store. Nothing grows in the grid, so a long run cannot overflow the sheet or bloat the per-snapshot payload. - **Browse history:** press `[` / `]` to scrub the timeline; the live values freeze to the recorded sample and dependents (charts, alerts) follow. Reopen the workbook later and scrub the accrued timeline. - **Cadence:** every tick (`B3 = 1`) gives a sample per interval; `5` (default, ~10s at the default 2s tick) keeps the DB modest. `sheet/auto-snapshot-s` still works as an independent cron — leave it at `0` when using the model cadence to avoid duplicate rows. - **Headless:** the daemon records the same way, so a long unattended run leaves a durable, scrub-able timeline. ## Charts Two complementary paths: - **Named ranges (range mode).** In charts press `s` and pick a range: `SYSMON_APPS` for bar or pie, `SYSMON_NOW` for the value/bound/status columns. Charts subscribes to `sheet/sysmon/range/` and refetches over RPC on every tick and on every timeline scrub. - **Published cells (cells mode).** The `I17:I23` strip publishes each metric under its name (`rss`, `heap`, …), so charts' rolling cells-mode window keeps a live series (512 samples) independently of the snapshot cadence. Point charts at the topic with `charts/sheet-topic` = `sheet/sysmon/cells`, or press `r` after setting it. ## Boundary conditions / memory debugging `sysmon-tick` compares each metric against its bounds and records edge-triggered alerts (`:high`, `:low`, `:growing`, and the `:ok` recovery) — the `SYSMON_ALERTS` table holds the LATEST alert only (a single row; the snapshot timeline keeps the full history). Per-app heap (the `app-heap` row) and CPU (the `app-cpu` row) are checked for every running app, so an alert row carries the **app** beside the repeated `heap`/`cpu` metric; process-wide metrics log `sys` in the app column (times render as `HH:MM:SS`; raw millis stay in the pub/sub payloads). The `E2` worst-status cell is watched; its watch publishes `%{ :ts :status :old :new :last}` on `sysmon/alert`, which the notifications app (or any subscriber) can surface. Useful memory-debugger settings: - `cells` max `400`, `Δ 0` — the workbook's stored-cell count must not grow; a leak in the sheet model trips the alert. - `heap` / `peak` max — cap the runtime's GC heap. - `rss` / `vmdata` Δ — catch process growth between ticks. - `app-heap` max — cap any single app thread's heap (panel hosts only). Snapshots make the long view cheap: set a slower tick or cadence for a multi-hour run, then scrub the timeline to see heap/RSS/`cells` across the whole session. ## Headless daemon ```sh deft packages/sheet/src/run.dft sysmon ``` runs the full model with no panel: the interval cell samples on the event loop, alerts publish, and the model's snapshot cadence records the timeline. The debug repl is on `127.0.0.1:9312` (`sheet/debug-port`) — attach a shell or drive it over RPC. Without a workspace, `tui/app-threads` is absent, so the model reports its own runtime as the single `sheet` app row (heap/peak from `gc/stats`, CPU from the process share) — the apps view is never empty; `/proc` and `gc/stats` work. ## Caveats - `/proc` reads need the landlock sandbox off (`DEFT_SANDBOX` unset) or a prelude `:sandbox {:readonly @{"/proc"}}` grant. - Snapshot payloads include the program source; a fast cadence on a large prog grows the DB. Prefer `B3 ≥ 5` for long runs. - `sysmon-source` is the sampler seam: override it (as `test/test_sysmon.dft` does) to feed deterministic samples; the seed is idempotent and `sysmon-reset` drops histories/alerts/statuses. - Replacing the prog in an existing workbook leaves the old layout in place (the seed is a no-op) — run `[sysmon-reseed]` once from the debug repl to clear everything and re-lay the grid.