BackupEngine:
- fetchAssets() runs in Task.detached — prevents 100-500ms UI freeze during
PHFetchResult enumeration on large libraries
- Manifest JSON decode + ManifestIndex Set construction run in Task.detached
- pendingAssets filter runs in Task.detached — O(N) array filter off main thread
- Progress updates throttled to 8 Hz via ProgressThrottle — eliminates the
continuous SwiftUI full-tree redraws caused by per-byte upload callbacks
- bytesSoFar captured by value in upload closure — avoids mutable var capture
- reserveCapacity on manifestEntries array
- os_signpost intervals on FetchAssets, NASConnect, ManifestLoad,
BuildPendingQueue, UploadLoop, UploadFile for Instruments profiling
BackupStatusService:
- countSafe() marked nonisolated — runs in Task.detached off main actor,
keeping touch/animation responsive during PHFetchResult batch enumeration
- ManifestIndex JSON decode + Set construction run in Task.detached
- Bootstrap ManifestIndex(nasListing:) construction off main actor
- writeManifest JSON decode/encode/merge off main actor
- os_signpost intervals on Reconcile, ManifestLoad, CountSafe
Model types:
- ManifestIndex, ManifestEntry, PhotoAsset, BackupFilter marked Sendable —
safe to pass across task boundaries without warnings
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
Replace full [PhotoAsset] array enumeration with PHFetchResult batch
enumeration (autoreleasepool + Task.yield per batch) to keep memory
bounded during reconciliation. Introduce ManifestIndex with two O(1)
Set<String> lookups (localIdentifier + lowercased filename) instead of
O(n×m) linear scans. Scope manifest Data+struct inside buildManifestIndex
so they are released by ARC before the comparison loop begins. Add
UIApplication.didReceiveMemoryWarningNotification handler to cancel
in-flight tasks under memory pressure. Separate debounceTask from
refreshTask to avoid cancelling long-running reconciles on every photo
library event. fetchResultForReconciliation returns PHFetchResult
directly — no [PhotoAsset] array is ever allocated during reconciliation.
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>