ForecastSetup.swift 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import CoreData
  2. import Foundation
  3. extension Home.StateModel {
  4. // Asynchronously preprocess Forecast data in a background thread
  5. func preprocessForecastData() async -> [(
  6. id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]
  7. )] {
  8. do {
  9. // Get the Determination ID on the main context
  10. guard let determination = await viewContext.perform({
  11. self.enactedAndNonEnactedDeterminations.first
  12. }) else {
  13. debug(.default, "No determination found for forecast preprocessing")
  14. return []
  15. }
  16. let taskContext = CoreDataStack.shared.newTaskContext()
  17. taskContext.name = "HomeStateModel.preprocessForecastData"
  18. // Fetch complete forecast hierarchy with prefetched values
  19. return try await determinationStorage.fetchForecastHierarchy(
  20. for: determination.objectID,
  21. in: taskContext
  22. )
  23. } catch {
  24. debug(
  25. .default,
  26. "\(DebuggingIdentifiers.failed) Failed to preprocess forecast data: \(error)"
  27. )
  28. return []
  29. }
  30. }
  31. // Update forecast data and UI on the main thread
  32. //
  33. // Runs inside `forecastUpdateTask`; cancellation is checked after every suspension
  34. // point so a superseded run cannot overwrite fresher results.
  35. @MainActor func updateForecastData() async {
  36. let forecastDataIDs = await preprocessForecastData()
  37. guard !Task.isCancelled else { return }
  38. var allForecastValues = [[Int]]()
  39. var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
  40. // Prefetch all Forecasts with their forecastValues into viewContext in a single IN-query
  41. // to avoid N+1 individual SELECTs when materializing via existingObject below.
  42. let forecastObjectIDs = forecastDataIDs.map(\.forecastID)
  43. if !forecastObjectIDs.isEmpty {
  44. let prefetchRequest = NSFetchRequest<Forecast>(entityName: "Forecast")
  45. prefetchRequest.predicate = NSPredicate(format: "SELF IN %@", forecastObjectIDs)
  46. prefetchRequest.relationshipKeyPathsForPrefetching = ["forecastValues"]
  47. prefetchRequest.returnsObjectsAsFaults = false
  48. _ = try? viewContext.fetch(prefetchRequest)
  49. }
  50. // Process prefetched data directly
  51. for data in forecastDataIDs {
  52. if let forecast = try? viewContext.existingObject(with: data.forecastID) as? Forecast {
  53. let values = data.forecastValueIDs.compactMap {
  54. try? viewContext.existingObject(with: $0) as? ForecastValue
  55. }
  56. // Extract values for graph
  57. let forecastValueInts = values.map { Int($0.value) }
  58. allForecastValues.append(forecastValueInts)
  59. // Add data for further processing
  60. preprocessedData.append(contentsOf: values.map {
  61. (id: data.id, forecast: forecast, forecastValue: $0)
  62. })
  63. }
  64. }
  65. // Update UI-relevant data
  66. self.preprocessedData = preprocessedData
  67. guard !allForecastValues.isEmpty else {
  68. minForecast = []
  69. maxForecast = []
  70. return
  71. }
  72. minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
  73. let localMinCount = minCount
  74. guard localMinCount > 0 else { return }
  75. // Calculate min/max values for graph
  76. let (minResult, maxResult) = await Task.detached {
  77. let minForecast = (0 ..< localMinCount).map { index in
  78. allForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }
  79. .min() ?? 0
  80. }
  81. let maxForecast = (0 ..< localMinCount).map { index in
  82. allForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }
  83. .max() ?? 0
  84. }
  85. return (minForecast, maxForecast)
  86. }.value
  87. guard !Task.isCancelled else { return }
  88. minForecast = minResult
  89. maxForecast = maxResult
  90. }
  91. }