Skip to content

Commit dd8544c

Browse files
committed
Delve into clock rabbit hole
The worst issue I had with consts.sh for clock_gettime is how it defined too many clocks. So I looked into these clocks all day to figure out how how they overlap in functionality. I discovered counter-intuitive things such as how CLOCK_MONOTONIC should be CLOCK_UPTIME on MacOS and BSD, and that CLOCK_BOOTTIME should be CLOCK_MONOTONIC on MacOS / BSD. Windows 10 also has some incredible new APIs, that let us simplify clock_gettime(). - Linux CLOCK_REALTIME -> GetSystemTimePreciseAsFileTime() - Linux CLOCK_MONOTONIC -> QueryUnbiasedInterruptTimePrecise() - Linux CLOCK_MONOTONIC_RAW -> QueryUnbiasedInterruptTimePrecise() - Linux CLOCK_REALTIME_COARSE -> GetSystemTimeAsFileTime() - Linux CLOCK_MONOTONIC_COARSE -> QueryUnbiasedInterruptTime() - Linux CLOCK_BOOTTIME -> QueryInterruptTimePrecise() Documentation on the clock crew has been added to clock_gettime() in the docstring and in redbean's documentation too. You can read that to learn interesting facts about eight essential clocks that survived this purge. This is original research you will not find on Google, OpenAI, or Claude I've tested this change by porting *NSYNC to become fully clock agnostic since it has extensive tests for spotting irregularities in time. I have also included these tests in the default build so they no longer need to be run manually. Both CLOCK_REALTIME and CLOCK_MONOTONIC are good across the entire amd64 and arm64 test fleets.
1 parent 8f81451 commit dd8544c

File tree

87 files changed

+939
-900
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+939
-900
lines changed

libc/calls/BUILD.mk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ LIBC_CALLS_A_DIRECTDEPS = \
4848
LIBC_NT_PDH \
4949
LIBC_NT_POWRPROF \
5050
LIBC_NT_PSAPI \
51+
LIBC_NT_REALTIME \
5152
LIBC_NT_SYNCHRONIZATION \
5253
LIBC_NT_WS2_32 \
5354
LIBC_STR \
5455
LIBC_SYSV \
5556
LIBC_SYSV_CALLS \
56-
THIRD_PARTY_COMPILER_RT
57+
THIRD_PARTY_COMPILER_RT \
5758

5859
LIBC_CALLS_A_DEPS := \
5960
$(call uniq,$(foreach x,$(LIBC_CALLS_A_DIRECTDEPS),$($(x))))

libc/calls/clock_getres.c

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,49 @@
2020
#include "libc/dce.h"
2121
#include "libc/intrin/describeflags.h"
2222
#include "libc/intrin/strace.h"
23+
#include "libc/runtime/clktck.h"
2324
#include "libc/sysv/consts/clock.h"
2425
#include "libc/sysv/errfuns.h"
2526
#include "libc/time.h"
2627

27-
static int sys_clock_getres_poly(int clock, struct timespec *ts, int64_t real,
28-
int64_t real_coarse, int64_t boot) {
29-
ts->tv_sec = 0;
30-
if (clock == CLOCK_REALTIME) {
31-
ts->tv_nsec = real;
28+
static uint64_t hz_to_nanos(uint64_t frequency) {
29+
if (!frequency)
3230
return 0;
33-
} else if (clock == CLOCK_REALTIME_COARSE) {
34-
ts->tv_nsec = real_coarse;
35-
return 0;
36-
} else if (clock == CLOCK_MONOTONIC) {
37-
ts->tv_nsec = 10;
31+
uint64_t quotient = 1000000000 / frequency;
32+
uint64_t remainder = 1000000000 % frequency;
33+
if (remainder > 0)
34+
quotient += 1;
35+
return quotient;
36+
}
37+
38+
static int sys_clock_getres_poly(int clock, struct timespec *ts, int64_t prec) {
39+
if (ts)
40+
ts->tv_sec = 0;
41+
if (clock == CLOCK_REALTIME || //
42+
clock == CLOCK_BOOTTIME || //
43+
clock == CLOCK_MONOTONIC || //
44+
clock == CLOCK_MONOTONIC_RAW) {
45+
if (ts)
46+
ts->tv_nsec = prec;
3847
return 0;
39-
} else if (clock == CLOCK_BOOTTIME) {
40-
ts->tv_nsec = boot;
48+
} else if (clock == CLOCK_REALTIME_COARSE ||
49+
clock == CLOCK_MONOTONIC_COARSE ||
50+
clock == CLOCK_THREAD_CPUTIME_ID ||
51+
clock == CLOCK_PROCESS_CPUTIME_ID) {
52+
if (ts)
53+
*ts = timespec_fromnanos(hz_to_nanos(CLK_TCK));
4154
return 0;
4255
} else {
4356
return einval();
4457
}
4558
}
4659

4760
static int sys_clock_getres_nt(int clock, struct timespec *ts) {
48-
return sys_clock_getres_poly(clock, ts, 100, 1000000, 1000000);
61+
return sys_clock_getres_poly(clock, ts, 100);
4962
}
5063

5164
static int sys_clock_getres_xnu(int clock, struct timespec *ts) {
52-
return sys_clock_getres_poly(clock, ts, 1000, 1000, 1000);
65+
return sys_clock_getres_poly(clock, ts, 1000);
5366
}
5467

5568
/**

libc/calls/clock_gettime-mono.c

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,42 +18,63 @@
1818
╚─────────────────────────────────────────────────────────────────────────────*/
1919
#include "libc/atomic.h"
2020
#include "libc/calls/struct/timespec.h"
21+
#include "libc/calls/struct/timespec.internal.h"
22+
#include "libc/calls/struct/timeval.h"
2123
#include "libc/cosmo.h"
22-
#include "libc/errno.h"
24+
#include "libc/dce.h"
2325
#include "libc/nexgen32e/rdtsc.h"
24-
#include "libc/nexgen32e/x86feature.h"
2526

2627
/**
27-
* @fileoverview Fast Monotonic Clock Polyfill for XNU/NT.
28+
* @fileoverview Monotonic clock polyfill.
29+
*
30+
* This isn't quite `CLOCK_MONOTONIC` and isn't quite `CLOCK_BOOTTIME`
31+
* either; however it is fast and almost always goes in one direction.
32+
*
33+
* Intel architecture guarantees that a mapping exists between rdtsc &
34+
* nanoseconds only if the cpu advertises invariant timestamps support
35+
* however this shouldn't matter for a monotonic clock since we really
36+
* don't want to have it tick while suspended. Sadly that shall happen
37+
* since nearly all x86 microprocessors support invariant tsc which is
38+
* why we try to avoid this fallback when possible.
2839
*/
2940

41+
int sys_sysctl(int *, unsigned, void *, size_t *, void *, size_t) libcesque;
42+
3043
static struct {
3144
atomic_uint once;
32-
struct timespec base_wall;
33-
uint64_t base_tick;
45+
unsigned long base;
46+
struct timespec boot;
3447
} g_mono;
3548

49+
static struct timespec get_boot_time_xnu(void) {
50+
struct timeval t;
51+
size_t n = sizeof(t);
52+
int mib[] = {1 /* CTL_KERN */, 21 /* KERN_BOOTTIME */};
53+
if (sys_sysctl(mib, 2, &t, &n, 0, 0) == -1)
54+
__builtin_trap();
55+
return timeval_totimespec(t);
56+
}
57+
3658
static void sys_clock_gettime_mono_init(void) {
37-
g_mono.base_wall = timespec_real();
38-
g_mono.base_tick = rdtsc();
59+
g_mono.base = rdtsc();
60+
if (IsXnu()) {
61+
g_mono.boot = get_boot_time_xnu();
62+
} else {
63+
__builtin_trap();
64+
}
3965
}
4066

4167
int sys_clock_gettime_mono(struct timespec *time) {
4268
uint64_t nanos;
4369
uint64_t cycles;
44-
#ifdef __x86_64__
45-
// intel architecture guarantees that a mapping exists between rdtsc &
46-
// nanoseconds only if the cpu advertises invariant timestamps support
47-
if (!X86_HAVE(INVTSC))
48-
return -EINVAL;
49-
#endif
5070
cosmo_once(&g_mono.once, sys_clock_gettime_mono_init);
51-
cycles = rdtsc() - g_mono.base_tick;
71+
// ensure we get the full 64 bits of counting, which avoids wraparound
72+
cycles = rdtsc() - g_mono.base;
5273
// this is a crude approximation, that's worked reasonably well so far
5374
// only the kernel knows the actual mapping between rdtsc and nanosecs
5475
// which we could attempt to measure ourselves using clock_gettime but
5576
// we'd need to impose 100 ms of startup latency for a guess this good
5677
nanos = cycles / 3;
57-
*time = timespec_add(g_mono.base_wall, timespec_fromnanos(nanos));
78+
*time = timespec_add(g_mono.boot, timespec_fromnanos(nanos));
5879
return 0;
5980
}

libc/calls/clock_gettime-nt.c

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,71 +25,90 @@
2525
#include "libc/nt/runtime.h"
2626
#include "libc/nt/synchronization.h"
2727
#include "libc/nt/thread.h"
28+
#include "libc/nt/time.h"
2829

2930
#define _CLOCK_REALTIME 0
3031
#define _CLOCK_MONOTONIC 1
3132
#define _CLOCK_REALTIME_COARSE 2
3233
#define _CLOCK_BOOTTIME 3
3334
#define _CLOCK_PROCESS_CPUTIME_ID 4
3435
#define _CLOCK_THREAD_CPUTIME_ID 5
35-
36-
static struct {
37-
uint64_t base;
38-
uint64_t freq;
39-
} g_winclock;
36+
#define _CLOCK_MONOTONIC_COARSE 6
4037

4138
textwindows int sys_clock_gettime_nt(int clock, struct timespec *ts) {
42-
uint64_t t;
39+
uint64_t hectons;
4340
struct NtFileTime ft, ftExit, ftUser, ftKernel, ftCreation;
4441
switch (clock) {
4542
case _CLOCK_REALTIME:
46-
if (ts) {
47-
GetSystemTimePreciseAsFileTime(&ft);
48-
*ts = FileTimeToTimeSpec(ft);
49-
}
43+
GetSystemTimePreciseAsFileTime(&ft);
44+
*ts = FileTimeToTimeSpec(ft);
5045
return 0;
5146
case _CLOCK_REALTIME_COARSE:
52-
if (ts) {
53-
GetSystemTimeAsFileTime(&ft);
54-
*ts = FileTimeToTimeSpec(ft);
55-
}
47+
GetSystemTimeAsFileTime(&ft);
48+
*ts = FileTimeToTimeSpec(ft);
5649
return 0;
5750
case _CLOCK_MONOTONIC:
58-
if (ts) {
59-
QueryPerformanceCounter(&t);
60-
t = ((t - g_winclock.base) * 1000000000) / g_winclock.freq;
61-
*ts = timespec_fromnanos(t);
62-
}
51+
//
52+
// "If you need a higher resolution timer, use the
53+
// QueryUnbiasedInterruptTime function, a multimedia timer, or a
54+
// high-resolution timer. The elapsed time retrieved by the
55+
// QueryUnbiasedInterruptTime function includes only time that
56+
// the system spends in the working state."
57+
//
58+
// —Quoth MSDN § Windows Time
59+
//
60+
QueryUnbiasedInterruptTimePrecise(&hectons);
61+
*ts = timespec_fromnanos(hectons * 100);
62+
return 0;
63+
case _CLOCK_MONOTONIC_COARSE:
64+
//
65+
// "QueryUnbiasedInterruptTimePrecise is similar to the
66+
// QueryUnbiasedInterruptTime routine, but is more precise. The
67+
// interrupt time reported by QueryUnbiasedInterruptTime is based
68+
// on the latest tick of the system clock timer. The system clock
69+
// timer is the hardware timer that periodically generates
70+
// interrupts for the system clock. The uniform period between
71+
// system clock timer interrupts is referred to as a system clock
72+
// tick, and is typically in the range of 0.5 milliseconds to
73+
// 15.625 milliseconds, depending on the hardware platform. The
74+
// interrupt time value retrieved by QueryUnbiasedInterruptTime
75+
// is accurate within a system clock tick. ¶To provide a system
76+
// time value that is more precise than that of
77+
// QueryUnbiasedInterruptTime, QueryUnbiasedInterruptTimePrecise
78+
// reads the timer hardware directly, therefore a
79+
// QueryUnbiasedInterruptTimePrecise call can be slower than a
80+
// QueryUnbiasedInterruptTime call."
81+
//
82+
// —Quoth MSDN § QueryUnbiasedInterruptTimePrecise
83+
//
84+
QueryUnbiasedInterruptTime(&hectons);
85+
*ts = timespec_fromnanos(hectons * 100);
6386
return 0;
6487
case _CLOCK_BOOTTIME:
65-
if (ts) {
66-
*ts = timespec_frommillis(GetTickCount64());
67-
}
88+
//
89+
// "Unbiased interrupt-time means that only time that the system
90+
// is in the working state is counted; therefore, the interrupt
91+
// time count is not "biased" by time the system spends in sleep
92+
// or hibernation."
93+
//
94+
// —Quoth MSDN § Interrupt Time
95+
//
96+
QueryInterruptTimePrecise(&hectons);
97+
*ts = timespec_fromnanos(hectons * 100);
6898
return 0;
6999
case _CLOCK_PROCESS_CPUTIME_ID:
70-
if (ts) {
71-
GetProcessTimes(GetCurrentProcess(), &ftCreation, &ftExit, &ftKernel,
72-
&ftUser);
73-
*ts = WindowsDurationToTimeSpec(ReadFileTime(ftUser) +
74-
ReadFileTime(ftKernel));
75-
}
100+
GetProcessTimes(GetCurrentProcess(), &ftCreation, &ftExit, &ftKernel,
101+
&ftUser);
102+
*ts = WindowsDurationToTimeSpec(ReadFileTime(ftUser) +
103+
ReadFileTime(ftKernel));
76104
return 0;
77105
case _CLOCK_THREAD_CPUTIME_ID:
78-
if (ts) {
79-
GetThreadTimes(GetCurrentThread(), &ftCreation, &ftExit, &ftKernel,
80-
&ftUser);
81-
*ts = WindowsDurationToTimeSpec(ReadFileTime(ftUser) +
82-
ReadFileTime(ftKernel));
83-
}
106+
GetThreadTimes(GetCurrentThread(), &ftCreation, &ftExit, &ftKernel,
107+
&ftUser);
108+
*ts = WindowsDurationToTimeSpec(ReadFileTime(ftUser) +
109+
ReadFileTime(ftKernel));
84110
return 0;
85111
default:
86112
return -EINVAL;
87113
}
88114
}
89-
90-
__attribute__((__constructor__(40))) static textstartup void winclock_init() {
91-
if (IsWindows()) {
92-
QueryPerformanceCounter(&g_winclock.base);
93-
QueryPerformanceFrequency(&g_winclock.freq);
94-
}
95-
}

libc/calls/clock_gettime-xnu.c

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@
2525
#include "libc/sysv/consts/clock.h"
2626
#ifdef __x86_64__
2727

28-
#define CTL_KERN 1
29-
#define KERN_BOOTTIME 21
30-
3128
int sys_clock_gettime_xnu(int clock, struct timespec *ts) {
3229
long ax, dx;
3330
if (clock == CLOCK_REALTIME) {
@@ -47,31 +44,20 @@ int sys_clock_gettime_xnu(int clock, struct timespec *ts) {
4744
// 2. old xnu returns *ts in rax:rdx regs
4845
//
4946
// we assume this system call always succeeds
50-
if (ts) {
51-
asm volatile("syscall"
52-
: "=a"(ax), "=d"(dx)
53-
: "0"(0x2000000 | 116), "D"(ts), "S"(0), "1"(0)
54-
: "rcx", "r8", "r9", "r10", "r11", "memory");
55-
if (ax) {
56-
ts->tv_sec = ax;
57-
ts->tv_nsec = dx;
58-
}
59-
ts->tv_nsec *= 1000;
47+
asm volatile("syscall"
48+
: "=a"(ax), "=d"(dx)
49+
: "0"(0x2000000 | 116), "D"(ts), "S"(0), "1"(0)
50+
: "rcx", "r8", "r9", "r10", "r11", "memory");
51+
if (ax) {
52+
ts->tv_sec = ax;
53+
ts->tv_nsec = dx;
6054
}
55+
ts->tv_nsec *= 1000;
6156
return 0;
62-
} else if (clock == CLOCK_MONOTONIC) {
63-
if (!ts)
64-
return 0;
57+
} else if (clock == CLOCK_BOOTTIME || //
58+
clock == CLOCK_MONOTONIC || //
59+
clock == CLOCK_MONOTONIC_COARSE) {
6560
return sys_clock_gettime_mono(ts);
66-
} else if (clock == CLOCK_BOOTTIME) {
67-
struct timeval x;
68-
size_t n = sizeof(x);
69-
int mib[] = {CTL_KERN, KERN_BOOTTIME};
70-
if (sysctl(mib, ARRAYLEN(mib), &x, &n, 0, 0) == -1)
71-
return -1;
72-
if (ts)
73-
*ts = timeval_totimespec(timeval_sub(timeval_real(), x));
74-
return 0;
7561
} else {
7662
return -EINVAL;
7763
}

0 commit comments

Comments
 (0)