# 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 650–690 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.