Files
amethyst/commons
Claude 7a528d7127 perf(fitness): load My Fitness progressively instead of on one serial Health Connect read
The My Fitness dashboard sat on a spinner for 2-3s on a phone with a watch
connected. Three things compounded:

- `readWorkouts` made one `aggregate()` IPC per session, in series. The
  dashboard's window is 28 days (`WorkoutStats.WINDOW_DAYS`) against the New
  Workout carousel's 7, so a daily trainer paid 30-60 sequential round trips.
- `refresh()` launched on `viewModelScope` (`Dispatchers.Main.immediate`) and
  `healthConnectStatus()` never left it, so the PackageManager query and the
  lazy Health Connect service bind ran on the UI thread. `readWorkouts` already
  hopped to IO with a comment about exactly this hazard; the permission probe
  in front of it did not.
- The screen stayed on `State.Loading` until `healthConnectStatus` went
  non-null, which happened only after the whole read finished — so the user's
  published kind 1301 workouts, already indexed in `LocalCache`, waited behind
  Health Connect for data the screen's own KDoc calls "not a precondition".

Fixed all three, and split the read into two stages. The session list is one
IPC and already carries activity, title, start, duration and source — enough
for the counts, total time, streak, active days, per-activity split, recent
list and the longest-duration best. The metrics that need a per-session
aggregate (distance, calories, heart rate, steps, elevation) arrive second, now
fanned out under a permit cap rather than run one at a time.

That partial pass is honest rather than approximate: `WorkoutStats.total` and
`bests` already treat an absent metric as contributing nothing instead of zero,
and the screen already gates each metric cell on `> 0` / non-null, so an
unknown metric reads as a hidden cell, never as "0 km". `State.Ready` carries
`metricsPending` so the dashboard says what is still filling in.

Aggregation stays per raw session with merging after it, exactly as before:
`WorkoutMerger` sums its members' metrics, so aggregating a merged span would
fold in the breaks between segments and change the totals.

Two flicker guards: the status is published before the read starts (it is
cheap, and gating on it is what lets the published log render immediately), but
an *empty* report keeps waiting while the session list is in flight, so a user
whose workouts are one IPC away never sees the empty state or the connect
prompt flash. A resume re-reads in place rather than blanking a dashboard that
is already correct, and each refresh cancels the previous one so two reads
cannot interleave.

Tests: 6 new in `PartialMetricsReportTest` pinning that the first pass is
final for everything not metric-derived and absent (not zero) for everything
that is; 2 in `WorkoutMergerTest` for skeleton/loaded grouping parity and for
why per-session aggregation has to precede the merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgfgpDWnu635p2qc2K2yMX
2026-09-18 22:41:53 +00:00
..