Showing 3 changed files with 37 additions and 22 deletions
+8 -0
HealthProbe/Doc/04-project/Development-Log.md
@@ -67,6 +67,14 @@ A later retry on 2026-06-06 from the same device reported both Blood Pressure
67 67
 types as unavailable because HealthKit returned `Authorization not determined`
68 68
 for earliest/latest queries.
69 69
 
70
+Another 2026-06-06 report from the restored device later imported both Blood
71
+Pressure Systolic and Diastolic successfully, `5,124` samples each. After the
72
+device unfroze enough for Settings to open, Blood Pressure appeared not
73
+authorized there, even though the user had selected all types in the HealthProbe
74
+authorization prompt. This indicates the restored device's Health authorization
75
+state is changing or being reconciled independently of HealthProbe's read-only
76
+queries.
77
+
70 78
 Interpretation:
71 79
 - this is evidence of a device / iOS HealthKit authorization or Health database
72 80
   state change;
+4 -0
HealthProbe/Doc/04-project/Import-Optimization-Log.md
@@ -600,6 +600,7 @@ rows exist".
600 600
 | 2026-06-05 | `cfd9de8` | Split processing timing diagnostics. | Confirmed on a full-profile repeated capture with `buildFingerprint: 1.0(1)-1780683224-92064`: wall clock `14.7s`, `127/127` complete, `CaptureModes: unchangedDelta=120, delta=7`, and `DeltaEvents: 11`. `SummedProcessingElapsed` was `8.5s` and `SummedProcessingRecordArchiveRebuildElapsed` was also `8.5s`; delta apply, initial record processing, record archive finalization, and processing other all rounded to `0.0s`. Per-type rebuild cost dominated changed high-volume metrics: Heart Rate `4.4s`, Active Energy `1.7s`, Basal Energy `1.5s`, Steps `0.4s`, and Walking + Running Distance `0.4s`. Conclusion: the repeated-capture bottleneck is no longer SQLite finalization; it is the legacy compact `recordArchiveData` rebuild for changed types. |
601 601
 | 2026-06-05 | `0db2f5e` | Skip legacy compact archive rebuild for SQLite-backed deltas. | Confirmed on two full-profile repeated captures. First report, `buildFingerprint: 1.0(1)-1780689289-92064`, completed in `6.2s` with `127/127` complete, `CaptureModes: unchangedDelta=118, delta=9`, `DeltaEvents: 116`, `SummedProcessingElapsed: 0.0s`, and `SummedProcessingRecordArchiveRebuildElapsed: 0.0s`; Heart Rate and Active Energy each completed in `0.2s` with no rebuild. Second report, `buildFingerprint: 1.0(1)-1780689890-92064`, completed in `5.8s` with `CaptureModes: unchangedDelta=121, delta=6`, `DeltaEvents: 7`, fetch `2.0s`, insert `0.1s`, finalize `1.7s`, residual `1.1s`, and processing/rebuild still `0.0s`. Conclusion: the legacy compact archive rebuild was the repeated-capture bottleneck and is now removed from the normal SQLite-backed delta path; the repeated full-profile floor is now roughly `6s` on this device/database. |
602 602
 | 2026-06-06 | pending | Expose saved diagnostic reports in Settings. | Diagnostic reports were already persisted under Application Support at snapshot completion, but they were not discoverable from the app after dismissing the result sheet. Settings now lists the latest saved reports, opens them with the same chunked lazy diagnostics viewer, supports copy, and allows per-report deletion. Expected signal: future import analysis can recover the exact report text after the fact instead of depending on screenshots or manual copy at completion time. |
603
+| 2026-06-06 | pending | Stop HealthKit date-boundary queries on timeout cancellation. | A restored-device report with `buildFingerprint: 1.0(1)-1780695759-92064` completed as partial after `263m 4s`. `Zinc` was the only degraded metric and spent `262m 50s` in fetch, with both `earliest_sample` and `latest_sample` reported as timeout after `15770.00s` despite `timeoutConfigured: 15.0s`. Blood Pressure imported successfully in the same report, so this was not a general authorization failure. Diagnosis: the timeout task marked cancellation, but the underlying `HKSampleQuery` for earliest/latest date was not stopped, so the task group could not return until HealthKit eventually completed. Date-boundary queries now use the cancellable HealthKit continuation box and call `HKHealthStore.stop(_:)` when the timeout cancels the task. Expected signal: a wedged first metric should fail near its configured timeout, not after hours, and the remaining metrics should continue. |
603 604
 
604 605
 ## Current Diagnosis
605 606
 
@@ -815,6 +816,9 @@ The likely bottleneck is per-row SQLite work:
815 816
   Diagnostics sheet is useful, but future comparisons should not depend on the
816 817
   operator remembering to copy the visible report. Saved reports are now exposed
817 818
   in Settings so the exact text can be recovered later.
819
+- A timeout report with elapsed time far above `timeoutConfigured` means the
820
+  underlying HealthKit query ignored task cancellation. Date-boundary queries now
821
+  explicitly stop their `HKSampleQuery` on cancellation.
818 822
 - After a completed import, the app may remain unresponsive or crash in legacy
819 823
   post-import cache work. A 2026-06-03 console log showed Heart Rate and Active
820 824
   Energy `TypeCount.detailCacheData` precompute immediately before a Core Data
+25 -22
HealthProbe/Services/HealthKitService.swift
@@ -2001,32 +2001,35 @@ final class HealthKitService {
2001 2001
     }
2002 2002
 
2003 2003
     private func fetchEarliestDate(for sampleType: HKSampleType) async throws -> Date? {
2004
-        try await withCheckedThrowingContinuation { continuation in
2005
-            let query = HKSampleQuery(
2006
-                sampleType: sampleType,
2007
-                predicate: nil,
2008
-                limit: 1,
2009
-                sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)]
2010
-            ) { _, samples, error in
2011
-                if let error { continuation.resume(throwing: error); return }
2012
-                continuation.resume(returning: samples?.first?.startDate)
2013
-            }
2014
-            store.execute(query)
2015
-        }
2004
+        try await fetchBoundaryDate(for: sampleType, ascending: true)
2016 2005
     }
2017 2006
 
2018 2007
     private func fetchLatestDate(for sampleType: HKSampleType) async throws -> Date? {
2019
-        try await withCheckedThrowingContinuation { continuation in
2020
-            let query = HKSampleQuery(
2021
-                sampleType: sampleType,
2022
-                predicate: nil,
2023
-                limit: 1,
2024
-                sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]
2025
-            ) { _, samples, error in
2026
-                if let error { continuation.resume(throwing: error); return }
2027
-                continuation.resume(returning: samples?.first?.startDate)
2008
+        try await fetchBoundaryDate(for: sampleType, ascending: false)
2009
+    }
2010
+
2011
+    private func fetchBoundaryDate(for sampleType: HKSampleType, ascending: Bool) async throws -> Date? {
2012
+        let box = HealthKitQueryContinuationBox<Date?>()
2013
+        return try await withTaskCancellationHandler {
2014
+            try await withCheckedThrowingContinuation { continuation in
2015
+                box.setContinuation(continuation)
2016
+                let query = HKSampleQuery(
2017
+                    sampleType: sampleType,
2018
+                    predicate: nil,
2019
+                    limit: 1,
2020
+                    sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: ascending)]
2021
+                ) { _, samples, error in
2022
+                    if let error {
2023
+                        box.resume(throwing: error)
2024
+                        return
2025
+                    }
2026
+                    box.resume(returning: samples?.first?.startDate)
2027
+                }
2028
+                box.setQuery(query, store: store)
2029
+                store.execute(query)
2028 2030
             }
2029
-            store.execute(query)
2031
+        } onCancel: {
2032
+            box.cancel()
2030 2033
         }
2031 2034
     }
2032 2035