@@ -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; |
@@ -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 |
@@ -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 |
|