Просмотр исходного кода

Prefetch forecast values to eliminate N+1 queries

Three Core Data access paths were faulting ForecastValue relationships
one row at a time. Switch each to a single fetch with
relationshipKeyPathsForPrefetching so forecast values are materialized
in one batch IN-query:

- parseForecastValues: replace per-ID existingObject loop with a
  predicate fetch on Forecast scoped by determination + type.
- updateForecastData: add a prefetch hop on viewContext before
  materializing forecasts from background-context object IDs, since
  prefetching on the task context does not carry over.
- mapForecastsForChart: fetch forecasts directly with prefetching and
  drop the unused Set<Forecast>.extractValues extension.
Marvin Polscheit 1 месяц назад
Родитель
Сommit
a19b5f45a1

+ 20 - 23
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -120,31 +120,28 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
     func parseForecastValues(ofType type: String, from determinationID: NSManagedObjectID) async -> [Int]? {
         let context = makeContext()
         context.name = "parseForecastValues"
-        let forecastIDs = await getForecastIDs(for: determinationID, in: context)
-
-        var forecastValuesList: [Int] = []
-
-        for forecastID in forecastIDs {
-            await context.perform {
-                if let forecast = try? context.existingObject(with: forecastID) as? Forecast {
-                    // Filter the forecast based on the type
-                    if forecast.type == type {
-                        let forecastValueIDs = forecast.forecastValues?.sorted(by: { $0.index < $1.index }).map(\.objectID) ?? []
-
-                        for forecastValueID in forecastValueIDs {
-                            if let forecastValue = try? context
-                                .existingObject(with: forecastValueID) as? ForecastValue
-                            {
-                                let forecastValueInt = Int(forecastValue.value)
-                                forecastValuesList.append(forecastValueInt)
-                            }
-                        }
-                    }
-                }
+
+        return await context.perform {
+            let request = NSFetchRequest<Forecast>(entityName: "Forecast")
+            request.predicate = NSPredicate(
+                format: "orefDetermination = %@ AND type == %@",
+                determinationID,
+                type
+            )
+            request.fetchLimit = 1
+            request.relationshipKeyPathsForPrefetching = ["forecastValues"]
+
+            do {
+                guard let forecast = try context.fetch(request).first else { return nil }
+                let values = forecast.forecastValuesArray.map { Int($0.value) }
+                return values.isEmpty ? nil : values
+            } catch {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast of type \(type): \(error)"
+                )
+                return nil
             }
         }
-
-        return forecastValuesList.isEmpty ? nil : forecastValuesList
     }
 
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination? {

+ 11 - 0
Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift

@@ -39,6 +39,17 @@ extension Home.StateModel {
         var allForecastValues = [[Int]]()
         var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
 
+        // Prefetch all Forecasts with their forecastValues into viewContext in a single IN-query
+        // to avoid N+1 individual SELECTs when materializing via existingObject below.
+        let forecastObjectIDs = forecastDataIDs.map(\.forecastID)
+        if !forecastObjectIDs.isEmpty {
+            let prefetchRequest = NSFetchRequest<Forecast>(entityName: "Forecast")
+            prefetchRequest.predicate = NSPredicate(format: "SELF IN %@", forecastObjectIDs)
+            prefetchRequest.relationshipKeyPathsForPrefetching = ["forecastValues"]
+            prefetchRequest.returnsObjectsAsFaults = false
+            _ = try? viewContext.fetch(prefetchRequest)
+        }
+
         // Process prefetched data directly
         for data in forecastDataIDs {
             if let forecast = try? viewContext.existingObject(with: data.forecastID) as? Forecast {

+ 47 - 51
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -847,56 +847,62 @@ extension Treatments.StateModel {
     }
 
     private func mapForecastsForChart() async -> Determination? {
-        do {
-            let determinationFetchContext = CoreDataStack.shared.newTaskContext()
-            determinationFetchContext.name = "TreatmentsStateModel.mapForecastsForChart"
-
-            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
-                .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
+        guard let determinationID = await MainActor.run(body: { determinationObjectIDs.first }) else {
+            return nil
+        }
 
-            let determination = await determinationFetchContext.perform {
-                let determinationObject = determinationObjects.first
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "TreatmentsStateModel.mapForecastsForChart"
 
-                let forecastsSet = determinationObject?.forecasts ?? []
-                let predictions = Predictions(
-                    iob: forecastsSet.extractValues(for: "iob"),
-                    zt: forecastsSet.extractValues(for: "zt"),
-                    cob: forecastsSet.extractValues(for: "cob"),
-                    uam: forecastsSet.extractValues(for: "uam")
-                )
+        return await context.perform {
+            let request = NSFetchRequest<Forecast>(entityName: "Forecast")
+            request.predicate = NSPredicate(format: "orefDetermination = %@", determinationID)
+            request.relationshipKeyPathsForPrefetching = ["forecastValues"]
 
-                return Determination(
-                    id: UUID(),
-                    reason: "",
-                    units: 0,
-                    insulinReq: 0,
-                    sensitivityRatio: 0,
-                    rate: 0,
-                    duration: 0,
-                    iob: 0,
-                    cob: 0,
-                    predictions: predictions.isEmpty ? nil : predictions,
-                    carbsReq: 0,
-                    temp: nil,
-                    reservoir: 0,
-                    insulinForManualBolus: 0,
-                    manualBolusErrorString: 0,
-                    carbRatio: 0,
-                    received: false
+            let forecasts: [Forecast]
+            do {
+                forecasts = try context.fetch(request)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error mapping forecasts for chart: \(error)"
                 )
+                return nil
             }
 
-            guard !determinationObjects.isEmpty else {
-                return nil
+            func values(for type: String) -> [Int]? {
+                let result = forecasts.first { $0.type == type }?
+                    .forecastValuesArray
+                    .map { Int($0.value) }
+                return (result?.isEmpty ?? true) ? nil : result
             }
 
-            return determination
-        } catch {
-            debug(
-                .default,
-                "\(DebuggingIdentifiers.failed) Error mapping forecasts for chart: \(error)"
+            let predictions = Predictions(
+                iob: values(for: "iob"),
+                zt: values(for: "zt"),
+                cob: values(for: "cob"),
+                uam: values(for: "uam")
+            )
+
+            return Determination(
+                id: UUID(),
+                reason: "",
+                units: 0,
+                insulinReq: 0,
+                sensitivityRatio: 0,
+                rate: 0,
+                duration: 0,
+                iob: 0,
+                cob: 0,
+                predictions: predictions.isEmpty ? nil : predictions,
+                carbsReq: 0,
+                temp: nil,
+                reservoir: 0,
+                insulinForManualBolus: 0,
+                manualBolusErrorString: 0,
+                carbRatio: 0,
+                received: false
             )
-            return nil
         }
     }
 
@@ -988,16 +994,6 @@ extension Treatments.StateModel {
     }
 }
 
-private extension Set where Element == Forecast {
-    func extractValues(for type: String) -> [Int]? {
-        let values = first { $0.type == type }?
-            .forecastValues?
-            .sorted { $0.index < $1.index }
-            .compactMap { Int($0.value) }
-        return values?.isEmpty ?? true ? nil : values
-    }
-}
-
 private extension Predictions {
     var isEmpty: Bool {
         iob == nil && zt == nil && cob == nil && uam == nil