Showing 2 changed files with 27 additions and 28 deletions
+4 -2
HealthProbe/Doc/04-project/Import-Optimization-Log.md
@@ -314,6 +314,7 @@ from post-import UI recovery.
314 314
 | 2026-06-02 | `3dd5f48` | Fortified scheduled test database reset with a disk marker and extra SQLite sidecar cleanup. | Real-device report confirmed reset produced `previousSnapshotID: none`, `isChainStart: true`, and a clean first-snapshot timeline. |
315 315
 | 2026-06-02 | `06ee6be` | Removed unused `sample_versions(start_date, end_date)` and redundant `sample_visibility_ranges(sample_id, last_observation_id)` indexes. | Comparable first-import report was flat: wall clock 12m43s -> 12m39s and summed insert 10m11s -> 10m07s. Treat as no material performance change. |
316 316
 | 2026-06-02 | pending | Moved Dashboard archive cache refresh/rebuild off the UI path after snapshot completion. | Awaiting real-device confirmation that the app no longer stays unresponsive for roughly one minute after a completed snapshot. |
317
+| 2026-06-03 | pending | Fast-path unchanged samples whose current version already has an open visibility range. | Awaiting non-chain-start/full-scan report. Expected signal is lower insert elapsed on repeated captures with mostly unchanged rows, especially Heart Rate. |
317 318
 
318 319
 ## Current Diagnosis
319 320
 
@@ -329,6 +330,7 @@ The likely bottleneck is per-row SQLite work:
329 330
 - index maintenance while importing high-volume rows;
330 331
 - multiple dependent writes per sample;
331 332
 - commit / transaction shape;
333
+- no-op visibility range maintenance for unchanged existing samples;
332 334
 - Core Data or UI refresh work after SQLite completes. This became the active
333 335
   bottleneck after a no-delta incremental snapshot completed in 9.2s but the UI
334 336
   remained unresponsive for roughly one minute.
@@ -347,8 +349,8 @@ The likely bottleneck is per-row SQLite work:
347 349
 Prioritize experiments in this order:
348 350
 
349 351
 1. Confirm whether the background dashboard cache refresh removes the post-import UI freeze. If not, add explicit timings around cache rebuild, dashboard refresh, diagnostic report generation, and sheet dismissal.
350
-2. Run a non-chain-start/full-scan benchmark after skipping unchanged `verified` events and compare `SummedInsertElapsed`, `Heart Rate insertElapsed`, `Steps insertElapsed`, and `Walking + Running Distance insertElapsed`.
351
-3. Reduce remaining per-sample SQLite writes for unchanged existing samples during non-chain-start full scans, especially open visibility-range existence checks.
352
+2. 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`.
353
+3. Reduce any remaining per-sample SQLite writes for unchanged existing samples during non-chain-start full scans.
352 354
 4. Profile whether index maintenance dominates first-import insert cost.
353 355
 5. Consider a guarded bulk-import mode for first observations:
354 356
    - keep archive semantics unchanged;
+23 -26
HealthProbe/Services/SQLiteHealthArchiveStore.swift
@@ -2170,22 +2170,29 @@ actor SQLiteHealthArchiveStore: HealthArchiveStore {
2170 2170
                 statementCache: statementCache
2171 2171
             )
2172 2172
         } else {
2173
-            try closeOpenVisibilityRanges(
2174
-                sampleID: sampleResult.id,
2175
-                excludingVersionID: versionResult.id,
2176
-                closedAtObservationID: observationID,
2177
-                observedAt: row.observedAt,
2178
-                db: db,
2179
-                statementCache: statementCache
2180
-            )
2181
-            try insertOpenVisibilityRangeIfNeeded(
2173
+            if try !hasOpenVisibilityRange(
2182 2174
                 sampleID: sampleResult.id,
2183 2175
                 versionID: versionResult.id,
2184
-                observationID: observationID,
2185
-                observedAt: row.observedAt,
2186 2176
                 db: db,
2187 2177
                 statementCache: statementCache
2188
-            )
2178
+            ) {
2179
+                try closeOpenVisibilityRanges(
2180
+                    sampleID: sampleResult.id,
2181
+                    excludingVersionID: versionResult.id,
2182
+                    closedAtObservationID: observationID,
2183
+                    observedAt: row.observedAt,
2184
+                    db: db,
2185
+                    statementCache: statementCache
2186
+                )
2187
+                try insertOpenVisibilityRange(
2188
+                    sampleID: sampleResult.id,
2189
+                    versionID: versionResult.id,
2190
+                    observationID: observationID,
2191
+                    observedAt: row.observedAt,
2192
+                    db: db,
2193
+                    statementCache: statementCache
2194
+                )
2195
+            }
2189 2196
         }
2190 2197
 
2191 2198
         return ArchiveV2SampleWriteResult(
@@ -2866,17 +2873,15 @@ actor SQLiteHealthArchiveStore: HealthArchiveStore {
2866 2873
         }
2867 2874
     }
2868 2875
 
2869
-    private func insertOpenVisibilityRangeIfNeeded(
2876
+    private func hasOpenVisibilityRange(
2870 2877
         sampleID: Int64,
2871 2878
         versionID: Int64,
2872
-        observationID: Int64,
2873
-        observedAt: Date,
2874 2879
         db: OpaquePointer?,
2875 2880
         statementCache: SQLiteStatementCache? = nil
2876
-    ) throws {
2881
+    ) throws -> Bool {
2877 2882
         let existing = try optionalInt64(
2878 2883
             """
2879
-            SELECT first_observation_id
2884
+            SELECT 1
2880 2885
             FROM sample_visibility_ranges
2881 2886
             WHERE sample_id = ? AND version_id = ? AND last_observation_id IS NULL
2882 2887
             LIMIT 1
@@ -2887,15 +2892,7 @@ actor SQLiteHealthArchiveStore: HealthArchiveStore {
2887 2892
             bindInt64(sampleID, to: 1, in: statement)
2888 2893
             bindInt64(versionID, to: 2, in: statement)
2889 2894
         }
2890
-        guard existing == nil else { return }
2891
-        try insertOpenVisibilityRange(
2892
-            sampleID: sampleID,
2893
-            versionID: versionID,
2894
-            observationID: observationID,
2895
-            observedAt: observedAt,
2896
-            db: db,
2897
-            statementCache: statementCache
2898
-        )
2895
+        return existing != nil
2899 2896
     }
2900 2897
 
2901 2898
     private func insertOpenVisibilityRange(