Refresh puter UI, power telemetry and demand-aware monitoring
Build and test / macos (push) Waiting to run
Signed release / release (push) Waiting to run

This commit is contained in:
2026-09-22 20:03:16 -04:00
parent 0da94715be
commit 4bfa239aa0
38 changed files with 1728 additions and 492 deletions
+132
View File
@@ -0,0 +1,132 @@
# 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.