11import Dispatch
22import Foundation
33
4- // TODO: Keep weak references to cancellables for downstream observations (instead
5- // of the current strong references).
6-
74/// A type that produces valueless observations.
85public class Publisher {
96 /// The id for the next observation (ids are used to cancel observations).
107 private var nextObservationId = 0
118 /// All current observations keyed by their id (ids are used to cancel observations).
129 private var observations : [ Int : ( ) -> Void ] = [ : ]
13- /// Cancellable observations of downstream observers.
14- private var cancellables : [ Cancellable ] = [ ]
1510 /// Human-readable tag for debugging purposes.
1611 private var tag : String ?
1712
18- /// The time at which the last update merging event occurred in
19- /// `observeOnMainThreadAvoidingStarvation`.
20- private var lastUpdateMergeTime : TimeInterval = 0
21- /// The amount of time taken per state update, exponentially averaged over time.
22- private var exponentiallySmoothedUpdateLength : Double = 0
13+ /// We guard this against data races, with serialUpdateHandlingQueue, and
14+ /// with our lives.
15+ private class UpdateStatistics : @unchecked Sendable {
16+ /// The time at which the last update merging event occurred in
17+ /// `observeOnMainThreadAvoidingStarvation`.
18+ var lastUpdateMergeTime : TimeInterval = 0
19+ /// The amount of time taken per state update, exponentially averaged over time.
20+ var exponentiallySmoothedUpdateLength : Double = 0
21+ }
22+
23+ private let updateStatistics = UpdateStatistics ( )
24+
25+ private let serialUpdateHandlingQueue = DispatchQueue (
26+ label: " serial update handling "
27+ )
28+ private let semaphore = DispatchSemaphore ( value: 1 )
2329
2430 /// Creates a new independent publisher.
2531 public init ( ) { }
@@ -51,7 +57,6 @@ public class Publisher {
5157 self . send ( )
5258 } )
5359 cancellable. tag ( with: " \( tag ?? " no tag " ) <-> \( cancellable. tag ?? " no tag " ) " )
54- cancellables. append ( cancellable)
5560 return cancellable
5661 }
5762
@@ -91,12 +96,11 @@ public class Publisher {
9196 /// guaranteed that updates will always run serially.
9297 func observeAsUIUpdater< Backend: AppBackend > (
9398 backend: Backend ,
94- action closure : @escaping ( ) -> Void
99+ action: @escaping @ MainActor @ Sendable ( ) -> Void
95100 ) -> Cancellable {
96- let serialUpdateHandlingQueue = DispatchQueue (
97- label: " serial update handling "
98- )
99- let semaphore = DispatchSemaphore ( value: 1 )
101+ let semaphore = self . semaphore
102+ let serialUpdateHandlingQueue = self . serialUpdateHandlingQueue
103+ let updateStatistics = self . updateStatistics
100104 return observe {
101105 // Only allow one update to wait at a time.
102106 guard semaphore. wait ( timeout: . now( ) ) == . success else {
@@ -105,7 +109,7 @@ public class Publisher {
105109 // as long as it happens within the next update or two.
106110 let mergeTime = ProcessInfo . processInfo. systemUptime
107111 serialUpdateHandlingQueue. async {
108- self . lastUpdateMergeTime = mergeTime
112+ updateStatistics . lastUpdateMergeTime = mergeTime
109113 }
110114 return
111115 }
@@ -125,22 +129,23 @@ public class Publisher {
125129 // Run the closure and while we're at it measure how long it takes
126130 // so that we can use it when throttling if updates start backing up.
127131 let start = ProcessInfo . processInfo. systemUptime
128- closure ( )
132+ action ( )
129133 let elapsed = ProcessInfo . processInfo. systemUptime - start
130134
131135 // I chose exponential smoothing because it's simple to compute, doesn't
132136 // require storing a window of previous values, and quickly converges to
133- // a sensible value when the average moves while still somewhat ignoring
137+ // a sensible value when the average moves, while still somewhat ignoring
134138 // outliers.
135- self . exponentiallySmoothedUpdateLength =
136- elapsed / 2 + self . exponentiallySmoothedUpdateLength / 2
139+ updateStatistics . exponentiallySmoothedUpdateLength =
140+ elapsed / 2 + updateStatistics . exponentiallySmoothedUpdateLength / 2
137141 }
138142
139- if ProcessInfo . processInfo. systemUptime - self . lastUpdateMergeTime < 1 {
143+ if ProcessInfo . processInfo. systemUptime - updateStatistics . lastUpdateMergeTime < 1 {
140144 // The factor of 1.5 was determined empirically. This algorithm is
141145 // open for improvements since it's purely here to reduce the risk
142- // of UI freezes.
143- let throttlingDelay = self . exponentiallySmoothedUpdateLength * 1.5
146+ // of UI freezes. A factor of 1.5 equates to a gap between updates of
147+ // approximately 50% of the average update length.
148+ let throttlingDelay = updateStatistics. exponentiallySmoothedUpdateLength * 1.5
144149
145150 // Sleeping on a dispatch queue generally isn't a good idea because
146151 // you prevent the queue from servicing any other work, but in this
0 commit comments