macos-fanmon 0.1.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.
- macos_fanmon-0.1.0/LICENSE +21 -0
- macos_fanmon-0.1.0/PKG-INFO +254 -0
- macos_fanmon-0.1.0/README.md +209 -0
- macos_fanmon-0.1.0/fanmon/__init__.py +2 -0
- macos_fanmon-0.1.0/fanmon/__main__.py +5 -0
- macos_fanmon-0.1.0/fanmon/app.py +438 -0
- macos_fanmon-0.1.0/fanmon/cli.py +50 -0
- macos_fanmon-0.1.0/fanmon/engine.py +62 -0
- macos_fanmon-0.1.0/fanmon/fanmon.tcss +80 -0
- macos_fanmon-0.1.0/fanmon/memory.py +108 -0
- macos_fanmon-0.1.0/fanmon/procs.py +178 -0
- macos_fanmon-0.1.0/fanmon/regime.py +192 -0
- macos_fanmon-0.1.0/fanmon/render.py +192 -0
- macos_fanmon-0.1.0/fanmon/smc.py +102 -0
- macos_fanmon-0.1.0/fanmon/watchdog.py +122 -0
- macos_fanmon-0.1.0/macos_fanmon.egg-info/PKG-INFO +254 -0
- macos_fanmon-0.1.0/macos_fanmon.egg-info/SOURCES.txt +21 -0
- macos_fanmon-0.1.0/macos_fanmon.egg-info/dependency_links.txt +1 -0
- macos_fanmon-0.1.0/macos_fanmon.egg-info/entry_points.txt +2 -0
- macos_fanmon-0.1.0/macos_fanmon.egg-info/requires.txt +2 -0
- macos_fanmon-0.1.0/macos_fanmon.egg-info/top_level.txt +1 -0
- macos_fanmon-0.1.0/pyproject.toml +39 -0
- macos_fanmon-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jensen Loke
|
|
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.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: macos-fanmon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: macOS fan monitor โ why is the fan spinning, and what to close.
|
|
5
|
+
Author: Jensen Loke
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Jensen Loke
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/jensenloke/macos-fanMonitor
|
|
29
|
+
Project-URL: Repository, https://github.com/jensenloke/macos-fanMonitor
|
|
30
|
+
Project-URL: Changelog, https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/changelog.md
|
|
31
|
+
Keywords: macos,fan,monitor,tui,textual,thermal,diagnostics
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: MacOS
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Topic :: System :: Monitoring
|
|
39
|
+
Requires-Python: >=3.10
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE
|
|
42
|
+
Requires-Dist: rich>=13.0
|
|
43
|
+
Requires-Dist: textual>=0.60
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# macOS Fan Monitor (`fm`)
|
|
47
|
+
|
|
48
|
+
[](https://github.com/jensenloke/macos-fanMonitor/actions/workflows/docs.yml)
|
|
49
|
+
[](https://github.com/jensenloke/macos-fanMonitor/blob/main/LICENSE)
|
|
50
|
+
[-black)](#requirements)
|
|
51
|
+
[](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/getting-started.md)
|
|
52
|
+
[](https://textual.textualize.io/)
|
|
53
|
+
|
|
54
|
+
๐ **Full documentation:** <https://jensenloke.github.io/macos-fanMonitor/>
|
|
55
|
+
|
|
56
|
+
> **Why is my fan spinning โ and what should I close?**
|
|
57
|
+
> An interactive terminal app that answers that at a glance, without asking an LLM.
|
|
58
|
+
|
|
59
|
+
A lazygit / yazi-style **Textual** TUI. Runs natively on macOS โ **not Docker**
|
|
60
|
+
(see [why](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/how-it-works.md#the-two-regimes) Docker can't work here).
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
โ macOS Fan Monitor โโโโโโโโโโโโโโโโโโโโโโโโโโ 23:30 ยท up 18d ยท 1126 procs โ
|
|
64
|
+
โ FAN 6097 RPM โโโโโโโโโโ 93% TEMP 88ยฐC[TCMz] LOAD 15.6/10c MEM swap 98%โ
|
|
65
|
+
โ Verdict โ why is the fan spinning? โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
|
|
66
|
+
โ ๐พ Swap thrash โ fan is spinning from memory pressure, not CPU โ
|
|
67
|
+
โ load 15.6 on 10 cores but only 12% of a core busy; procs blocked on โ
|
|
68
|
+
โ disk, not computing. Close memory hogs below. โ
|
|
69
|
+
โ [ Close ] [ Processes ] [ Watchdog ] โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
|
|
70
|
+
โ # process / group RSS CPU age why โ
|
|
71
|
+
โ 1 claude@session-476111c5 x3 1626M 20% 24h frees RAM thrash โ
|
|
72
|
+
โ 2 Google x57 2120M 10% 190h frees RAM thrash โ
|
|
73
|
+
โ โฒ select a row and press [k] to SIGTERM it โ
|
|
74
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Contents
|
|
80
|
+
|
|
81
|
+
- [Why not Docker](#why-a-native-cli-instead-of-docker)
|
|
82
|
+
- [The core idea](#the-core-idea)
|
|
83
|
+
- [Install](#install)
|
|
84
|
+
- [Run & keys](#run)
|
|
85
|
+
- [Data sources](#data-sources-all-read-only)
|
|
86
|
+
- [Documentation](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/index.md) โ getting started, user guide, algorithm, roadmap, and more
|
|
87
|
+
- [Contributing](https://github.com/jensenloke/macos-fanMonitor/blob/main/CONTRIBUTING.md)
|
|
88
|
+
|
|
89
|
+
## Requirements
|
|
90
|
+
|
|
91
|
+
- **macOS** on Apple Silicon (Intel untested)
|
|
92
|
+
- **Python 3.10+**
|
|
93
|
+
- **[Stats.app](https://github.com/exelban/stats)** โ `fm` reuses its read-only
|
|
94
|
+
SMC helper to read fan RPM + temperatures, so no new privileged code. Without
|
|
95
|
+
it, fan/temp tiles are blank but everything else works.
|
|
96
|
+
|
|
97
|
+
## Why a native CLI instead of Docker
|
|
98
|
+
|
|
99
|
+
Docker Desktop on macOS runs containers inside a **Linux VM**. From inside a
|
|
100
|
+
container you **cannot** see:
|
|
101
|
+
|
|
102
|
+
- the **SMC** (fan RPM, temps) โ that's macOS IOKit, not exposed to the VM;
|
|
103
|
+
- the **macOS process list** (`ps` shows the VM's processes, not your apps);
|
|
104
|
+
- `vm_stat` / `sysctl vm.swapusage` / `memory_pressure` (macOS memory internals);
|
|
105
|
+
- the **watchdog** logs (they live on the macOS host).
|
|
106
|
+
|
|
107
|
+
Everything this tool needs is host-side, so the correct architecture is a small
|
|
108
|
+
native TUI that reads the SMC + `ps` + `vm_stat` + watchdog logs directly.
|
|
109
|
+
|
|
110
|
+
## The core idea
|
|
111
|
+
|
|
112
|
+
A spinning fan here comes from **one of two very different causes**, and the fix
|
|
113
|
+
for each is different. The app decides which one is active, then ranks what to
|
|
114
|
+
close accordingly.
|
|
115
|
+
|
|
116
|
+
| Regime | Signal | What's really happening | Fix |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| **CPU** | load high **and** measured CPU% high | something is genuinely computing | close / wait on the CPU hog |
|
|
119
|
+
| **MEMORY** (swap thrash) | load high **but** measured CPU% **low** | system ran out of RAM and is paging; processes are **blocked on disk**, not computing | close **memory** hogs to stop the thrash |
|
|
120
|
+
|
|
121
|
+
The memory regime is the sneaky one: `ps %cpu` and the watchdog's "top CPU"
|
|
122
|
+
attribution both **miss it**, because thrashing processes show low CPU. That is
|
|
123
|
+
the exact failure mode from the real incidents that motivated this tool.
|
|
124
|
+
|
|
125
|
+
### The recommendation algorithm
|
|
126
|
+
|
|
127
|
+
1. **Classify** every process: `agent` (claude / codex / omp / devin / node_repl),
|
|
128
|
+
`browser`, `chat`, `app`, or `system` (protected).
|
|
129
|
+
2. **Group** swarm siblings (all agents in one `session-โฆ`) into one batch.
|
|
130
|
+
3. **Detect the regime** from swap %, compressor ratio, RAM-free %, load vs.
|
|
131
|
+
core count, and measured CPU%.
|
|
132
|
+
4. **Score** closeable processes with regime-appropriate weights (memory regime
|
|
133
|
+
weights RAM + age; CPU regime weights CPU%), times a category prior.
|
|
134
|
+
5. **Never** recommend killing `system` daemons (WindowServer, Spotlight,
|
|
135
|
+
`suggestd`, โฆ) โ those are symptoms; those get an *advisory* instead.
|
|
136
|
+
|
|
137
|
+
Full, exact thresholds and the scoring formula:
|
|
138
|
+
[docs โ The Algorithm](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/algorithm.md).
|
|
139
|
+
|
|
140
|
+
## Install
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pipx install macos-fanmon # one command; fm lands on your PATH
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
(No pipx? `brew install pipx`, or from a clone: `./install.sh` builds a venv
|
|
147
|
+
and links `fm` into `~/.local/bin` โ make sure that is on your `PATH`.)
|
|
148
|
+
|
|
149
|
+
## Run
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
fm # interactive TUI
|
|
153
|
+
fm --once # single snapshot frame, then exit (for scripts / quick look)
|
|
154
|
+
fm --interval 3 # live refresh every 3s
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Keys (TUI)
|
|
158
|
+
|
|
159
|
+
| key | action |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `q` | quit |
|
|
162
|
+
| `r` | refresh now |
|
|
163
|
+
| `1` / `2` / `3` | sort Processes by CPU / memory / age |
|
|
164
|
+
| `k` | **SIGTERM the selected row** (asks to confirm first) |
|
|
165
|
+
| `tab` | move focus between the Close / Processes tables |
|
|
166
|
+
|
|
167
|
+
`k` re-checks each PID is still alive before sending `SIGTERM`, and shows a
|
|
168
|
+
confirm prompt โ killing stays a deliberate action, never automatic. It only ever
|
|
169
|
+
sends `SIGTERM`, never `SIGKILL`.
|
|
170
|
+
|
|
171
|
+
## Data sources (all read-only)
|
|
172
|
+
|
|
173
|
+
| Data | Source |
|
|
174
|
+
|---|---|
|
|
175
|
+
| fan RPM / target / range, temps | `/Applications/Stats.app/Contents/Resources/smc` |
|
|
176
|
+
| process list, CPU-time delta, RSS, age | `ps -axww -o pid,ppid,rss,etime,time,command` |
|
|
177
|
+
| swap / compressor / free / page I/O | `sysctl vm.swapusage`, `vm_stat`, `memory_pressure` |
|
|
178
|
+
| load / cores / uptime | `sysctl vm.loadavg`, `hw.ncpu`, `kern.boottime` |
|
|
179
|
+
| fan events, probe state, thresholds | `~/watchdogs/state/events/*.log`, `runner.log`, `watchdogs.json` |
|
|
180
|
+
|
|
181
|
+
`fm` never writes to the SMC. Sampling runs in a background worker thread so the
|
|
182
|
+
UI never blocks.
|
|
183
|
+
|
|
184
|
+
## Watchdog integration
|
|
185
|
+
|
|
186
|
+
The **Watchdog** tab reads `dev.jensen.watchdog` state (read-only): current
|
|
187
|
+
`fan-activity` probe status (OK / WARN / CRIT), trigger / re-arm thresholds from
|
|
188
|
+
`watchdogs.json`, and recent fan events with their recorded attribution โ so you
|
|
189
|
+
can correlate the live verdict against what the watchdog has been logging.
|
|
190
|
+
Details: [docs โ Watchdog Integration](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/watchdog.md).
|
|
191
|
+
|
|
192
|
+
## Documentation
|
|
193
|
+
|
|
194
|
+
The full site is built from `docs/` and published to GitHub Pages. Browse
|
|
195
|
+
[online](https://jensenloke.github.io/macos-fanMonitor/) or preview locally:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
make docs # serve at http://127.0.0.1:8000
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
| Page | What's in it |
|
|
202
|
+
|---|---|
|
|
203
|
+
| [Getting Started](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/getting-started.md) | install & first run |
|
|
204
|
+
| [User Guide](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/user-guide.md) | every screen, tile, and key |
|
|
205
|
+
| [How It Works](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/how-it-works.md) | the two-regime diagnosis logic |
|
|
206
|
+
| [The Algorithm](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/algorithm.md) | exact thresholds & scoring |
|
|
207
|
+
| [Watchdog Integration](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/watchdog.md) | correlating with your watchdog |
|
|
208
|
+
| [Troubleshooting](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/troubleshooting.md) | common issues |
|
|
209
|
+
| [Roadmap](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/roadmap.md) | shipped / planned / won't-do |
|
|
210
|
+
| [Contributing](https://github.com/jensenloke/macos-fanMonitor/blob/main/CONTRIBUTING.md) | dev setup, tests, safety rules |
|
|
211
|
+
| [Changelog](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/changelog.md) | release history |
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
make test # headless TUI smoke test (Textual run_test pilot)
|
|
217
|
+
make docs-build # strict docs build (what CI runs)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`smoke_test.py` drives the app headless via Textual's `run_test()`: it asserts the
|
|
221
|
+
Close / Processes / Watchdog tables populate, the `1/2/3` sort keys change the
|
|
222
|
+
sort, and `k` opens the confirm modal from both tables (and declines cleanly).
|
|
223
|
+
|
|
224
|
+
## Layout
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
macOS-fanMonitor/
|
|
228
|
+
pyproject.toml # PyPI packaging: `fm` console script, package data
|
|
229
|
+
fm # launcher -> .venv/bin/python -m fanmon (dev clone)
|
|
230
|
+
Makefile # run / once / test / docs / docs-build / clean
|
|
231
|
+
install.sh # venv + deps + PATH link (dev clone)
|
|
232
|
+
scripts/verify-package.sh # wheel build + clean-venv install rehearsal
|
|
233
|
+
requirements.txt # rich, textual (dev clone)
|
|
234
|
+
requirements-docs.txt # mkdocs-material
|
|
235
|
+
smoke_test.py # headless TUI test
|
|
236
|
+
mkdocs.yml # docs site config
|
|
237
|
+
docs/ # documentation site
|
|
238
|
+
fanmon/
|
|
239
|
+
__main__.py # python -m fanmon
|
|
240
|
+
cli.py # entry: default = TUI, --once = snapshot
|
|
241
|
+
app.py # Textual App: gauges, tabs, kill, sort
|
|
242
|
+
fanmon.tcss # Textual stylesheet
|
|
243
|
+
engine.py # shared sampler (snapshot dict)
|
|
244
|
+
smc.py # fan + temperature sensors
|
|
245
|
+
procs.py # process snapshot + CPU delta + classification
|
|
246
|
+
memory.py # swap / compressor / pressure / load / uptime
|
|
247
|
+
regime.py # verdict + recommendation algorithm
|
|
248
|
+
watchdog.py # read-only watchdog log/config parsing
|
|
249
|
+
render.py # rich layout used by --once
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT โ see [LICENSE](https://github.com/jensenloke/macos-fanMonitor/blob/main/LICENSE).
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# macOS Fan Monitor (`fm`)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jensenloke/macos-fanMonitor/actions/workflows/docs.yml)
|
|
4
|
+
[](https://github.com/jensenloke/macos-fanMonitor/blob/main/LICENSE)
|
|
5
|
+
[-black)](#requirements)
|
|
6
|
+
[](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/getting-started.md)
|
|
7
|
+
[](https://textual.textualize.io/)
|
|
8
|
+
|
|
9
|
+
๐ **Full documentation:** <https://jensenloke.github.io/macos-fanMonitor/>
|
|
10
|
+
|
|
11
|
+
> **Why is my fan spinning โ and what should I close?**
|
|
12
|
+
> An interactive terminal app that answers that at a glance, without asking an LLM.
|
|
13
|
+
|
|
14
|
+
A lazygit / yazi-style **Textual** TUI. Runs natively on macOS โ **not Docker**
|
|
15
|
+
(see [why](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/how-it-works.md#the-two-regimes) Docker can't work here).
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
โ macOS Fan Monitor โโโโโโโโโโโโโโโโโโโโโโโโโโ 23:30 ยท up 18d ยท 1126 procs โ
|
|
19
|
+
โ FAN 6097 RPM โโโโโโโโโโ 93% TEMP 88ยฐC[TCMz] LOAD 15.6/10c MEM swap 98%โ
|
|
20
|
+
โ Verdict โ why is the fan spinning? โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
|
|
21
|
+
โ ๐พ Swap thrash โ fan is spinning from memory pressure, not CPU โ
|
|
22
|
+
โ load 15.6 on 10 cores but only 12% of a core busy; procs blocked on โ
|
|
23
|
+
โ disk, not computing. Close memory hogs below. โ
|
|
24
|
+
โ [ Close ] [ Processes ] [ Watchdog ] โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
|
|
25
|
+
โ # process / group RSS CPU age why โ
|
|
26
|
+
โ 1 claude@session-476111c5 x3 1626M 20% 24h frees RAM thrash โ
|
|
27
|
+
โ 2 Google x57 2120M 10% 190h frees RAM thrash โ
|
|
28
|
+
โ โฒ select a row and press [k] to SIGTERM it โ
|
|
29
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Contents
|
|
35
|
+
|
|
36
|
+
- [Why not Docker](#why-a-native-cli-instead-of-docker)
|
|
37
|
+
- [The core idea](#the-core-idea)
|
|
38
|
+
- [Install](#install)
|
|
39
|
+
- [Run & keys](#run)
|
|
40
|
+
- [Data sources](#data-sources-all-read-only)
|
|
41
|
+
- [Documentation](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/index.md) โ getting started, user guide, algorithm, roadmap, and more
|
|
42
|
+
- [Contributing](https://github.com/jensenloke/macos-fanMonitor/blob/main/CONTRIBUTING.md)
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- **macOS** on Apple Silicon (Intel untested)
|
|
47
|
+
- **Python 3.10+**
|
|
48
|
+
- **[Stats.app](https://github.com/exelban/stats)** โ `fm` reuses its read-only
|
|
49
|
+
SMC helper to read fan RPM + temperatures, so no new privileged code. Without
|
|
50
|
+
it, fan/temp tiles are blank but everything else works.
|
|
51
|
+
|
|
52
|
+
## Why a native CLI instead of Docker
|
|
53
|
+
|
|
54
|
+
Docker Desktop on macOS runs containers inside a **Linux VM**. From inside a
|
|
55
|
+
container you **cannot** see:
|
|
56
|
+
|
|
57
|
+
- the **SMC** (fan RPM, temps) โ that's macOS IOKit, not exposed to the VM;
|
|
58
|
+
- the **macOS process list** (`ps` shows the VM's processes, not your apps);
|
|
59
|
+
- `vm_stat` / `sysctl vm.swapusage` / `memory_pressure` (macOS memory internals);
|
|
60
|
+
- the **watchdog** logs (they live on the macOS host).
|
|
61
|
+
|
|
62
|
+
Everything this tool needs is host-side, so the correct architecture is a small
|
|
63
|
+
native TUI that reads the SMC + `ps` + `vm_stat` + watchdog logs directly.
|
|
64
|
+
|
|
65
|
+
## The core idea
|
|
66
|
+
|
|
67
|
+
A spinning fan here comes from **one of two very different causes**, and the fix
|
|
68
|
+
for each is different. The app decides which one is active, then ranks what to
|
|
69
|
+
close accordingly.
|
|
70
|
+
|
|
71
|
+
| Regime | Signal | What's really happening | Fix |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| **CPU** | load high **and** measured CPU% high | something is genuinely computing | close / wait on the CPU hog |
|
|
74
|
+
| **MEMORY** (swap thrash) | load high **but** measured CPU% **low** | system ran out of RAM and is paging; processes are **blocked on disk**, not computing | close **memory** hogs to stop the thrash |
|
|
75
|
+
|
|
76
|
+
The memory regime is the sneaky one: `ps %cpu` and the watchdog's "top CPU"
|
|
77
|
+
attribution both **miss it**, because thrashing processes show low CPU. That is
|
|
78
|
+
the exact failure mode from the real incidents that motivated this tool.
|
|
79
|
+
|
|
80
|
+
### The recommendation algorithm
|
|
81
|
+
|
|
82
|
+
1. **Classify** every process: `agent` (claude / codex / omp / devin / node_repl),
|
|
83
|
+
`browser`, `chat`, `app`, or `system` (protected).
|
|
84
|
+
2. **Group** swarm siblings (all agents in one `session-โฆ`) into one batch.
|
|
85
|
+
3. **Detect the regime** from swap %, compressor ratio, RAM-free %, load vs.
|
|
86
|
+
core count, and measured CPU%.
|
|
87
|
+
4. **Score** closeable processes with regime-appropriate weights (memory regime
|
|
88
|
+
weights RAM + age; CPU regime weights CPU%), times a category prior.
|
|
89
|
+
5. **Never** recommend killing `system` daemons (WindowServer, Spotlight,
|
|
90
|
+
`suggestd`, โฆ) โ those are symptoms; those get an *advisory* instead.
|
|
91
|
+
|
|
92
|
+
Full, exact thresholds and the scoring formula:
|
|
93
|
+
[docs โ The Algorithm](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/algorithm.md).
|
|
94
|
+
|
|
95
|
+
## Install
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pipx install macos-fanmon # one command; fm lands on your PATH
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
(No pipx? `brew install pipx`, or from a clone: `./install.sh` builds a venv
|
|
102
|
+
and links `fm` into `~/.local/bin` โ make sure that is on your `PATH`.)
|
|
103
|
+
|
|
104
|
+
## Run
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
fm # interactive TUI
|
|
108
|
+
fm --once # single snapshot frame, then exit (for scripts / quick look)
|
|
109
|
+
fm --interval 3 # live refresh every 3s
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Keys (TUI)
|
|
113
|
+
|
|
114
|
+
| key | action |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `q` | quit |
|
|
117
|
+
| `r` | refresh now |
|
|
118
|
+
| `1` / `2` / `3` | sort Processes by CPU / memory / age |
|
|
119
|
+
| `k` | **SIGTERM the selected row** (asks to confirm first) |
|
|
120
|
+
| `tab` | move focus between the Close / Processes tables |
|
|
121
|
+
|
|
122
|
+
`k` re-checks each PID is still alive before sending `SIGTERM`, and shows a
|
|
123
|
+
confirm prompt โ killing stays a deliberate action, never automatic. It only ever
|
|
124
|
+
sends `SIGTERM`, never `SIGKILL`.
|
|
125
|
+
|
|
126
|
+
## Data sources (all read-only)
|
|
127
|
+
|
|
128
|
+
| Data | Source |
|
|
129
|
+
|---|---|
|
|
130
|
+
| fan RPM / target / range, temps | `/Applications/Stats.app/Contents/Resources/smc` |
|
|
131
|
+
| process list, CPU-time delta, RSS, age | `ps -axww -o pid,ppid,rss,etime,time,command` |
|
|
132
|
+
| swap / compressor / free / page I/O | `sysctl vm.swapusage`, `vm_stat`, `memory_pressure` |
|
|
133
|
+
| load / cores / uptime | `sysctl vm.loadavg`, `hw.ncpu`, `kern.boottime` |
|
|
134
|
+
| fan events, probe state, thresholds | `~/watchdogs/state/events/*.log`, `runner.log`, `watchdogs.json` |
|
|
135
|
+
|
|
136
|
+
`fm` never writes to the SMC. Sampling runs in a background worker thread so the
|
|
137
|
+
UI never blocks.
|
|
138
|
+
|
|
139
|
+
## Watchdog integration
|
|
140
|
+
|
|
141
|
+
The **Watchdog** tab reads `dev.jensen.watchdog` state (read-only): current
|
|
142
|
+
`fan-activity` probe status (OK / WARN / CRIT), trigger / re-arm thresholds from
|
|
143
|
+
`watchdogs.json`, and recent fan events with their recorded attribution โ so you
|
|
144
|
+
can correlate the live verdict against what the watchdog has been logging.
|
|
145
|
+
Details: [docs โ Watchdog Integration](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/watchdog.md).
|
|
146
|
+
|
|
147
|
+
## Documentation
|
|
148
|
+
|
|
149
|
+
The full site is built from `docs/` and published to GitHub Pages. Browse
|
|
150
|
+
[online](https://jensenloke.github.io/macos-fanMonitor/) or preview locally:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
make docs # serve at http://127.0.0.1:8000
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Page | What's in it |
|
|
157
|
+
|---|---|
|
|
158
|
+
| [Getting Started](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/getting-started.md) | install & first run |
|
|
159
|
+
| [User Guide](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/user-guide.md) | every screen, tile, and key |
|
|
160
|
+
| [How It Works](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/how-it-works.md) | the two-regime diagnosis logic |
|
|
161
|
+
| [The Algorithm](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/algorithm.md) | exact thresholds & scoring |
|
|
162
|
+
| [Watchdog Integration](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/watchdog.md) | correlating with your watchdog |
|
|
163
|
+
| [Troubleshooting](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/troubleshooting.md) | common issues |
|
|
164
|
+
| [Roadmap](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/roadmap.md) | shipped / planned / won't-do |
|
|
165
|
+
| [Contributing](https://github.com/jensenloke/macos-fanMonitor/blob/main/CONTRIBUTING.md) | dev setup, tests, safety rules |
|
|
166
|
+
| [Changelog](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/changelog.md) | release history |
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
make test # headless TUI smoke test (Textual run_test pilot)
|
|
172
|
+
make docs-build # strict docs build (what CI runs)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`smoke_test.py` drives the app headless via Textual's `run_test()`: it asserts the
|
|
176
|
+
Close / Processes / Watchdog tables populate, the `1/2/3` sort keys change the
|
|
177
|
+
sort, and `k` opens the confirm modal from both tables (and declines cleanly).
|
|
178
|
+
|
|
179
|
+
## Layout
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
macOS-fanMonitor/
|
|
183
|
+
pyproject.toml # PyPI packaging: `fm` console script, package data
|
|
184
|
+
fm # launcher -> .venv/bin/python -m fanmon (dev clone)
|
|
185
|
+
Makefile # run / once / test / docs / docs-build / clean
|
|
186
|
+
install.sh # venv + deps + PATH link (dev clone)
|
|
187
|
+
scripts/verify-package.sh # wheel build + clean-venv install rehearsal
|
|
188
|
+
requirements.txt # rich, textual (dev clone)
|
|
189
|
+
requirements-docs.txt # mkdocs-material
|
|
190
|
+
smoke_test.py # headless TUI test
|
|
191
|
+
mkdocs.yml # docs site config
|
|
192
|
+
docs/ # documentation site
|
|
193
|
+
fanmon/
|
|
194
|
+
__main__.py # python -m fanmon
|
|
195
|
+
cli.py # entry: default = TUI, --once = snapshot
|
|
196
|
+
app.py # Textual App: gauges, tabs, kill, sort
|
|
197
|
+
fanmon.tcss # Textual stylesheet
|
|
198
|
+
engine.py # shared sampler (snapshot dict)
|
|
199
|
+
smc.py # fan + temperature sensors
|
|
200
|
+
procs.py # process snapshot + CPU delta + classification
|
|
201
|
+
memory.py # swap / compressor / pressure / load / uptime
|
|
202
|
+
regime.py # verdict + recommendation algorithm
|
|
203
|
+
watchdog.py # read-only watchdog log/config parsing
|
|
204
|
+
render.py # rich layout used by --once
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT โ see [LICENSE](https://github.com/jensenloke/macos-fanMonitor/blob/main/LICENSE).
|