-
Notifications
You must be signed in to change notification settings - Fork 17
feat: implement battery optimizations with adaptive polling #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
989c769
6464cc3
916501c
bd8b313
3e276c0
61b45d7
91b9612
28b9202
8322eac
0fe91e0
b7b335a
da5e3a6
db55401
5916ea3
ddf5533
c79b09b
d2935ec
9269ac6
b2a3f49
0a167fe
53a5a8c
fd236dd
4eeda7b
0cfd0bf
a325acc
56985fd
2a44312
65cff11
ac20fe4
fb2dbb4
233a1fc
77c9adc
6fc69cb
819fa3c
b27ce99
c0eb283
de4d772
b9eeb74
524e845
c6db3e0
6c36bcc
e76586a
32515c2
ce25926
ef68b4e
e60fef8
805caf9
a6ed65b
bbd7e98
cc5db36
7392d2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,21 @@ public class NetworkExtensionAdapter: ObservableObject { | |
| var extensionID = "io.netbird.app.NetbirdNetworkExtension" | ||
| var extensionName = "NetBird Network Extension" | ||
|
|
||
| let decoder = PropertyListDecoder() | ||
| let decoder = PropertyListDecoder() | ||
|
|
||
| // Battery optimization: Adaptive polling | ||
| // All state variables must be accessed only from pollingQueue to prevent race conditions | ||
| private var currentPollingInterval: TimeInterval = 10.0 // Start with 10 seconds | ||
| private var consecutiveStablePolls: Int = 0 | ||
| private var lastStatusHash: Int = 0 | ||
| private var isInBackground: Bool = false | ||
| private var lastTimerInterval: TimeInterval = 10.0 // Track last set interval | ||
| private let pollingQueue = DispatchQueue(label: "com.netbird.polling", qos: .utility) | ||
|
|
||
| // Polling intervals (in seconds) | ||
| private let minPollingInterval: TimeInterval = 10.0 // When changes detected | ||
| private let stablePollingInterval: TimeInterval = 20.0 // When stable | ||
| private let backgroundPollingInterval: TimeInterval = 60.0 // In background | ||
|
|
||
| @Published var timer : Timer | ||
|
|
||
|
|
@@ -244,19 +258,49 @@ public class NetworkExtensionAdapter: ObservableObject { | |
| let messageString = "Status" | ||
| if let messageData = messageString.data(using: .utf8) { | ||
| do { | ||
| try session.sendProviderMessage(messageData) { response in | ||
| if let response = response { | ||
| try session.sendProviderMessage(messageData) { [weak self] response in | ||
| guard let self = self else { return } | ||
|
|
||
| // Serialize all response handling and state mutations through pollingQueue | ||
| self.pollingQueue.async { [weak self] in | ||
| guard let self = self else { return } | ||
|
|
||
| guard let response = response else { | ||
| let defaultStatus = StatusDetails(ip: "", fqdn: "", managementStatus: .disconnected, peerInfo: []) | ||
| completion(defaultStatus) | ||
| return | ||
| } | ||
|
|
||
| do { | ||
| let decodedStatus = try self.decoder.decode(StatusDetails.self, from: response) | ||
|
|
||
| // Calculate hash to detect changes | ||
| let statusHash = self.calculateStatusHash(decodedStatus) | ||
| let hasChanged = statusHash != self.lastStatusHash | ||
|
|
||
| if hasChanged { | ||
| // Status changed - use faster polling | ||
| self.consecutiveStablePolls = 0 | ||
| self.currentPollingInterval = self.minPollingInterval | ||
| self.lastStatusHash = statusHash | ||
| print("Status changed, using fast polling (\(self.currentPollingInterval)s)") | ||
| } else { | ||
| // Status stable - gradually increase interval | ||
| self.consecutiveStablePolls += 1 | ||
| if self.consecutiveStablePolls > 3 { | ||
| self.currentPollingInterval = self.stablePollingInterval | ||
| } | ||
| } | ||
|
|
||
| // Restart timer with new interval if needed | ||
| self.restartTimerIfNeeded(completion: completion) | ||
|
|
||
| completion(decodedStatus) | ||
| return | ||
| } catch { | ||
| print("Failed to decode status details.") | ||
| let defaultStatus = StatusDetails(ip: "", fqdn: "", managementStatus: .disconnected, peerInfo: []) | ||
| completion(defaultStatus) | ||
| } | ||
| } else { | ||
| let defaultStatus = StatusDetails(ip: "", fqdn: "", managementStatus: .disconnected, peerInfo: []) | ||
| completion(defaultStatus) | ||
| return | ||
| } | ||
| } | ||
| } catch { | ||
|
|
@@ -267,16 +311,123 @@ public class NetworkExtensionAdapter: ObservableObject { | |
| } | ||
| } | ||
|
|
||
| private func calculateStatusHash(_ status: StatusDetails) -> Int { | ||
| var hasher = Hasher() | ||
| hasher.combine(status.ip) | ||
| hasher.combine(status.fqdn) | ||
| hasher.combine(status.managementStatus) | ||
| hasher.combine(status.peerInfo.count) | ||
| for peer in status.peerInfo { | ||
| hasher.combine(peer.ip) | ||
| hasher.combine(peer.connStatus) | ||
| } | ||
| return hasher.finalize() | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private func restartTimerIfNeeded(completion: @escaping (StatusDetails) -> Void) { | ||
| // This function is called from pollingQueue, so we can safely access state variables | ||
| // Only restart if interval changed significantly (more than 2 seconds difference) | ||
| let targetInterval = isInBackground ? backgroundPollingInterval : currentPollingInterval | ||
|
|
||
| // Check if we need to restart timer | ||
| if abs(lastTimerInterval - targetInterval) > 2.0 { | ||
| lastTimerInterval = targetInterval | ||
| // Capture state values here (on pollingQueue) to avoid deadlock | ||
| let intervalToUse = targetInterval | ||
| let backgroundStateToUse = isInBackground | ||
| DispatchQueue.main.async { [weak self] in | ||
| guard let self = self else { return } | ||
| if self.timer.isValid { | ||
| self.timer.invalidate() | ||
| } | ||
| // Pass values directly to avoid pollingQueue.sync call from main thread | ||
| self.startTimer(interval: intervalToUse, backgroundState: backgroundStateToUse, completion: completion) | ||
| } | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| func startTimer(completion: @escaping (StatusDetails) -> Void) { | ||
| startTimer(interval: nil, backgroundState: nil, completion: completion) | ||
| } | ||
|
|
||
| private func startTimer(interval: TimeInterval?, backgroundState: Bool?, completion: @escaping (StatusDetails) -> Void) { | ||
| self.timer.invalidate() | ||
|
|
||
| // Initial fetch | ||
| self.fetchData(completion: completion) | ||
| self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: { _ in | ||
| self.fetchData(completion: completion) | ||
| }) | ||
|
|
||
| // Determine polling interval based on app state | ||
| // If values are provided (from restartTimerIfNeeded), use them to avoid deadlock | ||
| // Otherwise, read from pollingQueue (when called directly from main thread) | ||
| let intervalToUse: TimeInterval | ||
| let backgroundStateToUse: Bool | ||
|
|
||
| if let providedInterval = interval, let providedBackgroundState = backgroundState { | ||
| // Values already captured on pollingQueue, use them directly | ||
| intervalToUse = providedInterval | ||
| backgroundStateToUse = providedBackgroundState | ||
| // Update lastTimerInterval synchronously to prevent race condition | ||
| // This is safe because we're not in a deadlock situation (values already captured) | ||
| pollingQueue.sync { [weak self] in | ||
| guard let self = self else { return } | ||
| self.lastTimerInterval = providedInterval | ||
| } | ||
| } else { | ||
| // Called directly, must read from pollingQueue (but this is safe as we're not in a deadlock situation) | ||
| var intervalValue: TimeInterval = minPollingInterval | ||
| var backgroundValue: Bool = false | ||
| pollingQueue.sync { | ||
| backgroundValue = isInBackground | ||
| intervalValue = backgroundValue ? backgroundPollingInterval : currentPollingInterval | ||
| lastTimerInterval = intervalValue | ||
| } | ||
| intervalToUse = intervalValue | ||
| backgroundStateToUse = backgroundValue | ||
| } | ||
|
|
||
| // Create timer - must be on main thread for RunLoop | ||
| DispatchQueue.main.async { [weak self] in | ||
| guard let self = self else { return } | ||
|
|
||
| self.timer = Timer(timeInterval: intervalToUse, repeats: true) { [weak self] _ in | ||
| guard let self = self else { return } | ||
| // Use background queue for actual network work | ||
| self.pollingQueue.async { | ||
| self.fetchData(completion: completion) | ||
| } | ||
| } | ||
|
Comment on lines
455
to
464
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
self.timer = Timer(timeInterval: intervalToUse, repeats: true) { [weak self] _ in
guard let self = self else { return }
// Use background queue for actual network work
self.pollingQueue.async {
+ guard self.isPollingActive else { return }
self.fetchData(completion: completion)
}
}And consider invalidating immediately when already on main: func stopTimer() {
- DispatchQueue.main.async { [weak self] in
- self?.timer.invalidate()
- }
+ if Thread.isMainThread {
+ self.timer.invalidate()
+ } else {
+ DispatchQueue.main.async { [weak self] in self?.timer.invalidate() }
+ }
pollingQueue.sync { ... isPollingActive = false ... }
}Also applies to: 428-442 🤖 Prompt for AI Agents |
||
|
|
||
| // Add timer to main RunLoop | ||
| RunLoop.main.add(self.timer, forMode: .common) | ||
|
|
||
| print("Started polling with interval: \(intervalToUse)s (background: \(backgroundStateToUse))") | ||
| } | ||
| } | ||
|
|
||
| func stopTimer() { | ||
| self.timer.invalidate() | ||
| // Reset state variables - must be done on pollingQueue to avoid race conditions | ||
| pollingQueue.async { [weak self] in | ||
| guard let self = self else { return } | ||
| self.consecutiveStablePolls = 0 | ||
| self.currentPollingInterval = self.minPollingInterval | ||
| } | ||
| } | ||
|
|
||
| func setBackgroundMode(_ inBackground: Bool) { | ||
| // All state mutations must happen on pollingQueue to prevent race conditions | ||
| pollingQueue.async { [weak self] in | ||
| guard let self = self else { return } | ||
| let wasInBackground = self.isInBackground | ||
| self.isInBackground = inBackground | ||
|
|
||
| // Restart timer with appropriate interval if state changed | ||
| if wasInBackground != inBackground { | ||
| let interval = inBackground ? self.backgroundPollingInterval : self.currentPollingInterval | ||
| print("App state changed to \(inBackground ? "background" : "foreground"), adjusting polling interval to \(interval)s") | ||
| // Timer will be restarted on next fetchData call via restartTimerIfNeeded | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func getExtensionStatus(completion: @escaping (NEVPNStatus) -> Void) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.