Skip to content

Commit 9803ff8

Browse files
OutlyingWestElias Werner
andauthored
Config-driven monitor (#40)
* binder deployment bugs fixed * first version live plotting * layout fixes * init for multi node monitor slurm feature * testing * first working version based on srun, no visualizer yet * refactoring of monitors. provide the following monitors: - thread - subprocess (default) - native c (C process) - slurm (based on srun) * split visualizer into backends; templates directory tree reworked * rework live grid and header code as the template components * TESTING: performance optimization for monitors - do not retrieve the filtered processes (user/slurm processes) for multiple metrics multiple times but only once per tick - rss instead of uss (similar to top) - buffering of performance data (e.g. we do not need to read process a second time for user level) * TESTING: performance optimization for monitors - updates in C implementation - fixes in data plotting for high resolution data (~20Hz) * TESTING: performance optimization for monitors - fixed missed data point rate in Python-based monitors - add benchmark * add proper benchmark sbatch * TESTING: performance optimization for monitors - benchmark fixes * TESTING: performance optimization for monitors - benchmark fixes * TESTING: performance optimization for monitors - add sanity check for metrics at beginning of benchmark * TESTING: performance optimization for monitors - improve sanity check * TESTING: performance optimization for monitors - fix sanity check and benchmark * TESTING: performance optimization for monitors - tidy up monitor processes properly * TESTING: performance optimization for monitors - use float parsing instead int parsing for frequencies to allow frequencies < 1 * TESTING: performance optimization for monitors - fix in process count for thread backend * TESTING: performance optimization for monitors - proper packaging, incl. gcc build of native c monitor when calling perfmonitor_start - documentation updates in readme and doc * TESTING: performance optimization for monitors - sanity check, docs and build * fixes in running benchmark, different frequency range * avoid loading plotly from CDN on each update, updating existing chart in-place via Plotly.react instead * minor issues with tests compatibility and start monitor string parsing logic fixed * TESTING: performance optimization for monitors - benchmark fixes * TESTING: performance optimization for monitors - add debugging to benchmark * data modlue refactored to align multinode architecture * monitor backends and visualizer refactored to align with the new multinode monitor surface * visualizer logic and frontend djustments for handling multinode performance data * invisible <Aggregated> dropdown for single node setups * multinode monitor's time_mark time format bug fixed * config-driven monitor: StorageHandler pipeline, list buffer, YAML plot config * visualizer: renderer registry (render.py + renderers.py), replace if/elif dispatch * render: switch PlotResult/SeriesItem to Pydantic, attribute access in backends * remove duplicate hardware fields from PerformanceMonitor and SubprocessPerformanceMonitor * add pydantic>=2.0 and pyyaml dependencies, include config YAML in package data * monitor/metrics: add CollectionContext TypedDict * config: add instantiate_backend(), extend instantiate() with DI via inspect.signature * monitor/metrics: decouple ProcessBackend from monitor, add snapshot(context) * monitor/metrics: decouple CPU, memory, IO backends — inject NodeInfo, use CollectionContext * monitor/metrics: decouple GPU backends — inject uid/slurm_job, inline process filtering * monitor: wire two-phase pipeline — available dict, instantiate_backend, CollectionContext per tick * tests: update monitor tests for new architecture * monitor/metrics: split GpuBackendDiscovery into GpuDiscovery + MultiGpuBackend * config, monitor: build pipeline from collectors.yaml * config, monitor: inject collector deps via yaml inject: field * pipeline builder class added * monitor: add type annotations across monitor subsystem * tests: update for PerformanceData and NodeDataStore API * monitor: replace dict[str, Any] with bare dict * make all metric backends follow collector semantics * tests for metric backends follow collector semantics * fix: replace missing _get_process_pids() with _process_backend.get_process_pids() * refactor: align PsutilProcessCollector to common pipeline interface - snapshot() now sets context[process_pids] via get_process_pids() - add no-op collect() to satisfy pipeline protocol - add NoOpHandler / noop storage type for context-only backends - register process backend first in collectors.yaml (must stay first) - remove explicit _process_backend construction from PerformanceMonitor; expose via property backed by pipeline[0] - fix IndexError (psutil TOCTOU race) in process_iter prefetch * fix: guard _attach_cell_index against missing time column Regression from 1446ae7 where service switched from monitor.data.view() to monitor.nodes.view(). The C collector binary sends empty columns_by_level in its ready message, so the schema fallback DataFrame has no columns, and _attach_cell_index crashed on result["time"]. Fix: early return from _attach_cell_index when time column is absent. * refactor: replace storage type dispatch with _target_-based handler instantiation Rename storage.py to handlers.py, storage: key to handler: in collectors.yaml, and drop make_handler() in favour of instantiate() used directly in pipeline. * refactor: remove instantiate_backend, inline direct GPU backend construction * refactor: introduce CollectorBackend ABC and StorageHandler Protocol in metrics/common.py * chore: remove unnecessary `from __future__ import annotations` (Python 3.12) * feat: add NetworkCollector with psutil backend and network plot subsets Adds PsutilNetworkCollector (net_bytes_sent, net_bytes_recv, net_packets_sent, net_packets_recv at system level) registered in collectors.yaml with CumulativeRateHandler. Adds network subset to plots.yaml including composite net_vs_disk_read panel. Also adds `from __future__ import annotations` to common.py for Python 3.9 compatibility and improves CollectorBackend/StorageHandler docstrings. * feat: expose subprocess_python as explicit --monitor choice Adds subprocess_python to parser choices and service factory so users can select it directly via %perfmonitor_start --monitor subprocess_python. * docs: add custom collector guide and network metric visualization sections Main README: adds "Adding a Custom Collector" section (3-step guide, file layout, inject: key explanation) and "Visualizing Custom Collector Metrics" section (plot types table, composite_series example, --metrics usage note). Monitor README: updates directory layout to include network/. * Add default_subsets attribute and move resolution earlier in plot pipeline - Introduce self.default_subsets to replace hardcoded metric subset defaults - Load default_subsets from config in _load_subsets_from_config() - Move _resolve_metric_subsets call before level check in plot() to ensure GPU detection happens first - Prevent duplicate gpu/gpu_all metrics when default_subsets already contains them - Update plot method signatures to use None instead of tuple default for metric_subsets * Add default_subsets to plots.yaml * Update session exporter to use visualizer default_subsets * docs: document default_subsets config key in README * minor adjustments in README.md Custom Monitors section * docs: move custom collector sections from README to dedicated guides Add docs/guides/custom-collector.md and docs/guides/visualizing-custom-collector-metrics.md. Replace the two large README sections with short teasers linking to the new guides. Add a collector tip to the custom-monitor guide. Register both pages in mkdocs nav. * docs: enable admonition markdown extension * docs: minor README adjustments * Add guard and warning for missing time column in filter_perfdata * fix: move time field inside sample in C collector output * fix: emit flat handler-compatible columns from C collector * feat: add columns_by_level to C collector ready message * Disable network collector and remove from default subsets * refactor: move collectors config to config/python/ subdirectory Prepares for future C collector config under config/c/; each backend subdirectory owns its loader, config/utils.py reduced to instantiate(). * refactor: split native C collector into per-metric backends Replace monolithic collector.c with a structured layout mirroring the Python metrics hierarchy: native_c/ monitor.h — shared TickContext + CCollector interface monitor.c — main(), registry, emit_ready/emit_tick metrics/cpu/ — /proc/<pid>/stat + /proc/stat metrics/memory/ — /proc/<pid>/statm + /proc/meminfo metrics/io/ — /proc/<pid>/io + /proc/diskstats metrics/gpu/ — NVML via dynamic loading Active collectors are selected via --collectors arg driven by config/c/collectors.yaml, loaded by CSubprocessPerformanceMonitor. Makefile updated to compile all five translation units via make. * docs: split custom-collector guide into python and c variants - Replace custom-collector.md with python-custom-collector.md and c-custom-collector.md - Update README.md and monitor/README.md with links to both guides - Register both guides in mkdocs.yml nav * test: add native_c backend tests * version update, yaml include * fixes in jpeg export, reload_ext function * fix tests and native_c monitor make --------- Co-authored-by: Elias Werner <elias.werner@tu-dresden.de>
1 parent de648ce commit 9803ff8

125 files changed

Lines changed: 10384 additions & 3243 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,11 @@ poetry.toml
173173
# LSP config files
174174
pyrightconfig.json
175175

176+
# Compiled C monitoring collector and build artifacts
177+
jumper_collector
178+
*.o
179+
180+
# Benchmark results
181+
jumper_extension/monitor/benchmark/results/
182+
176183
# End of https://www.toptal.com/developers/gitignore/api/python

README.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,17 @@ Try it yourself:
8686

8787
1. **Start monitoring**:
8888
```python
89-
%perfmonitor_start [interval]
89+
%perfmonitor_start [interval] [--monitor TYPE] [--check-sanity]
9090
```
9191

9292
`interval` is an optional argument for configuring frequency of performance data gathering (in seconds), set to 1 by default. This command launches a performance monitoring daemon.
9393

94+
The `--monitor` option selects a backend (`default`, `native_c`, `subprocess_python`, `thread`, `slurm_multinode`). The default backend is the native C collector (`native_c`); it is compiled on first use — you will see a `[JUmPER] Compiling native_c monitor binary...` message. If no C compiler is available or compilation fails, JUmPER automatically falls back to the `subprocess_python` monitor.
95+
96+
The optional `--check-sanity` flag runs a short validation of the selected backend (collecting a few samples, verifying expected metric columns are present, non-NaN, and non-zero) before real monitoring starts.
97+
98+
> **IMPORTANT.** `--check-sanity` was tailored for the `thread`, `subprocess_python` and `native_c` monitors. When used with any other monitor (e.g. `slurm_multinode`, or a custom monitor provided via the programmatic API) it is **expected to fail** because those backends populate a different set of per-level metric columns. A warning is printed in that case and the check still runs, but a failure does not necessarily indicate a broken monitor — only that the tailored check does not apply.
99+
94100
2. **Run your code**
95101

96102
3. **View performance report**:
@@ -233,6 +239,41 @@ The following metric keys can be used with `--metrics` for both direct and live
233239
```
234240
Export performance measurements for entire notebook and cell execution history with timestamps, allowing you to project measurements onto specific cells.
235241

242+
### Custom Monitors
243+
244+
Any object implementing `MonitorProtocol`
245+
(`jumper_extension/monitor/common.py`) can be plugged in via the
246+
`monitor=` argument of `service.start_monitoring` — see
247+
[`jumper_extension/monitor/README.md`](jumper_extension/monitor/README.md)
248+
for a worked `SlurmMultinodeMonitor` example and the full
249+
[Custom Monitors guide](https://scads.github.io/jumper_jupyter_performance/latest/guides/custom-monitor/)
250+
for a step-by-step walkthrough.
251+
252+
### Custom Collectors
253+
254+
To extend the metric pipeline with a new group of logically related metrics —
255+
for example network I/O or a hardware sensor — without replacing the entire
256+
monitor backend, there are two ways to create a collector:
257+
258+
1. **Python collector** — a `CollectorBackend` + `StorageHandler` pair loaded
259+
automatically from `collectors.yaml`. Works with the `thread` and
260+
`subprocess_python` monitors. See the full
261+
[Custom Python Collectors guide](https://scads.github.io/jumper_jupyter_performance/latest/guides/python-custom-collector/)
262+
for a step-by-step walkthrough including a `NetworkCollector` example.
263+
264+
2. **C collector** — a C translation unit implementing the `CCollector` interface,
265+
compiled directly into the `native_c` binary. See the
266+
[Custom C Collectors guide](https://scads.github.io/jumper_jupyter_performance/latest/guides/c-custom-collector/)
267+
for a step-by-step walkthrough.
268+
269+
### Visualizing Custom Collector Metrics
270+
271+
Custom collector columns are available to `%perfmonitor_plot --metrics` once
272+
registered in `plots.yaml`. See the
273+
[Visualizing Custom Collector Metrics guide](https://scads.github.io/jumper_jupyter_performance/latest/guides/visualizing-custom-collector-metrics/)
274+
for the full reference — built-in plot types, `default_subsets`, and a
275+
disk-vs-network composite panel example.
276+
236277
### Monitoring Right in Your Code
237278
Run the monitor around any code block and save its performance profile to CSV/JSON.
238279

@@ -280,7 +321,6 @@ The extension supports four different levels of metric collection, each providin
280321
- **NVIDIA GPUs**: Full support for all monitoring levels (process, user, system, slurm) including per-process GPU memory tracking
281322
- **AMD GPUs**: System-level monitoring supported; per-process and per-user metrics are limited by AMD ADLX API capabilities
282323

283-
284324
## Full Documentation
285325

286326
- Online (latest): https://scads.github.io/jumper_jupyter_performance/latest/

docs/guides/c-custom-collector.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Custom C Collectors
2+
3+
The `native_c` monitor is a compiled binary that reads `/proc` directly. Its
4+
metric backends are C translation units — not Python classes — and are registered
5+
at compile time, not via `collectors.yaml`.
6+
7+
!!! note
8+
For the **Python** monitor backends (`thread`, `subprocess_python`) see
9+
[Custom Python Collectors](python-custom-collector.md).
10+
11+
---
12+
13+
## Source layout
14+
15+
```
16+
native_c/
17+
├── monitor.c / monitor.h # main loop, shared types, registry
18+
├── Makefile
19+
└── metrics/
20+
├── cpu/ cpu.c / cpu.h
21+
├── memory/ memory.c / memory.h
22+
├── io/ io.c / io.h
23+
└── gpu/ gpu.c / gpu.h
24+
```
25+
26+
Each backend exposes a single `const CCollector <name>_collector` symbol and
27+
includes `monitor.h` (available via the `-I$(DIR)` compile flag).
28+
29+
---
30+
31+
## `CCollector` interface
32+
33+
```c
34+
typedef struct {
35+
const char *name;
36+
void (*setup)(void);
37+
void (*snapshot)(TickContext *ctx);
38+
void (*emit_columns)(FILE *fp, int level_idx, int n_cpus, int n_gpus);
39+
void (*emit_sample)(FILE *fp, int level_idx, TickContext *ctx, double dt);
40+
void (*post_tick)(void);
41+
void (*teardown)(void);
42+
} CCollector;
43+
```
44+
45+
| Callback | When called | Set to `NULL` if |
46+
|---|---|---|
47+
| `setup` | once, before main loop | no one-time init needed |
48+
| `snapshot` | every tick, before any emit | no per-tick `/proc` reads needed |
49+
| `emit_columns` | once per level at start (ready handshake) | backend adds no columns |
50+
| `emit_sample` | every tick, per level | backend adds no columns |
51+
| `post_tick` | after all levels emitted | no post-emit cache work needed |
52+
| `teardown` | on clean shutdown | no resources to release |
53+
54+
`TickContext` carries the PID sets for the current tick (proc, user, slurm, all).
55+
Use `ctx->all_pids` / `ctx->n_all` to iterate over all tracked processes.
56+
57+
---
58+
59+
## Adding a new backend
60+
61+
**1.** Create `metrics/<name>/<name>.h`:
62+
```c
63+
#ifndef JUMPER_METRICS_<NAME>_H
64+
#define JUMPER_METRICS_<NAME>_H
65+
#include "monitor.h"
66+
extern const CCollector <name>_collector;
67+
#endif
68+
```
69+
70+
**2.** Implement `metrics/<name>/<name>.c` — define all needed callbacks and expose:
71+
```c
72+
const CCollector <name>_collector = {
73+
.name = "<name>",
74+
.setup = <name>_setup,
75+
.snapshot = <name>_snapshot,
76+
.emit_columns = <name>_emit_columns,
77+
.emit_sample = <name>_emit_sample,
78+
.post_tick = NULL,
79+
.teardown = NULL,
80+
};
81+
```
82+
83+
**3.** Add to `Makefile`:
84+
```makefile
85+
SRCS := \
86+
...
87+
metrics/<name>/<name>.c
88+
```
89+
90+
**4.** Register in `monitor.c`:
91+
```c
92+
#include "metrics/<name>/<name>.h"
93+
94+
static const CCollector * const g_registry[] = {
95+
&cpu_collector, &memory_collector, &io_collector, &gpu_collector,
96+
&<name>_collector,
97+
};
98+
```
99+
100+
**5.** Add to `config/collectors/c/collectors.yaml`:
101+
```yaml
102+
collectors:
103+
- cpu
104+
- memory
105+
- io
106+
- gpu
107+
- <name>
108+
```
109+
110+
After these changes, rebuild with `make` (or trigger `ensure_native_c(force_build=True)`
111+
from Python) — the new collector will be active on the next monitor start.

0 commit comments

Comments
 (0)