polscm32 1 년 전
부모
커밋
503966f68d

+ 19 - 1
Model/CoreDataStack.swift

@@ -56,9 +56,27 @@ class CoreDataStack: ObservableObject {
         return stack
     }()
 
+    // Shared managed object model
+    static var managedObjectModel: NSManagedObjectModel = {
+        let bundle = Bundle(for: CoreDataStack.self)
+        guard let url = bundle.url(forResource: "TrioCoreDataPersistentContainer", withExtension: "momd") else {
+            fatalError("Failed \(DebuggingIdentifiers.failed) to locate momd file")
+        }
+
+        guard let model = NSManagedObjectModel(contentsOf: url) else {
+            fatalError("Failed \(DebuggingIdentifiers.failed) to load momd file")
+        }
+
+        return model
+    }()
+
     /// A persistent container to set up the Core Data Stack
     lazy var persistentContainer: NSPersistentContainer = {
-        let container = NSPersistentContainer(name: "TrioCoreDataPersistentContainer")
+        // Use shared model instead of loading a new one
+        let container = NSPersistentContainer(
+            name: "TrioCoreDataPersistentContainer",
+            managedObjectModel: Self.managedObjectModel
+        )
 
         guard let description = container.persistentStoreDescriptions.first else {
             fatalError("Failed \(DebuggingIdentifiers.failed) to retrieve a persistent store description")

+ 1 - 1
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -80,7 +80,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         }
     }
 
-    // Fetch forecast value IDs for a given data set
+    // Fetch forecast objects for a given data set
     func fetchForecastObjects(
         for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
         in context: NSManagedObjectContext

+ 0 - 5
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -13,7 +13,6 @@ protocol PumpHistoryStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     func storePumpEvents(_ events: [NewPumpEvent]) async
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
-    func recent() -> [PumpHistoryEvent]
     func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent]
     func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent]
@@ -250,10 +249,6 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
     }
 
-    func recent() -> [PumpHistoryEvent] {
-        storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self)?.reversed() ?? []
-    }
-
     func determineBolusEventType(for event: PumpEventStored) -> PumpEventStored.EventType {
         if event.bolus!.isSMB {
             return .smb

+ 196 - 139
TrioTests/CoreDataTests/DeterminationStorageTests.swift

@@ -104,143 +104,200 @@ import Testing
         }
     }
 
-//    @Test("Test complete forecast hierarchy prefetching") func testForecastHierarchyPrefetching() async throws {
-//        // Given
-//        let date = Date()
-//        let backgroundContext = CoreDataStack.shared.newTaskContext()
-//        let forecastTypes = ["iob", "cob", "zt", "uam"]
-//
-//        // Create test determination with complete forecast hierarchy
-//        let determination = OrefDetermination(context: backgroundContext)
-//        determination.id = UUID()
-//        determination.deliverAt = date
-//
-//        // Create all forecast types with values
-//        for type in forecastTypes {
-//            let forecast = Forecast(context: backgroundContext)
-//            forecast.id = UUID()
-//            forecast.date = date
-//            forecast.type = type
-//            forecast.orefDetermination = determination
-//
-//            // Add test values
-//            for i in 0 ..< 5 {
-//                let value = ForecastValue(context: backgroundContext)
-//                value.index = Int32(i)
-//                value.value = Int32(100 + (i * 10))
-//                value.forecast = forecast
-//            }
-//        }
-//
-//        try await backgroundContext.save()
-//
-//        // When - Fetch complete hierarchy
-//        let request = NSFetchRequest<OrefDetermination>(entityName: "OrefDetermination")
-//        request.predicate = NSPredicate(format: "SELF = %@", determination.objectID)
-//        request.relationshipKeyPathsForPrefetching = ["forecasts", "forecasts.forecastValues"]
-//
-//        let fetchedDetermination = try await backgroundContext.perform {
-//            try request.execute().first
-//        }
-//
-//        // Then
-//        #expect(fetchedDetermination != nil)
-//        #expect(fetchedDetermination?.forecasts?.count == 4)
-//
-//        let forecasts = fetchedDetermination?.forecasts?.allObjects as? [Forecast] ?? []
-//        for forecast in forecasts {
-//            #expect(forecastTypes.contains(forecast.type ?? ""))
-//            #expect(forecast.forecastValues?.count == 5)
-//
-//            let values = forecast.forecastValuesArray
-//            #expect(values.count == 5)
-//            for (index, value) in values.enumerated() {
-//                #expect(value.value == Int32(100 + (index * 10)))
-//            }
-//        }
-//
-//        // Cleanup
-//        await backgroundContext.perform {
-//            backgroundContext.delete(determination)
-//            try? backgroundContext.save()
-//        }
-//    }
-
-//    @Test("Test forecast handling with multiple forecast types") func testMultipleForecastTypes() async throws {
-//        // Given
-//        let date = Date()
-//        let backgroundContext = CoreDataStack.shared.newTaskContext()
-//        var determinationId: NSManagedObjectID?
-//        let forecastTypes = ["iob", "cob", "zt", "uam"]
-//        var forecastIds: [NSManagedObjectID] = []
-//
-//        // Create test data with multiple forecast types
-//        await backgroundContext.perform {
-//            let determination = OrefDetermination(context: backgroundContext)
-//            determination.id = UUID()
-//            determination.deliverAt = date
-//
-//            // Create forecasts for each type
-//            for type in forecastTypes {
-//                let forecast = Forecast(context: backgroundContext)
-//                forecast.id = UUID()
-//                forecast.date = date
-//                forecast.type = type
-//                forecast.orefDetermination = determination
-//
-//                // Add some values
-//                for i in 0 ..< 3 {
-//                    let value = ForecastValue(context: backgroundContext)
-//                    value.index = Int32(i)
-//                    value.value = Int32(100 + i * 10)
-//                    value.forecast = forecast
-//                }
-//
-//                forecastIds.append(forecast.objectID)
-//            }
-//
-//            try? backgroundContext.save()
-//            determinationId = determination.objectID
-//        }
-//
-//        guard let determinationId = determinationId else {
-//            throw TestError("Failed to create test data")
-//        }
-//
-//        // When - Fetch all forecasts
-//        let allForecastIds = await storage.getForecastIDs(for: determinationId, in: backgroundContext)
-//
-//        // Then
-//        #expect(allForecastIds.count == forecastTypes.count, "Should have found all forecast types")
-//
-//        // Test each forecast type
-//        for forecastId in forecastIds {
-//            // When - Fetch values for this forecast
-//            let valueIds = await storage.getForecastValueIDs(for: forecastId, in: backgroundContext)
-//
-//            // Then
-//            #expect(valueIds.count == 3, "Each forecast should have 3 values")
-//
-//            // When - Fetch complete objects
-//            let (_, forecast, values) = await storage.fetchForecastObjects(
-//                for: (UUID(), forecastId, valueIds),
-//                in: backgroundContext
-//            )
-//
-//            // Then
-//            #expect(forecast != nil, "Should have found the forecast")
-//            #expect(forecastTypes.contains(forecast?.type ?? ""), "Should have valid forecast type")
-//            #expect(values.count == 3, "Should have all forecast values")
-//        }
-//
-//        // Test Nightscout format with multiple forecasts
-//        let nsFormat = await storage.getOrefDeterminationNotYetUploadedToNightscout([determinationId])
-//        #expect(nsFormat?.predictions?.iob?.count == 3, "Should have IOB predictions")
-//        #expect(nsFormat?.predictions?.cob?.count == 3, "Should have COB predictions")
-//        #expect(nsFormat?.predictions?.zt?.count == 3, "Should have ZT predictions")
-//        #expect(nsFormat?.predictions?.uam?.count == 3, "Should have UAM predictions")
-//
-//        // Cleanup
-//        await CoreDataStack.shared.deleteObject(identifiedBy: determinationId)
-//    }
+    @Test("Test complete forecast hierarchy prefetching") func testForecastHierarchyPrefetching() async throws {
+        // Given
+        let date = Date()
+        let forecastTypes = ["iob", "cob", "zt", "uam"]
+        var determinationId: NSManagedObjectID?
+        let expectedValuesPerForecast = 5
+
+        // STEP 1: Create test data
+        await testContext.perform {
+            let determination = OrefDetermination(context: testContext)
+            determination.id = UUID()
+            determination.deliverAt = date
+            determination.timestamp = date
+            determination.enacted = true
+
+            // Create all forecast types with values
+            for type in forecastTypes {
+                let forecast = Forecast(context: testContext)
+                forecast.id = UUID()
+                forecast.date = date
+                forecast.type = type
+                forecast.orefDetermination = determination
+
+                // Add test values with different patterns per type
+                for i in 0 ..< expectedValuesPerForecast {
+                    let value = ForecastValue(context: testContext)
+                    value.index = Int32(i)
+
+                    // Different value patterns for each type
+                    switch type {
+                    case "iob": value.value = Int32(100 + i * 10) // 100, 110, 120...
+                    case "cob": value.value = Int32(50 + i * 5) // 50, 55, 60...
+                    case "zt": value.value = Int32(80 + i * 8) // 80, 88, 96...
+                    case "uam": value.value = Int32(120 - i * 15) // 120, 105, 90...
+                    default: value.value = 0
+                    }
+
+                    value.forecast = forecast
+                }
+            }
+
+            try? testContext.save()
+            determinationId = determination.objectID
+        }
+
+        guard let determinationId = determinationId else {
+            throw TestError("Failed to create test data")
+        }
+
+        // STEP 2: Test hierarchy fetching
+        let hierarchy = await storage.fetchForecastHierarchy(
+            for: determinationId,
+            in: testContext
+        )
+
+        // Test hierarchy structure
+        #expect(hierarchy.count == forecastTypes.count, "Should have correct number of forecasts")
+
+        // STEP 3: Test individual forecasts
+        for data in hierarchy {
+            let (id, forecast, values) = await storage.fetchForecastObjects(
+                for: data,
+                in: testContext
+            )
+
+            // Test basic structure
+            #expect(id != UUID(), "Should have valid UUID")
+            #expect(forecast != nil, "Forecast should exist")
+            #expect(values.count == expectedValuesPerForecast, "Should have correct number of values")
+
+            // Test forecast type and values
+            if let forecast = forecast {
+                #expect(forecastTypes.contains(forecast.type ?? ""), "Should have valid forecast type")
+
+                // Test value patterns
+                let sortedValues = values.sorted { $0.index < $1.index }
+                switch forecast.type {
+                case "iob":
+                    #expect(sortedValues.first?.value == 100, "IOB should start at 100")
+                    #expect(sortedValues.last?.value == 140, "IOB should end at 140")
+                case "cob":
+                    #expect(sortedValues.first?.value == 50, "COB should start at 50")
+                    #expect(sortedValues.last?.value == 70, "COB should end at 70")
+                case "zt":
+                    #expect(sortedValues.first?.value == 80, "ZT should start at 80")
+                    #expect(sortedValues.last?.value == 112, "ZT should end at 112")
+                case "uam":
+                    #expect(sortedValues.first?.value == 120, "UAM should start at 120")
+                    #expect(sortedValues.last?.value == 60, "UAM should end at 60")
+                default:
+                    break
+                }
+            }
+        }
+
+        // STEP 4: Test relationship integrity
+        try await testContext.perform {
+            do {
+                let determination = try testContext.existingObject(with: determinationId) as? OrefDetermination
+                let forecasts = Array(determination?.forecasts ?? [])
+
+                #expect(forecasts.count == forecastTypes.count, "Determination should have all forecasts")
+                #expect(
+                    forecasts.allSatisfy { Array($0.forecastValues ?? []).count == expectedValuesPerForecast },
+                    "Each forecast should have correct number of values"
+                )
+            } catch {
+                throw TestError("Failed to verify relationships: \(error)")
+            }
+        }
+    }
+
+    @Test("Measure performance of Core Data fetch operations") func testCoreDataPerformance() async throws {
+        // STEP 1: Setup test data
+        let date = Date()
+        var determinationId: NSManagedObjectID?
+
+        // Create test data
+        await testContext.perform {
+            let determination = OrefDetermination(context: self.testContext)
+            determination.id = UUID()
+            determination.deliverAt = date
+            determination.timestamp = date
+            determination.enacted = true
+
+            // Add forecasts
+            for type in ["iob", "cob", "zt", "uam"] {
+                let forecast = Forecast(context: self.testContext)
+                forecast.id = UUID()
+                forecast.type = type
+                forecast.date = date
+                forecast.orefDetermination = determination
+
+                // Add 48 values (typical forecast length)
+                for i in 0 ..< 48 {
+                    let value = ForecastValue(context: self.testContext)
+                    value.index = Int32(i)
+                    value.value = Int32(100 + i)
+                    forecast.addToForecastValues(value)
+                }
+            }
+
+            try? self.testContext.save()
+            determinationId = determination.objectID
+        }
+
+        guard let determinationId = determinationId else {
+            throw TestError("Failed to create test data")
+        }
+
+        // STEP 2: Test fetchLastDeterminationObjectID
+        let lastDeterminationStartTime = CFAbsoluteTimeGetCurrent()
+
+        let lastDetermination = await storage.fetchLastDeterminationObjectID(
+            predicate: NSPredicate(format: "deliverAt == %@", date as NSDate)
+        )
+
+        let lastDeterminationTime = CFAbsoluteTimeGetCurrent() - lastDeterminationStartTime
+        debug(.default, "fetchLastDeterminationObjectID time: \(String(format: "%.4f", lastDeterminationTime)) seconds")
+
+        // STEP 3: Test fetchForecastHierarchy
+        let hierarchyStartTime = CFAbsoluteTimeGetCurrent()
+
+        let hierarchy = await storage.fetchForecastHierarchy(
+            for: determinationId,
+            in: testContext
+        )
+
+        let hierarchyTime = CFAbsoluteTimeGetCurrent() - hierarchyStartTime
+        debug(.default, "fetchForecastHierarchy time: \(String(format: "%.4f", hierarchyTime)) seconds")
+
+        // STEP 4: Test fetchForecastObjects
+        let objectsStartTime = CFAbsoluteTimeGetCurrent()
+        var individualFetchTimes: [Double] = []
+
+        for data in hierarchy {
+            let singleFetchStart = CFAbsoluteTimeGetCurrent()
+            _ = await storage.fetchForecastObjects(
+                for: data,
+                in: testContext
+            )
+            individualFetchTimes.append(CFAbsoluteTimeGetCurrent() - singleFetchStart)
+        }
+
+        let objectsTime = CFAbsoluteTimeGetCurrent() - objectsStartTime
+        let avgObjectTime = individualFetchTimes.reduce(0, +) / Double(individualFetchTimes.count)
+
+        debug(.default, "Total fetchForecastObjects time: \(String(format: "%.4f", objectsTime)) seconds")
+        debug(.default, "Average time per forecast object: \(String(format: "%.4f", avgObjectTime)) seconds")
+
+        // Performance expectations
+        #expect(lastDeterminationTime < 0.1, "fetchLastDeterminationObjectID should take less than 0.1 seconds")
+        #expect(hierarchyTime < 0.1, "fetchForecastHierarchy should take less than 0.1 seconds")
+        #expect(objectsTime < 0.2, "fetchForecastObjects should take less than 0.2 seconds")
+        #expect(avgObjectTime < 0.05, "Individual forecast fetches should take less than 0.05 seconds")
+    }
 }

+ 79 - 1
TrioTests/CoreDataTests/PumpHistoryStorageTests.swift

@@ -288,9 +288,87 @@ import Testing
         )
         #expect(fetchedEvent?.bolus?.isSMB == false, "Should not be a SMB")
         // TODO: - check this
-//        #expect(fetchedEvent?.bolus?.isExternal == false, "Should not be external Insulin")
+        #expect(fetchedEvent?.bolus?.isExternal == false, "Should not be external Insulin")
         #expect(fetchedEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
         #expect(fetchedEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
         #expect(fetchedEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
     }
+
+    @Test("Measure performance of PumpHistory storage operations") func testStoragePerformance() async throws {
+        // STEP 1: Setup test data
+        let date = Date()
+        let amount: Decimal = 4.0
+        let events = [
+            NewPumpEvent(
+                date: date,
+                dose: DoseEntry(
+                    type: .bolus,
+                    startDate: date,
+                    endDate: date.addingTimeInterval(1),
+                    value: Double(amount),
+                    unit: .units,
+                    deliveredUnits: Double(amount),
+                    description: nil,
+                    syncIdentifier: "test_bolus_1",
+                    scheduledBasalRate: nil,
+                    insulinType: .lyumjev,
+                    automatic: false,
+                    manuallyEntered: true,
+                    isMutable: false
+                ),
+                raw: Data(),
+                title: "Test Bolus",
+                type: .bolus
+            )
+        ]
+
+        // STEP 2: Test storePumpEvents performance
+        let storeStartTime = CFAbsoluteTimeGetCurrent()
+
+        await storage.storePumpEvents(events)
+
+        let storeTime = CFAbsoluteTimeGetCurrent() - storeStartTime
+        debug(.default, "storePumpEvents time: \(String(format: "%.4f", storeTime)) seconds")
+
+        // STEP 3: Test Nightscout upload fetch performance
+        let nsStartTime = CFAbsoluteTimeGetCurrent()
+
+        let nsEvents = await storage.getPumpHistoryNotYetUploadedToNightscout()
+
+        let nsTime = CFAbsoluteTimeGetCurrent() - nsStartTime
+        debug(.default, "Nightscout fetch time: \(String(format: "%.4f", nsTime)) seconds")
+
+        // STEP 4: Test HealthKit upload fetch performance
+        let healthStartTime = CFAbsoluteTimeGetCurrent()
+
+        let healthEvents = await storage.getPumpHistoryNotYetUploadedToHealth()
+
+        let healthTime = CFAbsoluteTimeGetCurrent() - healthStartTime
+        debug(.default, "HealthKit fetch time: \(String(format: "%.4f", healthTime)) seconds")
+
+        // STEP 5: Test Tidepool upload fetch performance
+        let tidepoolStartTime = CFAbsoluteTimeGetCurrent()
+
+        let tidepoolEvents = await storage.getPumpHistoryNotYetUploadedToTidepool()
+
+        let tidepoolTime = CFAbsoluteTimeGetCurrent() - tidepoolStartTime
+        debug(.default, "Tidepool fetch time: \(String(format: "%.4f", tidepoolTime)) seconds")
+
+        // Performance expectations
+        #expect(storeTime < 0.1, "Storing events should take less than 0.1 seconds")
+        #expect(nsTime < 0.01, "Fetching Nightscout events should take less than 0.05 seconds")
+        #expect(healthTime < 0.01, "Fetching HealthKit events should take less than 0.05 seconds")
+        #expect(tidepoolTime < 0.01, "Fetching Tidepool events should take less than 0.05 seconds")
+
+        // Log each total time
+        debug(.default, "Total storePumpEvents time: \(String(format: "%.4f", storeTime)) seconds")
+        debug(.default, "Total Nightscout fetch time: \(String(format: "%.4f", nsTime)) seconds")
+        debug(.default, "Total HealthKit fetch time: \(String(format: "%.4f", healthTime)) seconds")
+        debug(.default, "Total Tidepool fetch time: \(String(format: "%.4f", tidepoolTime)) seconds")
+
+        // Verify data integrity
+        #expect(!nsEvents.isEmpty, "Should have found event for Nightscout")
+        #expect(!healthEvents.isEmpty, "Should have found event for HealthKit")
+        #expect(!tidepoolEvents.isEmpty, "Should have found event for Tidepool")
+    }
 }