Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I create monotonic clock on Windows which doesn't tick during suspend?

Tags:

time

winapi

I'm looking for a way to obtain a guaranteed-monotonic clock which excludes time spent during suspend, just like POSIX CLOCK_MONOTONIC.

Solutions requiring Windows 7 (or later) are acceptable.

Here's an example of something that doesn't work:

LONGLONG suspendTime, uiTime1, uiTime2;
do {
  QueryUnbiasedInterruptTime((ULONGLONG*)&uiTime1);
  suspendTime = GetTickCount64()*10000 - uiTime1;
  QueryUnbiasedInterruptTime((ULONGLONG*)&uiTime2);
} while (uiTime1 != uiTime2);
static LARGE_INTEGER firstSuspend = suspendTime;
static LARGE_INTERER lastSuspend = suspendTime;
assert(suspendTime > lastSuspend);
lastSuspend = suspendTime;

LARGE_INTEGER now;
QueryPerformanceCounter(&now);
static LONGLONG firstQpc = now.QuadPart;

return (now.QuadPart - firstQpc)*qpcFreqNumer/qpcFreqDenom -
    (suspendTime - firstSuspend);

The problem with this (my first attempt) is that GetTickCount only ticks every 15ms, wheras QueryUnbiasedInterruptTime seems to tick a little more often, so every now and then my method observes the suspend time go back by a little.

I've also tried using CallNtPowerInformation, but it's not clear how to use those values either to get a nice, race-free measure of suspend time.

like image 557
Nicholas Wilson Avatar asked Sep 16 '25 06:09

Nicholas Wilson


2 Answers

The full procedure for calculating monotonic time, which does not tick during suspend, is as follows:

typedef struct _KSYSTEM_TIME {
  ULONG LowPart;
  LONG High1Time;
  LONG High2Time;
} KSYSTEM_TIME;
#define KUSER_SHARED_DATA 0x7ffe0000
#define InterruptTime ((KSYSTEM_TIME volatile*)(KUSER_SHARED_DATA + 0x08))
#define InterruptTimeBias ((ULONGLONG volatile*)(KUSER_SHARED_DATA + 0x3b0))

static LONGLONG readInterruptTime() {
  // Reading the InterruptTime from KUSER_SHARED_DATA is much better than
  // using GetTickCount() because it doesn't wrap, and is even a little quicker.
  // This works on all Windows NT versions (NT4 and up).
  LONG timeHigh;
  ULONG timeLow;
  do {
    timeHigh = InterruptTime->High1Time;
    timeLow = InterruptTime->LowPart;
  } while (timeHigh != InterruptTime->High2Time);
  LONGLONG now = ((LONGLONG)timeHigh << 32) + timeLow;
  static LONGLONG d = now;
  return now - d;
}

static LONGLONG scaleQpc(LONGLONG qpc) {
  // We do the actual scaling in fixed-point rather than floating, to make sure
  // that we don't violate monotonicity due to rounding errors.  There's no
  // need to cache QueryPerformanceFrequency().
  LARGE_INTEGER frequency;
  QueryPerformanceFrequency(&frequency);
  double fraction = 10000000/double(frequency.QuadPart);
  LONGLONG denom = 1024;
  LONGLONG numer = std::max(1LL, (LONGLONG)(fraction*denom + 0.5));
  return qpc * numer / denom;
}

static ULONGLONG readUnbiasedQpc() {
  // We remove the suspend bias added to QueryPerformanceCounter results by
  // subtracting the interrupt time bias, which is not strictly speaking legal,
  // but the units are correct and I think it's impossible for the resulting
  // "unbiased QPC" value to go backwards.
  LONGLONG interruptTimeBias, qpc;
  do {
    interruptTimeBias = *InterruptTimeBias;
    LARGE_INTEGER counter;
    QueryPerformanceCounter(&counter);
    qpc = counter.QuadPart;
  } while (interruptTimeBias != *InterruptTimeBias);
  static std::pair<LONGLONG,LONGLONG> d(qpc, interruptTimeBias);
  return scaleQpc(qpc - d.first) - (interruptTimeBias - d.second);
}

/// getMonotonicTime() returns the time elapsed since the application's first
/// call to getMonotonicTime(), in 100ns units.  The values returned are
/// guaranteed to be monotonic.  The time ticks in 15ms resolution and advances
/// during suspend on XP and Vista, but we manage to avoid this on Windows 7
/// and 8, which also use a high-precision timer.  The time does not wrap after
/// 49 days.
uint64_t getMonotonicTime()
{
  OSVERSIONINFOEX ver = { sizeof(OSVERSIONINFOEX), };
  GetVersionEx(&ver);
  bool win7OrLater = (ver.dwMajorVersion > 6 ||
      (ver.dwMajorVersion == 6 && ver.dwMinorVersion >= 1));
  // On Windows XP and earlier, QueryPerformanceCounter is not monotonic so we
  // steer well clear of it; on Vista, it's just a bit slow.
  return win7OrLater ? readUnbiasedQpc() : readInterruptTime();
}
like image 122
Nicholas Wilson Avatar answered Sep 17 '25 22:09

Nicholas Wilson


The suspend bias time is available in kernel mode (_KUSER_SHARED_DATA.QpcBias in ntddk.h). A read-only copy is available in user mode.

#include <nt.h>
#include <ntrtl.h>
#include <nturtl.h>

LONGLONG suspendTime, uiTime1, uiTime2;
QueryUnbiasedInterruptTime((ULONGLONG*)&uiTime1);
uiTime1 -= USER_SHARED_DATA->QpcBias;  // subtract off the suspend bias
like image 34
Eric Brown Avatar answered Sep 17 '25 21:09

Eric Brown