@@ -24,7 +24,7 @@ There are no real deployments, only test installations. Existing prototype datab |
||
| 24 | 24 |
| Area | Current Status | Target / Next Work | |
| 25 | 25 |
|------|----------------|--------------------| |
| 26 | 26 |
| Product docs | Updated | Keep `HealthProbe/Doc/README.md` as canonical index | |
| 27 |
-| HealthKit capture | Capture now opens one archive observation per user-visible snapshot, attaches HealthKit pages, deleted-object evidence, and type verification to that observation id before finishing it, no longer aborts initial full-history imports after a fixed 30-minute wall-clock cap while page-level HealthKit timeouts remain in place, and defers grouped observation summary/daily aggregate rebuilds until per-type verification instead of rebuilding after every imported page | Continue moving UI/cache reads to archive-backed observation ids and revisit adaptive paging/background collection separately | |
|
| 27 |
+| HealthKit capture | Capture now opens one archive observation per user-visible snapshot, attaches HealthKit pages, deleted-object evidence, and type verification to that observation id before finishing it, no longer aborts initial full-history imports after a fixed 30-minute wall-clock cap while page-level HealthKit timeouts remain in place, defers grouped observation summary/daily aggregate rebuilds until per-type verification instead of rebuilding after every imported page, and persists large HealthKit pages in smaller archive chunks while using a smaller anchored-query page size to avoid long monolithic SQLite stalls | Continue moving UI/cache reads to archive-backed observation ids and revisit fully adaptive paging/background collection separately | |
|
| 28 | 28 |
| SQLite archive | Archive v2 schema, snapshot-level observation grouping, differential write path, v2 verification/delete bookkeeping, daily aggregate rebuilds, integrity report, v2 record reads, SQL diff/count/aggregate/provenance/consolidation-evidence APIs, large synthetic diff pagination coverage, formal timing/memory metrics, and XCTest coverage are in place; the legacy `archive_samples` mirror has been removed | Continue moving capture/Dashboard actions to archive/cache DTOs | |
| 29 | 29 |
| Core Data cache | Initial programmatic Core Data model, full-cache rebuild service, read DTOs for observation/type/diff/health rows, and Dashboard archive-cache status wiring are in place | Move remaining export/report paths to cache DTOs and add targeted partial invalidation | |
| 30 | 30 |
| SwiftData cache | Exists; test builds now reset legacy prototype UI/archive/cache stores once for archive v2 so old SwiftData-only snapshots are not treated as backed-up observations. Metric timeout calibration, local device profile settings, operation logging, ContentView preview, Settings data maintenance, legacy detail/PDF views, unused legacy repair/observer services, Dashboard view/view-model access, and legacy anomaly/count-drop review have moved outside SwiftData or been removed. Remaining SwiftData imports are inventoried in [`SwiftData-Retirement-Inventory.md`](SwiftData-Retirement-Inventory.md) | Treat as disposable prototype data; stop returning/storing `HealthSnapshot` bridge handles before removing `ModelContainer` | |
@@ -1099,17 +1099,17 @@ final class HealthKitService {
|
||
| 1099 | 1099 |
anchor: anchor |
| 1100 | 1100 |
) |
| 1101 | 1101 |
} |
| 1102 |
- progress?.updateBlockProgress( |
|
| 1103 |
- typeIdentifier, |
|
| 1104 |
- detail: "Persisting delta page \(pageNumber)", |
|
| 1102 |
+ try await archivePage( |
|
| 1103 |
+ page, |
|
| 1104 |
+ sampleType: sampleType, |
|
| 1105 |
+ observationID: archiveObservationID, |
|
| 1106 |
+ progress: progress, |
|
| 1107 |
+ typeIdentifier: typeIdentifier, |
|
| 1108 |
+ progressDetail: "Persisting delta page \(pageNumber)", |
|
| 1105 | 1109 |
recordCount: previousDistribution.count, |
| 1106 |
- elapsedSeconds: Date().timeIntervalSince(progressStarted), |
|
| 1107 |
- samplesPerSecond: Self.samplesPerSecond( |
|
| 1108 |
- processedCount: processedEventCount, |
|
| 1109 |
- elapsedSeconds: Date().timeIntervalSince(progressStarted) |
|
| 1110 |
- ) |
|
| 1110 |
+ progressStarted: progressStarted, |
|
| 1111 |
+ processedEventCount: processedEventCount |
|
| 1111 | 1112 |
) |
| 1112 |
- try await archivePage(page, sampleType: sampleType, observationID: archiveObservationID) |
|
| 1113 | 1113 |
anchor = page.anchor |
| 1114 | 1114 |
|
| 1115 | 1115 |
if page.samples.isEmpty, page.deletedObjects.isEmpty, |
@@ -1196,17 +1196,17 @@ final class HealthKitService {
|
||
| 1196 | 1196 |
anchor: anchor |
| 1197 | 1197 |
) |
| 1198 | 1198 |
} |
| 1199 |
- progress?.updateBlockProgress( |
|
| 1200 |
- typeIdentifier, |
|
| 1201 |
- detail: "Persisting delta page \(pageNumber)", |
|
| 1199 |
+ try await archivePage( |
|
| 1200 |
+ page, |
|
| 1201 |
+ sampleType: sampleType, |
|
| 1202 |
+ observationID: archiveObservationID, |
|
| 1203 |
+ progress: progress, |
|
| 1204 |
+ typeIdentifier: typeIdentifier, |
|
| 1205 |
+ progressDetail: "Persisting delta page \(pageNumber)", |
|
| 1202 | 1206 |
recordCount: recordMap.count, |
| 1203 |
- elapsedSeconds: Date().timeIntervalSince(progressStarted), |
|
| 1204 |
- samplesPerSecond: Self.samplesPerSecond( |
|
| 1205 |
- processedCount: processedEventCount, |
|
| 1206 |
- elapsedSeconds: Date().timeIntervalSince(progressStarted) |
|
| 1207 |
- ) |
|
| 1207 |
+ progressStarted: progressStarted, |
|
| 1208 |
+ processedEventCount: processedEventCount |
|
| 1208 | 1209 |
) |
| 1209 |
- try await archivePage(page, sampleType: sampleType, observationID: archiveObservationID) |
|
| 1210 | 1210 |
anchor = page.anchor |
| 1211 | 1211 |
|
| 1212 | 1212 |
applyDistributionPage(page, sampleType: sampleType, to: &recordMap) |
@@ -1344,17 +1344,17 @@ final class HealthKitService {
|
||
| 1344 | 1344 |
anchor: anchor |
| 1345 | 1345 |
) |
| 1346 | 1346 |
} |
| 1347 |
- progress?.updateBlockProgress( |
|
| 1348 |
- typeIdentifier, |
|
| 1349 |
- detail: "Persisting import page \(pageNumber)", |
|
| 1347 |
+ try await archivePage( |
|
| 1348 |
+ page, |
|
| 1349 |
+ sampleType: sampleType, |
|
| 1350 |
+ observationID: archiveObservationID, |
|
| 1351 |
+ progress: progress, |
|
| 1352 |
+ typeIdentifier: typeIdentifier, |
|
| 1353 |
+ progressDetail: "Persisting import page \(pageNumber)", |
|
| 1350 | 1354 |
recordCount: recordCount, |
| 1351 |
- elapsedSeconds: Date().timeIntervalSince(progressStarted), |
|
| 1352 |
- samplesPerSecond: Self.samplesPerSecond( |
|
| 1353 |
- processedCount: processedEventCount, |
|
| 1354 |
- elapsedSeconds: Date().timeIntervalSince(progressStarted) |
|
| 1355 |
- ) |
|
| 1355 |
+ progressStarted: progressStarted, |
|
| 1356 |
+ processedEventCount: processedEventCount |
|
| 1356 | 1357 |
) |
| 1357 |
- try await archivePage(page, sampleType: sampleType, observationID: archiveObservationID) |
|
| 1358 | 1358 |
anchor = page.anchor |
| 1359 | 1359 |
|
| 1360 | 1360 |
for sample in page.samples {
|
@@ -1542,9 +1542,58 @@ final class HealthKitService {
|
||
| 1542 | 1542 |
} |
| 1543 | 1543 |
} |
| 1544 | 1544 |
|
| 1545 |
- private func archivePage(_ page: SampleDistributionPage, sampleType: HKSampleType, observationID: Int64) async throws {
|
|
| 1545 |
+ private func archivePage( |
|
| 1546 |
+ _ page: SampleDistributionPage, |
|
| 1547 |
+ sampleType: HKSampleType, |
|
| 1548 |
+ observationID: Int64, |
|
| 1549 |
+ progress: SnapshotFetchProgress? = nil, |
|
| 1550 |
+ typeIdentifier: String? = nil, |
|
| 1551 |
+ progressDetail: String? = nil, |
|
| 1552 |
+ recordCount: Int? = nil, |
|
| 1553 |
+ progressStarted: Date? = nil, |
|
| 1554 |
+ processedEventCount: Int? = nil |
|
| 1555 |
+ ) async throws {
|
|
| 1546 | 1556 |
let observedAt = Date() |
| 1547 |
- _ = try await archiveStore.upsertSamples(page.samples, observedAt: observedAt, observationID: observationID) |
|
| 1557 |
+ if !page.samples.isEmpty {
|
|
| 1558 |
+ let totalSampleBatches = max( |
|
| 1559 |
+ 1, |
|
| 1560 |
+ Int(ceil(Double(page.samples.count) / Double(DistributionCaptureConfiguration.archiveWriteChunkSize))) |
|
| 1561 |
+ ) |
|
| 1562 |
+ var batchNumber = 0 |
|
| 1563 |
+ var batchStart = 0 |
|
| 1564 |
+ while batchStart < page.samples.count {
|
|
| 1565 |
+ batchNumber += 1 |
|
| 1566 |
+ let batchEnd = min( |
|
| 1567 |
+ batchStart + DistributionCaptureConfiguration.archiveWriteChunkSize, |
|
| 1568 |
+ page.samples.count |
|
| 1569 |
+ ) |
|
| 1570 |
+ let sampleBatch = Array(page.samples[batchStart..<batchEnd]) |
|
| 1571 |
+ if let progress, let typeIdentifier, let progressDetail {
|
|
| 1572 |
+ let detail = totalSampleBatches == 1 |
|
| 1573 |
+ ? progressDetail |
|
| 1574 |
+ : "\(progressDetail) (\(batchNumber)/\(totalSampleBatches))" |
|
| 1575 |
+ progress.updateBlockProgress( |
|
| 1576 |
+ typeIdentifier, |
|
| 1577 |
+ detail: detail, |
|
| 1578 |
+ recordCount: recordCount, |
|
| 1579 |
+ elapsedSeconds: progressStarted.map { Date().timeIntervalSince($0) },
|
|
| 1580 |
+ samplesPerSecond: {
|
|
| 1581 |
+ guard let progressStarted, let processedEventCount else { return nil }
|
|
| 1582 |
+ return Self.samplesPerSecond( |
|
| 1583 |
+ processedCount: processedEventCount, |
|
| 1584 |
+ elapsedSeconds: Date().timeIntervalSince(progressStarted) |
|
| 1585 |
+ ) |
|
| 1586 |
+ }() |
|
| 1587 |
+ ) |
|
| 1588 |
+ } |
|
| 1589 |
+ _ = try await archiveStore.upsertSamples( |
|
| 1590 |
+ sampleBatch, |
|
| 1591 |
+ observedAt: observedAt, |
|
| 1592 |
+ observationID: observationID |
|
| 1593 |
+ ) |
|
| 1594 |
+ batchStart = batchEnd |
|
| 1595 |
+ } |
|
| 1596 |
+ } |
|
| 1548 | 1597 |
for deletedObject in page.deletedObjects {
|
| 1549 | 1598 |
try await archiveStore.recordDisappearance( |
| 1550 | 1599 |
sampleUUIDHash: HashService.sampleUUIDHash(deletedObject.uuid.uuidString), |
@@ -2390,6 +2439,7 @@ private extension SnapshotFetchProgress {
|
||
| 2390 | 2439 |
} |
| 2391 | 2440 |
|
| 2392 | 2441 |
private enum DistributionCaptureConfiguration {
|
| 2393 |
- static let queryPageLimit = 25_000 |
|
| 2442 |
+ static let queryPageLimit = 5_000 |
|
| 2394 | 2443 |
static let pageTimeoutSeconds: TimeInterval = 60 |
| 2444 |
+ static let archiveWriteChunkSize = 1_000 |
|
| 2395 | 2445 |
} |