Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does DispatchWallTime do on iOS?

I thought the difference between DispatchTime and DispatchWallTime had to do with whether the app was suspended or the device screen was locked or something: DispatchTime should pause, whereas DispatchWallTime should keep going because clocks in the real world keep going.

So I wrote a little test app:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }
    func applicationDidEnterBackground(_ application: UIApplication) {
        print("backgrounding the app, starting timers for 60 seconds", Date())
        DispatchQueue.main.asyncAfter(deadline: .now() + 60) {
            print("deadline 60 seconds ended", Date())
        }
        DispatchQueue.main.asyncAfter(wallDeadline: .now() + 60) {
            print("wallDeadline 60 seconds ended", Date())
        }
    }
    func applicationWillEnterForeground(_ application: UIApplication) {
        print("app coming to front", Date())
    }
}

I ran the app on my device. I backgrounded the app, waited for a while, then brought the app to the foreground. Sometimes "waited for a while" included switching off the screen. I got results like this:

backgrounding the app, starting timers for 60 seconds 2018-08-15 17:41:18 +0000
app coming to front 2018-08-15 17:41:58 +0000
wallDeadline 60 seconds ended 2018-08-15 17:42:24 +0000
deadline 60 seconds ended 2018-08-15 17:42:24 +0000

backgrounding the app, starting timers for 60 seconds 2018-08-15 17:42:49 +0000
app coming to front 2018-08-15 17:43:21 +0000
wallDeadline 60 seconds ended 2018-08-15 17:43:55 +0000
deadline 60 seconds ended 2018-08-15 17:43:55 +0000

The delay before the deadline timer fires is not as long as I expected: it's 6 seconds over the 60 second deadline, even though I "slept" the app for considerably longer than that. But even more surprising, both timers fire at the same instant.

So what does wallDeadline do on iOS that's different from what deadline does?

like image 887
matt Avatar asked Aug 15 '18 17:08

matt


People also ask

What is dispatchwalltime and how to use it?

DispatchWallTime is the time according to the wall clock, who doesn’t sleep at all. A Night Watch.⚔️ In the above extension, I have combined the functions in a single function to make it easier.

How does the downtime on the iPhone work?

Moreover, the Downtime on the iPhone will apply to all iOS devices that are using the same iCloud for screen time. Before the schedule, there is a warning with a pop-up screen 5 minutes before your iPhone reaches to the downtime.

How to turn off Downtime on iPhone SMS schedule?

However, you can turn off the downtime manually anytime you want as below. (Find other SMS schedule apps here) Step 1 Go to the Settings app on your iPhone, you can tap on the Screen Time option and select the Downtime option from the list to disable the downtime schedule.

What is the difference between app limits and downtime?

Downtime is another feature within Apple's Screen Time, like App Limits. Rather than putting a time limit on apps or app categories like App Limits does however, Downtime allows you to schedule a block of time where only apps that you choose will work.


2 Answers

There's nothing wrong with The Dreams Wind's answer, but I wanted to understand these APIs more precisely. Here's my analysis.

DispatchTime

Here's the comment above DispatchTime.init:

/// Creates a `DispatchTime` relative to the system clock that
/// ticks since boot.
///
/// - Parameters:
///   - uptimeNanoseconds: The number of nanoseconds since boot, excluding
///                        time the system spent asleep
/// - Returns: A new `DispatchTime`
/// - Discussion: This clock is the same as the value returned by
///               `mach_absolute_time` when converted into nanoseconds.
///               On some platforms, the nanosecond value is rounded up to a
///               multiple of the Mach timebase, using the conversion factors
///               returned by `mach_timebase_info()`. The nanosecond equivalent
///               of the rounded result can be obtained by reading the
///               `uptimeNanoseconds` property.
///               Note that `DispatchTime(uptimeNanoseconds: 0)` is
///               equivalent to `DispatchTime.now()`, that is, its value
///               represents the number of nanoseconds since boot (excluding
///               system sleep time), not zero nanoseconds since boot.

So DispatchTime is based on mach_absolute_time. But what is mach_absolute_time? It's defined in mach_absolute_time.s. There is a separate definition for each CPU type, but the key is that it uses rdtsc on x86-like CPUs and reads the CNTPCT_EL0 register on ARMs. In both cases, it's getting a value that increases monotonically, and only increases while the processor is not in a sufficiently deep sleep state.

Note that the CPU is not necessarily sleeping deeply enough even if the device appears to be asleep.

DispatchWallTime

There's no similarly helpful comment in the DispatchWallTime definition, but we can look at the definition of its now method:

public static func now() -> DispatchWallTime {
    return DispatchWallTime(rawValue: CDispatch.dispatch_walltime(nil, 0))
}

and then we can consult the definition of dispatch_walltime:

dispatch_time_t
dispatch_walltime(const struct timespec *inval, int64_t delta)
{
  int64_t nsec;
  if (inval) {
      nsec = (int64_t)_dispatch_timespec_to_nano(*inval);
  } else {
      nsec = (int64_t)_dispatch_get_nanoseconds();
  }
  nsec += delta;
  if (nsec <= 1) {
      // -1 is special == DISPATCH_TIME_FOREVER == forever
      return delta >= 0 ? DISPATCH_TIME_FOREVER : (dispatch_time_t)-2ll;
  }
  return (dispatch_time_t)-nsec;
}

When inval is nil, it calls _dispatch_get_nanoseconds, so let's check that out:

static inline uint64_t
_dispatch_get_nanoseconds(void)
{
  dispatch_static_assert(sizeof(NSEC_PER_SEC) == 8);
  dispatch_static_assert(sizeof(USEC_PER_SEC) == 8);

#if TARGET_OS_MAC
  return clock_gettime_nsec_np(CLOCK_REALTIME);
#elif HAVE_DECL_CLOCK_REALTIME
  struct timespec ts;
  dispatch_assume_zero(clock_gettime(CLOCK_REALTIME, &ts));
  return _dispatch_timespec_to_nano(ts);
#elif defined(_WIN32)
  static const uint64_t kNTToUNIXBiasAdjustment = 11644473600 * NSEC_PER_SEC;
  // FILETIME is 100-nanosecond intervals since January 1, 1601 (UTC).
  FILETIME ft;
  ULARGE_INTEGER li;
  GetSystemTimePreciseAsFileTime(&ft);
  li.LowPart = ft.dwLowDateTime;
  li.HighPart = ft.dwHighDateTime;
  return li.QuadPart * 100ull - kNTToUNIXBiasAdjustment;
#else
  struct timeval tv;
  dispatch_assert_zero(gettimeofday(&tv, NULL));
  return _dispatch_timeval_to_nano(tv);
#endif
}

It consults the POSIX CLOCK_REALTIME clock. So it is based on the common idea of time and will change if you change your device's time in Settings (or System Preferences on a Mac).

The Mysterious Six Seconds

You said your timer fired

6 seconds over the 60 second deadline

so let's see where that came from.

Both asyncAfter(deadline:execute:) and asyncAfter(wallDeadline:execute:) call the same C API, dispatch_after. The kind of deadline (or “clock”) is encoded into a dispatch_time_t along with the time value. The dispatch_after function calls the internal GCD function _dispatch_after, which I quote in part here:

static inline void
_dispatch_after(dispatch_time_t when, dispatch_queue_t dq,
      void *ctxt, void *handler, bool block)
{
  dispatch_timer_source_refs_t dt;
  dispatch_source_t ds;
  uint64_t leeway, delta;

snip

  delta = _dispatch_timeout(when);
  if (delta == 0) {
      if (block) {
          return dispatch_async(dq, handler);
      }
      return dispatch_async_f(dq, ctxt, handler);
  }
  leeway = delta / 10; // <rdar://problem/13447496>

  if (leeway < NSEC_PER_MSEC) leeway = NSEC_PER_MSEC;
  if (leeway > 60 * NSEC_PER_SEC) leeway = 60 * NSEC_PER_SEC;

snip

  dispatch_clock_t clock;
  uint64_t target;
  _dispatch_time_to_clock_and_value(when, &clock, &target);
  if (clock != DISPATCH_CLOCK_WALL) {
      leeway = _dispatch_time_nano2mach(leeway);
  }
  dt->du_timer_flags |= _dispatch_timer_flags_from_clock(clock);
  dt->dt_timer.target = target;
  dt->dt_timer.interval = UINT64_MAX;
  dt->dt_timer.deadline = target + leeway;
  dispatch_activate(ds);
}

The _dispatch_timeout function can be found in time.c. Suffice to say it returns the number of nanoseconds between the current time and the time passed to it. It determines the “current time” based on the clock of the time passed to it.

So _dispatch_after gets the number of nanoseconds to wait before executing your block. Then it computes leeway as one tenth of that duration. When it sets the timer's deadline, it adds leeway to the deadline you passed in.

In your case, delta is about 60 seconds (= 60 * 109 nanoseconds), so leeway is about 6 seconds. Hence your block is executed about 66 seconds after you call asyncAfter.

like image 55
rob mayoff Avatar answered Sep 29 '22 08:09

rob mayoff


This question has been here for quite a while without any answers, so I'd like to give it a try and point out subtle difference I noticed in practice.

DispatchTime should pause, whereas DispatchWallTime should keep going because clocks in the real world keep going

You are correct here, at least they are supposed to act this way. However it tends to be really tricky to check, that DispatchTime works as expected. When iOS app is running under Xcode session, it has unlimited background time and isn't getting suspended. I also couldn't achieve that by running application without Xcode connected, so it's still a big question if DispatchTime is paused under whatever conditions. However the main thing to note is that DispatchTime doesn't depend on the system clock.

DispatchWallTime works pretty much the same (it's not being suspended), apart from that it depends on the system clock. In order to see the difference, you can try out a little longer timer, say, 5 minutes. After that go to the system settings and set time 1 hour forward. If you now open the application you can notice, that WallTimer immediately expires whereas DispatchTime will keep waiting its time.

like image 32
The Dreams Wind Avatar answered Sep 29 '22 08:09

The Dreams Wind