Showing 2 changed files with 38 additions and 10 deletions
+33 -4
HealthProbe/Doc/04-project/Import-Optimization-Log.md
@@ -446,6 +446,31 @@ The Snapshots list should also read observation timeline rows directly from
446 446
 SQLite so it does not depend on a delayed Core Data cache rebuild to show the
447 447
 freshly finished observation.
448 448
 
449
+### 2026-06-03 Successful Snapshot Still Freezes After Copying Report
450
+
451
+Commit context: after `1229f19`. Source: user-provided diagnostic report copied
452
+from the app before a new freeze. Unlike the previous overnight log, this report
453
+contains no `healthKit.detailCache.buildBegin` evidence and the snapshot itself
454
+completed successfully.
455
+
456
+| Metric | Previous | Current | Change |
457
+|--------|----------|---------|--------|
458
+| Total records | 1,579,253 | 1,579,445 | +192 |
459
+| Wall clock | 31.3s | 35.5s | +4.2s |
460
+| Summed metric total | 30.6s | 35.0s | +4.4s |
461
+| Summed fetch | 0.2s | 0.2s | flat |
462
+| Summed processing | 18.1s | 21.0s | +2.9s |
463
+| Summed insert | 0.0s | 0.2s | +0.2s |
464
+| Summed finalize | 11.5s | 12.6s | +1.1s |
465
+
466
+Conclusion: the legacy detail-cache crash path was removed, but the app can
467
+still freeze after the user copies the diagnostic report. The remaining
468
+post-snapshot culprit is the automatic Dashboard Core Data cache rebuild, which
469
+can consume enough device I/O/CPU to make the app appear frozen even when run
470
+from a detached task. Automatic post-snapshot refresh should read the small
471
+timeline/status data directly from SQLite; full Core Data cache rebuild should
472
+remain explicit/manual until partial invalidation exists.
473
+
449 474
 ## Optimization Iterations
450 475
 
451 476
 | Date | Commit | Change | Result / Status |
@@ -473,6 +498,7 @@ freshly finished observation.
473 498
 | 2026-06-03 | `e49a79d` | Skip legacy SwiftData detail-cache precompute for large type archives. | Triggered by a crash after building Heart Rate and Active Energy detail caches. Expected signal: no post-import `NSGenericException`, lower post-import wall-clock gap, and `healthKit.detailCache.skipLargeLegacyArchive` logs for high-volume types. |
474 499
 | 2026-06-03 | `7d52262` | Start Dashboard archive cache refresh without awaiting it after snapshot completion. | Triggered by continued app unresponsiveness after a successful 31.3s incremental snapshot. Expected signal: progress sheet/result UI remains responsive while cache rows refresh later. |
475 500
 | 2026-06-03 | `1229f19` | Disable legacy SwiftData detail-cache precompute completely and load Snapshots timeline from SQLite. | Triggered by overnight crash after two small detail caches were built. Expected signal: no `healthKit.detailCache.buildBegin` logs during snapshot save, no Core Data mutated-while-enumerated abort, and the new SQLite observation appears in Snapshots without waiting for cache rebuild. |
501
+| 2026-06-03 | pending | Stop automatic Dashboard Core Data cache rebuild after snapshot; refresh latest rows from SQLite only. | Triggered by freeze after copying a successful diagnostic report. Expected signal: copying diagnostics and returning to Dashboard/Snapshots remains responsive; Core Data cache rebuild is no longer started automatically after snapshot completion. |
476 502
 
477 503
 ## Current Diagnosis
478 504
 
@@ -494,7 +520,9 @@ The likely bottleneck is per-row SQLite work:
494 520
   the active crash/performance issue even for small caches; any snapshot-save
495 521
   mutation of `TypeCount.detailCacheData` is unsafe and should remain disabled.
496 522
 - Dashboard Core Data archive cache refresh after snapshot completion, when the
497
-  UI path awaits the rebuild instead of publishing results asynchronously.
523
+  app starts a full rebuild immediately after the import. Even detached rebuilds
524
+  can overwhelm real-device I/O/CPU, so automatic post-snapshot UI refresh should
525
+  use SQLite summary rows only.
498 526
 
499 527
 ## Open Issues / Observations
500 528
 
@@ -515,9 +543,10 @@ The likely bottleneck is per-row SQLite work:
515 543
 
516 544
 Prioritize experiments in this order:
517 545
 
518
-1. Run an incremental snapshot after disabling legacy detail-cache precompute.
519
-   Confirm there are no `healthKit.detailCache.buildBegin` logs, no Core Data
520
-   mutated-while-enumerated abort, and the new observation appears in Snapshots.
546
+1. Run an incremental snapshot after removing automatic Core Data cache rebuild.
547
+   Confirm there are no `healthKit.detailCache.buildBegin` logs, copying the
548
+   diagnostic report does not freeze the app, and Dashboard/Snapshots show the
549
+   latest observation from SQLite.
521 550
 2. Run a repeated no-delta benchmark after copying unchanged metric summaries and daily aggregates. Compare `SummedFinalizeElapsed`, `Heart Rate finalizeElapsed`, `Active Energy finalizeElapsed`, and wall clock.
522 551
 3. 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.
523 552
 4. 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`.
+5 -6
HealthProbe/ViewModels/DashboardViewModel.swift
@@ -384,13 +384,10 @@ final class DashboardViewModel {
384 384
         archiveCacheRefreshTask?.cancel()
385 385
         archiveCacheRefreshTask = Task.detached(priority: .utility) { [weak self] in
386 386
             do {
387
-                let cache = try CoreDataArchiveCacheStore()
388
-                _ = try cache.rebuild(fromArchiveAt: SQLiteHealthArchiveStore.defaultDatabaseURL)
389
-                let rows = try cache.observationRows(limit: 2)
390
-                let healthStatus = try cache.latestArchiveHealthStatus()
387
+                let rows = try await SQLiteHealthArchiveStore.shared.observationRows(limit: 2)
391 388
                 let result = ArchiveCacheRefreshResult(
392 389
                     observationRows: rows,
393
-                    healthStatus: healthStatus
390
+                    healthStatus: nil
394 391
                 )
395 392
 
396 393
                 guard !Task.isCancelled else { return }
@@ -408,7 +405,9 @@ final class DashboardViewModel {
408 405
     private func applyArchiveCacheRefreshResult(_ result: ArchiveCacheRefreshResult) {
409 406
         archiveObservationRows = result.observationRows
410 407
         latestArchiveObservation = result.observationRows.first
411
-        archiveHealthStatus = result.healthStatus
408
+        if let healthStatus = result.healthStatus {
409
+            archiveHealthStatus = healthStatus
410
+        }
412 411
         archiveCacheError = nil
413 412
     }
414 413