133 lines
7.3 KiB
Markdown
133 lines
7.3 KiB
Markdown
# 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.
|