Skip to content

Make Thread/sleep sleep for what it is asked - #1340

Merged
yogthos merged 5 commits into
mainfrom
fix/sleep-overshoot
Oct 14, 2026
Merged

yogthos merged 5 commits into
mainfrom
fix/sleep-overshoot

Conversation

@yogthos

@yogthos yogthos commented Oct 13, 2026

Copy link
Copy Markdown
Member

Since 0.8.21 (45bfe3d, rounding deadlines up) Thread/sleep overshoots, and sleep(0) / sleep(0, n) wait for a clock tick instead of returning. Measured on an M1 Max, 200 calls each:

0.8.20   sleep(0, 100000) 0.000 ms   sleep(1) 1.00 ms
0.8.21   sleep(0, 100000) 1.258 ms   sleep(1) 2.48 ms
this     sleep(0, 100000) 0.132 ms   sleep(1) 1.25 ms   sleep(0) 0.000 ms

45bfe3d^ vs 45bfe3d confirms the change: 0.000 ms vs 1.254 ms for sleep(0, 100000). It showed up as an intermittent timeout in ebb's sample stress test, whose notifier and reads both use sub-millisecond sleeps (jlt-commons/ebb#14 has the ebb side; the root of that hang is in ebb).

Three causes:

  • jolt-deadline->timeout and the timer thread computed the wait from the truncated millisecond, so every thread timed wait ran up to 1ms past its deadline. Both now measure from the precise monotonic clock (mono-duration-until).
  • Thread.sleep(ms, nanos) dropped the nanos. The sleep deadline is now an exact rational millisecond (mono-deadline-exact) and due checks compare at full precision (mono-due?). Whole deadlines compare exactly as before, so every other timed wait keeps its rounded-up deadline. TimeUnit.sleep passes sub-millisecond amounts through instead of truncating them.
  • sleep(0) waited for the next tick. It now checks the interrupt flag and yields, as the JDK does.

Section 10 of the asynctimer gate covers it: the timeout handed to condition-wait never exceeds the time left (failed before), sleep(0) does not wait for a tick (failed before), sleep(0, 100000) is at least 0.1ms and well under 1ms. make test passes locally.

Merging this before jlt-commons/ebb#14 would turn ebb CI red every run: with real 100us/500us sleeps the stress test's notifier outruns its reads 5x and the unfixed sample never finishes.

@yogthos
yogthos force-pushed the fix/sleep-overshoot branch 2 times, most recently from 4565ac9 to 85ed91a Compare October 14, 2026 01:39
A thread's timed wait counted its relative timeout from the truncated
millisecond, so it overran the deadline by up to 1ms, and since 45bfe3d
rounded deadlines up as well (Thread/sleep 1) took ~2.5ms. (Thread/sleep 0)
waited for the next clock tick, ~1.25ms, and (Thread/sleep ms nanos) dropped
the nanos, so (Thread/sleep 0 100000) was the same 1.25ms where 0.8.20
returned at once. ebb's sample stress test leaned on that timing and started
timing out on CI.

Timeouts are now measured from the precise monotonic clock, and a sleep's
deadline is an exact rational millisecond compared at full precision (whole
deadlines compare exactly as before). sleep(0) checks the interrupt flag and
yields, as the JDK does. TimeUnit.sleep passes sub-millisecond amounts through
instead of truncating them to zero.
Thread/sleep took a negative millis or out-of-range nanos as a zero sleep
where the JDK throws IllegalArgumentException. TimeUnit.sleep(0) went through
the new zero-length path and threw on an interrupted thread; the JDK's guard
skips the sleep entirely for a timeout <= 0.
10b failed on the Android runner with an absolute 0.5ms ceiling on a yield.
Each row now compares two things measured interleaved, so load stretches
both sides.
mono-deadline rounds now up so a wait is never early, which also made a zero
timeout wait for the next millisecond tick: (.poll q 0 ms) and (deref p 0 nil)
cost ~1.25ms a call.
@yogthos
yogthos merged commit b2dd6ab into main Oct 14, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant