actop 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. actop-1.0.0/LICENSE +21 -0
  2. actop-1.0.0/PKG-INFO +353 -0
  3. actop-1.0.0/README.md +326 -0
  4. actop-1.0.0/actop/__init__.py +18 -0
  5. actop-1.0.0/actop/actop.py +219 -0
  6. actop-1.0.0/actop/api.py +202 -0
  7. actop-1.0.0/actop/config.py +95 -0
  8. actop-1.0.0/actop/export.py +175 -0
  9. actop-1.0.0/actop/ioreport.py +298 -0
  10. actop-1.0.0/actop/models.py +34 -0
  11. actop-1.0.0/actop/native_sys.py +643 -0
  12. actop-1.0.0/actop/power_scaling.py +36 -0
  13. actop-1.0.0/actop/sampler.py +508 -0
  14. actop-1.0.0/actop/smc.py +360 -0
  15. actop-1.0.0/actop/soc_profiles.py +201 -0
  16. actop-1.0.0/actop/tui/__init__.py +0 -0
  17. actop-1.0.0/actop/tui/app.py +434 -0
  18. actop-1.0.0/actop/tui/widgets.py +799 -0
  19. actop-1.0.0/actop/utils.py +217 -0
  20. actop-1.0.0/actop.egg-info/PKG-INFO +353 -0
  21. actop-1.0.0/actop.egg-info/SOURCES.txt +42 -0
  22. actop-1.0.0/actop.egg-info/dependency_links.txt +1 -0
  23. actop-1.0.0/actop.egg-info/entry_points.txt +2 -0
  24. actop-1.0.0/actop.egg-info/requires.txt +8 -0
  25. actop-1.0.0/actop.egg-info/top_level.txt +1 -0
  26. actop-1.0.0/pyproject.toml +47 -0
  27. actop-1.0.0/setup.cfg +4 -0
  28. actop-1.0.0/tests/test_api.py +91 -0
  29. actop-1.0.0/tests/test_braille_chart_render.py +122 -0
  30. actop-1.0.0/tests/test_chart_rendering.py +117 -0
  31. actop-1.0.0/tests/test_cli_contract.py +215 -0
  32. actop-1.0.0/tests/test_config.py +96 -0
  33. actop-1.0.0/tests/test_dashboard_metrics.py +177 -0
  34. actop-1.0.0/tests/test_e2e.py +42 -0
  35. actop-1.0.0/tests/test_export.py +138 -0
  36. actop-1.0.0/tests/test_integration.py +24 -0
  37. actop-1.0.0/tests/test_ioreport.py +24 -0
  38. actop-1.0.0/tests/test_native_sys.py +98 -0
  39. actop-1.0.0/tests/test_power_scaling.py +60 -0
  40. actop-1.0.0/tests/test_runtime_contracts.py +119 -0
  41. actop-1.0.0/tests/test_sampler.py +155 -0
  42. actop-1.0.0/tests/test_smc.py +48 -0
  43. actop-1.0.0/tests/test_soc_profiles.py +60 -0
  44. actop-1.0.0/tests/test_tui_app.py +123 -0
actop-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 binlecode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
actop-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,353 @@
1
+ Metadata-Version: 2.4
2
+ Name: actop
3
+ Version: 1.0.0
4
+ Summary: Performance monitoring CLI tool for Apple Silicon
5
+ Author-email: binlecode <bin.le.code@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/binlecode/actop
8
+ Project-URL: Repository, https://github.com/binlecode/actop
9
+ Keywords: actop,apple-silicon,ioreport,performance-monitor
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: textual>=0.60
21
+ Provides-Extra: dev
22
+ Requires-Dist: ruff; extra == "dev"
23
+ Requires-Dist: pytest; extra == "dev"
24
+ Provides-Extra: pandas
25
+ Requires-Dist: pandas>=1.3; extra == "pandas"
26
+ Dynamic: license-file
27
+
28
+ # actop
29
+
30
+ **Watch your Apple Silicon Mac the way it actually works — and profile your own workloads from Python.**
31
+
32
+ `actop` is a sudoless, in-process performance monitor for M1–M4 Macs: a real-time
33
+ TUI for CPU/GPU/ANE utilization, per-core frequency, memory **bandwidth**, power,
34
+ and thermals — plus a **Python API** (`Monitor` / `Profiler`, `to_pandas()`) so you
35
+ can instrument your *own* local LLM / MLX / CoreML inference and training runs with
36
+ SoC-accurate power and energy context.
37
+
38
+ <!-- TODO: replace the static screenshot below with an animated capture (GIF/SVG) of
39
+ the dashboard live during an MLX/Ollama inference run — motion is what gets shared. -->
40
+ ![actop dashboard: live E-CPU/P-CPU/GPU/ANE utilization, per-core frequency, memory bandwidth, and power charts on Apple Silicon](images/actop.png)
41
+
42
+ **Who it's for**
43
+
44
+ - **Running LLMs locally** (MLX, llama.cpp, Ollama) and want to see whether you're
45
+ GPU-bound, memory-**bandwidth**-bound, or leaving the ANE idle — at a glance.
46
+ - **Profiling your own code**: wrap a workload in `Monitor`/`Profiler`, get a
47
+ pandas frame of power, frequency, residency, and cumulative session energy.
48
+ - **Just want a clean `*top`** for Apple Silicon that needs **no `sudo`**.
49
+
50
+ Install in one line — [Homebrew](#homebrew-recommended) or [uv](#uv-recommended-for-non-homebrew-users) — then run `actop`.
51
+
52
+ ## Background
53
+
54
+ `actop` is an independent project with its own idea, architecture, codebase, and release cycle. It was inspired by `tlkh/asitop` — built to fill the gaps that tool left for whole-chip, sudoless, programmable monitoring. The name carries the Unix `*top` lineage: `actop` (*Apple **C**hip top*) follows `asitop` (*Apple Silicon top*), and covers the whole SoC — CPU, GPU, ANE, power, memory bandwidth, and thermal.
55
+
56
+ The original `asitop` shells out to Apple's `powermetrics` CLI, a high-level tool that requires `sudo`, writes to temp files, and returns pre-aggregated metrics at a fixed cadence. `actop` instead calls the underlying IOReport C library directly via Python ctypes — the same library that `powermetrics` itself uses internally. This low-level approach runs unprivileged, avoids subprocess and file I/O overhead, gives access to raw per-core residency states and energy counters, and lets the application control its own sampling interval and delta computation.
57
+
58
+ **Why another `*top`?** `mactop` (Go) and `macmon` (Rust) are excellent sudoless TUIs — mactop the broadest in features, macmon the leanest binary. `actop`'s reason to exist is different: it is the **programmable, Python-native** one. A public API (`Monitor` / `Profiler`, `to_pandas()`) lets you instrument and profile your *own* workloads — local LLM / CoreML / MLX inference, training loops — from Python, with SoC-accurate power context and cumulative session energy. It's the data scientist's profiler, not just a dashboard — and unlike the `sudo`-bound `asitop` that inspired it, it needs no `sudo`. See [Where actop fits](#where-actop-fits) for the head-to-head.
59
+
60
+ ## Key Features
61
+
62
+ - **Textual TUI dashboard**: `Sparkline` charts for E-CPU, P-CPU, GPU, ANE, RAM, and power — rendered by the [Textual](https://textual.textualize.io/) framework. Supports `dots` (braille) and `block` glyph styles. Resizes cleanly; no raw ANSI escape sequences.
63
+ - **In-process IOReport sampling**: reads Apple Silicon power, frequency, and residency metrics via Python ctypes bindings to `libIOReport.dylib` and CoreFoundation. No subprocesses, no temp files.
64
+ - **Per-core visibility**: per-core panels on by default; toggle with `--no-show_cores` for a cluster-level view.
65
+ - **Diagnosis-oriented alerts**: configurable sustained-sample thresholds for thermal pressure, bandwidth saturation, swap growth, and package power. Active alerts are shown inline in the status line.
66
+ - **Process monitoring (optional)**: top CPU/RSS processes panel is off by default. Enable at launch with `--show-processes` or press `t` in the TUI. Regex filtering is available via `--proc-filter` or `/` interactively.
67
+ - **Profile-aware power scaling**: `profile` mode (default) scales charts against the SoC's known reference wattage for stable cross-session comparison; `auto` mode scales against rolling peak.
68
+ - **SoC compatibility**: 16 built-in M1–M4 profiles (base, Pro, Max, Ultra). Unknown future chips fall back to tier-based defaults using the latest generation's reference values.
69
+ - **CPU/GPU temperature**: reads die temperatures from the Apple SMC (System Management Controller) via IOKit ctypes. Displayed inline in gauge titles (e.g. "P-CPU Usage: 12% @ 3504 MHz (58°C)"). No sudo required.
70
+
71
+ ## Where actop fits
72
+
73
+ How the sudoless, in-process field stacks up:
74
+
75
+ | | actop | [mactop](https://github.com/metaspartan/mactop) | [macmon](https://github.com/vladkens/macmon) |
76
+ |---|:---:|:---:|:---:|
77
+ | Unprivileged, in-process (no sudo) | ✅ | ✅ | ✅ |
78
+ | CPU/GPU/ANE power · temps · bandwidth | ✅ | ✅ | ✅ |
79
+ | Python API (`Monitor`/`Profiler`, `to_pandas()`) | ✅ | — | — |
80
+ | SoC-accurate power scaling (M1–M4 profiles) | ✅ | rolling peak | rolling peak |
81
+ | Session energy (∫ package power) | ✅ | — | — |
82
+ | Net/disk I/O · fan RPM · menu bar | — | ✅ | fan only |
83
+
84
+ For the broadest TUI and DevOps feature set (network/disk I/O, a menu-bar app, more export formats), use **mactop**; for the leanest single Rust binary, **macmon**. Full head-to-head: [docs/REVIEW-architecture-comparison.md](docs/REVIEW-architecture-comparison.md).
85
+
86
+ ## Installation
87
+
88
+ ### Homebrew (recommended)
89
+
90
+ ```shell
91
+ brew tap --custom-remote binlecode/actop https://github.com/binlecode/actop.git
92
+ brew install binlecode/actop/actop
93
+ ```
94
+
95
+ To tell Homebrew to trust your tap or specific formula:
96
+
97
+ - Trust the specific formula only (Recommended):
98
+ ```shell
99
+ brew trust --formula binlecode/actop/actop
100
+ ```
101
+
102
+ Upgrade / uninstall:
103
+
104
+ ```shell
105
+ brew upgrade binlecode/actop/actop
106
+ brew uninstall binlecode/actop/actop
107
+ ```
108
+
109
+ ### uv (recommended for non-Homebrew users)
110
+
111
+ [`uv`](https://docs.astral.sh/uv/) installs `actop` into a sandboxed, per-tool
112
+ environment with its own managed CPython — no system Python required and no
113
+ interpreter drift:
114
+
115
+ ```shell
116
+ uv tool install git+https://github.com/binlecode/actop.git
117
+ ```
118
+
119
+ Upgrade / uninstall:
120
+
121
+ ```shell
122
+ uv tool upgrade actop
123
+ uv tool uninstall actop
124
+ ```
125
+
126
+ ### pip
127
+
128
+ ```shell
129
+ pip install git+https://github.com/binlecode/actop.git
130
+ ```
131
+
132
+ ## Quick Start
133
+
134
+ ```shell
135
+ actop # full dashboard with per-core panels, profile power scaling
136
+ actop --interval 1 --avg 10 # faster refresh, shorter rolling window
137
+ actop --show-processes # include top process panel at startup
138
+ actop --proc-filter "python|ollama|vllm|docker|mlx" # filter process panel at launch
139
+ actop --no-show_cores # cluster-level view without per-core panels
140
+ actop --chart-glyph block # square block chart glyphs
141
+ actop --json # stream NDJSON metrics to stdout (no TUI)
142
+ actop --serve 9095 # serve Prometheus metrics at :9095/metrics (no TUI)
143
+ ```
144
+
145
+ Interactive keys: `p` pause · `s` cycle sort (CPU%→RSS→PID) · `g` toggle chart glyph (`dots`/`block`) · `t` toggle process panel · `/` filter processes · `?` help overlay · `q` quit
146
+
147
+ ## CLI Reference
148
+
149
+ | Option | Purpose | Default |
150
+ | --- | --- | --- |
151
+ | `--interval` | Sampling and refresh interval (seconds) | `2` |
152
+ | `--avg` | Rolling average window (seconds) | `30` |
153
+ | `--subsamples` | Internal sampler deltas per interval (≥1) | `1` |
154
+ | `--show_cores` / `--no-show_cores` | Per-core panels | `on` |
155
+ | `--show-processes` | Show top process panel at startup | `off` |
156
+ | `--power-scale profile\|auto` | Power chart scaling | `profile` |
157
+ | `--chart-glyph dots\|block` | Chart glyph style | `dots` |
158
+ | `--proc-filter REGEX` | Filter process panel by command name | all (applies when panel is enabled) |
159
+ | `--alert-bw-sat-percent` | Bandwidth saturation alert threshold | `85` |
160
+ | `--alert-package-power-percent` | Package power alert threshold (profile-relative) | `85` |
161
+ | `--alert-swap-rise-gb` | Swap growth alert threshold (GB) | `0.3` |
162
+ | `--alert-sustain-samples` | Consecutive samples for sustained alerts | `3` |
163
+ | `--json` | Stream metrics as NDJSON to stdout instead of the TUI | `off` |
164
+ | `--serve PORT` | Serve Prometheus metrics on `http://0.0.0.0:PORT/metrics` instead of the TUI | `off` |
165
+
166
+ ## Metrics Export
167
+
168
+ Beyond the interactive dashboard, actop can act as an observability source. Both
169
+ modes reuse the same unprivileged IOReport backend and exit on `Ctrl-C`.
170
+
171
+ - **NDJSON stream** (`--json`): emits one compact JSON snapshot per `--interval`
172
+ to stdout — every `SystemSnapshot` field, including per-core lists. Pipe it to
173
+ `jq`, a log shipper, or a file:
174
+
175
+ ```shell
176
+ actop --json --interval 1 | jq '{cpu: .cpu_watts, pkg: .package_watts}'
177
+ ```
178
+
179
+ - **Prometheus endpoint** (`--serve PORT`): exposes gauges at `/metrics`
180
+ (`actop_cpu_power_watts`, `actop_pcpu_utilization_percent`, per-core
181
+ `actop_core_utilization_percent{cluster,core}`, …). A background sampler keeps
182
+ the latest reading warm so scrapes return immediately:
183
+
184
+ ```shell
185
+ actop --serve 9095
186
+ curl -s localhost:9095/metrics
187
+ ```
188
+
189
+ ## How It Works
190
+
191
+ actop accesses Apple Silicon hardware telemetry through three OS-level interfaces, all called in-process:
192
+
193
+ ### IOReport framework (`libIOReport.dylib`)
194
+
195
+ The primary data source. actop loads `libIOReport.dylib` and `CoreFoundation.framework` via `ctypes.cdll.LoadLibrary`, then:
196
+
197
+ - Subscribes to three IOReport channel groups: **Energy Model** (CPU/GPU/ANE energy in nanojoules), **CPU Core Performance States** (per-core ECPU/PCPU DVFS residency), and **GPU Performance States** (GPU DVFS residency).
198
+ - Takes periodic snapshots with `IOReportCreateSamples` and computes deltas between consecutive snapshots with `IOReportCreateSamplesDelta`.
199
+ - Extracts per-channel energy values (`IOReportSimpleGetIntegerValue`) and per-state residency tables (`IOReportStateGetCount`, `IOReportStateGetNameForIndex`, `IOReportStateGetResidency`).
200
+ - Converts raw items into a `SampleResult` — power (watts), frequency (MHz), and activity (percent). Energy values are converted from nanojoules to joules and scaled by elapsed time for correct wattage.
201
+
202
+ All CoreFoundation objects are managed via `CFRelease` to prevent memory leaks.
203
+
204
+ ### IOKit registry (`ioreg`)
205
+
206
+ At startup, reads `ioreg -a -r -d 1 -n pmgr` to get DVFS frequency tables from the power manager device node. Parses `voltage-states*` binary blobs as 8-byte `(freq_hz, voltage)` pairs and heuristically assigns tables to E-CPU, P-CPU, and GPU clusters. These translate opaque `V{n}P{m}` (CPU) and `P{n}` (GPU) state names into actual MHz values, computed as weighted averages across active P-states by residency time.
207
+
208
+ ### SMC (System Management Controller)
209
+
210
+ Reads CPU and GPU die temperatures via IOKit ctypes bindings to the `AppleSMC` kernel service. Discovers temperature sensor keys (Tp*/Te* for CPU, Tg* for GPU) at startup and reads `flt ` (IEEE 754 float) values each sample. Runs unprivileged.
211
+
212
+ ### Thermal pressure (`NSProcessInfo`)
213
+
214
+ Reads the macOS thermal state via the Objective-C runtime (`libobjc.A.dylib` + `Foundation.framework`) using ctypes. Calls `[NSProcessInfo processInfo].thermalState` each sample. The result is shown in the status line above the per-core history tracks:
215
+
216
+ | State | Meaning |
217
+ | --- | --- |
218
+ | **Nominal** | Normal operating conditions — no throttling |
219
+ | **Fair** | Mild thermal pressure — light throttling may begin |
220
+ | **Serious** | Significant thermal pressure — noticeable throttling in effect |
221
+ | **Critical** | Severe thermal pressure — aggressive throttling, system is very hot |
222
+
223
+ No sudo required. Degrades to `Unknown` if the ObjC runtime call fails.
224
+
225
+ ### System context
226
+
227
+ - `sysctl`: SoC chip name, total/P/E core counts.
228
+ - `system_profiler`: GPU core count.
229
+ - `psutil`: RAM/swap usage (`virtual_memory()`, `swap_memory()`), and process enumeration.
230
+
231
+ ### Signal Sources
232
+
233
+ | Signal | Source | Notes |
234
+ | --- | --- | --- |
235
+ | CPU/GPU/ANE power (W) | IOReport Energy Model | nJ per sample interval → watts |
236
+ | Per-core frequency (MHz) | IOReport residency + DVFS tables | Weighted average of active P-states |
237
+ | Per-core activity (%) | IOReport CPU Core Performance States | Via `CoreSample` (residency-weighted active%) |
238
+ | GPU frequency and activity | IOReport GPU Performance States | Weighted average of GPUPH residencies |
239
+ | CPU/GPU temperature (°C) | SMC via IOKit ctypes | Max die temp per cluster |
240
+ | RAM / swap | `psutil.virtual_memory()` + `psutil.swap_memory()` | `total - available` for used |
241
+ | SoC profile | `sysctl` brand → 16 M1–M4 profiles | Tier fallbacks for unknown chips |
242
+ | Top processes | `psutil.process_iter` | Optional `--proc-filter` regex |
243
+ | Bandwidth | IOReport (when available) | N/A if DCS counters not exposed |
244
+ | Thermal pressure | `NSProcessInfo.thermalState` via ObjC runtime | Nominal / Fair / Serious / Critical |
245
+
246
+ ## Architecture
247
+
248
+ | Module | Role |
249
+ | --- | --- |
250
+ | `actop/actop.py` | CLI entry point and argument parsing; thin wrapper launching the Textual TUI |
251
+ | `actop/ioreport.py` | ctypes bindings to `libIOReport.dylib` and CoreFoundation — `IOReportSubscription` lifecycle, snapshot, delta, and CF helpers |
252
+ | `actop/sampler.py` | `IOReportSampler`: two-snapshot delta logic, `SampleResult` conversion, DVFS table discovery from `ioreg pmgr`, SMC temperature integration |
253
+ | `actop/smc.py` | SMC temperature reader: IOKit ctypes bindings to `AppleSMC`, key discovery, CPU/GPU die temperature reads |
254
+ | `actop/utils.py` | System context: `psutil` RAM/swap, `sysctl`/`system_profiler` SoC info, process enumeration |
255
+ | `actop/soc_profiles.py` | 16 `SocProfile` dataclasses (M1–M4) with reference wattage/bandwidth; tier fallbacks for unknown chips |
256
+ | `actop/power_scaling.py` | `power_to_percent()`: profile mode (SoC reference) vs auto mode (rolling peak x1.25) |
257
+ | `actop/config.py` | `DashboardConfig` frozen dataclass; `create_dashboard_config()` merges CLI args with SoC info |
258
+ | `actop/models.py` | `SystemSnapshot` and `CoreSample` dataclasses (public API types) |
259
+ | `actop/api.py` | `Monitor`, `Profiler`, `AsyncMonitor` — public Python API for hardware profiling |
260
+ | `actop/tui/app.py` | `ActopApp`: Textual `App` with polling worker, process table, interactive sort/filter/pause |
261
+ | `actop/tui/widgets.py` | `HardwareDashboard` widget with braille `Sparkline` charts, core rows, and alert computation |
262
+ | `actop/tui/styles.tcss` | Textual CSS layout for the dashboard |
263
+
264
+ ```mermaid
265
+ graph TD
266
+ subgraph "macOS Frameworks"
267
+ IOR[libIOReport.dylib]
268
+ CF[CoreFoundation.framework]
269
+ IOKIT[IOKit Registry<br/>ioreg pmgr device]
270
+ end
271
+
272
+ subgraph "macOS System Commands"
273
+ SYSCTL[sysctl<br/>CPU brand, core counts]
274
+ SYSPROF[system_profiler<br/>GPU core count]
275
+ end
276
+
277
+ subgraph "Python Libraries"
278
+ PSUTIL[psutil<br/>RAM, swap,<br/>process enumeration]
279
+ TEXTUAL[textual<br/>terminal TUI framework]
280
+ end
281
+
282
+ subgraph "actop Modules"
283
+ IORPY[ioreport.py<br/>ctypes bindings]
284
+ SAMPLER[sampler.py<br/>IOReportSampler]
285
+ SMC[smc.py<br/>SMC temperature reader]
286
+ UTILS[utils.py<br/>system context]
287
+ PROFILES[soc_profiles.py<br/>M1-M4 profiles]
288
+ POWER[power_scaling.py<br/>chart scaling]
289
+ CONFIG[config.py<br/>DashboardConfig]
290
+ TUI[tui/app.py + widgets.py<br/>Textual dashboard]
291
+ MAIN[actop.py<br/>CLI entry point]
292
+ end
293
+
294
+ IOR -->|ctypes.cdll| IORPY
295
+ CF -->|ctypes.cdll| IORPY
296
+ IOKIT -->|native ctypes| SAMPLER
297
+
298
+ IORPY -->|IOReportSubscription<br/>sample/delta| SAMPLER
299
+ SMC -->|TemperatureReading| SAMPLER
300
+ SAMPLER -->|SampleResult| TUI
301
+
302
+ SYSCTL --> UTILS
303
+ SYSPROF --> UTILS
304
+ PSUTIL --> UTILS
305
+ UTILS -->|SoC info, RAM, processes| TUI
306
+
307
+ PROFILES --> CONFIG
308
+ CONFIG --> TUI
309
+ POWER --> TUI
310
+ TEXTUAL --> TUI
311
+ TUI --> MAIN
312
+ ```
313
+
314
+ ## Troubleshooting
315
+
316
+ - **Bandwidth shows N/A**: IOReport does not expose memory bandwidth counters on all SoCs.
317
+ - **Thermal shows "Unknown"**: ObjC runtime failed to read `NSProcessInfo.thermalState` (unexpected on macOS 12+).
318
+ - **Frequencies show 0 MHz**: DVFS table discovery failed. File an issue with `sysctl -n machdep.cpu.brand_string` output.
319
+ - **Metric differences vs other tools**: expected due to sampling window and source timing differences.
320
+
321
+ ## Development
322
+
323
+ ```bash
324
+ .venv/bin/python -m pip install -e ".[dev]" # install editable + dev deps
325
+ .venv/bin/python -m actop.actop --help # validate CLI
326
+ .venv/bin/python -m actop.actop # run with defaults
327
+ .venv/bin/pytest -q # run tests
328
+ .venv/bin/python -m ruff check . && .venv/bin/python -m ruff format . # lint + format
329
+ ```
330
+
331
+ ## Release
332
+
333
+ See `GUIDE-release-operations.md` for the full runbook.
334
+
335
+ ```bash
336
+ # 1. Bump version and changelog
337
+ edit pyproject.toml CHANGELOG.md
338
+
339
+ # 2. Run checks
340
+ .venv/bin/python -m ruff check --fix . && .venv/bin/python -m ruff format .
341
+ .venv/bin/python -m actop.actop --help
342
+ .venv/bin/pytest -q
343
+
344
+ # 3. Commit and tag
345
+ git add pyproject.toml CHANGELOG.md
346
+ git commit -m "Release v$VERSION"
347
+ scripts/tag_release.sh "$VERSION"
348
+
349
+ # 4. Verify
350
+ brew update && brew upgrade binlecode/actop/actop
351
+ ```
352
+
353
+ CI handles formula sync automatically on tag push.