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

133 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.