Skip to content

sleep: measure the remaining time against a clock, not the kernel's remainder - #642

Open
cyclistmass wants to merge 1 commit into
Clozure:masterfrom
cyclistmass:sleep-clock-remainder
Open

cyclistmass wants to merge 1 commit into
Clozure:masterfrom
cyclistmass:sleep-clock-remainder

Conversation

@cyclistmass

Copy link
Copy Markdown
Contributor

(sleep n) overruns when another thread allocates, and the error grows with the collection rate. This is issue #639.

%nanosleep sleeps again for the remaining time the kernel writes back on EINTR. The kernel computes that remainder before it runs the signal handler, and suspend_resume_handler blocks inside the handler until the collection finishes, so the time the thread spends parked is never subtracted. Every world stop adds its own duration to the sleep.

There is one definition, under #-windows-target, with no per-port override, so every non-Windows target has this. Three callers reach it: sleep, the periodic-task sleep in housekeeping-loop, and process-wait once per poll tick.

The patch works the deadline out once, from a clock the Lisp reads itself, then sleeps again for the remaining time it measures on each EINTR. It never reads the kernel write-back, so #_nanosleep now gets a NULL rem, and the Darwin negative-remainder workaround goes with the code it guarded.

That gives one loop for every target. The clock is CLOCK_MONOTONIC through #_clock_gettime, which lib/time.lisp already reads the same way, and gettimeofday on Darwin, a kernel import that needs no interface-database entry. I avoided clock_nanosleep with TIMER_ABSTIME because Darwin does not have it, which would mean a second loop with a different error convention for one target.

Measured

Red then green on both architectures at 6526e21c, changing only this patch, with extended-tests/threads/sleep-vs-alloc.lisp from Clozure/ccl-tests#10.

arch without the patch with it
linuxarm64 170.1 s for a 10 s sleep, ratio 17.0 10.0 s, ratio 1.0
linuxx8664 31.8 s, ratio 3.2 10.0 s, ratio 1.0

On arm64 the kernel md5 is identical in both halves, as it should be, since the patch touches no kernel file. Both machines are 2-vCPU burstable instances, so read those as verdicts and not as benchmark figures.

ANSI reports 21679/0 and ccl-tests 243/0 with the patch applied.

I have not tested Darwin. There is no Darwin build here, so the gettimeofday arm rests on construction rather than measurement.

…emainder

(sleep n) returns late, and on a machine that collects often enough it does
not return at all, when another thread allocates.  This is issue Clozure#639.

%nanosleep calls #_nanosleep and, when a signal interrupts the call, sleeps
again for the remaining time the kernel wrote back.  Each GC suspends the
sleeping thread with a signal, and suspend_resume_handler blocks inside the
handler until the collection is over.  The kernel computes the remaining time
before it runs the handler, so the time the thread spends blocked in the
handler is not subtracted.  Every world stop adds its own duration to the
sleep.  The error grows with the collection rate.  On two 2-vCPU burstable
instances a 10 s sleep took 31.8 s on linuxx8664 and 170.1 s on linuxarm64;
those are the red halves of the measurement below.  Earlier runs on the same
arm64 cell were killed at 90 s without returning, so the overrun has no bound
that I have established.

The fix computes the deadline once, from a clock the Lisp reads itself, and
on each EINTR sleeps again for (deadline - now).  The kernel's write-back is
no longer read at all, so #_nanosleep now gets a NULL rem pointer.

The clock is CLOCK_MONOTONIC through #_clock_gettime where the target has it,
which is every non-Darwin POSIX target; lib/time.lisp already reads that
clock the same way for current-time-in-nanoseconds.  On Darwin the deadline
comes from gettimeofday, which is a kernel import and needs no entry in the
interface database.  clock_nanosleep with TIMER_ABSTIME was considered and
not used: Darwin does not have it, so it would need a second loop with a
different error convention for one target.  This form is one loop for every
target.

The Darwin-only check for a negative, zero-extended remainder goes with the
code it guarded: that value is not consulted any more.

Cost: one clock_gettime per %nanosleep call and one more per interruption.
process-wait polls through %nanosleep once per tick, so this is noise there.

Measured red then green with ccl-tests extended-tests/threads/sleep-vs-alloc.lisp
on both targets; see the pull request for the cell.
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