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.
- actop-1.0.0/LICENSE +21 -0
- actop-1.0.0/PKG-INFO +353 -0
- actop-1.0.0/README.md +326 -0
- actop-1.0.0/actop/__init__.py +18 -0
- actop-1.0.0/actop/actop.py +219 -0
- actop-1.0.0/actop/api.py +202 -0
- actop-1.0.0/actop/config.py +95 -0
- actop-1.0.0/actop/export.py +175 -0
- actop-1.0.0/actop/ioreport.py +298 -0
- actop-1.0.0/actop/models.py +34 -0
- actop-1.0.0/actop/native_sys.py +643 -0
- actop-1.0.0/actop/power_scaling.py +36 -0
- actop-1.0.0/actop/sampler.py +508 -0
- actop-1.0.0/actop/smc.py +360 -0
- actop-1.0.0/actop/soc_profiles.py +201 -0
- actop-1.0.0/actop/tui/__init__.py +0 -0
- actop-1.0.0/actop/tui/app.py +434 -0
- actop-1.0.0/actop/tui/widgets.py +799 -0
- actop-1.0.0/actop/utils.py +217 -0
- actop-1.0.0/actop.egg-info/PKG-INFO +353 -0
- actop-1.0.0/actop.egg-info/SOURCES.txt +42 -0
- actop-1.0.0/actop.egg-info/dependency_links.txt +1 -0
- actop-1.0.0/actop.egg-info/entry_points.txt +2 -0
- actop-1.0.0/actop.egg-info/requires.txt +8 -0
- actop-1.0.0/actop.egg-info/top_level.txt +1 -0
- actop-1.0.0/pyproject.toml +47 -0
- actop-1.0.0/setup.cfg +4 -0
- actop-1.0.0/tests/test_api.py +91 -0
- actop-1.0.0/tests/test_braille_chart_render.py +122 -0
- actop-1.0.0/tests/test_chart_rendering.py +117 -0
- actop-1.0.0/tests/test_cli_contract.py +215 -0
- actop-1.0.0/tests/test_config.py +96 -0
- actop-1.0.0/tests/test_dashboard_metrics.py +177 -0
- actop-1.0.0/tests/test_e2e.py +42 -0
- actop-1.0.0/tests/test_export.py +138 -0
- actop-1.0.0/tests/test_integration.py +24 -0
- actop-1.0.0/tests/test_ioreport.py +24 -0
- actop-1.0.0/tests/test_native_sys.py +98 -0
- actop-1.0.0/tests/test_power_scaling.py +60 -0
- actop-1.0.0/tests/test_runtime_contracts.py +119 -0
- actop-1.0.0/tests/test_sampler.py +155 -0
- actop-1.0.0/tests/test_smc.py +48 -0
- actop-1.0.0/tests/test_soc_profiles.py +60 -0
- 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
|
+

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