perf: move heavy work off main actor, throttle progress updates, add signposts

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>
This commit is contained in:
Robin Kutesa
2026-05-17 16:42:48 +03:00
parent f40cf9ee37
commit 0f62268af1
4 changed files with 183 additions and 123 deletions

View File

@@ -4,7 +4,8 @@ import Photos
// MARK: ManifestIndex
// Lightweight comparison structure two Sets for O(1) lookups.
// Built from BackupManifest and then the manifest is released.
struct ManifestIndex {
// Sendable: all stored properties are value types safe to pass across task boundaries.
struct ManifestIndex: Sendable {
let localIdentifiers: Set<String> // PHAsset.localIdentifier (primary key)
let lowercasedFilenames: Set<String> // filename fallback for legacy/bootstrap entries
let totalCount: Int // non-hidden file count for NAS Archive stat
@@ -50,7 +51,7 @@ struct ManifestIndex {
// MARK: ManifestEntry
struct ManifestEntry: Codable {
struct ManifestEntry: Codable, Sendable {
let localIdentifier: String // PHAsset.localIdentifier primary key
let filename: String
let creationDate: Date?