macverdict 0.2.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.
- macverdict-0.2.0/.gitignore +6 -0
- macverdict-0.2.0/LICENSE +21 -0
- macverdict-0.2.0/PKG-INFO +227 -0
- macverdict-0.2.0/README.md +203 -0
- macverdict-0.2.0/pyproject.toml +44 -0
- macverdict-0.2.0/src/macverdict/__init__.py +8 -0
- macverdict-0.2.0/src/macverdict/__main__.py +3 -0
- macverdict-0.2.0/src/macverdict/checks.py +147 -0
- macverdict-0.2.0/src/macverdict/cli.py +336 -0
- macverdict-0.2.0/src/macverdict/collectors.py +245 -0
- macverdict-0.2.0/src/macverdict/fmt.py +37 -0
- macverdict-0.2.0/src/macverdict/insights.py +434 -0
- macverdict-0.2.0/src/macverdict/notifier/MacverdictNotifier.swift +121 -0
- macverdict-0.2.0/src/macverdict/notifier/__init__.py +129 -0
- macverdict-0.2.0/src/macverdict/report.py +221 -0
- macverdict-0.2.0/src/macverdict/scheduler.py +140 -0
- macverdict-0.2.0/src/macverdict/storage.py +144 -0
- macverdict-0.2.0/tests/conftest.py +25 -0
- macverdict-0.2.0/tests/test_insights.py +199 -0
- macverdict-0.2.0/tests/test_macverdict.py +228 -0
macverdict-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anirudh Jayaraman
|
|
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,227 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: macverdict
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Stop watching graphs: macverdict tells you why your Mac was slow this week and what to do about it. Slowdown blame, RAM verdict, battery forecast and MacBook Air throttling, from quiet background sampling.
|
|
5
|
+
Project-URL: Homepage, https://github.com/anirudhjayaraman/macverdict
|
|
6
|
+
Project-URL: Source, https://github.com/anirudhjayaraman/macverdict
|
|
7
|
+
Project-URL: Issues, https://github.com/anirudhjayaraman/macverdict/issues
|
|
8
|
+
Author: Anirudh Jayaraman
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: battery,diagnostics,launchd,macbook,macos,monitoring,performance
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: System :: Monitoring
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: psutil>=5.9
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# macverdict
|
|
26
|
+
|
|
27
|
+
**Stop watching graphs. macverdict tells you why your Mac was slow this week, and what to do about it.**
|
|
28
|
+
|
|
29
|
+
*Formerly called macpulse. If you installed it under that name, run `macverdict setup` once: it brings your history across and removes the old parts.*
|
|
30
|
+
|
|
31
|
+
Activity Monitor, Stats and iStat Menus show you numbers *right now*, and leave the diagnosis to you.
|
|
32
|
+
macverdict quietly samples your Mac in the background (about one second every 15 minutes) and turns
|
|
33
|
+
weeks of history into four plain answers:
|
|
34
|
+
|
|
35
|
+
1. **What's slowing me down?** It blames the app that was actually the biggest memory or CPU user
|
|
36
|
+
each time your Mac struggled, grouping helper processes so Chrome's 60 processes count as Chrome.
|
|
37
|
+
2. **Do I need more RAM?** A verdict from weeks of memory pressure and swap, not the misleading
|
|
38
|
+
"memory used" figure, including what to buy next time.
|
|
39
|
+
3. **How long will my battery last?** Your real wear rate vs. Apple's rating, and roughly when you'll
|
|
40
|
+
hit 80% capacity.
|
|
41
|
+
4. **Is my Air overheating?** How often the fanless MacBook Air slowed itself down to cool off, and
|
|
42
|
+
which apps were running when it did.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
$ macverdict insights # illustration: three weeks on an 8 GB MacBook Air
|
|
46
|
+
macverdict insights · MacBook Air · 8.0 GB RAM · last 30 days
|
|
47
|
+
|
|
48
|
+
● What's slowing you down
|
|
49
|
+
Google Chrome was the biggest memory user in 85% of your 794 slowdown moments.
|
|
50
|
+
· Google Chrome: the biggest memory user in 85% of slowdowns, typically 5.0 GB.
|
|
51
|
+
· Final Cut Pro: the biggest CPU user in 15% of slowdowns.
|
|
52
|
+
· Biggest win: quit or trim Google Chrome when your Mac feels slow.
|
|
53
|
+
|
|
54
|
+
● Do you need more RAM?
|
|
55
|
+
8 GB isn't enough for how you work. Your next Mac should have 16 GB.
|
|
56
|
+
· Memory pressure was elevated in 33% of samples.
|
|
57
|
+
· Swap: typically under 9.0 GB, peak 13.5 GB.
|
|
58
|
+
|
|
59
|
+
● How long will your battery last?
|
|
60
|
+
At your pace, it reaches 80% capacity around May 2027.
|
|
61
|
+
· It's wearing faster than Apple's rating: 3.8% of capacity per 100 cycles, vs 2% expected.
|
|
62
|
+
|
|
63
|
+
● Is your Air overheating?
|
|
64
|
+
Your Mac slowed itself down to cool off in 6% of samples.
|
|
65
|
+
· Busiest apps when it ran hot: Final Cut Pro.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## See it on a real Mac
|
|
69
|
+
|
|
70
|
+
A real week on a MacBook Air (Apple M5, 16 GB). `macverdict report` turns the history into a page
|
|
71
|
+
like this. **[Open the full sample report](https://anirudhjayaraman.github.io/macverdict/sample-report.html)**.
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
The trends make the story visible: memory pressure (shaded) on Mon 19 and Tue 20, matching the
|
|
76
|
+
swap spike, then a quiet rest of the week.
|
|
77
|
+
|
|
78
|
+

|
|
79
|
+
|
|
80
|
+
Free and open source, one dependency (`psutil`), no sudo, no cloud: nothing leaves your Mac.
|
|
81
|
+
Built for Apple-silicon MacBooks, works on any Mac running macOS 12+.
|
|
82
|
+
|
|
83
|
+
## How it compares
|
|
84
|
+
|
|
85
|
+
| | Live stats | Keeps history | Tells you *why* & what to do | Price |
|
|
86
|
+
|---|---|---|---|---|
|
|
87
|
+
| Activity Monitor (built in) | ✅ | ❌ | ❌ | Free |
|
|
88
|
+
| [Stats](https://github.com/exelban/stats) | ✅ menu bar | ❌ | ❌ | Free |
|
|
89
|
+
| [iStat Menus](https://bjango.com/mac/istatmenus/) | ✅ very detailed | Some | Rule-based alerts | Paid |
|
|
90
|
+
| [macmon](https://github.com/vladkens/macmon) / [mactop](https://github.com/anemll/mactop) | ✅ power & GPU | ❌ | ❌ | Free |
|
|
91
|
+
| [Glances](https://github.com/nicolargo/glances) | ✅ any OS | Short buffer / export | ❌ | Free |
|
|
92
|
+
| [Mole](https://github.com/tw93/mole) `mo status` | ✅ | ❌ | Live health score | Free |
|
|
93
|
+
| [coconutBattery](https://www.coconut-flavour.com/coconutbattery/) | Battery only | ✅ battery | ❌ | Free / paid |
|
|
94
|
+
| **macverdict** | Snapshot (`macverdict`) | ✅ everything | ✅ **blame, RAM verdict, battery forecast, throttling** | **Free** |
|
|
95
|
+
|
|
96
|
+
**Use macverdict alongside a live monitor, not instead of one.** If you want to watch GPU power or fan
|
|
97
|
+
speed in real time, Stats and macmon are excellent. macverdict answers the questions those tools can't:
|
|
98
|
+
what happened over the last few weeks, and what you should change.
|
|
99
|
+
|
|
100
|
+
## Everyday health check
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
$ macverdict
|
|
104
|
+
macverdict · Apple M5 · macOS 26.5.2 · 16.0 GB RAM · up 1d 17h
|
|
105
|
+
|
|
106
|
+
Health score: 100 / 100
|
|
107
|
+
|
|
108
|
+
OK cpu CPU 6%, load 1.9 on 10 cores.
|
|
109
|
+
OK memory Memory pressure normal, 66% used, swap 0 B.
|
|
110
|
+
OK disk 698.7 GB free (75%).
|
|
111
|
+
OK battery 52% charged, capacity 100%, 71 cycles, 30.6°C.
|
|
112
|
+
OK thermal No thermal throttling.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## What it checks
|
|
116
|
+
|
|
117
|
+
| Area | Signals | Why it matters |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| CPU | usage, 5-min load vs. core count, runaway processes | sustained load means work is queuing |
|
|
120
|
+
| Memory | **macOS memory pressure** (the real signal, not "% used"), swap, biggest apps | the #1 cause of slowdowns on 8–16 GB Macs |
|
|
121
|
+
| Disk | free space on the Data volume | macOS needs room for swap and updates |
|
|
122
|
+
| Battery | maximum capacity, cycle count, temperature | <80% capacity = service territory; heat = throttling |
|
|
123
|
+
| Thermal | macOS's own thermal state, plus CPU speed limit | fanless Airs slow themselves down when hot |
|
|
124
|
+
| Uptime | days since restart | long uptimes accumulate swap and leaks |
|
|
125
|
+
|
|
126
|
+
Each finding comes with plain-English advice, and everything rolls up into a 0–100 health score.
|
|
127
|
+
|
|
128
|
+
## Install
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
brew install anirudhjayaraman/tap/macverdict
|
|
132
|
+
macverdict setup
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
That's it. `macverdict setup` starts background recording, builds the small alert app and sends a
|
|
136
|
+
test alert (click **Allow** if macOS asks). Then run `macverdict` any time.
|
|
137
|
+
|
|
138
|
+
To remove it: `macverdict uninstall`, then `brew uninstall macverdict`.
|
|
139
|
+
|
|
140
|
+
<details>
|
|
141
|
+
<summary>Not using Homebrew?</summary>
|
|
142
|
+
|
|
143
|
+
Install with [uv](https://docs.astral.sh/uv/) or pipx instead, then run `macverdict setup`:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
uv tool install git+https://github.com/anirudhjayaraman/macverdict
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
</details>
|
|
150
|
+
|
|
151
|
+
Needs macOS 12 or later. Clickable alerts need Apple's Command Line Tools (`xcode-select --install`);
|
|
152
|
+
without them, everything else still works.
|
|
153
|
+
|
|
154
|
+
## Use
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
macverdict # diagnose right now
|
|
158
|
+
macverdict insights # blame, RAM verdict, battery forecast, throttling
|
|
159
|
+
macverdict report # HTML report with insights and trends, opens in your browser
|
|
160
|
+
macverdict history --days 30 # min/avg/max summary in the terminal
|
|
161
|
+
macverdict export history.csv # take your data to a spreadsheet or notebook
|
|
162
|
+
macverdict check --json # machine-readable snapshot
|
|
163
|
+
macverdict setup --every 5 # record every 5 minutes instead of 15
|
|
164
|
+
macverdict notify-test # check that alerts reach you
|
|
165
|
+
macverdict uninstall # stop background recording and remove the alert app
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The background agent shows up as **macverdict** in System Settings → General → Login Items &
|
|
169
|
+
Extensions (or as `python3` if the alert app couldn't be built).
|
|
170
|
+
macOS labels it "unidentified developer" because it's signed on your own Mac rather than with a
|
|
171
|
+
paid Apple Developer ID, which is normal for open-source command-line tools. Switching it off
|
|
172
|
+
there stops recording.
|
|
173
|
+
|
|
174
|
+
The background agent runs at low priority, survives reboots, and shows a macOS notification only
|
|
175
|
+
when something is critical (e.g. memory pressure critical, battery overheating, disk nearly full).
|
|
176
|
+
|
|
177
|
+
## Notifications
|
|
178
|
+
|
|
179
|
+
The background agent only alerts you when something is critical, and clicking an alert opens your
|
|
180
|
+
macverdict report. `macverdict setup` builds the small alert app that makes this work.
|
|
181
|
+
|
|
182
|
+
It's built from source on your machine (`src/macverdict/notifier/MacverdictNotifier.swift`, about
|
|
183
|
+
100 lines) and signed locally, so there's no downloaded binary and no Apple account involved.
|
|
184
|
+
It has no Dock icon and only runs for a few seconds when posting an alert or handling a click.
|
|
185
|
+
|
|
186
|
+
Without it, macverdict falls back to macOS's built-in `osascript`. Those alerts show as coming
|
|
187
|
+
from **Script Editor**, and clicking one opens Script Editor. If no banner appears at all, check
|
|
188
|
+
System Settings → Notifications (macverdict or Script Editor) and that no Focus mode is on.
|
|
189
|
+
|
|
190
|
+
## Use it from Python
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from macverdict import collect, evaluate, health_score, insights, storage
|
|
194
|
+
|
|
195
|
+
snap = collect()
|
|
196
|
+
findings = evaluate(snap)
|
|
197
|
+
print(health_score(findings), [f.message for f in findings if f.level != "ok"])
|
|
198
|
+
|
|
199
|
+
rows = storage.load(storage.connect(), days=30, full=True)
|
|
200
|
+
for i in insights.build(rows, snap["system"]["memory_total"], insights.hardware_model()):
|
|
201
|
+
print(i.title, "→", i.headline)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Where data lives
|
|
205
|
+
|
|
206
|
+
`~/Library/Application Support/macverdict/` holds `history.db` (SQLite), `report.html` and the agent
|
|
207
|
+
logs. Samples older than 90 days are pruned automatically. Set `MACVERDICT_HOME` to use another folder.
|
|
208
|
+
|
|
209
|
+
## How it works
|
|
210
|
+
|
|
211
|
+
- `psutil` for CPU, memory, swap, disk, network and processes
|
|
212
|
+
- `sysctl kern.memorystatus_vm_pressure_level` for macOS memory pressure
|
|
213
|
+
- `ioreg -rn AppleSmartBattery` for battery capacity, cycles and temperature
|
|
214
|
+
- `NSProcessInfo.thermalState` (read via ctypes, no PyObjC) and `pmset -g therm` for throttling
|
|
215
|
+
- a per-user `launchd` agent for scheduling
|
|
216
|
+
- a tiny Swift app (built locally) for clickable notifications, with `osascript` as a fallback
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
uv venv && uv pip install -e ".[dev]"
|
|
222
|
+
.venv/bin/pytest
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# macverdict
|
|
2
|
+
|
|
3
|
+
**Stop watching graphs. macverdict tells you why your Mac was slow this week, and what to do about it.**
|
|
4
|
+
|
|
5
|
+
*Formerly called macpulse. If you installed it under that name, run `macverdict setup` once: it brings your history across and removes the old parts.*
|
|
6
|
+
|
|
7
|
+
Activity Monitor, Stats and iStat Menus show you numbers *right now*, and leave the diagnosis to you.
|
|
8
|
+
macverdict quietly samples your Mac in the background (about one second every 15 minutes) and turns
|
|
9
|
+
weeks of history into four plain answers:
|
|
10
|
+
|
|
11
|
+
1. **What's slowing me down?** It blames the app that was actually the biggest memory or CPU user
|
|
12
|
+
each time your Mac struggled, grouping helper processes so Chrome's 60 processes count as Chrome.
|
|
13
|
+
2. **Do I need more RAM?** A verdict from weeks of memory pressure and swap, not the misleading
|
|
14
|
+
"memory used" figure, including what to buy next time.
|
|
15
|
+
3. **How long will my battery last?** Your real wear rate vs. Apple's rating, and roughly when you'll
|
|
16
|
+
hit 80% capacity.
|
|
17
|
+
4. **Is my Air overheating?** How often the fanless MacBook Air slowed itself down to cool off, and
|
|
18
|
+
which apps were running when it did.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
$ macverdict insights # illustration: three weeks on an 8 GB MacBook Air
|
|
22
|
+
macverdict insights · MacBook Air · 8.0 GB RAM · last 30 days
|
|
23
|
+
|
|
24
|
+
● What's slowing you down
|
|
25
|
+
Google Chrome was the biggest memory user in 85% of your 794 slowdown moments.
|
|
26
|
+
· Google Chrome: the biggest memory user in 85% of slowdowns, typically 5.0 GB.
|
|
27
|
+
· Final Cut Pro: the biggest CPU user in 15% of slowdowns.
|
|
28
|
+
· Biggest win: quit or trim Google Chrome when your Mac feels slow.
|
|
29
|
+
|
|
30
|
+
● Do you need more RAM?
|
|
31
|
+
8 GB isn't enough for how you work. Your next Mac should have 16 GB.
|
|
32
|
+
· Memory pressure was elevated in 33% of samples.
|
|
33
|
+
· Swap: typically under 9.0 GB, peak 13.5 GB.
|
|
34
|
+
|
|
35
|
+
● How long will your battery last?
|
|
36
|
+
At your pace, it reaches 80% capacity around May 2027.
|
|
37
|
+
· It's wearing faster than Apple's rating: 3.8% of capacity per 100 cycles, vs 2% expected.
|
|
38
|
+
|
|
39
|
+
● Is your Air overheating?
|
|
40
|
+
Your Mac slowed itself down to cool off in 6% of samples.
|
|
41
|
+
· Busiest apps when it ran hot: Final Cut Pro.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## See it on a real Mac
|
|
45
|
+
|
|
46
|
+
A real week on a MacBook Air (Apple M5, 16 GB). `macverdict report` turns the history into a page
|
|
47
|
+
like this. **[Open the full sample report](https://anirudhjayaraman.github.io/macverdict/sample-report.html)**.
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
The trends make the story visible: memory pressure (shaded) on Mon 19 and Tue 20, matching the
|
|
52
|
+
swap spike, then a quiet rest of the week.
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
Free and open source, one dependency (`psutil`), no sudo, no cloud: nothing leaves your Mac.
|
|
57
|
+
Built for Apple-silicon MacBooks, works on any Mac running macOS 12+.
|
|
58
|
+
|
|
59
|
+
## How it compares
|
|
60
|
+
|
|
61
|
+
| | Live stats | Keeps history | Tells you *why* & what to do | Price |
|
|
62
|
+
|---|---|---|---|---|
|
|
63
|
+
| Activity Monitor (built in) | ✅ | ❌ | ❌ | Free |
|
|
64
|
+
| [Stats](https://github.com/exelban/stats) | ✅ menu bar | ❌ | ❌ | Free |
|
|
65
|
+
| [iStat Menus](https://bjango.com/mac/istatmenus/) | ✅ very detailed | Some | Rule-based alerts | Paid |
|
|
66
|
+
| [macmon](https://github.com/vladkens/macmon) / [mactop](https://github.com/anemll/mactop) | ✅ power & GPU | ❌ | ❌ | Free |
|
|
67
|
+
| [Glances](https://github.com/nicolargo/glances) | ✅ any OS | Short buffer / export | ❌ | Free |
|
|
68
|
+
| [Mole](https://github.com/tw93/mole) `mo status` | ✅ | ❌ | Live health score | Free |
|
|
69
|
+
| [coconutBattery](https://www.coconut-flavour.com/coconutbattery/) | Battery only | ✅ battery | ❌ | Free / paid |
|
|
70
|
+
| **macverdict** | Snapshot (`macverdict`) | ✅ everything | ✅ **blame, RAM verdict, battery forecast, throttling** | **Free** |
|
|
71
|
+
|
|
72
|
+
**Use macverdict alongside a live monitor, not instead of one.** If you want to watch GPU power or fan
|
|
73
|
+
speed in real time, Stats and macmon are excellent. macverdict answers the questions those tools can't:
|
|
74
|
+
what happened over the last few weeks, and what you should change.
|
|
75
|
+
|
|
76
|
+
## Everyday health check
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
$ macverdict
|
|
80
|
+
macverdict · Apple M5 · macOS 26.5.2 · 16.0 GB RAM · up 1d 17h
|
|
81
|
+
|
|
82
|
+
Health score: 100 / 100
|
|
83
|
+
|
|
84
|
+
OK cpu CPU 6%, load 1.9 on 10 cores.
|
|
85
|
+
OK memory Memory pressure normal, 66% used, swap 0 B.
|
|
86
|
+
OK disk 698.7 GB free (75%).
|
|
87
|
+
OK battery 52% charged, capacity 100%, 71 cycles, 30.6°C.
|
|
88
|
+
OK thermal No thermal throttling.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## What it checks
|
|
92
|
+
|
|
93
|
+
| Area | Signals | Why it matters |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| CPU | usage, 5-min load vs. core count, runaway processes | sustained load means work is queuing |
|
|
96
|
+
| Memory | **macOS memory pressure** (the real signal, not "% used"), swap, biggest apps | the #1 cause of slowdowns on 8–16 GB Macs |
|
|
97
|
+
| Disk | free space on the Data volume | macOS needs room for swap and updates |
|
|
98
|
+
| Battery | maximum capacity, cycle count, temperature | <80% capacity = service territory; heat = throttling |
|
|
99
|
+
| Thermal | macOS's own thermal state, plus CPU speed limit | fanless Airs slow themselves down when hot |
|
|
100
|
+
| Uptime | days since restart | long uptimes accumulate swap and leaks |
|
|
101
|
+
|
|
102
|
+
Each finding comes with plain-English advice, and everything rolls up into a 0–100 health score.
|
|
103
|
+
|
|
104
|
+
## Install
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
brew install anirudhjayaraman/tap/macverdict
|
|
108
|
+
macverdict setup
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
That's it. `macverdict setup` starts background recording, builds the small alert app and sends a
|
|
112
|
+
test alert (click **Allow** if macOS asks). Then run `macverdict` any time.
|
|
113
|
+
|
|
114
|
+
To remove it: `macverdict uninstall`, then `brew uninstall macverdict`.
|
|
115
|
+
|
|
116
|
+
<details>
|
|
117
|
+
<summary>Not using Homebrew?</summary>
|
|
118
|
+
|
|
119
|
+
Install with [uv](https://docs.astral.sh/uv/) or pipx instead, then run `macverdict setup`:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
uv tool install git+https://github.com/anirudhjayaraman/macverdict
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
</details>
|
|
126
|
+
|
|
127
|
+
Needs macOS 12 or later. Clickable alerts need Apple's Command Line Tools (`xcode-select --install`);
|
|
128
|
+
without them, everything else still works.
|
|
129
|
+
|
|
130
|
+
## Use
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
macverdict # diagnose right now
|
|
134
|
+
macverdict insights # blame, RAM verdict, battery forecast, throttling
|
|
135
|
+
macverdict report # HTML report with insights and trends, opens in your browser
|
|
136
|
+
macverdict history --days 30 # min/avg/max summary in the terminal
|
|
137
|
+
macverdict export history.csv # take your data to a spreadsheet or notebook
|
|
138
|
+
macverdict check --json # machine-readable snapshot
|
|
139
|
+
macverdict setup --every 5 # record every 5 minutes instead of 15
|
|
140
|
+
macverdict notify-test # check that alerts reach you
|
|
141
|
+
macverdict uninstall # stop background recording and remove the alert app
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The background agent shows up as **macverdict** in System Settings → General → Login Items &
|
|
145
|
+
Extensions (or as `python3` if the alert app couldn't be built).
|
|
146
|
+
macOS labels it "unidentified developer" because it's signed on your own Mac rather than with a
|
|
147
|
+
paid Apple Developer ID, which is normal for open-source command-line tools. Switching it off
|
|
148
|
+
there stops recording.
|
|
149
|
+
|
|
150
|
+
The background agent runs at low priority, survives reboots, and shows a macOS notification only
|
|
151
|
+
when something is critical (e.g. memory pressure critical, battery overheating, disk nearly full).
|
|
152
|
+
|
|
153
|
+
## Notifications
|
|
154
|
+
|
|
155
|
+
The background agent only alerts you when something is critical, and clicking an alert opens your
|
|
156
|
+
macverdict report. `macverdict setup` builds the small alert app that makes this work.
|
|
157
|
+
|
|
158
|
+
It's built from source on your machine (`src/macverdict/notifier/MacverdictNotifier.swift`, about
|
|
159
|
+
100 lines) and signed locally, so there's no downloaded binary and no Apple account involved.
|
|
160
|
+
It has no Dock icon and only runs for a few seconds when posting an alert or handling a click.
|
|
161
|
+
|
|
162
|
+
Without it, macverdict falls back to macOS's built-in `osascript`. Those alerts show as coming
|
|
163
|
+
from **Script Editor**, and clicking one opens Script Editor. If no banner appears at all, check
|
|
164
|
+
System Settings → Notifications (macverdict or Script Editor) and that no Focus mode is on.
|
|
165
|
+
|
|
166
|
+
## Use it from Python
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from macverdict import collect, evaluate, health_score, insights, storage
|
|
170
|
+
|
|
171
|
+
snap = collect()
|
|
172
|
+
findings = evaluate(snap)
|
|
173
|
+
print(health_score(findings), [f.message for f in findings if f.level != "ok"])
|
|
174
|
+
|
|
175
|
+
rows = storage.load(storage.connect(), days=30, full=True)
|
|
176
|
+
for i in insights.build(rows, snap["system"]["memory_total"], insights.hardware_model()):
|
|
177
|
+
print(i.title, "→", i.headline)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Where data lives
|
|
181
|
+
|
|
182
|
+
`~/Library/Application Support/macverdict/` holds `history.db` (SQLite), `report.html` and the agent
|
|
183
|
+
logs. Samples older than 90 days are pruned automatically. Set `MACVERDICT_HOME` to use another folder.
|
|
184
|
+
|
|
185
|
+
## How it works
|
|
186
|
+
|
|
187
|
+
- `psutil` for CPU, memory, swap, disk, network and processes
|
|
188
|
+
- `sysctl kern.memorystatus_vm_pressure_level` for macOS memory pressure
|
|
189
|
+
- `ioreg -rn AppleSmartBattery` for battery capacity, cycles and temperature
|
|
190
|
+
- `NSProcessInfo.thermalState` (read via ctypes, no PyObjC) and `pmset -g therm` for throttling
|
|
191
|
+
- a per-user `launchd` agent for scheduling
|
|
192
|
+
- a tiny Swift app (built locally) for clickable notifications, with `osascript` as a fallback
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
uv venv && uv pip install -e ".[dev]"
|
|
198
|
+
.venv/bin/pytest
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "macverdict"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Stop watching graphs: macverdict tells you why your Mac was slow this week and what to do about it. Slowdown blame, RAM verdict, battery forecast and MacBook Air throttling, from quiet background sampling."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Anirudh Jayaraman" }]
|
|
14
|
+
keywords = ["macos", "macbook", "monitoring", "diagnostics", "performance", "battery", "launchd"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: End Users/Desktop",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: MacOS",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: System :: Monitoring",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["psutil>=5.9"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/anirudhjayaraman/macverdict"
|
|
28
|
+
Source = "https://github.com/anirudhjayaraman/macverdict"
|
|
29
|
+
Issues = "https://github.com/anirudhjayaraman/macverdict/issues"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=7"]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
macverdict = "macverdict.cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/macverdict"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
exclude = ["docs", "legacy"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""macverdict: tells you why your Mac was slow, and what to do about it. (Formerly macpulse.)"""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
from .checks import Finding, evaluate, health_score
|
|
6
|
+
from .collectors import collect
|
|
7
|
+
|
|
8
|
+
__all__ = ["collect", "evaluate", "health_score", "Finding", "__version__"]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Turn a snapshot into findings with plain-English advice, and a 0-100 score."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .fmt import human_bytes
|
|
9
|
+
|
|
10
|
+
GB = 1024 ** 3
|
|
11
|
+
LEVELS = ("ok", "info", "warn", "critical")
|
|
12
|
+
PENALTY = {"ok": 0, "info": 2, "warn": 10, "critical": 25}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Finding:
|
|
17
|
+
level: str # one of LEVELS
|
|
18
|
+
area: str
|
|
19
|
+
message: str
|
|
20
|
+
advice: str = ""
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict[str, Any]:
|
|
23
|
+
return asdict(self)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _cpu(s: dict) -> list[Finding]:
|
|
27
|
+
cpu, cores = s["cpu"], s["system"]["cpu_count"] or 1
|
|
28
|
+
load_ratio = cpu["load_avg"][1] / cores
|
|
29
|
+
out = []
|
|
30
|
+
if cpu["percent"] >= 90:
|
|
31
|
+
out.append(Finding("warn", "cpu", f"CPU is busy at {cpu['percent']:.0f}%.",
|
|
32
|
+
"Check the top CPU processes below; quit anything you are not using."))
|
|
33
|
+
if load_ratio >= 1.5:
|
|
34
|
+
out.append(Finding("warn", "cpu", f"5-minute load is {cpu['load_avg'][1]:.1f} on {cores} cores.",
|
|
35
|
+
"Sustained load above core count means work is queuing up."))
|
|
36
|
+
if not out:
|
|
37
|
+
out.append(Finding("ok", "cpu", f"CPU {cpu['percent']:.0f}%, load {cpu['load_avg'][1]:.1f} on {cores} cores."))
|
|
38
|
+
for p in s.get("top_cpu", [])[:1]:
|
|
39
|
+
if p["cpu_percent"] >= 80:
|
|
40
|
+
out.append(Finding("info", "cpu", f"{p['name']} (pid {p['pid']}) is using {p['cpu_percent']:.0f}% CPU.",
|
|
41
|
+
"Expected if it is doing real work; otherwise it may be stuck."))
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _memory(s: dict) -> list[Finding]:
|
|
46
|
+
m = s["memory"]
|
|
47
|
+
level = (m.get("pressure") or {}).get("level")
|
|
48
|
+
swap = m["swap_used"]
|
|
49
|
+
out = []
|
|
50
|
+
if level == "critical":
|
|
51
|
+
out.append(Finding("critical", "memory", "Memory pressure is CRITICAL.",
|
|
52
|
+
"macOS is compressing and swapping heavily. Close large apps or browser tabs."))
|
|
53
|
+
elif level == "warn":
|
|
54
|
+
out.append(Finding("warn", "memory", "Memory pressure is elevated.",
|
|
55
|
+
"Close apps you are not using; this is the main cause of slowdowns on 8-16 GB Macs."))
|
|
56
|
+
if swap >= 6 * GB:
|
|
57
|
+
out.append(Finding("critical", "memory", f"Swap in use: {human_bytes(swap)}.",
|
|
58
|
+
"Heavy swapping wears the SSD and slows everything. A restart clears it."))
|
|
59
|
+
elif swap >= 2 * GB:
|
|
60
|
+
out.append(Finding("warn", "memory", f"Swap in use: {human_bytes(swap)}.",
|
|
61
|
+
"Some swapping is normal, but sustained growth means you need more free RAM."))
|
|
62
|
+
if not out:
|
|
63
|
+
out.append(Finding("ok", "memory", f"Memory pressure {level or 'n/a'}, {m['percent']:.0f}% used, "
|
|
64
|
+
f"swap {human_bytes(swap)}."))
|
|
65
|
+
biggest = (s.get("apps") or s.get("top_memory") or [])[:1]
|
|
66
|
+
for p in biggest:
|
|
67
|
+
if p["rss"] >= 0.25 * m["total"]:
|
|
68
|
+
out.append(Finding("info", "memory", f"{p['name']} is using {human_bytes(p['rss'])} of RAM.",
|
|
69
|
+
"Restarting a long-running app often returns leaked memory."))
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _disk(s: dict) -> list[Finding]:
|
|
74
|
+
d = s["disk"]
|
|
75
|
+
pct, free = d["free_percent"], d["free"]
|
|
76
|
+
if pct < 5 or free < 10 * GB:
|
|
77
|
+
return [Finding("critical", "disk", f"Only {human_bytes(free)} free ({pct:.0f}%).",
|
|
78
|
+
"macOS needs free space for swap and updates. Clear Downloads, caches, or old iOS backups.")]
|
|
79
|
+
if pct < 15:
|
|
80
|
+
return [Finding("warn", "disk", f"Disk is getting full: {human_bytes(free)} free ({pct:.0f}%).",
|
|
81
|
+
"Use System Settings > General > Storage to find large files.")]
|
|
82
|
+
return [Finding("ok", "disk", f"{human_bytes(free)} free ({pct:.0f}%).")]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _battery(s: dict) -> list[Finding]:
|
|
86
|
+
b = s.get("battery")
|
|
87
|
+
if not b:
|
|
88
|
+
return []
|
|
89
|
+
out = []
|
|
90
|
+
health = b.get("max_capacity_percent") or b.get("health_percent")
|
|
91
|
+
if health is not None and health < 80:
|
|
92
|
+
out.append(Finding("warn", "battery", f"Battery maximum capacity is {health:.0f}%.",
|
|
93
|
+
"Apple considers batteries below 80% consumed; consider a service appointment."))
|
|
94
|
+
if (b.get("cycle_count") or 0) >= 1000:
|
|
95
|
+
out.append(Finding("info", "battery", f"Battery has {b['cycle_count']} cycles (rated for 1000).", ""))
|
|
96
|
+
temp = b.get("temperature_c")
|
|
97
|
+
if temp is not None and temp >= 45:
|
|
98
|
+
out.append(Finding("critical", "battery", f"Battery is hot: {temp:.0f}°C.",
|
|
99
|
+
"Move the Mac off soft surfaces and out of the sun; stop heavy work until it cools."))
|
|
100
|
+
elif temp is not None and temp >= 38:
|
|
101
|
+
out.append(Finding("warn", "battery", f"Battery is warm: {temp:.0f}°C.",
|
|
102
|
+
"The MacBook Air is fanless, so sustained heat leads to throttling."))
|
|
103
|
+
if not out:
|
|
104
|
+
out.append(Finding("ok", "battery", f"{b['percent']:.0f}% charged, capacity {health}%, "
|
|
105
|
+
f"{b.get('cycle_count')} cycles, {temp}°C."))
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _thermal(s: dict) -> list[Finding]:
|
|
110
|
+
t = s["thermal"]
|
|
111
|
+
state = t.get("state")
|
|
112
|
+
if state == "critical":
|
|
113
|
+
return [Finding("critical", "thermal", "macOS reports CRITICAL heat and is slowing the Mac hard.",
|
|
114
|
+
"Stop heavy work now and give it airflow: hard surface, out of the sun.")]
|
|
115
|
+
if t["throttled"]:
|
|
116
|
+
detail = f"limited to {t['cpu_speed_limit']}% speed" if t["cpu_speed_limit"] < 100 else "thermal state is serious"
|
|
117
|
+
return [Finding("warn", "thermal", f"Your Mac is slowing itself down to cool off ({detail}).",
|
|
118
|
+
"Reduce load or improve airflow. On a fanless MacBook Air this is the main cause of "
|
|
119
|
+
"slowdowns during long heavy tasks.")]
|
|
120
|
+
if state == "fair":
|
|
121
|
+
return [Finding("info", "thermal", "Your Mac is warm (thermal state: fair).",
|
|
122
|
+
"Not slowing down yet, but sustained heavy work may start throttling.")]
|
|
123
|
+
return [Finding("ok", "thermal", "No thermal throttling.")]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _uptime(s: dict) -> list[Finding]:
|
|
127
|
+
days = (s["timestamp"] - s["system"]["boot_time"]) / 86400
|
|
128
|
+
if days >= 14:
|
|
129
|
+
return [Finding("info", "uptime", f"Up for {days:.0f} days.",
|
|
130
|
+
"A restart clears swap, leaked memory and pending updates.")]
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
RULES = (_cpu, _memory, _disk, _battery, _thermal, _uptime)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def evaluate(snapshot: dict) -> list[Finding]:
|
|
138
|
+
findings = [f for rule in RULES for f in rule(snapshot)]
|
|
139
|
+
return sorted(findings, key=lambda f: -LEVELS.index(f.level))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def health_score(findings: list[Finding]) -> int:
|
|
143
|
+
return max(0, 100 - sum(PENALTY[f.level] for f in findings))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def worst_level(findings: list[Finding]) -> str:
|
|
147
|
+
return max((f.level for f in findings), key=LEVELS.index, default="ok")
|