From 7c024e315a18e226281116f9db766d968d9b4c77 Mon Sep 17 00:00:00 2001 From: Tim Morgan Date: Tue, 23 Jun 2026 01:37:37 -0700 Subject: [PATCH] Observe NOTAM changes instead of polling on the main actor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `setupRunwayNOTAMObservation` ran a 500ms `Task { @MainActor … }` loop for the lifetime of the takeoff/landing screen, faulting the selected runway's NOTAM through the main `ModelContext` twice a second to detect edits. That recurring main-thread SwiftData access contends with the persistent store coordinator and contributes to the launch-time `performBlockAndWait` hangs (SF50-TOLD-26/-20), which remain active on 3.5.6. Replace the poll with `withObservationTracking` — the same Observation mechanism the NOTAM editing views already use. The main actor now reads the NOTAM only when it actually changes, never on a timer, and the observation re-arms itself after each change. A generation token invalidates the observation registered for a previous runway selection. Refs SF50-TOLD-26, SF50-TOLD-20. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ViewModel/BasePerformanceViewModel.swift | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/SF50 Shared/Performance/ViewModel/BasePerformanceViewModel.swift b/SF50 Shared/Performance/ViewModel/BasePerformanceViewModel.swift index 557c3e0..c812847 100644 --- a/SF50 Shared/Performance/ViewModel/BasePerformanceViewModel.swift +++ b/SF50 Shared/Performance/ViewModel/BasePerformanceViewModel.swift @@ -45,7 +45,7 @@ open class BasePerformanceViewModel: WithIdentifiableError { private let container: ModelContainer internal var model: PerformanceModel? private var cancellables: Set> = [] - private var runwayNOTAMObservationTask: Task? + private var notamObservationToken = UUID() internal let calculationService: PerformanceCalculationService // MARK: - Inputs (to be overridden or used by subclasses) @@ -233,35 +233,30 @@ open class BasePerformanceViewModel: WithIdentifiableError { // MARK: - NOTAM Observation private func setupRunwayNOTAMObservation() { - // Cancel any existing observation - runwayNOTAMObservationTask?.cancel() - runwayNOTAMObservationTask = nil - + // Invalidate any observation registered for a previous runway selection. + notamObservationToken = UUID() guard runway != nil else { return } + observeNOTAMChanges(token: notamObservationToken) + } - // Poll for changes to the NOTAM's snapshot - runwayNOTAMObservationTask = Task { @MainActor [weak self] in - var lastSnapshot = self?.notam.map { NOTAMInput(from: $0) } - while !Task.isCancelled { - try? await Task.sleep(for: .milliseconds(500)) - guard let self else { return } - - // Access the current NOTAM (SwiftData should automatically fetch latest) - let currentSnapshot = notam.map { NOTAMInput(from: $0) } - - // Compare snapshots to detect changes - if let last = lastSnapshot, let current = currentSnapshot { - if last != current { - lastSnapshot = currentSnapshot - model = initializeModel() - recalculate() - } - } else if lastSnapshot != nil || currentSnapshot != nil { - // NOTAM added or removed - lastSnapshot = currentSnapshot - model = initializeModel() - recalculate() - } + /// Recomputes performance whenever the selected runway's NOTAM changes. + /// + /// Observes the NOTAM with `withObservationTracking` rather than a recurring + /// timer. The previous 500ms poll faulted the runway's NOTAM through the main + /// context twice a second for the lifetime of the screen, contending with + /// other main-thread SwiftData access and contributing to launch-time app + /// hangs. Observation touches SwiftData on the main actor only when the NOTAM + /// actually changes — the same mechanism the NOTAM editing views rely on — and + /// re-arms itself after each change. + private func observeNOTAMChanges(token: UUID) { + withObservationTracking { + _ = notam.map { NOTAMInput(from: $0) } + } onChange: { [weak self] in + Task { @MainActor [weak self] in + guard let self, token == notamObservationToken else { return } + model = initializeModel() + recalculate() + observeNOTAMChanges(token: token) } } }