Monitoring#
The read-only run snapshot and the usage sampling that fills it.
The monitor’s data layer: one read-only snapshot of a run, fully computed.
build_snapshot is everything the monitor command calls per refresh
tick: it reads the durable state through a (read-only) StateStore and
returns a MonitorSnapshot – plain frozen dataclasses the UI layer renders
without further queries. The split keeps the CLAUDE.md arrows intact: SQL
lives in core/state.py, rendering lives in ui/, and this module is
pure computation between the two – no Rich, no SQLite, no config imports
(the caller hands in plain names and mappings, exactly like the
Orchestrator’s constructor).
Semantics worth naming:
ready counts materials sitting at a stage’s input state and not in flight; running counts materials linked to a live ledger job of that stage (a material’s state only advances at completion, so the two never double-count). Input states are derived from
STAGE_OUTPUT_STATEby name over the configured stage order – an omitted optional stage (calculate) shifts its consumer’s input exactly as the orchestrator does.done_total is the all-time completions-ledger count for the stage – a fan-out parent that completed
structurestays counted even after it retires asEXPANDED, which per-state counting would lose.speed / throughput are rolling-window rates from ledger timestamps; overall throughput counts
FINALIZE_STAGErows (materials actually finishing the run).eta_sis the naive remaining/throughput estimate andNonewhenever throughput is zero – the renderer shows a dash, never a division by zero.tracked rows exist only when the caller passes
tracked_ids(the--trackflow): oneTrackedMaterialper requested id, caller order, unknown ids kept as honestfound=Falserows.tracked is Noneis the renderer’s signal that no tracking was requested at all.running rows are the per-material view of the in-flight ledger – one
RunningMaterialper material riding a live job, longest-running first.cpu_percentis filled from the caller’scpu_by_jobsample (the command layer owns the probing, exactly likebackend_health); a job the sampler could not see staysNone, rendered as a dash.recent_successes mirrors
recent_failures: the newestDONEmaterials, each record carryingupdated_at(finished at) andcreated_at(soupdated_at - created_atis how long the ride took).
- class matmelt.core.monitor.StageSnapshot(name, backend_name, backend_kind, backend_healthy, ready, running, done_total, failed, speed_per_min)[source]#
Bases:
objectOne configured stage’s live numbers, as rendered in the pipeline panel.
- backend_healthy#
The caller’s cached
health_checkverdict.Nonemeans “not probed”, rendered neutrally and never as a failure.
- ready#
Materials sitting at this stage’s input state and not in flight. Disjoint from
runningby construction.- Type:
- done_total#
All-time completions of this stage, counted from the ledger, so a fan-out parent stays counted after it retires as
EXPANDED.- Type:
- class matmelt.core.monitor.RunningMaterial(material_id, stage, backend_name, job_key, elapsed_s, attempts, cpu_percent, gpu_percent)[source]#
Bases:
objectOne row of the running-now panel: a material currently riding a live job.
Built from the in-flight ledger (one row per material of every live job) plus that material’s store record – the panel’s per-material live view.
- elapsed_s#
Seconds since the job was ledgered, i.e. how long this material has been computing in its current job.
- Type:
- attempts#
Consecutive failed attempts at the current stage, so a requeued material shows which try this is.
0when the record is missing – a foreign ledger row must render, never crash the dashboard.- Type:
- cpu_percent#
Caller-sampled CPU usage of the job’s process subtree, as a percentage of one core.
Nonewhenever the sampler had no reading, rendered as a dash and never as0.
- class matmelt.core.monitor.TrackedMaterial(material_id, found, state, stage, backend_name, in_state_s, attempts, fail_reason, job_key)[source]#
Bases:
objectOne row of the tracked-detail panel: a single followed material’s whereabouts.
Built only when the caller passes
tracked_idstobuild_snapshot(the--trackflow); the renderer swaps the failures panel for a tracked-materials table keyed on these rows.- found#
Whether the id exists in the store at all.
Falserows render a dim “not found” – a typo’d or not-yet-enqueued id must show up honestly, never crash or silently vanish – and every other field is then a neutral zero orNonefiller.- Type:
- stage#
The stage of the ledger job currently carrying this material, or
Nonewhen it sits between jobs (or is terminal).
- in_state_s#
Seconds since the record’s
updated_at, i.e. time in the current state.0.0when not found.- Type:
- fail_reason#
The first line only of the record’s failure reason – reasons are multi-line tracebacks, and the panel shows one line while the detail stays with
status.Nonewhen the material has not failed.
- class matmelt.core.monitor.MonitorSnapshot(run_name, generated_at, started_at, elapsed_s, total, by_state, in_flight, stages, recent_failures, recent_successes, running, throughput_per_min, eta_s, stop_after, window_s, tracked=None)[source]#
Bases:
objectEverything one refresh tick of the monitor shows, fully computed.
- started_at#
The run’s wall-clock anchor – the earliest material’s creation time.
Noneon an empty store.
- by_state#
The full per-state tally. Absent states are zero-filled by the renderer, not here.
- Type:
- recent_failures#
Newest-first terminal failures. Each record carries
fail_stage,fail_reason, andupdated_atas the failed-at time.- Type:
- recent_successes#
Newest-first
DONEmaterials, whereupdated_atis the finish time andupdated_at - created_atis how long the ride took.- Type:
- throughput_per_min#
Run-level finish rate over the rolling window, counted from
FINALIZE_STAGEledger rows.- Type:
- eta_s#
The naive remaining-over-throughput estimate, and
Nonewhenever throughput is zero – the renderer shows a dash rather than dividing by zero.
- matmelt.core.monitor.build_snapshot(store, *, run_name, stage_names, stage_backends, backend_kinds, backend_health=None, stop_after=None, now=None, window_s=300.0, failure_limit=10, success_limit=10, cpu_by_job=None, gpu_by_job=None, tracked_ids=None)[source]#
Read the store once and compute the full monitor snapshot.
Read-only by construction: only
StateStoreread methods are called, so passing aread_only=Truestore is the expected usage.- Parameters:
store (
StateStore) – The state store to read. Every query the tick needs happens here, once.run_name (
str) – The configured run name, passed through to the snapshot.stage_names (
Sequence[str]) – The configured funnel in order. Canonical names only.stage_backends (
Mapping[str,str]) – Each stage’s backend name – plain strings, so this module never touches config or backend objects.backend_kinds (
Mapping[str,str]) – Each backend name’s kind. A name absent here renders as"?".backend_health (
Mapping[str,bool] |None, default:None) – The caller’s cached probe results. A missing entry means “unknown” and renders neutrally.stop_after (
str|None, default:None) – The configured stop stage, echoed into the snapshot.now (
float|None, default:None) – The timestamp to compute against. Injectable for tests; defaults totime.time().window_s (
float, default:300.0) – Width of the rolling window for the per-stage speeds and the run throughput.failure_limit (
int, default:10) – How many recent failures to carry.success_limit (
int, default:10) – How many recent successes to carry.cpu_by_job (
Mapping[str,float] |None, default:None) – The caller’s latest usage samples as{job_key: percent}. The command layer owns the probing and its throttling, exactly as it does forbackend_health; a job absent from a mapping rendersNonein that column.gpu_by_job (
Mapping[str,float] |None, default:None) – The caller’s latest usage samples as{job_key: percent}. The command layer owns the probing and its throttling, exactly as it does forbackend_health; a job absent from a mapping rendersNonein that column.tracked_ids (
Sequence[str] |None, default:None) – Ids to follow (the--trackflow): caller order is preserved, duplicates dropped keeping the first occurrence, and unknown ids come back asfound=Falserows. Each id costs onestore.get, which is fine at tracked scale. LeftNone, the snapshot is exactly what it was before this parameter existed.
- Returns:
Everything one refresh tick renders, with no further queries needed.
- Return type:
- Raises:
ValueError – If a stage name is not a canonical funnel stage, or has no entry in
stage_backends– fail-fast, like the Orchestrator’s wiring checks.
Per-job CPU/GPU usage sampling for the monitor: best-effort, never fatal.
The dashboard’s usage columns are cosmetic telemetry, and this module treats
them that way everywhere: every function returns None (or an empty
mapping) instead of raising, a dead PID or unreachable host is a dash on
screen, and nothing here is ever allowed to take the monitor down.
The seam mirrors backend health probing: backends that can measure their jobs expose the duck-typed hooks
sample_cpu_percents(handles) -> list[float | None]sample_gpu_percents(handles) -> list[float | None]
taking the ledger’s serialized handle dicts (asdict(Handle)) in order
and answering one reading (or None) per handle, never raising. For CPU
the local backend answers from /proc deltas (true instantaneous
usage of the job’s whole process subtree, via LocalCpuTracker below) and
ssh_exec from one batched remote ps (lifetime-average pcpu,
ps semantics – honest enough for long-running science jobs). For GPU
both answer from nvidia-smi pmon (per-process SM utilisation, summed
over the job’s subtree); a machine without nvidia-smi – or a backend
without the hook – simply contributes no readings, so a CPU-only rig shows
a dash, never an error. UsageSampler groups the in-flight ledger by
backend, applies a per-backend TTL (local sampling is free and can run
every tick; a remote sample is an SSH round-trip and is cached, the
_HealthCache discipline), and folds everything into the
{job_key: percent} mappings build_snapshot takes as cpu_by_job
and gpu_by_job.
The /proc/pmon parsing and subtree arithmetic live here as pure,
injectable functions so both the backends and the tests use one
implementation.
- matmelt.core.cpusample.REMOTE_CPU_TTL_S = 10.0#
How long one remote CPU sample stays fresh (seconds). Remote sampling is an SSH round-trip; local sampling passes
0.0(every tick).
- matmelt.core.cpusample.clock_ticks_per_s()[source]#
The kernel’s
USER_HZ, the unit/procCPU times are counted in.- Returns:
Clock ticks per second, or
_DEFAULT_HZ(100) wheresysconfcannot answer – a wrong-but-sane divisor beats no reading at all for cosmetic telemetry.- Return type:
- matmelt.core.cpusample.parse_proc_stat(text)[source]#
Parse one
/proc/<pid>/statline.The comm field is parenthesised and may itself contain spaces or parentheses (
(Web Content)), so fields are taken after the LAST)– the only parse that is correct for every process name.- Parameters:
text (
str) – The full contents of a/proc/<pid>/statfile.- Returns:
(pid, ppid, cpu_ticks), wherecpu_ticksisutime + stime(user + system jiffies of this process alone, excluding children). Anything malformed isNone, never an exception.- Return type:
Examples
>>> parse_proc_stat("42 (my proc) S 7 42 42 0 -1 0 0 0 0 0 30 12 0 0") (42, 7, 42.0) >>> parse_proc_stat("not a stat line") is None True
- matmelt.core.cpusample.scan_proc(proc_root=PosixPath('/proc'))[source]#
Read every process’s parent and CPU time in one pass.
- Parameters:
proc_root (
Path, default:PosixPath('/proc')) – Where to scan. Injectable so tests can build a fake/procintmp_path; the default is the real one.- Returns:
{pid: (ppid, cpu_ticks)}. Unreadable or vanished entries (processes exit mid-scan constantly) are skipped silently, and an absentproc_rootyields an empty table.- Return type:
- matmelt.core.cpusample.subtree_pids(table, root)[source]#
Collect
rootand every process descended from it.A job is a process tree – the wrapper shell plus the runner plus whatever
multiprocessingspawned – so per-job accounting must cover the whole subtree, not just the pid the backend recorded.- Parameters:
- Returns:
rootplus all descendants, orNonewhenrootis not in the table (the process is gone). A visited set guards traversal against a corrupt table; real ppid chains cannot cycle.- Return type:
Examples
>>> table = {1: (0, 0.0), 2: (1, 0.0), 3: (2, 0.0), 9: (0, 0.0)} >>> sorted(subtree_pids(table, 1)) [1, 2, 3] >>> subtree_pids(table, 404) is None True
- matmelt.core.cpusample.subtree_total(table, root)[source]#
Sum the per-process values over
rootand all its descendants.- Parameters:
- Returns:
The subtree total, or
Nonewhenrootis not in the table (the process is gone). Seesubtree_pidsfor the traversal contract.- Return type:
- matmelt.core.cpusample.parse_pmon(text)[source]#
Parse
nvidia-smi pmon -c 1 -s uoutput into per-process SM usage.- Parameters:
text (
str) – The command’s stdout. Its table isgpu pid type sm mem enc dec command, with#header lines and-for idle or unavailable cells.- Returns:
{pid: sm_percent}. A-cell reads as0.0– a listed process doing no GPU work is at 0% – and a pid appearing on several GPUs is summed. Garbage lines are skipped; a machine without the tool never reaches this parser, because the caller answersNoneinstead.- Return type:
Examples
>>> parse_pmon("# gpu pid type sm mem enc dec command\n0 1234 C 45 12 - - python3") {1234: 45.0}
- matmelt.core.cpusample.gpu_percent_for_roots(process_table, pmon, roots)[source]#
Per-root GPU percentages: pmon SM readings summed over each subtree.
- Parameters:
process_table (
Mapping[int,tuple[int,float]]) –{pid: (ppid, value)}for the machine the jobs run on, used only for its parent links.pmon (
Mapping[int,float]) –{pid: sm_percent}fromparse_pmon.roots (
Sequence[int|None]) – One root pid per job, in the caller’s order.Nonemarks a job whose pid is not known.
- Returns:
One reading per root, in the same order.
Noneroots and roots absent from the process table (gone) stayNone; a live subtree with no pmon entries is an honest0.0– running, but not on the GPU.- Return type:
- class matmelt.core.cpusample.LocalCpuTracker(*, clock=<built-in function monotonic>, proc_root=PosixPath('/proc'), ticks_per_s=None)[source]#
Bases:
objectDelta-based CPU usage for local process subtrees, one
/procscan per call.Readings are a percentage of one core, so a 4-worker job legitimately reads ~400%. State for vanished pids is dropped, so a recycled PID can never inherit a stale baseline.
- Parameters:
clock (
Callable[[],float], default:<built-in function monotonic>) – Monotonic time source, injectable for tests.proc_root (
Path, default:PosixPath('/proc')) – Where to scan, injectable for tests.ticks_per_s (
float|None, default:None) – Override for the kernel’sUSER_HZ.Noneasksclock_ticks_per_s.
- sample(root_pids)[source]#
Take one reading per requested pid, against the previous call.
- Parameters:
- Returns:
{pid: percent or None}. The percentage is the subtree’s tick delta since this tracker last saw that pid, over the wall-clock delta. The first sighting of a pid has no delta and answersNone– an honest “measuring”, never a fabricated0.0– as does a pid that has exited.- Return type:
- class matmelt.core.cpusample.UsageSampler(backends, *, hook_name='sample_cpu_percents', ttl_by_backend=None, default_ttl_s=10.0)[source]#
Bases:
objectThe monitor’s per-tick usage sweep over the in-flight ledger.
Groups jobs by backend, asks each backend exposing the sampling hook for readings, and folds the answers into
{job_key: percent}. Jobs without a reading are simply absent, so the renderer’s.getshows a dash. A hook that raises despite its contract is treated as “no readings” for that sweep – telemetry must never take the dashboard down.- Parameters:
backends (
Mapping[str,object]) –{backend_name: backend}. A backend without the hook contributes nothing, which is how a CPU-only rig shows dashes rather than errors.hook_name (
str, default:'sample_cpu_percents') – Which duck-typed hook to call:sample_cpu_percentsby default. Construct a second sampler withsample_gpu_percentsfor the GPU column.ttl_by_backend (
Mapping[str,float] |None, default:None) – Per-backend cache lifetime in seconds. The caller passes0.0for backends whose sampling is free (local, every tick).default_ttl_s (
float, default:10.0) – Lifetime for backends not named inttl_by_backend, so an SSH round-trip happens at most everyREMOTE_CPU_TTL_Sregardless of the dashboard’s refresh interval.
- sample(jobs, now)[source]#
Read usage for the given in-flight jobs, throttled per backend.
- Parameters:
jobs (
Sequence[InFlightJob]) – The in-flight ledger rows to measure, in any order.now (
float) – The current monotonic time, against which each backend’s cache TTL is judged. The caller owns the clock so a tick measures once.
- Returns:
{job_key: percent}, merged across backends. A backend still inside its TTL contributes its cached readings unchanged.- Return type: