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

Fixes Race Conditions and App Crash on Override Edit (#94)

* Fix several Race conditions in JS Worker and Bolus State, fix Crash when editing an Override
* Set default fetch order for CarbEntryStored to descending for more reading efficiency
polscm32 1 год назад
Родитель
Сommit
3b079e47b7

+ 28 - 24
FreeAPS/Sources/APS/OpenAPS/JavaScriptWorker.swift

@@ -28,9 +28,6 @@ final class JavaScriptWorker {
     private let virtualMachine: JSVirtualMachine
     private var contextPool: [JSContext] = []
     private let contextPoolLock = NSLock()
-    @SyncAccess(lock: contextLock) private var commonContext: JSContext? = nil
-    private var consoleLogs: [String] = []
-    private var logContext: String = ""
 
     init(poolSize: Int = 5) {
         virtualMachine = JSVirtualMachine()!
@@ -47,10 +44,15 @@ final class JavaScriptWorker {
                 warning(.openAPS, "JavaScript Error: \(error)")
             }
         }
-        let consoleLog: @convention(block) (String) -> Void = { message in
+        let consoleLog: @convention(block) (String) -> Void = { [weak context] message in
+            guard let context = context else { return }
             let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
             if !trimmedMessage.isEmpty {
-                self.consoleLogs.append("\(trimmedMessage)")
+                let fileName = context.objectForKeyedSubscript("scriptName").toString() ?? "Unknown"
+                let threadSafeLog = "\(trimmedMessage)"
+                self.processQueue.async(flags: .barrier) {
+                    self.outputLogs(for: fileName, message: threadSafeLog)
+                }
             }
         }
         context.setObject(consoleLog, forKeyedSubscript: "_consoleLog" as NSString)
@@ -70,36 +72,40 @@ final class JavaScriptWorker {
         contextPoolLock.unlock()
     }
 
-    // New method to flush aggregated logs
-    private func outputLogs() {
-        var outputLogs = consoleLogs.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
-        consoleLogs.removeAll()
+    private func outputLogs(for fileName: String, message: String) {
+        let logs = message.trimmingCharacters(in: .whitespacesAndNewlines)
 
-        if outputLogs.isEmpty { return }
+        if logs.isEmpty { return }
 
-        if logContext == "autosens.js" {
-            outputLogs = outputLogs.split(separator: "\n").map { logLine in
+        if fileName == "autosens.js" {
+            let sanitizedLogs = logs.split(separator: "\n").map { logLine in
                 logLine.replacingOccurrences(
                     of: "^[-+=x!]|u\\(|\\)|\\d{1,2}h$",
                     with: "",
                     options: .regularExpression
                 )
             }.joined(separator: "\n")
-        }
 
-        if !outputLogs.isEmpty {
-            outputLogs.split(separator: "\n").forEach { logLine in
-                if !"\(logLine)".trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
-                    debug(.openAPS, "\(logContext): \(logLine)")
+            sanitizedLogs.split(separator: "\n").forEach { logLine in
+                if !logLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+                    debug(.openAPS, "\(fileName): \(logLine)")
+                }
+            }
+        } else {
+            logs.split(separator: "\n").forEach { logLine in
+                if !logLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+                    debug(.openAPS, "\(fileName): \(logLine)")
                 }
             }
         }
     }
 
     @discardableResult func evaluate(script: Script) -> JSValue! {
-        logContext = URL(fileURLWithPath: script.name).lastPathComponent
-        let result = evaluate(string: script.body)
-        outputLogs()
+        let context = getContext()
+        defer { returnContext(context) }
+        let fileName = URL(fileURLWithPath: script.name).lastPathComponent
+        context.setObject(fileName, forKeyedSubscript: "scriptName" as NSString)
+        let result = context.evaluateScript(script.body)
         return result
     }
 
@@ -122,7 +128,6 @@ final class JavaScriptWorker {
         let context = getContext()
         defer {
             returnContext(context)
-            outputLogs()
         }
         return execute(self)
     }
@@ -130,13 +135,12 @@ final class JavaScriptWorker {
     func evaluateBatch(scripts: [Script]) {
         let context = getContext()
         defer {
-            // Ensure the context is returned to the pool
             returnContext(context)
         }
         scripts.forEach { script in
-            logContext = URL(fileURLWithPath: script.name).lastPathComponent
+            let fileName = URL(fileURLWithPath: script.name).lastPathComponent
+            context.setObject(fileName, forKeyedSubscript: "scriptName" as NSString)
             context.evaluateScript(script.body)
-            outputLogs()
         }
     }
 }

+ 7 - 7
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -104,11 +104,11 @@ extension Bolus {
         var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         var predictionsForChart: Predictions?
         var simulatedDetermination: Determination?
-        var determinationObjectIDs: [NSManagedObjectID] = []
+        @MainActor var determinationObjectIDs: [NSManagedObjectID] = []
 
         var minForecast: [Int] = []
         var maxForecast: [Int] = []
-        var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
+        @MainActor var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
         var forecastDisplayType: ForecastDisplayType = .cone
         var isSmoothingEnabled: Bool = false
         var stops: [Gradient.Stop] = []
@@ -736,7 +736,7 @@ extension Bolus.StateModel {
         if let forecastData = forecastData {
             simulatedDetermination = forecastData
         } else {
-            simulatedDetermination = await Task.detached { [self] in
+            simulatedDetermination = await Task { [self] in
                 await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
             }.value
         }
@@ -761,14 +761,14 @@ extension Bolus.StateModel {
         minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
         guard minCount > 0 else { return }
 
-        async let minForecastResult = Task.detached {
-            (0 ..< self.minCount).map { index in
+        async let minForecastResult = Task {
+            await (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
             }
         }.value
 
-        async let maxForecastResult = Task.detached {
-            (0 ..< self.minCount).map { index in
+        async let maxForecastResult = Task {
+            await (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
             }
         }.value

+ 1 - 1
FreeAPS/Sources/Modules/OverrideConfig/View/AddOverrideForm.swift

@@ -3,7 +3,7 @@ import SwiftUI
 
 struct AddOverrideForm: View {
     @Environment(\.presentationMode) var presentationMode
-    @StateObject var state: OverrideConfig.StateModel
+    @Bindable var state: OverrideConfig.StateModel
     @State private var isEditing = false
     @State private var overrideTarget = false
     @Environment(\.colorScheme) var colorScheme

+ 2 - 3
FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift

@@ -2,8 +2,7 @@ import Foundation
 import SwiftUI
 
 struct EditOverrideForm: View {
-    @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
-    @ObservedObject var override: OverrideStored
+    var override: OverrideStored
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.colorScheme) var colorScheme
     @Bindable var state: OverrideConfig.StateModel
@@ -296,7 +295,7 @@ struct EditOverrideForm: View {
                         guard moc.hasChanges else { return }
                         try moc.save()
                         Task {
-                            await nightscoutManager.uploadProfiles()
+                            await state.nightscoutManager.uploadProfiles()
                         }
                         if let currentActiveOverride = state.currentActiveOverride {
                             Task {

+ 2 - 2
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24A348" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -19,7 +19,7 @@
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
-            <fetchIndexElement property="date" type="Binary" order="ascending"/>
+            <fetchIndexElement property="date" type="Binary" order="descending"/>
         </fetchIndex>
         <fetchIndex name="byIsFPU">
             <fetchIndexElement property="isFPU" type="Binary" order="ascending"/>