Files
puter/PROFILING.md
T
soconnor 4bfa239aa0
Build and test / macos (push) Waiting to run
Signed release / release (push) Waiting to run
Refresh puter UI, power telemetry and demand-aware monitoring
2026-09-22 20:03:16 -04:00

7.3 KiB
Raw Blame History

Release profiling — 22 September 2026

Setup

Apple M4 Pro, 48 GiB RAM, macOS 27.0 (26A428), Xcode 27. Optimized SwiftPM Release build of the current uncommitted working tree. Isolated ad-hoc signed app: /private/tmp/puter-profile-release.app. Processes page, default Normal cadence, grouped categories, about 650690 processes. Installed application was not replaced. No application source was changed during profiling.

Measurements

Each counter measurement lasted 20 seconds, without Instruments attached. CPU is a percentage of one core, unlike puter's machine-normalized CPU column. Counters came from proc_pid_rusage v4, converting CPU Mach ticks with mach_timebase_info; memory is physical footprint, not resident size.

State CPU (one core) Interrupt wake-ups / 20 s Physical footprint, start → end
Processes visible, updating 9.032% 35 66.55 → 65.72 MiB
After minimize action (activity not independently verified) 8.992% 52 71.64 → 66.84 MiB
Hidden/inactive, menu-bar consumer enabled 8.929% 58 70.09 → 69.81 MiB
Paused 0.014% 3 69.83 → 69.36 MiB

For the hidden measurement, NSRunningApplication reported isActive=false and isHidden=true; those flags were checked again after its separate trace. The 3-second sample capture reported 81.6 MiB footprint, 95.4 MiB lifetime peak. These short observations do not establish a memory leak or long-term plateau.

Trace-backed findings

Two separate 15-second Time Profiler traces were captured. Percentages below are inclusive sampled CPU weights; nested paths overlap and must not be added.

  1. Repeated process grouping dominates the UI work. In the visible trace, 85.9% of sampled CPU weight belonged to the main thread; 42.0% passed through ProcessesView.grouped, and 29.6% through ProcessGrouping.appPath(for:). displayedGroups invokes this computed property from category filtering, table rendering and empty-state checks. Each computation enumerates running applications, groups processes, parses app paths, and sorts the result. Next fix: prepare one stable grouped/sorted snapshot per relevant input change, cache executable-path → application identity, and reuse category partitions.

  2. Background demand is not behaving as intended at runtime. The hidden trace still includes full nativeProcessRecords scans (5.1% inclusive), grouping (38.6%) and SwiftUI graph transactions (58.0%). CPU did not materially fall when hidden. Pure SamplingDemand unit tests therefore do not validate lifecycle integration. The precise reason for the stale demand is not established. Next fix: instrument scheduler demand transitions and connect them to actual window visibility/occlusion and application activity; add integration tests.

  3. Disk-capacity lookup remains in every telemetry capture. diskUsage() is unconditional even on the lightweight path. It accounts for 8.4% of visible trace weight and 14.4% of hidden trace weight. It requests available capacity for important usage, which is not a high-frequency activity counter. Next fix: cache capacity and refresh it on a slower, disk-view-specific cadence.

  4. Pause provides a genuinely quiet baseline. It reduced measured CPU to 0.014% and produced no process-attributed disk I/O during the sample. This makes always-on rendering alone unlikely to explain the updating-state cost.

Evidence

  • Visible trace: /private/tmp/puter-foreground-release.trace
  • Hidden trace: /private/tmp/puter-background-release.trace
  • Exported tables: /private/tmp/puter-time-profile.xml, /private/tmp/puter-background-profile.xml
  • Stack sample: /private/tmp/puter-release-sample.txt
  • Counter helper: /private/tmp/puter-profile-counters.c
  • Trace aggregation: /private/tmp/analyze-puter-trace.py

Traces are local only and may contain process names, paths and environment metadata. They were not uploaded. Temporary artifacts may be removed by macOS.

Limits and next validation

This was a short live-machine study, not a before/after comparison against the old poller. Other applications and an earlier preview were running; system memory utilization was high. No GPU energy, scroll hitch rate or frame-time claim is made. Instrumented samples are used for attribution, not as the low-overhead CPU metric.

Fix lifecycle integration first, then repeated grouping and capacity lookup. Repeat these exact 20-second scenarios and add a SwiftUI scrolling trace before calling the performance work complete.

Fix pass — 22 September 2026

Implemented window-visibility-driven demand with notification-driven rescheduling and live-state reconciliation before a scheduled sample. Visible unfocused windows remain live; hidden/minimized/occluded windows do not request process inventories. Recording and menu-bar consumers retain their separate demand policies. Initial load and explicit Refresh may request a single inventory.

Moved process grouping to inventory application, added a bounded app-path cache (including negative results), and shared each table projection across category rendering and empty-state checks. Cached disk capacity separately from activity counters, refreshing on initial/manual requests, volume enumeration and every 60 seconds on visible Performance.

Release preview: /private/tmp/puter-profile-fixed2.app, PID 31379. 32 tests passed, including new grouped-search, identity, totals and path-cache regressions. git diff --check passed. Installed app was not replaced.

Scenario, 20 seconds Before, one-core CPU After, one-core CPU
Processes visible, updating 9.032% 3.541%
Hidden, menu-bar consumer enabled 8.929% 0.914%

Visible wake-ups: 34; footprint 73.41 → 75.24 MiB. Hidden wake-ups: 19; footprint 141.45 → 118.56 MiB, after visiting Hardware. Hidden state was checked before and after the valid run (active=false, hidden=true). An earlier hidden attempt was discarded because the preview was externally changed to Hardware and unhidden mid-measurement. No memory-improvement claim is made.

Visible 15-second trace: 963 ms sampled CPU versus the earlier 2114 ms. ProcessesView.grouped fell from 42.0% to 2.8% of sampled weight; ProcessGrouping.groups was 6.5%, and appPath about 0.1% (previously 29.6%). No diskUsage stack was sampled. Percentages are inclusive, overlapping weights. Trace: /private/tmp/puter-fixed-visible.trace; export: /private/tmp/puter-fixed-visible.xml.

The separate hidden trace sampled 300 ms CPU in 15 seconds (previously 1444 ms). It contained no sampled process-inventory, process-group construction or disk capacity stacks. The remaining sampled work is mostly SwiftUI/AppKit updates; menu-bar system counters intentionally remain enabled. Hidden state was again verified after capture. Evidence: /private/tmp/puter-fixed-hidden.trace and /private/tmp/puter-fixed-hidden.xml.

These are short live-machine observations, not a controlled benchmark. Process counts were around 700, system memory pressure differed from the first pass, and a test build ran during the visible counter sample. The savings are supported by the changed call stacks, but precise percentages will vary with workload. Scrolling/frame-time and long-term energy measurements remain separate work.