Skip to content

Perf cpu memory#11

Open
arthur-hsu wants to merge 4 commits into
ProducerGuy:mainfrom
arthur-hsu:perf-cpu-memory
Open

Perf cpu memory#11
arthur-hsu wants to merge 4 commits into
ProducerGuy:mainfrom
arthur-hsu:perf-cpu-memory

Conversation

@arthur-hsu

Copy link
Copy Markdown

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

  • perf(monitor): thermal tick 100ms → 250ms. Fan control tracks thermal mass that
    moves over seconds, so 10 Hz was unnecessary. tickInterval is now the single source
    of truth (set from start(interval:)); monitor (2s) / UI (1s) cadences and the
    ramp-governor math derive from it, and the dispatch timer gets leeway to coalesce
    wakeups. ~60% fewer SMC/IOKit round trips per second.
  • perf(smc): cache the firmware-static key data-size so readKey() skips the
    readKeyInfo round trip on repeat reads — halves IOKit calls on the hot status()
    path. Absence is never cached, so a transiently-failed sensor can't be latched as
    missing (the 95°C override keeps seeing every sensor).
  • perf(ui): migrate AppState to @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.
  • fix(daemon): wrap the two infinite GCD loops in autoreleasepool. They never
    drained 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.

… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant