# 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.