@@ -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`. |
@@ -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 |
|