@@ -581,6 +581,7 @@ rows exist". |
||
| 581 | 581 |
| 2026-06-03 | pending | Fast-path unchanged verification after an empty HealthKit delta page. | The successful full-profile reimport spent `15.1s` in finalize despite `0.1s` insert time; Heart Rate alone spent `9.0s` finalizing. The empty-delta path now calls an explicit unchanged-verification store method that copies the previous type summary and daily aggregates without first scanning sample events to prove zero changes. Expected signal: repeated no-delta/full-profile `SummedFinalizeElapsed` and Heart Rate finalize time drop sharply. | |
| 582 | 582 |
| 2026-06-03 | pending | Persist HealthKit capture anchors in SQLite. | A later no-delta attempt still reimported full high-volume types (`Records: 2,646,590`, `WallClock: 45.8s`, Heart Rate `922,521`, Active Energy `346,473`) because incremental anchors were still read from legacy SwiftData `TypeCount`, while the refactored app now chains observations from SQLite. SQLite now stores per-type capture state (`anchor_data`, count, content hash, yearly counts, range) and the import path uses it when legacy `TypeCount` state is missing. Expected signal: the first run after this change may seed anchors; the following no-delta run should report empty HealthKit delta pages instead of full `record_import` counts for large stable types. | |
| 583 | 583 |
| 2026-06-03 | pending | Clarify capture mode in record-import diagnostics. | Two full-profile repeated snapshots after SQLite capture-state persistence completed successfully with stable checksum and identical record count (`2,646,613`). The first ran in `6.3s` with `SummedProcessingElapsed: 0.0s`, `SummedInsertElapsed: 0.0s`, `SummedFinalizeElapsed: 2.6s`; the second ran in `6.4s` with `0.0s` processing/insert and `2.7s` finalize. Heart Rate `922,526` and Active Energy `346,478` each completed around `0.2s`, proving the heavy full reimport path was avoided. The report wording still said "`N` samples in 1 anchored segment", which is ambiguous for inherited unchanged summaries; diagnostics now label unchanged empty-delta, delta-apply, and full-import modes explicitly. | |
| 584 |
+| 2026-06-03 | pending | Add capture-mode summary to diagnostics. | Repeated full-profile captures rarely produce a perfect no-delta report because at least one metric can change between manual runs. Diagnostic reports now include aggregate `CaptureModes` counts plus per-metric `captureMode`, so comparisons can separate unchanged empty-delta metrics from delta-applied metrics and full imports without manually reading every `record_import` line. Expected signal: stable checksum plus high `unchangedDelta` count and zero summed processing/insert confirms the fast path even when a few metrics changed. | |
|
| 584 | 585 |
|
| 585 | 586 |
## Current Diagnosis |
| 586 | 587 |
|
@@ -667,8 +668,9 @@ Prioritize experiments in this order: |
||
| 667 | 668 |
3. Continue repeated full-profile captures, but do not require perfect no-delta. |
| 668 | 669 |
Compare `snapshotChecksum`, `Records`, `WallClockDuration`, |
| 669 | 670 |
`SummedProcessingElapsed`, `SummedInsertElapsed`, `SummedFinalizeElapsed`, |
| 670 |
- and high-volume type timings. Treat stable checksum + zero processing/insert |
|
| 671 |
- as the main unchanged-path signal. |
|
| 671 |
+ `CaptureModes`, and high-volume type timings. Treat stable checksum, a high |
|
| 672 |
+ `unchangedDelta` count, and zero processing/insert as the main unchanged-path |
|
| 673 |
+ signal. |
|
| 672 | 674 |
4. Add or inspect timing around per-record processing for changed high-volume metrics, especially Heart Rate, to separate sample DTO/fingerprint work from SQLite idempotency checks. |
| 673 | 675 |
5. Run a non-chain-start/full-scan benchmark after skipping unchanged `verified` events and fast-pathing already-open visibility ranges. Compare `SummedInsertElapsed`, `Heart Rate insertElapsed`, `Steps insertElapsed`, and `Walking + Running Distance insertElapsed`. |
| 674 | 676 |
6. Reduce any remaining per-sample SQLite writes for unchanged existing samples during non-chain-start full scans. |
@@ -1029,6 +1029,7 @@ final class HealthKitService {
|
||
| 1029 | 1029 |
records: [], |
| 1030 | 1030 |
recordArchiveData: recordArchiveData |
| 1031 | 1031 |
) |
| 1032 |
+ result.captureMode = distribution.captureMode.diagnosticValue |
|
| 1032 | 1033 |
result.timingBreakdown = timingBreakdown |
| 1033 | 1034 |
await persistTypeCaptureState(from: result, observationID: archiveObservationID) |
| 1034 | 1035 |
return result |
@@ -2451,6 +2452,17 @@ private struct SampleDistribution: Sendable {
|
||
| 2451 | 2452 |
case initialImport |
| 2452 | 2453 |
case delta |
| 2453 | 2454 |
case unchangedDelta |
| 2455 |
+ |
|
| 2456 |
+ var diagnosticValue: String {
|
|
| 2457 |
+ switch self {
|
|
| 2458 |
+ case .initialImport: |
|
| 2459 |
+ return "initialImport" |
|
| 2460 |
+ case .delta: |
|
| 2461 |
+ return "delta" |
|
| 2462 |
+ case .unchangedDelta: |
|
| 2463 |
+ return "unchangedDelta" |
|
| 2464 |
+ } |
|
| 2465 |
+ } |
|
| 2454 | 2466 |
} |
| 2455 | 2467 |
|
| 2456 | 2468 |
struct Bin: Sendable {
|
@@ -2747,6 +2759,7 @@ private struct TypeCountFetchResult: Sendable {
|
||
| 2747 | 2759 |
let distributionBins: [DistributionBinData] |
| 2748 | 2760 |
let records: [RecordData] |
| 2749 | 2761 |
let recordArchiveData: Data? |
| 2762 |
+ var captureMode: String = "unavailable" |
|
| 2750 | 2763 |
var timeoutConfiguredSeconds: TimeInterval = 0 |
| 2751 | 2764 |
var totalElapsedSeconds: TimeInterval = 0 |
| 2752 | 2765 |
var timeoutMode: String = "default" |
@@ -2844,7 +2857,8 @@ private extension SnapshotFetchProgress {
|
||
| 2844 | 2857 |
suggestedRetryTimeout: result.suggestedRetryTimeout, |
| 2845 | 2858 |
timeoutCount: result.timeoutCount, |
| 2846 | 2859 |
successCount: result.successCount, |
| 2847 |
- timingBreakdown: result.timingBreakdown |
|
| 2860 |
+ timingBreakdown: result.timingBreakdown, |
|
| 2861 |
+ captureMode: result.captureMode |
|
| 2848 | 2862 |
) |
| 2849 | 2863 |
} |
| 2850 | 2864 |
|
@@ -113,6 +113,7 @@ final class SnapshotFetchProgress {
|
||
| 113 | 113 |
var timeoutCount: Int = 0 |
| 114 | 114 |
var successCount: Int = 0 |
| 115 | 115 |
var timingBreakdown: ImportTimingBreakdown = .zero |
| 116 |
+ var captureMode: String = "unavailable" |
|
| 116 | 117 |
var blockProgress: String = "" |
| 117 | 118 |
var blockElapsedSeconds: TimeInterval = 0 |
| 118 | 119 |
var blockSamplesPerSecond: Double = 0 |
@@ -213,7 +214,8 @@ final class SnapshotFetchProgress {
|
||
| 213 | 214 |
suggestedRetryTimeout: TimeInterval, |
| 214 | 215 |
timeoutCount: Int, |
| 215 | 216 |
successCount: Int, |
| 216 |
- timingBreakdown: ImportTimingBreakdown |
|
| 217 |
+ timingBreakdown: ImportTimingBreakdown, |
|
| 218 |
+ captureMode: String = "unavailable" |
|
| 217 | 219 |
) {
|
| 218 | 220 |
let index = visibleTypeIndex(for: id) |
| 219 | 221 |
types[index].quality = quality |
@@ -233,6 +235,7 @@ final class SnapshotFetchProgress {
|
||
| 233 | 235 |
types[index].timeoutCount = timeoutCount |
| 234 | 236 |
types[index].successCount = successCount |
| 235 | 237 |
types[index].timingBreakdown = timingBreakdown |
| 238 |
+ types[index].captureMode = captureMode |
|
| 236 | 239 |
} |
| 237 | 240 |
|
| 238 | 241 |
func updateTimeoutProfile( |
@@ -382,12 +382,15 @@ struct DashboardView: View {
|
||
| 382 | 382 |
let summedMetricResidualElapsed = progress.types.reduce(0) { partial, type in
|
| 383 | 383 |
partial + timingResidual(totalElapsed: type.totalElapsedSeconds, breakdown: type.timingBreakdown) |
| 384 | 384 |
} |
| 385 |
+ let captureModeCounts = Dictionary(grouping: progress.types, by: \.captureMode) |
|
| 386 |
+ .mapValues(\.count) |
|
| 385 | 387 |
lines.append("SummedMetricTotalElapsed: \(formatDuration(summedMetricTotalElapsed))")
|
| 386 | 388 |
lines.append("SummedFetchElapsed: \(formatDuration(aggregateTiming.fetchElapsedSeconds))")
|
| 387 | 389 |
lines.append("SummedProcessingElapsed: \(formatDuration(aggregateTiming.processingElapsedSeconds))")
|
| 388 | 390 |
lines.append("SummedInsertElapsed: \(formatDuration(aggregateTiming.insertElapsedSeconds))")
|
| 389 | 391 |
lines.append("SummedFinalizeElapsed: \(formatDuration(aggregateTiming.finalizeElapsedSeconds))")
|
| 390 | 392 |
lines.append("SummedResidualElapsed: \(formatDuration(summedMetricResidualElapsed))")
|
| 393 |
+ lines.append("CaptureModes: unchangedDelta=\(captureModeCounts["unchangedDelta", default: 0]), delta=\(captureModeCounts["delta", default: 0]), initialImport=\(captureModeCounts["initialImport", default: 0]), unavailable=\(captureModeCounts["unavailable", default: 0])")
|
|
| 391 | 394 |
lines.append("")
|
| 392 | 395 |
lines.append(failedLines) |
| 393 | 396 |
|
@@ -418,6 +421,7 @@ struct DashboardView: View {
|
||
| 418 | 421 |
lines.append(" identifier: \(type.id)")
|
| 419 | 422 |
lines.append(" quality: \(type.quality)")
|
| 420 | 423 |
lines.append(" count: \(type.recordCount)")
|
| 424 |
+ lines.append(" captureMode: \(type.captureMode)")
|
|
| 421 | 425 |
lines.append(" timeoutMode: \(type.timeoutMode)")
|
| 422 | 426 |
lines.append(" timeoutConfigured: \(formatDuration(type.timeoutConfiguredSeconds))")
|
| 423 | 427 |
lines.append(" lastSuccessfulElapsed: \(type.lastSuccessfulElapsed > 0 ? formatDuration(type.lastSuccessfulElapsed) : "none")")
|