فهرست منبع

concurrency issues ...wip

polscm32 2 سال پیش
والد
کامیت
3ae9d272a4

+ 20 - 1
FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme

@@ -62,7 +62,11 @@
       </BuildableProductRunnable>
       <CommandLineArguments>
          <CommandLineArgument
-            argument = "-com.apple.CoreData.ConcurrencyDebug 4"
+            argument = "-com.apple.CoreData.ConcurrencyDebug 1"
+            isEnabled = "NO">
+         </CommandLineArgument>
+         <CommandLineArgument
+            argument = "-com.apple.CoreData.SQLDebug 1"
             isEnabled = "NO">
          </CommandLineArgument>
       </CommandLineArguments>
@@ -72,6 +76,21 @@
             value = ""
             isEnabled = "YES">
          </EnvironmentVariable>
+         <EnvironmentVariable
+            key = "SQLITE_ENABLE_THREAD_ASSERTIONS 1"
+            value = ""
+            isEnabled = "YES">
+         </EnvironmentVariable>
+         <EnvironmentVariable
+            key = "SQLITE_AUTO_TRACE 1"
+            value = ""
+            isEnabled = "NO">
+         </EnvironmentVariable>
+         <EnvironmentVariable
+            key = "SQLITE_ENABLE_FILE_ASSERTIONS 1"
+            value = ""
+            isEnabled = "YES">
+         </EnvironmentVariable>
       </EnvironmentVariables>
    </LaunchAction>
    <ProfileAction

+ 24 - 22
FreeAPS/Sources/APS/APSManager.swift

@@ -794,28 +794,30 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func tir(_ glucose: [GlucoseStored]) -> (TIR: Double, hypos: Double, hypers: Double, normal_: Double) {
-        let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-        let totalReadings = justGlucoseArray.count
-        let highLimit = settingsManager.settings.high
-        let lowLimit = settingsManager.settings.low
-        let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
-        let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
-        let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
-        let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
-        let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
-        let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
-        // Euglyccemic range
-        let normalArray = glucose.filter({ $0.glucose >= 70 && $0.glucose <= 140 })
-        let normalReadings = normalArray.compactMap({ each in each.glucose as Int16 }).count
-        let normalPercentage = Double(normalReadings) / Double(totalReadings) * 100
-        // TIR
-        let tir = 100 - (hypoPercentage + hyperPercentage)
-        return (
-            roundDouble(tir, 1),
-            roundDouble(hypoPercentage, 1),
-            roundDouble(hyperPercentage, 1),
-            roundDouble(normalPercentage, 1)
-        )
+        privateContext.perform {
+            let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+            let totalReadings = justGlucoseArray.count
+            let highLimit = settingsManager.settings.high
+            let lowLimit = settingsManager.settings.low
+            let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
+            let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
+            let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
+            let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
+            let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
+            let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
+            // Euglyccemic range
+            let normalArray = glucose.filter({ $0.glucose >= 70 && $0.glucose <= 140 })
+            let normalReadings = normalArray.compactMap({ each in each.glucose as Int16 }).count
+            let normalPercentage = Double(normalReadings) / Double(totalReadings) * 100
+            // TIR
+            let tir = 100 - (hypoPercentage + hyperPercentage)
+            return (
+                roundDouble(tir, 1),
+                roundDouble(hypoPercentage, 1),
+                roundDouble(hyperPercentage, 1),
+                roundDouble(normalPercentage, 1)
+            )
+        }
     }
 
     private func glucoseStats(_ fetchedGlucose: [GlucoseStored])

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -7,7 +7,7 @@ struct CurrentGlucoseView: View {
     @Binding var alarm: GlucoseAlarm?
     @Binding var lowGlucose: Decimal
     @Binding var highGlucose: Decimal
-    @Binding var glucoseFromPersistence: [GlucoseStored]
+    var glucoseFromPersistence: [GlucoseStored]
 
     @State private var rotationDegrees: Double = 0.0
     @State private var angularGradient = AngularGradient(colors: [

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -170,7 +170,7 @@ extension Home {
                 alarm: $state.alarm,
                 lowGlucose: $state.lowGlucose,
                 highGlucose: $state.highGlucose,
-                glucoseFromPersistence: $state.glucoseFromPersistence
+                glucoseFromPersistence: state.glucoseFromPersistence
             ).scaleEffect(0.9)
                 .onTapGesture {
                     if state.alarm == nil {

+ 15 - 21
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -229,30 +229,24 @@ final class BaseCalendarManager: CalendarManager, Injectable {
         return formatter
     }
 
-    private func fetchGlucose() -> [GlucoseStored]? {
-        CoreDataStack.shared.fetchEntities(
+    func setupGlucose() {
+        CoreDataStack.shared.fetchEntitiesAndUpdateUI(
             ofType: GlucoseStored.self,
             predicate: NSPredicate.predicateFor30MinAgo,
             key: "date",
-            ascending: false,
-            fetchLimit: 4
-        )
-    }
-
-    func setupGlucose() {
-        guard let glucose = fetchGlucose(), glucose.count >= 2 else {
-            debugPrint("Not enough glucose data available")
-            return
-        }
-
-        // Safely unwrapping glucose readings
-        if let lastGlucose = glucose.first,
-           let secondLastReading = glucose.dropFirst().first?.glucose
-        {
-            let glucoseDelta = lastGlucose.glucose - secondLastReading
-            createEvent(for: lastGlucose, delta: Int(glucoseDelta))
-        } else {
-            debugPrint("Failed to unwrap necessary glucose readings")
+            ascending: false
+        ) { glucose in
+            guard glucose.count >= 2 else { return }
+            debug(.default, "setup Glucose func on thread: \(Thread.current)")
+            // Safely unwrapping glucose readings
+            if let lastGlucose = glucose.first,
+               let secondLastReading = glucose.dropFirst().first?.glucose
+            {
+                let glucoseDelta = lastGlucose.glucose - secondLastReading
+                self.createEvent(for: lastGlucose, delta: Int(glucoseDelta))
+            } else {
+                debugPrint("Failed to unwrap necessary glucose readings")
+            }
         }
     }
 }

+ 85 - 4
Model/CoreDataStack.swift

@@ -34,6 +34,12 @@ class CoreDataStack: ObservableObject {
         return viewContext
     }()
 
+    // MARK: - Fetch Requests
+
+    //
+    // the first I define here is for background work...I decided to pass a parameter context to the function to execute it on the viewContext if necessary, but for updating the UI I've decided to rather create a second generic fetch function with a completion handler which results are returned on the main thread
+    //
+    // first fetch function
     // fetch on the thread of the backgroundContext
     func fetchEntities<T: NSManagedObject>(
         ofType type: T.Type,
@@ -43,6 +49,7 @@ class CoreDataStack: ObservableObject {
         fetchLimit: Int? = nil,
         batchSize: Int? = nil,
         propertiesToFetch: [String]? = nil,
+        context: NSManagedObjectContext? = CoreDataStack.shared.backgroundContext,
         callingFunction: String = #function,
         callingClass: String = #fileID
     ) -> [T] {
@@ -65,19 +72,93 @@ class CoreDataStack: ObservableObject {
         var result: [T]?
 
         /// we need to ensure that the fetch immediately returns a value as long as the whole app does not use the async await pattern, otherwise we could perform this asynchronously with backgroundContext.perform and not block the thread
-        backgroundContext.performAndWait {
+        context?.performAndWait {
             do {
-                debugPrint("Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded)")
-                result = try self.backgroundContext.fetch(request)
+                debugPrint(
+                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on Thread: \(Thread.current)"
+                )
+                result = try context?.fetch(request)
             } catch let error as NSError {
                 debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error)"
+                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on Thread: \(Thread.current)"
                 )
             }
         }
+
+//        if result == nil {
+//            debugPrint("Fetch result is nil in \(callingFunction) from \(callingClass) on thread \(Thread.current)")
+//        } else {
+//            debugPrint(
+//                "Fetch result count: \(result?.count ?? 0) in \(callingFunction) from \(callingClass) on thread \(Thread.current)"
+//            )
+//        }
+
         return result ?? []
     }
 
+    // second fetch function
+    // fetch and update UI
+    func fetchEntitiesAndUpdateUI<T: NSManagedObject>(
+        ofType type: T.Type,
+        predicate: NSPredicate,
+        key: String,
+        ascending: Bool,
+        fetchLimit: Int? = nil,
+        batchSize: Int? = nil,
+        propertiesToFetch: [String]? = nil,
+        callingFunction: String = #function,
+        callingClass: String = #fileID,
+        completion: @escaping ([T]) -> Void
+    ) {
+        let request = NSFetchRequest<T>(entityName: String(describing: type))
+        request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
+        request.predicate = predicate
+        if let limit = fetchLimit {
+            request.fetchLimit = limit
+        }
+        if let batchSize = batchSize {
+            request.fetchBatchSize = batchSize
+        }
+        if let propertiesToFetch = propertiesToFetch {
+            request.propertiesToFetch = propertiesToFetch
+            request.resultType = .managedObjectResultType
+        } else {
+            request.resultType = .managedObjectResultType
+        }
+
+        backgroundContext.perform {
+            var result: [T]?
+
+            do {
+                debugPrint(
+                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on thread \(Thread.current)"
+                )
+                result = try self.backgroundContext.fetch(request)
+            } catch let error as NSError {
+                debugPrint(
+                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on thread \(Thread.current)"
+                )
+            }
+
+            // Ensure that the fetch immediately returns a value
+            DispatchQueue.main.async {
+                if let result = result {
+                    debugPrint(
+                        "Returning fetch result to main thread in \(callingFunction) from \(callingClass) on thread \(Thread.current)"
+                    )
+                    completion(result)
+                } else {
+                    debugPrint("Fetch result is nil in \(callingFunction) from \(callingClass) on thread \(Thread.current)")
+                    completion([])
+                }
+            }
+        }
+    }
+
+    // MARK: - Save
+
+    //
+    // takes a context as a parameter to be executed either on the main thread or on a background thread
     // save on the thread of the backgroundContext
     func saveContext(useViewContext: Bool = false, callingFunction: String = #function, callingClass: String = #fileID) throws {
         let contextToUse = useViewContext ? viewContext : backgroundContext