Skip to content

Stream Health Connect reads page-by-page + optional native downsampling (fixes HR OOM)#18

Merged
dmarttila merged 2 commits into
health-10.2.0from
health-10.2.0-hr-downsample
Jul 14, 2026
Merged

Stream Health Connect reads page-by-page + optional native downsampling (fixes HR OOM)#18
dmarttila merged 2 commits into
health-10.2.0from
health-10.2.0-hr-downsample

Conversation

@ojwalch

@ojwalch ojwalch commented Jul 12, 2026

Copy link
Copy Markdown
Member

Fixes the production OOM crash and the slowness of reading dense heart-rate histories on Android.

java.lang.OutOfMemoryError ... <1% of heap free after GC
cachet.plugins.health.HealthPlugin.convertRecord (HealthPlugin.kt:3369)
cachet.plugins.health.HealthPlugin$getHCData$1.invokeSuspend (HealthPlugin.kt:3171)

getHCData paged all raw records into one list, then converted every HR sample into its own LinkedHashMap. For a wearable writing a sample every few seconds, that's millions of maps at once on a 256 MB heap — and even without the crash, paging millions of records across IPC is very slow.

Changes (Android / Health Connect path only)

1. Page-by-page conversion in getHCData — non-workout, non-sleep types are converted as each page arrives, so peak memory is bounded by one ~1,000-record page regardless of history size. A new optional samplingIntervalMillis argument (Dart: getHealthDataFromTypes(samplingInterval: ...)) keeps at most one converted sample per interval as data is read. Omitted → behavior unchanged.

2. Make interval (aggregate) queries usable for heart rate — this is what makes dense HR reads fast, not just safe:

  • MapToHCAggregateMetric: HEART_RATE now aggregates BPM_AVG instead of MEASUREMENTS_COUNT (which returned sample counts, not heart rates, so the interval API was unusable for HR).
  • getAggregateHCData skips empty buckets instead of fabricating value: 0 data points (a 0 BPM sample is not real data).
  • getAggregateHCData gets the same try/catch getHCData already has (better error handeling if hc request comes when app is in background #17), so provider errors return an empty result instead of crashing the coroutine.

With this, getHealthIntervalDataFromTypes returns one average BPM per bucket computed inside Health Connect — raw samples never cross IPC. A 30-day window at 5-min buckets is ~8.6k values instead of potentially millions of records.

Behavior notes

  • No API break: samplingInterval is optional; raw-read callers are unaffected.
  • iOS ignores these changes (its getIntervalData only supports cumulative types via .cumulativeSum).
  • HR interval-query semantics change from measurement counts to average BPM — nothing in our apps used the old semantics.

Companion app change: Arcascope/shiftapp_flutter#1411

🤖 Generated with Claude Code

https://claude.ai/code/session_016ssFfNA35kFNFqSP8uCzyx

oliviawalch and others added 2 commits July 12, 2026 17:04
…ownsampling

Reading a dense heart-rate history previously materialized every raw record
plus one map per sample in memory at once, OOM-crashing the app for heavy
users. Non-workout, non-sleep types are now converted one page at a time so
raw records are dropped as soon as each page is processed, and an optional
samplingIntervalMillis argument keeps at most one sample per interval as the
data is read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ssFfNA35kFNFqSP8uCzyx
Interval queries for heart rate previously aggregated MEASUREMENTS_COUNT,
which returns sample counts rather than heart rates, making the interval API
unusable for HR. Mapping it to BPM_AVG lets callers read dense HR histories
as bucketed averages computed inside Health Connect, so raw samples never
cross IPC into the app — this is what makes heavy HR reads fast, not just
memory-safe.

Also in getAggregateHCData: skip buckets with no data instead of fabricating
zero values (a 0 BPM sample is not a real data point), and add the same
try/catch getHCData already has so provider errors return an empty result
instead of crashing the coroutine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ssFfNA35kFNFqSP8uCzyx

@dmarttila dmarttila left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@dmarttila
dmarttila merged commit 88a86b8 into health-10.2.0 Jul 14, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants