Browse Source

signal when connectivity is "connected" again (#1552)

* signal when connectivity is "connected" again

this finishes the background-fetch much faster,
instead of always 11 seconds, things finish in less than 2 seconds usually
and take longer only when here is really something to download.

fetchSemaphore is signalled when we receive the DC_CONNECTIVITY_CHANGED event
with connectivity set to DC_CONNECTIVITY_CONNECTED - we have called maybeNetwork()
just before which sets connectivity to CONNECTING,
we know pretty sure that one round is done.

we check that on the selected context only,
that is maybe not perfect, but probably good enough
(other accounts are checked as well, but may be stopped too soon)

* double max. fetching time to 20 seconds

before, we were always using exact 10 seconds to not eat too much battery.
as we now terminate much sooner, usually in less than 2 seconds,
we can also allow to take more time as needed.
(in total, we can spend 30 seconds in the background fetch,
the remaining 10 seconds are saved for shutdown and as headroom)

* more exact calculation of notify-fetch-duration

we know for sure that the system may put our app to suspend mode when calling
DispatchQueue.main.async -
therefore, check if the timing is okay when we get back from the semaphore
and write that to the debug stats.

* stop performFetch() also on not-connected, check all account

we created a dedicated function dc_accounts_all_work_done that time,
so, that was easy :)

* cache background/foreground state to allow easier checking from any thread

* nicer and more realiable code again: avoid two thread switches as self.appIsInForeground() is allowed to be called from any thread now

* count fetches only if we are really fetching
bjoern 3 years ago
parent
commit
c2d10c88bb
3 changed files with 46 additions and 32 deletions
  1. 5 0
      DcCore/DcCore/DC/Wrapper.swift
  2. 3 0
      DcCore/DcCore/DC/events.swift
  3. 38 32
      deltachat-ios/AppDelegate.swift

+ 5 - 0
DcCore/DcCore/DC/Wrapper.swift

@@ -9,6 +9,7 @@ public class DcAccounts {
     let applicationGroupIdentifier = "group.chat.delta.ios"
     let applicationGroupIdentifier = "group.chat.delta.ios"
     var accountsPointer: OpaquePointer?
     var accountsPointer: OpaquePointer?
     public var logger: Logger?
     public var logger: Logger?
+    public var fetchSemaphore: DispatchSemaphore?
 
 
     public init() {
     public init() {
     }
     }
@@ -55,6 +56,10 @@ public class DcAccounts {
         dc_accounts_maybe_network_lost(accountsPointer)
         dc_accounts_maybe_network_lost(accountsPointer)
     }
     }
 
 
+    public func isAllWorkDone() -> Bool {
+        return dc_accounts_all_work_done(accountsPointer) != 0
+    }
+
     public func startIo() {
     public func startIo() {
         dc_accounts_start_io(accountsPointer)
         dc_accounts_start_io(accountsPointer)
     }
     }

+ 3 - 0
DcCore/DcCore/DC/events.swift

@@ -210,6 +210,9 @@ public class DcEventHandler {
             }
             }
 
 
         case DC_EVENT_CONNECTIVITY_CHANGED:
         case DC_EVENT_CONNECTIVITY_CHANGED:
+            if let sem = dcAccounts.fetchSemaphore, dcAccounts.isAllWorkDone() {
+                sem.signal()
+            }
             if dcContext.id != dcAccounts.getSelected().id {
             if dcContext.id != dcAccounts.getSelected().id {
                 return
                 return
             }
             }

+ 38 - 32
deltachat-ios/AppDelegate.swift

@@ -22,6 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
     var reachability = Reachability()!
     var reachability = Reachability()!
     var window: UIWindow?
     var window: UIWindow?
     var notifyToken: String?
     var notifyToken: String?
+    var applicationInForeground: Bool = false
 
 
     // purpose of `bgIoTimestamp` is to block rapidly subsequent calls to remote- or local-wakeups:
     // purpose of `bgIoTimestamp` is to block rapidly subsequent calls to remote- or local-wakeups:
     //
     //
@@ -189,8 +190,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
 
 
     // MARK: - app lifecycle
     // MARK: - app lifecycle
 
 
+    // applicationWillEnterForeground() is _not_ called on initial app start
     func applicationWillEnterForeground(_: UIApplication) {
     func applicationWillEnterForeground(_: UIApplication) {
         logger.info("➡️ applicationWillEnterForeground")
         logger.info("➡️ applicationWillEnterForeground")
+        applicationInForeground = true
         dcAccounts.startIo()
         dcAccounts.startIo()
 
 
         DispatchQueue.global(qos: .background).async { [weak self] in
         DispatchQueue.global(qos: .background).async { [weak self] in
@@ -215,6 +218,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         }
         }
     }
     }
 
 
+    // applicationDidBecomeActive() is called on initial app start _and_ after applicationWillEnterForeground()
+    func applicationDidBecomeActive(_: UIApplication) {
+        logger.info("➡️ applicationDidBecomeActive")
+        applicationInForeground = true
+    }
+
     func applicationWillResignActive(_: UIApplication) {
     func applicationWillResignActive(_: UIApplication) {
         logger.info("⬅️ applicationWillResignActive")
         logger.info("⬅️ applicationWillResignActive")
         registerBackgroundTask()
         registerBackgroundTask()
@@ -222,6 +231,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
 
 
     func applicationDidEnterBackground(_: UIApplication) {
     func applicationDidEnterBackground(_: UIApplication) {
         logger.info("⬅️ applicationDidEnterBackground")
         logger.info("⬅️ applicationDidEnterBackground")
+        applicationInForeground = false
     }
     }
 
 
     func applicationWillTerminate(_: UIApplication) {
     func applicationWillTerminate(_: UIApplication) {
@@ -435,7 +445,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             return
             return
         }
         }
         bgIoTimestamp = nowTimestamp
         bgIoTimestamp = nowTimestamp
-        addDebugFetchTimestamp()
 
 
         // make sure to balance each call to `beginBackgroundTask` with `endBackgroundTask`
         // make sure to balance each call to `beginBackgroundTask` with `endBackgroundTask`
         var backgroundTask: UIBackgroundTaskIdentifier = .invalid
         var backgroundTask: UIBackgroundTaskIdentifier = .invalid
@@ -450,8 +459,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             }
             }
         }
         }
 
 
-        let fetchSemaphore = DispatchSemaphore(value: 0)
-
         // move work to non-main thread to not block UI (otherwise, in case we get suspended, the app is blocked totally)
         // move work to non-main thread to not block UI (otherwise, in case we get suspended, the app is blocked totally)
         // (we are using `qos: default` as `qos: .background` or `main.asyncAfter` may be delayed by tens of minutes)
         // (we are using `qos: default` as `qos: .background` or `main.asyncAfter` may be delayed by tens of minutes)
         DispatchQueue.global().async { [weak self] in
         DispatchQueue.global().async { [weak self] in
@@ -461,38 +468,34 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             self.dcAccounts.startIo()
             self.dcAccounts.startIo()
             self.dcAccounts.maybeNetwork()
             self.dcAccounts.maybeNetwork()
 
 
-            _ = fetchSemaphore.wait(timeout: .now() + 10)
+            self.addDebugFetchTimestamp()
+
+            // create a new semaphore to make sure the received DC_CONNECTIVITY_CONNECTED really belongs to maybeNetwork() from above
+            // (maybeNetwork() sets connectivity to DC_CONNECTIVITY_CONNECTING, when fetch is done, we're back at DC_CONNECTIVITY_CONNECTED)
+            self.dcAccounts.fetchSemaphore = DispatchSemaphore(value: 0)
+            _ = self.dcAccounts.fetchSemaphore?.wait(timeout: .now() + 20)
+            self.dcAccounts.fetchSemaphore = nil
 
 
             // TOCHECK: it seems, we are not always reaching this point in code,
             // TOCHECK: it seems, we are not always reaching this point in code,
-            // the semaphore.wait does not exit after 10 seconds and the app gets suspended -
+            // semaphore?.wait() does not always exit after the given timeout and the app gets suspended -
             // maybe that is on purpose somehow to suspend inactive apps, not sure.
             // maybe that is on purpose somehow to suspend inactive apps, not sure.
             // this does not happen often, but still.
             // this does not happen often, but still.
             // cmp. https://github.com/deltachat/deltachat-ios/pull/1542#pullrequestreview-951620906
             // cmp. https://github.com/deltachat/deltachat-ios/pull/1542#pullrequestreview-951620906
             logger.info("⬅️ finishing fetch")
             logger.info("⬅️ finishing fetch")
+            self.pushToDebugArray(name: "notify-fetch-durations", value: Double(Date().timeIntervalSince1970)-nowTimestamp)
 
 
-            // dispatch back to main as we cannot check the foreground state from non-main thread
-            // (this again has the risk to be delayed by tens of minutes, however, fetch is done and we're mostly fine)
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { completionHandler(.failed); return }
-
-                if !self.appIsInForeground() {
-                    self.dcAccounts.stopIo()
-                }
+            if !self.appIsInForeground() {
+                self.dcAccounts.stopIo()
+            }
 
 
-                // to avoid 0xdead10cc exceptions, scheduled jobs need to be done before we get suspended;
-                // we increase the probabilty that this happens by waiting a moment before calling completionHandler()
-                DispatchQueue.global().async { [weak self] in
-                    guard let self = self else { completionHandler(.failed); return }
-
-                    _ = fetchSemaphore.wait(timeout: .now() + 1)
-                    logger.info("⬅️ fetch done")
-                    self.pushToDebugArray(name: "notify-fetch-durations", value: Double(Date().timeIntervalSince1970)-nowTimestamp)
-                    completionHandler(.newData)
-                    if backgroundTask != .invalid {
-                        UIApplication.shared.endBackgroundTask(backgroundTask)
-                        backgroundTask = .invalid
-                    }
-                }
+            // to avoid 0xdead10cc exceptions, scheduled jobs need to be done before we get suspended;
+            // we increase the probabilty that this happens by waiting a moment before calling completionHandler()
+            usleep(1_000_000)
+            logger.info("⬅️ fetch done")
+            completionHandler(.newData)
+            if backgroundTask != .invalid {
+                UIApplication.shared.endBackgroundTask(backgroundTask)
+                backgroundTask = .invalid
             }
             }
         }
         }
     }
     }
@@ -688,11 +691,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
     }
     }
 
 
     func appIsInForeground() -> Bool {
     func appIsInForeground() -> Bool {
-        switch UIApplication.shared.applicationState {
-        case .background, .inactive:
-            return false
-        case .active:
-            return true
+        if Thread.isMainThread {
+            switch UIApplication.shared.applicationState {
+            case .background, .inactive:
+                applicationInForeground = false
+            case .active:
+                applicationInForeground = true
+            }
         }
         }
+        return applicationInForeground
     }
     }
 }
 }