Skip to content

Improve Battery Usage and Memory Pressure - #181

Open
PSchmiedmayer wants to merge 13 commits into
mainfrom
memoryUsageAndBatter
Open

Improve Battery Usage and Memory Pressure#181
PSchmiedmayer wants to merge 13 commits into
mainfrom
memoryUsageAndBatter

Conversation

@PSchmiedmayer

@PSchmiedmayer PSchmiedmayer commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Improve Battery Usage and Memory Pressure

Background

Xcode Organizer diagnostics showed avoidable resource pressure in background health and SensorKit processing.

Area Observed issue Addressed in this PR
Battery Heavy work could start on battery, in Low Power Mode, or too frequently. Adds power-aware work allowances, rolling background triggers, cancellation checks, and limited stale work.
Memory Large health batches and concurrent historical exports could leave too little process headroom. Uses bounded/adaptive batches, pauses below 64 MiB available memory, limits export concurrency, and drains one chunk at a time.
Disk SQLite churn and growing upload backlogs increased writes and retained redundant data. Enables WAL, adds drain indexes, uses bounded transactions, elides matching samples/deletions, and makes file staging durable and collision-safe.

Uploads are bounded to two concurrent files and account cleanup prevents pending data from crossing sign-ins. UI tests now build once and run as four validated shards, reducing the work repeated after a flaky failure.

⚙️ Release Notes

  • Reduce battery, memory, and disk pressure during health and SensorKit processing.
  • Make staged uploads bounded, durable, and account-safe.
  • Build UI tests once and execute validated shards in parallel.

Code of Conduct & Contributing Guidelines

By creating and submitting this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PSchmiedmayer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 99b45e62-c0a0-4c63-9a90-6eb7090c21e1

📥 Commits

Reviewing files that changed from the base of the PR and between 2091726 and 4bb6cb3.

📒 Files selected for processing (3)
  • .github/workflows/build-and-test.yml
  • MyHeartCounts/Resources/Localizable.xcstrings
  • MyHeartCounts/Task Handling/Active Tasks/Timed Walk Test/TimedWalkingTest.swift
📝 Walkthrough

Walkthrough

This pull request adds a four-way iOS UI-test pipeline with manifest validation, cached test products, parallel shard execution, and result aggregation. It also redesigns health upload persistence and delivery around typed GRDB records, chunked draining, durable file staging, account-generation cleanup gates, adaptive scheduling, and cancellation-aware uploads. Background task handling, SensorKit processing, HealthKit staging, localization, dependency references, submodules, and repository metadata are updated accordingly.

Sequence Diagram(s)

sequenceDiagram
  participant Account
  participant HealthUploadStaging
  participant ManagedFileUpload
  participant FirebaseStorage
  Account->>HealthUploadStaging: add health samples
  HealthUploadStaging->>ManagedFileUpload: stage compressed health payload
  ManagedFileUpload->>FirebaseStorage: upload captured-account file
  FirebaseStorage-->>ManagedFileUpload: report completion or failure
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the PR’s main theme, though it omits disk, upload, and CI shard changes.
Description check ✅ Passed The description accurately summarizes the battery, memory, disk, upload, and UI test shard changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PSchmiedmayer
PSchmiedmayer marked this pull request as ready for review July 26, 2026 04:51
@PSchmiedmayer

Copy link
Copy Markdown
Collaborator Author

@lukaskollmer Let me know what you think about the changes.

Also cleaned up and improved some of the CI to avoid long re-runs; should especially pay off once we have more macOS runners.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
MyHeartCounts/SensorKit/SensorKitDataFetcher.swift (1)

126-141: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Dedup check happens after an await, so two callers can still start competing fetches.

fetchAndUploadNewData suspends at await standard.shouldCollectHealthData (Line 130) before inspecting activeFetch. Two callers (e.g. the launch fallback and the background task) can both suspend there, both observe activeFetch == nil, and both create a task; the second assignment overwrites the first, so the first task is never awaited or cancellable via cancelAllActiveCollection() and both fetch/upload runs proceed concurrently — exactly what the comment says must not happen.

Hoisting the dedup check above the suspension point (or having callers await a memoized task) closes the window.

🔒 Sketch of the reordering
 `@MainActor`
 private func fetchAndUploadNewData() async {
     guard SensorKit.isAvailable else {
         return
     }
+    if let activeFetch {
+        _ = await activeFetch.result
+        return
+    }
     guard await standard.shouldCollectHealthData else {
         return
     }
-    if let activeFetch {
-        // if we're already performing this task, we simply wait on that task's result, ...
-        _ = await activeFetch.result
-        if self.activeFetch == activeFetch {
-            self.activeFetch = nil
-        }
-        return
-    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/SensorKit/SensorKitDataFetcher.swift` around lines 126 - 141,
Move the activeFetch deduplication check in fetchAndUploadNewData above the
await standard.shouldCollectHealthData suspension point, while preserving the
existing wait, identity check, and return behavior. Ensure concurrent callers
cannot both pass an activeFetch == nil check and create competing fetch tasks.
🧹 Nitpick comments (7)
MyHeartCountsTests/HealthUploadStagingMigrationTests.swift (2)

31-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Cleanup misses the WAL sidecar files.

Now that the on-disk configuration enables WAL, opening this database also creates -wal and -shm files next to dbUrl; the defer only removes the main file, so each run leaks two temp files.

🧹 Suggested cleanup
         defer {
             try? FileManager.default.removeItem(at: dbUrl)
+            try? FileManager.default.removeItem(at: URL(filePath: dbUrl.path() + "-wal"))
+            try? FileManager.default.removeItem(at: URL(filePath: dbUrl.path() + "-shm"))
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCountsTests/HealthUploadStagingMigrationTests.swift` around lines 31 -
34, Update the deferred cleanup around dbUrl in the staging migration tests to
remove the SQLite WAL and shared-memory sidecar files alongside the main
database file, using the same temporary-directory cleanup flow and preserving
best-effort removal.

43-104: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider covering the unhappy migration path too.

The test only exercises timestamps written the way GRDB's Date binding writes them. A row whose timestamp SQLite can't parse currently fails the whole v2 migration (see the note on HealthUploadStaging+Schema.swift); a case asserting the migration still completes would lock in whatever behaviour you decide there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCountsTests/HealthUploadStagingMigrationTests.swift` around lines 43 -
104, Extend the migration test around HealthUploadStaging.applyMigrations and
HealthUploadStaging initialization to insert at least one v1 row with an
SQLite-unparseable timestamp, then assert the v2 migration completes
successfully and preserves the expected migration behavior for that row. Cover
the relevant pendingSamples or pendingDeletions path without weakening the
existing valid-timestamp assertions.
MyHeartCounts/Health Import/HistoricalHealthSamplesExportManager.swift (1)

95-99: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Timestamp records session start, so an aborted export still suppresses the next resume for a day.

lastHistoricalExportSessionStart is written as soon as session.start returns. If the app is terminated shortly after (common for a memory-heavy bulk export), the next launch sees a fresh timestamp and defers the resume for up to TimeConstants.day unless the device happens to be charging. Consider only persisting once the session reports meaningful progress/completion, or using a shorter staleness for this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Health` Import/HistoricalHealthSamplesExportManager.swift
around lines 95 - 99, Change the historical export timestamp handling around
managedFileUpload.scheduleForUpload and .lastHistoricalExportSessionStart so an
export start is not recorded immediately after session.start. Persist the
timestamp only after the session reports meaningful progress or completion, or
apply the established shorter staleness policy for interrupted exports, while
preserving the existing upload scheduling behavior.
MyHeartCounts/Modules/HealthUploadStagingUploader.swift (1)

134-141: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Low Power Mode guard sits after the chunk fetch in both drain helpers. Each helper evaluates fetchNextDrainChunk — a full bounded read of up to drainChunkSize rows — before checking isLowPowerModeEnabled, so entering a drain in Low Power Mode still costs one wasted chunk read per helper.

  • MyHeartCounts/Modules/HealthUploadStagingUploader.swift#L134-L141: hoist the isLowPowerModeEnabled check above the while let chunk = … loop in drainPendingSamples.
  • MyHeartCounts/Modules/HealthUploadStagingUploader.swift#L165-L172: do the same in drainPendingDeletions, and have _process skip this helper entirely when didDrainSamples is already false.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Modules/HealthUploadStagingUploader.swift` around lines 134 -
141, Move the low-power-mode guard in both drain helpers before their respective
fetch loops: update drainPendingSamples and drainPendingDeletions so they return
before calling fetchNextDrainChunk when low-power mode is enabled. In _process,
skip drainPendingDeletions entirely when didDrainSamples is false.
MyHeartCounts/Modules/HealthUploadStaging.swift (1)

57-65: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Prefer GRDB’s built-in journalMode for WAL.

Configuration.journalMode = .wal is the intended way to make DatabaseQueue use WAL without manually executing PRAGMA journal_mode = WAL, and it leaves the concurrent-reader benefit to DatabasePool where it actually comes from.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Modules/HealthUploadStaging.swift` around lines 57 - 65, Update
the DatabaseQueue configuration in the visible initialization block to set
GRDB’s built-in journalMode property to .wal, and remove the prepareDatabase
closure that manually executes PRAGMA journal_mode = WAL. Preserve the existing
DatabaseQueue path and other configuration behavior.
.github/workflows/build-and-test.yml (2)

52-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cache-key formula is duplicated verbatim across two jobs.

Both jobs independently compute cache-key with the same shell expression. Since mhc_app_ui_test_shards restores with fail-on-cache-miss: true, any future edit to one copy without the other would break all 4 shard jobs with a hard cache-miss failure. Consider extracting this into a small composite action or a single reusable step to keep the two computations in sync.

Also applies to: 120-125

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-and-test.yml around lines 52 - 57, Deduplicate the
cache-key computation used by the two jobs, including the Resolve test products
cache key step and its counterpart near the later job. Extract the shared Xcode
version and cache-key generation into a reusable composite action or shared
workflow step, then have both jobs consume its output while preserving the
existing key format and cache behavior.

148-152: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Firebase Functions are rebuilt independently in all 4 UI test shards.

npm ci + npm run build for MyHeartCounts-Firebase/functions runs once per matrix shard (4x total on self-hosted macOS runners), even though the output is identical across shards. This duplicated compile work runs counter to the PR's goal of reducing CI battery/CPU usage. Consider building/caching the Functions output once (e.g. in mhc_app_ui_test_products, keyed on a hash of functions/package-lock.json + source) and restoring it in each shard instead of rebuilding from scratch 4 times.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-and-test.yml around lines 148 - 152, Update the UI
test workflow around the “Prepare Firebase functions” step so Firebase Functions
dependencies and build output are produced once and reused by all four matrix
shards. Move or add a cache/artifact mechanism keyed by the Functions package
lockfile and source inputs, restore the prepared output in each shard, and avoid
rerunning npm ci and npm run build per shard.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build-and-test.yml:
- Around line 46-48: Update both checkout steps in the mhc_app_ui_test_products
and mhc_app_ui_test_shards jobs to set persist-credentials to false alongside
submodules: recursive. Leave the existing checkout action and other options
unchanged.

In `@MyHeartCounts-Firebase`:
- Line 1: Update the MyHeartCounts-Firebase submodule pointer to a fetchable
intended commit, or restore the previously referenced repository if that commit
was removed; verify the submodule initializes and updates successfully.

In `@MyHeartCounts/Modules/HealthUploadStaging`+Schema.swift:
- Around line 169-179: Update the migration INSERT around the v1-to-v2
conversion to handle timestamps that SQLite cannot parse, rather than inserting
NULL into the strict table’s non-null timestamp column. Use the existing
migration symbols to default invalid timestamps to a valid value or exclude
those rows, while preserving normal strftime conversion for parseable timestamps
so the migration completes without leaving the database unavailable.

In `@MyHeartCounts/Modules/HealthUploadStagingUploader.swift`:
- Around line 36-48: Update drainChunkSize so its lower bound does not force a
fixed 1000-record chunk under memory pressure. Replace the hardcoded max(1000,
…) floor with a much smaller or availableMemory-derived minimum while preserving
the 10,000-record upper cap and the quarter-of-available-memory sizing behavior.

In `@MyHeartCounts/Modules/ManagedFileUpload.swift`:
- Around line 181-188: Update upload in ManagedFileUpload to delegate staging to
the existing stage method instead of directly constructing the staging URL and
calling moveItem. Preserve the subsequent uploadAndDelete flow by using the URL
returned by stage, ensuring the staging directory guard is consistently applied.
- Around line 168-174: The stage method’s FileManager.moveItem call must handle
an existing destination instead of allowing the upload to be lost. Before moving
in stage(_:category:), detect or catch a destination conflict at stagingUrl and
remove the existing file or generate a unique destination, while preserving
normal move errors and ensuring the current upload is staged successfully.

---

Outside diff comments:
In `@MyHeartCounts/SensorKit/SensorKitDataFetcher.swift`:
- Around line 126-141: Move the activeFetch deduplication check in
fetchAndUploadNewData above the await standard.shouldCollectHealthData
suspension point, while preserving the existing wait, identity check, and return
behavior. Ensure concurrent callers cannot both pass an activeFetch == nil check
and create competing fetch tasks.

---

Nitpick comments:
In @.github/workflows/build-and-test.yml:
- Around line 52-57: Deduplicate the cache-key computation used by the two jobs,
including the Resolve test products cache key step and its counterpart near the
later job. Extract the shared Xcode version and cache-key generation into a
reusable composite action or shared workflow step, then have both jobs consume
its output while preserving the existing key format and cache behavior.
- Around line 148-152: Update the UI test workflow around the “Prepare Firebase
functions” step so Firebase Functions dependencies and build output are produced
once and reused by all four matrix shards. Move or add a cache/artifact
mechanism keyed by the Functions package lockfile and source inputs, restore the
prepared output in each shard, and avoid rerunning npm ci and npm run build per
shard.

In `@MyHeartCounts/Health` Import/HistoricalHealthSamplesExportManager.swift:
- Around line 95-99: Change the historical export timestamp handling around
managedFileUpload.scheduleForUpload and .lastHistoricalExportSessionStart so an
export start is not recorded immediately after session.start. Persist the
timestamp only after the session reports meaningful progress or completion, or
apply the established shorter staleness policy for interrupted exports, while
preserving the existing upload scheduling behavior.

In `@MyHeartCounts/Modules/HealthUploadStaging.swift`:
- Around line 57-65: Update the DatabaseQueue configuration in the visible
initialization block to set GRDB’s built-in journalMode property to .wal, and
remove the prepareDatabase closure that manually executes PRAGMA journal_mode =
WAL. Preserve the existing DatabaseQueue path and other configuration behavior.

In `@MyHeartCounts/Modules/HealthUploadStagingUploader.swift`:
- Around line 134-141: Move the low-power-mode guard in both drain helpers
before their respective fetch loops: update drainPendingSamples and
drainPendingDeletions so they return before calling fetchNextDrainChunk when
low-power mode is enabled. In _process, skip drainPendingDeletions entirely when
didDrainSamples is false.

In `@MyHeartCountsTests/HealthUploadStagingMigrationTests.swift`:
- Around line 31-34: Update the deferred cleanup around dbUrl in the staging
migration tests to remove the SQLite WAL and shared-memory sidecar files
alongside the main database file, using the same temporary-directory cleanup
flow and preserving best-effort removal.
- Around line 43-104: Extend the migration test around
HealthUploadStaging.applyMigrations and HealthUploadStaging initialization to
insert at least one v1 row with an SQLite-unparseable timestamp, then assert the
v2 migration completes successfully and preserves the expected migration
behavior for that row. Cover the relevant pendingSamples or pendingDeletions
path without weakening the existing valid-timestamp assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bbc26e21-ecbb-445d-83d1-ce2b7373a481

📥 Commits

Reviewing files that changed from the base of the PR and between 8da4436 and 13e518d.

⛔ Files ignored due to path filters (1)
  • MyHeartCounts.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved is excluded by !**/Package.resolved
📒 Files selected for processing (23)
  • .github/scripts/run-ui-test-shard.sh
  • .github/scripts/validate-ui-test-shards.sh
  • .github/ui-test-shards.json
  • .github/ui-test-shards.json.license
  • .github/workflows/build-and-test.yml
  • MyHeartCounts-Firebase
  • MyHeartCounts-StudyDefinitions
  • MyHeartCounts.xcodeproj/project.pbxproj
  • MyHeartCounts.xcodeproj/xcshareddata/xcschemes/MyHeartCountsWatchCompanion.xcscheme
  • MyHeartCounts/Account/Demographics/DemographicsData.swift
  • MyHeartCounts/Account/VerifyEmailSheet.swift
  • MyHeartCounts/Health Import/HistoricalHealthSamplesExportManager.swift
  • MyHeartCounts/Modules/HealthUploadStaging+Schema.swift
  • MyHeartCounts/Modules/HealthUploadStaging.swift
  • MyHeartCounts/Modules/HealthUploadStagingUploader.swift
  • MyHeartCounts/Modules/ManagedFileUpload.swift
  • MyHeartCounts/MyHeartCountsStandard+HealthKit.swift
  • MyHeartCounts/Resources/Localizable.xcstrings
  • MyHeartCounts/SensorKit/SensorKitDataFetcher.swift
  • MyHeartCounts/Utils/DeviceBattery.swift
  • MyHeartCountsTests/HealthSampleProcessingTests.swift
  • MyHeartCountsTests/HealthUploadStagingMigrationTests.swift
  • README.md
💤 Files with no reviewable changes (1)
  • MyHeartCounts.xcodeproj/xcshareddata/xcschemes/MyHeartCountsWatchCompanion.xcscheme

Comment thread .github/workflows/build-and-test.yml
Comment thread MyHeartCounts-Firebase
Comment thread MyHeartCounts/Modules/HealthUploadStaging+Schema.swift Outdated
Comment thread MyHeartCounts/Modules/HealthUploadStagingUploader.swift Outdated
Comment thread MyHeartCounts/Modules/ManagedFileUpload.swift Outdated
Comment thread MyHeartCounts/Modules/ManagedFileUpload.swift

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

🧹 Nitpick comments (6)
MyHeartCounts/Modules/HealthUploadStaging+Schema.swift (2)

123-163: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

elidePendingUploads loop has no cancellation checkpoint.

This runs from configure() on every launch via a background-priority Task, and can loop indefinitely over large backlogs without ever calling Task.checkCancellation(), unlike the sibling drain loops (drainPendingSamples/drainPendingDeletions in HealthUploadStagingUploader.swift) which check cancellation on every iteration. Given the PR's stated goal of reducing battery/memory pressure, an uncooperative long-running elision pass could keep the app active longer than intended after backgrounding.

🔋 Suggested fix
     func elidePendingUploads(in dbQueue: DatabaseQueue) throws -> [String: Int] {
         var summary: [String: Int] = [:]
         while true {
+            try Task.checkCancellation()
             let batch = try dbQueue.write { db -> [PendingRecordKey] in
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Modules/HealthUploadStaging`+Schema.swift around lines 123 -
163, Add a Task.checkCancellation() checkpoint at the start of each iteration in
elidePendingUploads, before beginning the database write, so cancellation
promptly stops long-running backlog processing while preserving the existing
batching and summary behavior.

97-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

createDrainIndexes hardcodes PendingSampleRecord.Columns for both tables.

createDrainIndexes(in:for:) is invoked generically for both pendingSamples and pendingDeletions, but always resolves column names via typealias Column = PendingSampleRecord.Columns. This only works today because PendingDeletionRecord.Columns happens to use identical raw names for timestamp/sampleType. A future pending-record type with differently-named columns would silently produce an index on the wrong column name.

♻️ Suggested fix: derive column names from the actual record type
-    private static func createDrainIndexes(in db: Database) throws {
-        let recordTypes: [any TableRecord.Type] = [
-            PendingSampleRecord.self,
-            PendingDeletionRecord.self
-        ]
-        for recordType in recordTypes {
-            try createDrainIndexes(in: db, for: recordType.databaseTableName)
-        }
-    }
-
-    private static func createDrainIndexes(in db: Database, for table: String) throws {
-        typealias Column = PendingSampleRecord.Columns
-        try db.create(
-            index: "\(table)_on_timestamp_sampleType",
-            on: table,
-            columns: [Column.timestamp.name, Column.sampleType.name],
-            options: .ifNotExists
-        )
-        try db.create(
-            index: "\(table)_on_sampleType_timestamp",
-            on: table,
-            columns: [Column.sampleType.name, Column.timestamp.name],
-            options: .ifNotExists
-        )
-    }
+    private static func createDrainIndexes(in db: Database) throws {
+        let recordTypes: [any _PendingEntityRecord.Type] = [
+            PendingSampleRecord.self,
+            PendingDeletionRecord.self
+        ]
+        for recordType in recordTypes {
+            try createDrainIndexes(in: db, for: recordType)
+        }
+    }
+
+    private static func createDrainIndexes(in db: Database, for recordType: some _PendingEntityRecord.Type) throws {
+        let table = recordType.databaseTableName
+        try db.create(
+            index: "\(table)_on_timestamp_sampleType",
+            on: table,
+            columns: [recordType.timestampColumn.name, recordType.sampleTypeColumn.name],
+            options: .ifNotExists
+        )
+        try db.create(
+            index: "\(table)_on_sampleType_timestamp",
+            on: table,
+            columns: [recordType.sampleTypeColumn.name, recordType.timestampColumn.name],
+            options: .ifNotExists
+        )
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Modules/HealthUploadStaging`+Schema.swift around lines 97 -
121, Update createDrainIndexes(in:for:) and its caller to derive timestamp and
sampleType column names from the actual record type being indexed, rather than
hardcoding PendingSampleRecord.Columns. Pass the relevant TableRecord type or
column metadata through the generic loop, preserving the existing index names,
column order, and ifNotExists behavior for both PendingSampleRecord and
PendingDeletionRecord.
MyHeartCounts/Modules/ManagedFileUpload+Firebase.swift (1)

52-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider preserving the underlying Firebase error.

String(describing: error) discards the StorageErrorCode, which makes it impossible for callers to distinguish retryable failures (network, quota) from permanent ones later. Rethrowing the original error (and reserving StorageUploadError for the missing-metadata case) keeps that information.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Modules/ManagedFileUpload`+Firebase.swift around lines 52 - 63,
In the upload continuation around putFile, rethrow the original Firebase error
directly instead of wrapping it in StorageUploadError.failed(String(describing:
error)). Keep StorageUploadError.failed for the missing-metadata branch only,
preserving the underlying StorageErrorCode for callers.
MyHeartCounts/Modules/ManagedFileUpload.swift (2)

94-103: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Pause ordering can be undone by an in-flight replay.

cancelAndWaitForQuiescence pauses the queue and only then awaits the replay task. If that replay task is already suspended inside resumeQueueIfReplayIsCurrent's await uploadQueue.resume(), the resume can land after pauseAndCancel, flipping acceptsUploads back to true for a queue that was just cancelled. The generation re-check afterwards detects the stale replay but does not undo the resume. Awaiting the cancelled replay task before calling pauseAndCancel (or having resumeQueueIfReplayIsCurrent pass the generation into the queue so resume is a no-op for stale generations) closes the window.

Also applies to: 253-259

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Modules/ManagedFileUpload.swift` around lines 94 - 103, Update
cancelAndWaitForQuiescence so it awaits the captured orphanReplayTask after
cancellation before calling uploadQueue.pauseAndCancel, preventing
resumeQueueIfReplayIsCurrent from resuming the queue after it is paused.
Preserve replayGeneration invalidation, task clearing, and final quiescence
behavior.

139-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep Category.stagingDirUrl aligned with the owning directory.

FileUploadInsights.swift still reads category.stagingDirUrl, and now stage()/replay use stagingDirectory(for:) instead. Derive this URL from the instance owning Category (for example via a ManagedFileUpload helper) rather than hard-wiring it to the default ManagedFileUpload.directory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Modules/ManagedFileUpload.swift` around lines 139 - 151, Update
Category initialization so stagingDirUrl is derived from the owning
ManagedFileUpload instance or its stagingDirectory(for:) helper, instead of the
static ManagedFileUpload.directory. Keep Category.stagingDirUrl consistent with
the directory used by stage() and replay, while preserving
FileUploadInsights.swift access.
MyHeartCounts/SensorKit/SensorKitDataFetcher.swift (1)

160-186: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The while true coalescing loop can iterate more than once.

Reusing an in-flight .limited fetch and then retrying for .full is sound, but if another caller registers a fresh .limited fetch between the defer clearing activeFetch and the next iteration, this call can loop again. Each iteration awaits a real task so it can't spin, but bounding it to a single retry would make the intent explicit and rule out pathological chains.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/SensorKit/SensorKitDataFetcher.swift` around lines 160 - 186,
Bound the coalescing loop in the fetch flow around activeFetch to one retry at
most. Preserve the existing behavior of retrying when a full allowance reuses a
limited fetch, but prevent subsequent iterations from forming a chain if another
limited fetch is registered after activeFetch is cleared. Keep awaiting the
selected task and returning normally for all other cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MyHeartCounts/Health` Import/HistoricalHealthSamplesExportManager.swift:
- Around line 83-90: Update setupAndStartExportSession() to establish a retry
path when charging or low-power guards defer the export: either register a
background processing task requiring external power or observe
UIDevice.batteryStateDidChangeNotification, then retry the historical export
when power conditions become suitable. Preserve the existing cleanup guard
behavior and avoid relying solely on future configure() or enroll(...) calls.

In `@MyHeartCounts/Modules/HealthUploadStaging.swift`:
- Around line 86-97: Update excludeStoreFromBackup to stop silently swallowing
setResourceValues failures: capture the error for each database, -wal, and -shm
URL and surface it through the module’s existing logging or telemetry mechanism,
while preserving the current file-existence checks and backup-exclusion
behavior.

In `@MyHeartCounts/MyHeartCountsStandard.swift`:
- Around line 124-132: Update the startup cleanup flow around
clearPendingAccountData so a thrown cleanup error does not leave health
collection disabled for the remainder of the session. Add a retry mechanism on a
timer or subsequent background task, or surface a user-visible diagnostic, while
preserving the existing logging and pending-upload resume behavior.

In `@MyHeartCounts/SensorKit/SensorKitDataFetcher.swift`:
- Around line 188-208: Update _fetchAndUploadNewData so .limited runs do not
update .lastSensorKitFetch, since they process only one batch per sensor; record
the timestamp only after an unrestricted run completes successfully. Preserve
cancellation and upload behavior, and use the existing allowance value to
distinguish partial from complete drains.

In `@MyHeartCounts/SensorKit/SensorKitDataFetcher`+Uploading+Base.swift:
- Around line 88-94: Update the upload flow around sensorCollection to write the
reference and observation documents through a single Firestore WriteBatch,
committing them atomically. Keep cancellation checking before the batch commit,
and remove the cancellation boundary between the two individual writes so
cancellation cannot leave only the reference document persisted.

In `@MyHeartCountsTests/ManagedFileUploadTests.swift`:
- Around line 30-44: Update the upload coordination in the probe’s upload()
method to stop concurrently consuming the shared releaseStream AsyncStream.
Replace it with a multi-consumer-safe mechanism, such as a shared
CheckedContinuation collection, ensuring each upload waits for and is released
independently while preserving cancellation accounting and active-count cleanup.

---

Nitpick comments:
In `@MyHeartCounts/Modules/HealthUploadStaging`+Schema.swift:
- Around line 123-163: Add a Task.checkCancellation() checkpoint at the start of
each iteration in elidePendingUploads, before beginning the database write, so
cancellation promptly stops long-running backlog processing while preserving the
existing batching and summary behavior.
- Around line 97-121: Update createDrainIndexes(in:for:) and its caller to
derive timestamp and sampleType column names from the actual record type being
indexed, rather than hardcoding PendingSampleRecord.Columns. Pass the relevant
TableRecord type or column metadata through the generic loop, preserving the
existing index names, column order, and ifNotExists behavior for both
PendingSampleRecord and PendingDeletionRecord.

In `@MyHeartCounts/Modules/ManagedFileUpload.swift`:
- Around line 94-103: Update cancelAndWaitForQuiescence so it awaits the
captured orphanReplayTask after cancellation before calling
uploadQueue.pauseAndCancel, preventing resumeQueueIfReplayIsCurrent from
resuming the queue after it is paused. Preserve replayGeneration invalidation,
task clearing, and final quiescence behavior.
- Around line 139-151: Update Category initialization so stagingDirUrl is
derived from the owning ManagedFileUpload instance or its stagingDirectory(for:)
helper, instead of the static ManagedFileUpload.directory. Keep
Category.stagingDirUrl consistent with the directory used by stage() and replay,
while preserving FileUploadInsights.swift access.

In `@MyHeartCounts/Modules/ManagedFileUpload`+Firebase.swift:
- Around line 52-63: In the upload continuation around putFile, rethrow the
original Firebase error directly instead of wrapping it in
StorageUploadError.failed(String(describing: error)). Keep
StorageUploadError.failed for the missing-metadata branch only, preserving the
underlying StorageErrorCode for callers.

In `@MyHeartCounts/SensorKit/SensorKitDataFetcher.swift`:
- Around line 160-186: Bound the coalescing loop in the fetch flow around
activeFetch to one retry at most. Preserve the existing behavior of retrying
when a full allowance reuses a limited fetch, but prevent subsequent iterations
from forming a chain if another limited fetch is registered after activeFetch is
cleared. Keep awaiting the selected task and returning normally for all other
cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ccc084b1-ee53-4898-abb4-32a0cf4e87f2

📥 Commits

Reviewing files that changed from the base of the PR and between 13e518d and e61b7d4.

📒 Files selected for processing (23)
  • .github/workflows/build-and-test.yml
  • MyHeartCounts/Account/VerifyEmailSheet.swift
  • MyHeartCounts/Health Import/HealthKitSamplesFHIRUploader.swift
  • MyHeartCounts/Health Import/HistoricalHealthSamplesExportManager.swift
  • MyHeartCounts/Modules/HealthUploadStaging+Schema.swift
  • MyHeartCounts/Modules/HealthUploadStaging.swift
  • MyHeartCounts/Modules/HealthUploadStagingUploader.swift
  • MyHeartCounts/Modules/MHCBackgroundTasks.swift
  • MyHeartCounts/Modules/ManagedFileUpload+Firebase.swift
  • MyHeartCounts/Modules/ManagedFileUpload+Queue.swift
  • MyHeartCounts/Modules/ManagedFileUpload.swift
  • MyHeartCounts/Modules/SetupTestEnvironment.swift
  • MyHeartCounts/MyHeartCountsStandard+HealthKit.swift
  • MyHeartCounts/MyHeartCountsStandard.swift
  • MyHeartCounts/SensorKit/SensorKitDataFetcher+Uploading+Base.swift
  • MyHeartCounts/SensorKit/SensorKitDataFetcher.swift
  • MyHeartCounts/SharedContext/Preferences.swift
  • MyHeartCounts/Utils/DeviceBattery.swift
  • MyHeartCountsTests/DeviceBatteryTests.swift
  • MyHeartCountsTests/HealthSampleProcessingTests.swift
  • MyHeartCountsTests/HealthUploadStagingMigrationTests.swift
  • MyHeartCountsTests/MHCBackgroundTasksTests.swift
  • MyHeartCountsTests/ManagedFileUploadTests.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • MyHeartCounts/Account/VerifyEmailSheet.swift
  • .github/workflows/build-and-test.yml
  • MyHeartCounts/Modules/HealthUploadStagingUploader.swift

Comment thread MyHeartCounts/Modules/HealthUploadStaging.swift
Comment thread MyHeartCounts/MyHeartCountsStandard.swift
Comment thread MyHeartCounts/SensorKit/SensorKitDataFetcher.swift
Comment thread MyHeartCounts/SensorKit/SensorKitDataFetcher+Uploading+Base.swift
Comment thread MyHeartCountsTests/ManagedFileUploadTests.swift

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
MyHeartCounts/SensorKit/SensorKitDataFetcher+Uploading+Base.swift (2)

40-42: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Register temporary-file cleanup before writing the file.

If write(to:) fails after creating a partial file, this defer is never registered and the temporary upload file remains on disk. Install the cleanup immediately after creating url, before the write.

Proposed fix
 let url = URL.temporaryDirectory
     .appending(component: UUID().uuidString)
     .appendingPathExtension("\(fileExtension)\(shouldCompress ? ".zstd" : "")")
+defer {
+    try? FileManager.default.removeItem(at: url)
+}
 try (consume data).write(to: url)
-defer {
-    try? FileManager.default.removeItem(at: url)
-}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/SensorKit/SensorKitDataFetcher`+Uploading+Base.swift around
lines 40 - 42, Move the defer cleanup in the temporary-file upload flow to
immediately after `url` is created and before calling `write(to:)`. Keep the
existing `FileManager.default.removeItem` cleanup behavior, ensuring partial
files are removed even when writing fails.

44-45: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make SensorKit upload staging and Firestore metadata atomic.

uploadSensorKitFile(...) moves the file into durable staging and queues upload, so stage() returns before Firestore commits. If task cancellation is detected before batch.commit(), the caller exits without creating the reference/observation documents and the deferred cleanup removes the temporary copy; the file remains queued in ManagedFileUpload staging with no associated FHIR documents. Defer file staging/enqueueing until after metadata metadata is reserved/committed, or remove the staged upload entry when the Firestore batch is skipped or fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/SensorKit/SensorKitDataFetcher`+Uploading+Base.swift around
lines 44 - 45, Make the SensorKit upload flow atomic across file staging and
Firestore metadata: update the surrounding upload method so metadata is reserved
and successfully committed before calling standard.uploadSensorKitFile, or
explicitly remove the staged upload when batch.commit is cancelled or fails.
Ensure every exit path avoids leaving a ManagedFileUpload entry without its
associated FHIR reference and observation documents.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MyHeartCounts/Task` Handling/Active Tasks/Timed Walk
Test/TimedWalkingTest.swift:
- Around line 198-199: Make finalization in the TimedWalkingTest flow idempotent
by introducing a shared stopping state/task that both automatic and
user-triggered stop paths reuse. Keep session.completeSessionTask set until
stop() finishes, and have concurrent callers await the in-flight finalization
instead of querying or uploading again. Ensure the FHIR resource and
uploadHealthObservation path are immutable/idempotent for the same result so
directFirestore cannot overwrite it with duplicate finalization data.

---

Outside diff comments:
In `@MyHeartCounts/SensorKit/SensorKitDataFetcher`+Uploading+Base.swift:
- Around line 40-42: Move the defer cleanup in the temporary-file upload flow to
immediately after `url` is created and before calling `write(to:)`. Keep the
existing `FileManager.default.removeItem` cleanup behavior, ensuring partial
files are removed even when writing fails.
- Around line 44-45: Make the SensorKit upload flow atomic across file staging
and Firestore metadata: update the surrounding upload method so metadata is
reserved and successfully committed before calling standard.uploadSensorKitFile,
or explicitly remove the staged upload when batch.commit is cancelled or fails.
Ensure every exit path avoids leaving a ManagedFileUpload entry without its
associated FHIR reference and observation documents.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f095563-5ad7-4876-b6dc-2670dad51f29

📥 Commits

Reviewing files that changed from the base of the PR and between e61b7d4 and 2091726.

📒 Files selected for processing (8)
  • MyHeartCounts/Health Import/HistoricalHealthSamplesExportManager.swift
  • MyHeartCounts/Modules/HealthUploadStaging+Schema.swift
  • MyHeartCounts/Modules/HealthUploadStaging.swift
  • MyHeartCounts/Modules/ManagedFileUpload+Firebase.swift
  • MyHeartCounts/Modules/ManagedFileUpload+Queue.swift
  • MyHeartCounts/Modules/ManagedFileUpload.swift
  • MyHeartCounts/SensorKit/SensorKitDataFetcher+Uploading+Base.swift
  • MyHeartCounts/Task Handling/Active Tasks/Timed Walk Test/TimedWalkingTest.swift
🚧 Files skipped from review as they are similar to previous changes (6)
  • MyHeartCounts/Modules/ManagedFileUpload+Firebase.swift
  • MyHeartCounts/Modules/HealthUploadStaging+Schema.swift
  • MyHeartCounts/Health Import/HistoricalHealthSamplesExportManager.swift
  • MyHeartCounts/Modules/ManagedFileUpload+Queue.swift
  • MyHeartCounts/Modules/ManagedFileUpload.swift
  • MyHeartCounts/Modules/HealthUploadStaging.swift

# Conflicts:
#	MyHeartCounts.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Comment on lines +121 to +125
guard DeviceBattery.isCharging, !ProcessInfo.processInfo.isLowPowerModeEnabled else {
logger.notice("Deferring historical upload until the device is charging")
waitForSuitablePower()
return false
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i'm worried that this might cause us to miss uploads, or end up in a situation where the historical data collection ends up taking way longer than is the case currently (do we know how much potential execution time we will have left if we limit it to "user is actively using the device while it is plugged in and low power mode is disabled", compared to the current much less strict upload timing?)

we might wanna consider instead to register a background task for this (like we already have dedicated tasks for the regular HealthKit uploading and the SensorKit uploading), and then configure that task to have iOS schedule it only if the device is connected to power.

currently the historical health collection+upload actually has no dedicated background task of its own, and instead simply schedules itself when the app is being launched for some other reason (be it a normal user-initiated launch, or any one of the other background tasks we have), so adding a dedicated background task for this action might actually be worthwhile anyway...

try Self.applyMigrations(to: dbQueue)
if case .onDisk(let url) = persistence,
url.standardizedFileURL == Persistence.defaultDatabaseUrl.standardizedFileURL {
Self.excludeStoreFromBackup(at: url)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

are we sure we want this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It might grow very large with every backup? Ideally we have an easy way to figure out when we wouldd get the past data we didn't capture between a new installation and this? Technically our complete historical data upload would probably capture this anyways?

Comment on lines -155 to +119
let tables = try String.fetchAll(db, sql: """
SELECT name FROM sqlite_master WHERE type = 'table'
AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'grdb_%'
""")
return try tables.allSatisfy { table in
try Int.fetchOne(db, sql: "SELECT 1 FROM \(table.quotedDatabaseIdentifier) LIMIT 1") == nil
}
try PendingSampleRecord.fetchCount(db) == 0
&& PendingDeletionRecord.fetchCount(db) == 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

it previously was intentionally implemented the way it was, so that the property would be guaranteed to stay correct if we make changes to the schema down the road, without us needing to manually keep the implemenation here in sync...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what's the overall idea behind these changes? are we sure that the new code matches the semantics and behaviour of the previous implementation?

do {
try Task.checkCancellation()
try await handler()
try Task.checkCancellation()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what if the task gets cancelled inbetween the handler returning and the cancellation check in the next line? why would we want to consider that a failure?

@concurrent
private func _fetchAndUploadNewData(_ allowance: DeviceBattery.WorkAllowance) async throws {
try Task.checkCancellation()
let maximumBatchesPerSensor = allowance == .limited ? 1 : nil

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do we impose this limit? wouldn't that mean that if the allowance is limited, we would fetch only a single batch per sensor per background task run?

Comment on lines +249 to +256
} catch is CancellationError {
throw CancellationError()
} catch let error as SensorKitProcessingError {
throw error
} catch {
if Task.isCancelled {
throw CancellationError()
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do we add both a catch is CancellationError as well as a if Task.isCancelled, both of which throw CancellationError()s? the second (generic catch with isCancelled check) sumsumes the first...

private(set) var inProgressResult: TimedWalkingTestResult
/// The `Task` that waits for the session's duration to pass, and then ends the session
fileprivate var completeSessionTask: Task<TimedWalkingTestResult?, any Error>?
fileprivate var finalizationTask: Task<TimedWalkingTestResult?, any Error>?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do we need (want) this?

Comment on lines -194 to 199
switch context {
case .explicitUserLogoutEvent:
LocalPreferencesStore.standard[.pendingAccountDataCleanupRequired] = true
if case .explicitUserLogoutEvent = context {
await appState.setIsLoggingOut(true)
case .onLaunchCleanupBcNoUser:
break
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

using switch instead of if case .foo = bar is an intentional choice on my end; i always strictly prefer the switch and never use if case in such situations, the reason being that the switch forces us to explicitly enumerate all cases in the enum, meaning that if we add/remove a case down the road (and change the semantics of the enum) the compiler will emit an error in all places in the program where we were using the enum, making it easy for us to validate that these use sites still operate on the correct cases

eg if we were to add a new case to the enum here, with semantics overlapping those of the existing explicitUserLogoutEvent (eg implicitUserLogoutEvent), the switch-based impl would force us to add the case (and make a decision how it should be handled), whereas the if case impl would simply silently ignore the new case, thereby leading to incorrect behaviour

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

noticed this in a couple of other places as well; will probably rewrite all of them back to using a switch...

logger.error("Unable to clear \(description): \(error)")
return false
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this should not be a type-level function here. it's called attemptAccountDataCleanup, but it really just calls the closure it is provided, swallows the error, and returns true/false to indicate success/failure.

it doesn't enforce that the closure perform cleanup work, or that it operate on account data.

lukaskollmer added a commit that referenced this pull request Aug 6, 2026
Co-authored-by: Paul Schmiedmayer <PSchmiedmayer@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants