Perf cpu memory#11
Open
arthur-hsu wants to merge 4 commits into
Open
Conversation
… timer leeway ThermalMonitor.status() runs FanControl.status() every tick, which probes ~50 SMC keys (2 IOConnectCallStructMethod round trips each) plus per-fan keys — the dominant CPU cost behind the GUI app's sustained CPU%. - Default tick 100ms -> 250ms (~4 Hz): fan control follows thermal mass that moves over seconds, so ~60% fewer SMC/IOKit round trips per second. - tickInterval is now the single source of truth, set from start(interval:), removing the prior desync between the hardcoded 0.1 in init and the start() default. - monitorCadence/uiUpdateCadence derive from tickInterval as ticks-per wall-clock-target, so the 2s monitor cadence and 500ms UI cadence (and the 30s anomaly/process-buffer windows) hold regardless of tick rate. - Add DispatchSourceTimer leeway (interval/5) so the kernel can coalesce the tick wakeup, lowering idle-wake energy. Safety unchanged: 95C override, sustained-trigger and ramp-governor math all key off tickInterval/temperature; worst-case override reaction rises to ~300ms, far inside hardware-protection margin. 24/24 tests pass.
readKey() made two IOConnectCallStructMethod kernel round trips per call: a readKeyInfo to fetch the firmware-static dataSize, then the readBytes value read. The dataSize never changes for a key on a given machine, so cache it keyed by 4-char code and skip readKeyInfo on every repeat read — halving the IOKit round trips on the hot FanControl.status() path (~50 temp + per-fan keys, every tick). Only successful (size > 0) reads populate the cache; absence and transient readBytes failures are never cached, so a momentarily-failed real sensor can never be permanently latched as missing (would blind the 95C override). SMC access is externally serialized (daemon smcLock / app monitor queue), so a plain dictionary is safe. 24/24 tests pass.
…cadence The menu-bar app's own CPU was dominated by SwiftUI redraws, not SMC reads (sample profile: AttributeGraph/SwiftUICore/objc_msgSend/CFStringHash on the active threads; IOConnectCallStructMethod absent). Root cause: AppState was a classic ObservableObject, so reassigning the full latestStatus struct every tick fired objectWillChange and forced the always-visible menu-bar label to re-render even with the dropdown closed. - Migrate AppState to @observable so views invalidate only on the properties they actually read: the label (maxTemp/monitorState/useFahrenheit) no longer re-renders when latestStatus churns; the dropdown reads latestStatus but renders only while open. - Publish on change only: guard activeProfile/monitorState (Equatable) and republish maxTemp solely when its displayed integer degree changes, so a steady temperature stops label redraws at idle. - UI cadence 500ms -> 1s (onUpdate); a menu-bar readout needs no sub-second refresh. Halves residual per-update work. - @ObservationIgnored on non-UI internals (monitor/executor/heartbeatTimer); @Environment + @bindable wiring in MenuBarView. Fan-control path untouched. 24/24 tests pass.
Both DispatchQueue.global() infinite loops (accept loop in run(), heartbeat
watchdog) never exit, so GCD never drains their implicit autorelease pool.
NSLog/os_log routes through XPC and autoreleases NSURL/CFString/_FileCache
per call, accumulating ~42M live objects = 4.8GB over a 7-day uptime.
Wrap each loop body in autoreleasepool { } so those objects are released
every iteration, bounding daemon memory. Also changed the guard's continue
-> return inside the pool so a failed accept doesn't skip the drain.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Performance and stability improvements to the daemon and menu-bar app. Fan-control
logic, profile curves, and the 95°C safety override are unchanged; all 24 tests pass.
On a MacBook Pro M2 Pro the menu-bar app's CPU dropped from ~12.4% to ~3.6% (~71%),
and a multi-day daemon memory leak was eliminated.
Changes
moves over seconds, so 10 Hz was unnecessary.
tickIntervalis now the single sourceof truth (set from
start(interval:)); monitor (2s) / UI (1s) cadences and theramp-governor math derive from it, and the dispatch timer gets leeway to coalesce
wakeups. ~60% fewer SMC/IOKit round trips per second.
readKey()skips thereadKeyInforound trip on repeat reads — halves IOKit calls on the hotstatus()path. Absence is never cached, so a transiently-failed sensor can't be latched as
missing (the 95°C override keeps seeing every sensor).
AppStateto@Observable+ publish-on-change. The always-visible menu-bar label no longer re-renders every tick when temps are steady;
eliminates the SwiftUI redraw churn that dominated app CPU.
autoreleasepool. They neverdrained their pool, so NSLog/os_log-internal objects accumulated to ~4.8 GB resident
over a 7-day uptime.
Notes
CPU figures are workload/machine dependent. No change to fan behavior or safety thresholds.