Skip to content

Utilizing Monotonic Clocks for Timers

Daniel Adam edited this page Sep 6, 2023 · 7 revisions

Version: 2.2.5.6

In this update, the internal structures oc_timer and oc_etimer have undergone significant revisions to incorporate the use of a monotonic clock instead of the absolute time system clock (CLOCK_REALTIME to CLOCK_MONOTONIC/CLOCK_MONOTONIC_RAW on a POSIX system). The purpose of these timers is to measure durations accurately, and the adoption of a steady monotonic clock provides a more reliable implementation.

The shift from relying on the system clock is motivated by its vulnerability to system time changes. Such changes can occur either through user intervention or by synchronization with a timer server. In a particularly problematic scenario, adjusting the system time backward by one hour would result in no timers firing for at least one hour.

By transitioning to a monotonic clock, the timers become immune to variations in the system time. This ensures consistent and predictable behavior, eliminating the risk of timers failing to trigger due to unexpected system time adjustments.

Supported Platforms

Monotonic clock has been implemented on Linux, Android, Windows, and ESP32. The new available function oc_clock_time_monotonic should return a steady clock not affected by time adjustments, which counts ticks since boot time or some other point in the past.

On Arduino, OpenThread, and Zephyr, the oc_clock_time_monotonic is not yet implemented, and the implementation fallbacks to oc_clock_time.

To check at runtime whether monotonic clock is available, call oc_clock_time_has_monotonic_clock.

Event Loop

Changes to timers affect the main event loop. The standard implementation of the loop in applications previously used oc_main_poll which returns the absolute time of the next event. This time was used to calculate the next wake-up time.

Run Loop Implementation using oc_main_poll() on Linux:

while (!terminated) {
  oc_clock_time_t next_event = oc_main_poll();
  pthread_mutex_lock(&mutex);
  if (next_event == 0) {
    pthread_cond_wait(&cv, &mutex);
  } else {
    struct timespec ts;
    ts.tv_sec = (next_event / OC_CLOCK_SECOND);
    ts.tv_nsec = (next_event % OC_CLOCK_SECOND) * 1.e09 / OC_CLOCK_SECOND;
    pthread_cond_timedwait(&cv, &mutex, &ts);
  }
  pthread_mutex_unlock(&mutex);
}

The event loop utilizes oc_etimer, so the returned time was an absolute time obtained by oc_clock_time. However, since oc_etimer has been reworked, it no longer returns absolute time. It still uses oc_etimer, but to avoid changing the behavior of oc_main_poll, it calculates the absolute time of the next event based on the monotonic time of the next event, the current monotonic time, and the current absolute time. oc_main_poll has been deprecated, and the new call oc_main_poll_v1 is intended to replace it. It returns the time of the next event in monotonic time.

Migration to oc_main_poll_v1 on Linux

The new run loop on Linux should utilize oc_main_poll_v1. By default, pthread_cond_timedwait uses absolute time (CLOCK_REALTIME clock), but newer Linux platforms support the pthread_condattr_setclock system call. It allows changing the clock used by the condition variable. In the IoT library, the POSIX CLOCK_MONOTONIC_RAW is used to get monotonic time, unfortunately, this clock is not supported by pthread_condattr_setclock, so the time returned by oc_main_poll_v1 must be translated to time in CLOCK_MONOTONIC before it can be used by pthread_cond_timedwait.

For example, on Ubuntu 22.04, a possible implementation looks like this:

int main() {
  // ...
  pthread_mutex_t mutex;
  pthread_cond_t cv;
  pthread_condattr_t attr;
  err = pthread_condattr_init(&attr);
  if (err != 0) {
    return -1;
  }
  err = pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
  if (err != 0) {
    pthread_condattr_destroy(&attr);
    return -1;
  }
  err = pthread_cond_init(&cv, &attr);
  if (err != 0) {
    pthread_condattr_destroy(&attr);
    return -1;
  }
  pthread_condattr_destroy(&attr);

  while (!terminated) {
    oc_clock_time_t next_event_mt = oc_main_poll_v1();
    pthread_mutex_lock(&mutex);
    if (oc_main_needs_poll()) {
      pthread_mutex_unlock(&mutex);
      continue;
    }
    if (next_event_mt == 0) {
      pthread_cond_wait(&cv, &mutex);
    } else {
      struct timespec next_event = { 0, 0 };
      oc_clock_time_t next_event_cv;
      if (oc_clock_monotonic_time_to_posix(next_event_mt, CLOCK_MONOTONIC,
                                           &next_event_cv)) {
        next_event = oc_clock_time_to_timespec(next_event_cv);
      }
      pthread_cond_timedwait(&cv, &mutex, &next_event);
    }
    pthread_mutex_unlock(&mutex);
  }
  // ...
}

To ensure correct synchronization when using this form the run loop your signalization callback must call pthread_cond_signal with locked mutex.

void
signal_event_loop(void)
{
  pthread_mutex_lock(&mutex);
  pthread_cond_signal(&cv);
  pthread_mutex_unlock(&mutex);
}

Migration to oc_main_poll_v1 on Windows

Condition variables on Windows use relative time, so the migration is simpler:

int main() {
  // ...
  CRITICAL_SECTION cs;
  InitializeCriticalSection(&cs);
  CONDITION_VARIABLE cv;
  InitializeConditionVariable(&cv);
  while (!terminated) {
    oc_clock_time_t next_event_mt = oc_main_poll_v1();
    EnterCriticalSection(&cs);
    if (oc_main_needs_poll()) {
      LeaveCriticalSection(&cs);
      continue;
    }
    if (next_event_mt == 0) {
      SleepConditionVariableCS(&cv, &cs, INFINITE);
    } else {
      oc_clock_time_t now_mt = oc_clock_time_monotonic();
      if (now_mt < next_event_mt) {
        SleepConditionVariableCS(
          &cv, &cs, (DWORD)((next_event_mt - now_mt) * 1000 / OC_CLOCK_SECOND));
      }
    }
    LeaveCriticalSection(&g_cs);
  }
  // ...
}

To ensure correct synchronization when using this form the run loop your signalization callback must call WakeConditionVariable inside the critical section cs.

void
signal_event_loop(void)
{
  EnterCriticalSection(&cs);
  WakeConditionVariable(&cv);
  LeaveCriticalSection(&cs);
}

Race condition in main run loop

A race condition between pthread_cond_signal and pthread_cond_wait/pthread_cond_timedwait (Linux, Android, ESP32); WakeConditionVariable and SleepConditionVariableCS (Windows) was found in an earlier version of the code snippets. See https://github.com/iotivity/iotivity-lite/issues/519 . The code has been updating to avoid the race condition.

Next Steps

  • Implement scheduling at an absolute time (update).
  • Update ESP32 port to use oc_main_poll_v1.
  • Update Android port to use oc_main_poll_v1.

By following these steps, the utilization of monotonic clocks for timers will be more efficient and reliable across different platforms, ensuring accurate measurement of durations and eliminating the impact of system time changes.