[GH-ISSUE #46] kernel: setitimer and select timeouts fire up to two ticks early (hzto truncates, timeout() counts from the last tick) #38

Closed
opened 2026-11-03 22:02:45 -07:00 by gitea-mirror · 1 comment
Owner

Originally created by @raynorpat on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/46

Symptom

setitimer(ITIMER_REAL) and select() timeouts fire before the requested interval has passed, by 7-16 ms (up to about two 10 ms ticks). POSIX requires these timers never to expire early.

Seen with the stress suite's stress-misc timer (ffs-perf branch) on the v2 image's kernel, i386 under QEMU TCG. It failed in 6 of 7 single-worker seeds, for both setitimer and select:

STRESS misc timer FAIL what=early want=86 got=79 kind=itimer seed=1791980622
STRESS misc timer FAIL what=early want=51 got=43 kind=itimer worker=1 iter=0 seed=1791981966

The test takes its start time before arming and its end stamp in the SIGALRM handler, or right after select returns. It allows 1 ms of slack and ignores negative intervals, which are #40's backward clock steps. Signal latency can only make a timer look late, so these are real early expiries.

Cause (from source)

Two roundings, both in the early direction:

  1. hzto() truncates (bsd/kern/kern_clock.c:391-418):
    • It converts the remaining time to whole ticks with integer division: ticks = (ms) / (tick / 1000). An 86 ms request becomes 8 ticks (80 ms).
    • It also measures from time, the BSD clock updated once per tick, not from the current time. So the remaining interval is computed against a stale "now".
  2. timeout() counts from the last tick, not from now (kern_clock.c:347-368):
    • The deadline is ticks_to_tvalspec(ticks) plus tick_stamp, the time of the most recent clock tick.
    • A request made 9 ms after a tick expires 9 ms sooner than intended.

Together, an interval can end up to about 2 ticks (20 ms at HZ=100) early. 86 ms became 8 ticks counted from a stale tick, so 70-80 ms; 79 ms was observed.

Callers that inherit this:

  • setitimer/getitimer (kern_time.c, realitexpire);
  • select timeouts (sys_generic.c);
  • nanosleep/sleep, and anything else that goes through hzto + timeout/tsleep.

Suggested fix

  • hzto: round up instead of truncating: (ms + tickms - 1) / tickms. Then add one tick, the classic BSD rule (FreeBSD's tvtohz does "+1 because we don't know where in the current tick we are"), so a timeout never fires before the full interval. Compute "now" with microtime() rather than the per-tick time.
  • Alternatively, fix timeout(): base the deadline on the current counter (clock_get_counter(System)) instead of tick_stamp, so no partial tick is lost.

Regression test: stress-misc timer should PASS on every seed once fixed. Its 1 ms tolerance may be tighter than tick granularity allows for late wakeups; it only checks early ones.

Related: #40 (gettimeofday backward steps), #44.

Originally created by @raynorpat on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/46 ## Symptom `setitimer(ITIMER_REAL)` and `select()` timeouts fire before the requested interval has passed, by 7-16 ms (up to about two 10 ms ticks). POSIX requires these timers never to expire early. Seen with the stress suite's `stress-misc timer` (ffs-perf branch) on the v2 image's kernel, i386 under QEMU TCG. It failed in 6 of 7 single-worker seeds, for both `setitimer` and `select`: ``` STRESS misc timer FAIL what=early want=86 got=79 kind=itimer seed=1791980622 STRESS misc timer FAIL what=early want=51 got=43 kind=itimer worker=1 iter=0 seed=1791981966 ``` The test takes its start time before arming and its end stamp in the SIGALRM handler, or right after `select` returns. It allows 1 ms of slack and ignores negative intervals, which are #40's backward clock steps. Signal latency can only make a timer look late, so these are real early expiries. ## Cause (from source) Two roundings, both in the early direction: 1. **`hzto()` truncates** (`bsd/kern/kern_clock.c:391-418`): - It converts the remaining time to whole ticks with integer division: `ticks = (ms) / (tick / 1000)`. An 86 ms request becomes 8 ticks (80 ms). - It also measures from `time`, the BSD clock updated once per tick, not from the current time. So the remaining interval is computed against a stale "now". 2. **`timeout()` counts from the last tick, not from now** (`kern_clock.c:347-368`): - The deadline is `ticks_to_tvalspec(ticks)` plus `tick_stamp`, the time of the most recent clock tick. - A request made 9 ms after a tick expires 9 ms sooner than intended. Together, an interval can end up to about 2 ticks (20 ms at HZ=100) early. 86 ms became 8 ticks counted from a stale tick, so 70-80 ms; 79 ms was observed. Callers that inherit this: - `setitimer`/`getitimer` (`kern_time.c`, `realitexpire`); - `select` timeouts (`sys_generic.c`); - `nanosleep`/`sleep`, and anything else that goes through `hzto` + `timeout`/`tsleep`. ## Suggested fix - **`hzto`:** round up instead of truncating: `(ms + tickms - 1) / tickms`. Then add one tick, the classic BSD rule (FreeBSD's `tvtohz` does "+1 because we don't know where in the current tick we are"), so a timeout never fires before the full interval. Compute "now" with `microtime()` rather than the per-tick `time`. - **Alternatively, fix `timeout()`:** base the deadline on the current counter (`clock_get_counter(System)`) instead of `tick_stamp`, so no partial tick is lost. **Regression test:** `stress-misc timer` should PASS on every seed once fixed. Its 1 ms tolerance may be tighter than tick granularity allows for late wakeups; it only checks early ones. Related: #40 (gettimeofday backward steps), #44.
gitea-mirror 2026-11-03 22:02:45 -07:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@raynorpat commented on GitHub (Oct 15, 2026):

Fixed in ffs-perf 3b6598be1 (kernel: round hzto() up and add a tick so setitimer and select timeouts never fire early).

hzto() now rounds the interval up to whole ticks at microsecond precision and adds one tick, because timeout() counts from the last clock tick (the FreeBSD tvtohz rule). A deadline that is now or past still returns 0, so waitevent's poll path is unchanged. It keeps measuring from time: every caller builds its deadline as time + interval, so the stale base cancels out, and switching only hzto to microtime() would shorten timers.

Tested with an A/B on a v2 snapshot guest (v2-kernel + the patch vs the stock v2 kernel):

  • stock: stress-misc -n 1 timer failed 10/10 seeds, including 1791980622 and 1791981966 (what=early, itimer and select)
  • patched: 35/35 PASS (the same 10 seeds, 20 more, and 5 with 4 workers); maxlat about 520 ms for requests up to 500 ms, so at most about 2 ticks late
  • patched: a full stress-misc run passes every category
<!-- gh-comment-id:6192746168 --> @raynorpat commented on GitHub (Oct 15, 2026): Fixed in ffs-perf 3b6598be1 (`kernel: round hzto() up and add a tick so setitimer and select timeouts never fire early`). `hzto()` now rounds the interval up to whole ticks at microsecond precision and adds one tick, because `timeout()` counts from the last clock tick (the FreeBSD `tvtohz` rule). A deadline that is now or past still returns 0, so `waitevent`'s poll path is unchanged. It keeps measuring from `time`: every caller builds its deadline as `time + interval`, so the stale base cancels out, and switching only `hzto` to `microtime()` would shorten timers. Tested with an A/B on a v2 snapshot guest (v2-kernel + the patch vs the stock v2 kernel): - stock: `stress-misc -n 1 timer` failed 10/10 seeds, including 1791980622 and 1791981966 (what=early, itimer and select) - patched: 35/35 PASS (the same 10 seeds, 20 more, and 5 with 4 workers); maxlat about 520 ms for requests up to 500 ms, so at most about 2 ticks late - patched: a full `stress-misc` run passes every category
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
github-starred/RhapsodiOS#38
No description provided.