[hackers] [PATCH slstatus v2] align ticks to wall-clock seconds

From: Jialu Xu <xujialu_AT_vimux.org>
Date: Mon, 4 May 2026 22:56:29 +0800

Use clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME) anchored to
whole-second boundaries. After each wakeup, classify the
next - now delta to advance the deadline, re-sleep (signal early
wake), or snap forward on jumps (suspend, NTP step in either
direction).

Switch datetime to clock_gettime(CLOCK_REALTIME) -- time() reads
the COARSE clock and truncates backward at the boundary.

Signed-off-by: Jialu Xu <xujialu_AT_vimux.org>
---
v1 -> v2:
- after wakeup, classify next - now in ms into three cases
  (advance / re-sleep / reanchor) so backwards NTP steps that
  arrive via signal also re-anchor instead of stalling on the
  now-unreachable deadline
> What about bigger backward jumps? I think they would cause a
> big gap in renders.
Good catch.  v1 only handled forward jumps; clock_nanosleep with
TIMER_ABSTIME keeps blocking through a backwards step, so if a
signal happened to interrupt the sleep my guard kept the old
deadline and the next clock_nanosleep stalled.
> Maybe simply always adjust the deadline?
I tried that first but it loses the SIGUSR1 fix from the simplify
pass: a signal firing at next-500ms would advance the deadline
past the original, and the next iteration would skip a scheduled
tick.  Classifying the next-now delta keeps both properties.
A backwards NTP step that does not also interrupt the sleep is
still undetectable here -- clock_nanosleep blocks until the
post-step wall clock reaches the deadline.  Fixing that needs
timerfd with TFD_TIMER_CANCEL_ON_SET or polling in shorter
chunks; both felt disproportionate.
 components/datetime.c | 15 ++++++---
 slstatus.c            | 72 ++++++++++++++++++++++++++++---------------
 2 files changed, 58 insertions(+), 29 deletions(-)
diff --git a/components/datetime.c b/components/datetime.c
index 5b10daf..0b8107f 100644
--- a/components/datetime.c
+++ b/components/datetime.c
_AT_@ -1,5 +1,4 @@
 /* See LICENSE file for copyright and license details. */
-#include <stdio.h>
 #include <time.h>
 
 #include "../slstatus.h"
_AT_@ -8,10 +7,18 @@
 const char *
 datetime(const char *fmt)
 {
-	time_t t;
+	struct timespec ts;
 
-	t = time(NULL);
-	if (!strftime(buf, sizeof(buf), fmt, localtime(&t))) {
+	/* Use CLOCK_REALTIME (high-resolution) rather than time(),
+	 * which on glibc reads CLOCK_REALTIME_COARSE and lags the
+	 * precise clock by up to one jiffy. At a whole-second
+	 * boundary the lag truncates the displayed second backwards
+	 * by one, making the clock appear to tick a second late. */
+	if (clock_gettime(CLOCK_REALTIME, &ts) < 0) {
+		warn("clock_gettime:");
+		return NULL;
+	}
+	if (!strftime(buf, sizeof(buf), fmt, localtime(&ts.tv_sec))) {
 		warn("strftime: Result string exceeds buffer size");
 		return NULL;
 	}
diff --git a/slstatus.c b/slstatus.c
index 16d88fe..a8845bb 100644
--- a/slstatus.c
+++ b/slstatus.c
_AT_@ -30,14 +30,6 @@ terminate(const int signo)
 		done = 1;
 }
 
-static void
-difftimespec(struct timespec *res, struct timespec *a, struct timespec *b)
-{
-	res->tv_sec = a->tv_sec - b->tv_sec - (a->tv_nsec < b->tv_nsec);
-	res->tv_nsec = a->tv_nsec - b->tv_nsec +
-	               (a->tv_nsec < b->tv_nsec) * 1E9;
-}
-
 static void
 usage(void)
 {
_AT_@ -48,8 +40,9 @@ int
 main(int argc, char *argv[])
 {
 	struct sigaction act;
-	struct timespec start, current, diff, intspec, wait;
+	struct timespec next, now;
 	size_t i, len;
+	long diff_ms;
 	int sflag, ret;
 	char status[MAXLEN];
 	const char *res;
_AT_@ -82,10 +75,17 @@ main(int argc, char *argv[])
 	if (!sflag && !(dpy = XOpenDisplay(NULL)))
 		die("XOpenDisplay: Failed to open display");
 
-	do {
-		if (clock_gettime(CLOCK_MONOTONIC, &start) < 0)
-			die("clock_gettime:");
-
+	/* Anchor the first scheduled tick to the next whole-second
+	 * boundary on the wall clock. Subsequent ticks use absolute
+	 * CLOCK_REALTIME deadlines advanced by `interval`, so the
+	 * displayed phase stays locked to wall-clock seconds across
+	 * NTP adjustments and suspend/resume. */
+	if (clock_gettime(CLOCK_REALTIME, &next) < 0)
+		die("clock_gettime:");
+	next.tv_nsec = 0;
+	next.tv_sec += 1;
+
+	for (;;) {
 		status[0] = '\0';
 		for (i = len = 0; i < LEN(args); i++) {
 			if (!(res = args[i].func(args[i].args)))
_AT_@ -109,21 +109,43 @@ main(int argc, char *argv[])
 			XFlush(dpy);
 		}
 
-		if (!done) {
-			if (clock_gettime(CLOCK_MONOTONIC, &current) < 0)
-				die("clock_gettime:");
-			difftimespec(&diff, &current, &start);
+		if (done)
+			break;
 
-			intspec.tv_sec = interval / 1000;
-			intspec.tv_nsec = (interval % 1000) * 1E6;
-			difftimespec(&wait, &intspec, &diff);
+		ret = clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME,
+		                      &next, NULL);
+		if (ret != 0 && ret != EINTR) {
+			errno = ret;
+			die("clock_nanosleep:");
+		}
+
+		if (clock_gettime(CLOCK_REALTIME, &now) < 0)
+			die("clock_gettime:");
 
-			if (wait.tv_sec >= 0 &&
-			    nanosleep(&wait, NULL) < 0 &&
-			    errno != EINTR)
-					die("nanosleep:");
+		/* Advance the deadline if it has elapsed; otherwise keep
+		 * it so the next iteration sleeps the remainder (a signal
+		 * woke us early). Then re-anchor to the next aligned tick
+		 * if `next` is unreachable within one interval -- now is
+		 * past `next` (suspend/resume or a forwards NTP step) or
+		 * `next` has drifted unexpectedly far into the future (a
+		 * backwards NTP step delivered while we were sleeping). */
+		diff_ms = (next.tv_sec - now.tv_sec) * 1000L
+		        + (next.tv_nsec - now.tv_nsec) / 1000000L;
+		if (diff_ms <= 0) {
+			next.tv_sec  += interval / 1000;
+			next.tv_nsec += (interval % 1000) * 1000000L;
+			if (next.tv_nsec >= 1000000000L) {
+				next.tv_sec  += 1;
+				next.tv_nsec -= 1000000000L;
+			}
+			diff_ms += (long)interval;
 		}
-	} while (!done);
+		if (diff_ms < 0 || diff_ms > (long)interval) {
+			next = now;
+			next.tv_nsec = 0;
+			next.tv_sec += 1;
+		}
+	}
 
 	if (!sflag) {
 		XStoreName(dpy, DefaultRootWindow(dpy), NULL);
-- 
2.47.3
Received on Mon May 04 2026 - 16:56:29 CEST

This archive was generated by hypermail 2.3.0 : Mon May 04 2026 - 17:00:40 CEST