GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Fix timers firing up to 1ms early by pctablet505 · Pull Request #755 · MagicStack/uvloop

Conversation

@pctablet505

@pctablet505 pctablet505 commented Jul 16, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Fixes #739

Under uvloop, asyncio.sleep(n) can return up to ~1 ms before n has actually elapsed. This change pads the timeout passed to libuv by 1 ms, so timers never fire early.

Problem

libuv refreshes loop->time as the monotonic clock floored to whole milliseconds, and uv_timer_start() schedules the callback at loop->time + timeout. Whatever fraction of a millisecond had already passed when the clock was sampled is lost, so the timer comes due early in real time. The issue shows asyncio.sleep(0.1) returning after 0.0991 s.

Example with a 100 ms timer started at real time 1000.9 ms:

loop->time at start due at real time slept
before 1000 (floored) 1000 + 100 = 1100 99.1 ms ❌
after 1000 (floored) 1000 + 100 + 1 = 1101 100.1 ms ✅

Fix

Only the value handed to libuv changes (self.timeout + 1). self.timeout, and therefore TimerHandle.when() / get_when(), still reports the requested deadline. The trade-off is that a timer can fire up to ~1 ms later than before, which is within libuv's 1 ms resolution.

This is a different truncation from #233, which was fixed by rounding the float-to-ms conversion in call_at(). That path is unchanged.

Tests

test_call_later_not_early runs asyncio.sleep(0.01) 300 times and asserts that each one lasts at least the delay, measured with time.monotonic(). The existing test_call_later_rounding (#233) can't catch this, because it measures with the loop's own millisecond-floored clock.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@pctablet505
pctablet505 marked this pull request as ready for review July 17, 2026 12:47
@HyunsikParker HyunsikParker mentioned this pull request Sep 1, 2026
Preserve file descriptor blocking mode in add_reader and add_writer #761
Open
@pctablet505
Fix timers firing up to 1ms early
f2b3a0d
libuv resamples its internal clock as the floor of the current
monotonic time truncated to whole milliseconds, then schedules a
timer for loop->time + timeout. Since the fractional millisecond
already elapsed at the moment of sampling is discarded, the timer can
fire up to ~1ms before the requested delay has actually passed. This
made asyncio.sleep(n) occasionally return slightly under n, as
reported in MagicStack#739.

Pad the timeout handed to uv_timer_start() by 1ms to compensate, while
leaving the reported deadline (get_when()/TimerHandle.when()) as the
originally requested value.
@pctablet505
pctablet505 force-pushed the fix-early-timer-firing branch from 66f1983 to f2b3a0d Compare October 8, 2026 11:35
@pctablet505
Merge master into fix-early-timer-firing
2fce0a7
dex4er added a commit to pyjoyment/Pyjo-Reactor-Uvloop that referenced this pull request Nov 5, 2026
@dex4er @claude
Pyjo.Reactor.Uvloop: a timer not due before its time, and event_loop_…
b164de4
…modules of asyncio

libuv counts a timer in whole milliseconds of a clock that it rounds down,
so a timer of uvloop was due up to a millisecond early by time.monotonic,
which failed the tests of Pyjo.Steps of Pyjoyment now and then.
_Loop.call_later rounds the delay up, as MagicStack/uvloop#755 does.

event_loop_modules stays the one of Pyjo.Reactor.Asyncio: the loop of
uvloop makes no frames of Python below a callback (#7).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Nov 5, 2026
uv.lock: Pyjoyment of Git main at b6d623e, and a timer of uvloop not due before its time pyjoyment/Pyjo-Reactor-Uvloop#8
Merged
Tests: a sleep of Pyjo.Steps on an event loop of libuv, whose timer may be due a millisecond early pyjoyment/Pyjoyment#553
Merged
dex4er added a commit to pyjoyment/Pyjo-Reactor-Winloop that referenced this pull request Nov 5, 2026
@dex4er @claude
Pyjo.Reactor.Winloop: a timer not due before its time
1751a93
libuv counts a timer in whole milliseconds from uv_now, its clock
rounded down, and Winloop rounds the delay to them, as uvloop does, so a
timer, as asyncio.sleep, was due up to a millisecond before its time,
where the loop of asyncio is never early: a sleep of 2 ms took 1.5 ms.
call_later of _Loop, which call_at of Winloop calls too, rounds the
delay up to a millisecond and adds the one that the clock loses, as
_Loop of Pyjo.Reactor.Uvloop does, after MagicStack/uvloop#755.

The subtest of Pyjo-Reactor-Uvloop measures with time.perf_counter,
which time.monotonic is on Windows since Python 3.13, and which counted
in the ticks of the system of 15.6 ms before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@dex4er dex4er mentioned this pull request Nov 5, 2026
Pyjo.Reactor.Winloop: a socket that loops watch, polled by one at a time, and a timer not due before its time pyjoyment/Pyjo-Reactor-Winloop#6
Merged
4 of 5 tasks
dex4er added a commit to pyjoyment/Pyjo-Reactor-Winloop that referenced this pull request Nov 5, 2026
@dex4er @claude
Pyjo.Reactor.Winloop: a socket that loops watch, polled by one at a t…
09c7d51
…ime, and a timer not due before its time (#6)

* Pyjo.Reactor.Winloop: a socket that loops watch, polled by one at a time

libuv of Windows polls a socket with an exclusive request of AFD, which
ends the request of any other loop for the same socket, also of a copy
of it, and that loop polls it again, in a busy loop of C with no event
at all. The workers of Pyjo.Server.Prethread watch the listen sockets of
the manager on Windows, the same sockets in every thread: four idle
loops took 240% of the processors and starved the process and the tests
next to it, with or without the GIL.

_Loop keeps the readers and the writers of each socket, and a registry
of the process has the loops that watch it, the owner that libuv polls
it for, and the loops that run and wait for it. The owner hands it over
to the next one after each of its events, so that the connections go to
every worker in turn, and at the end of its run; a loop changes the
polls of libuv only in its thread. A socket that one loop watches goes
to libuv as before.

Closes #5

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* uv.lock: Pyjoyment of Git main at 1fc984a

Pyjoyment of Git main with pyjoyment/Pyjoyment#550 and
pyjoyment/Pyjoyment#552, the tests of a page of GitHub without its title
in the tutorial and of the servers of Hot and Reload that did not answer
or did not stop.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* uv.lock: Pyjoyment of Git main at 7b09754

Pyjoyment of Git main with pyjoyment/Pyjoyment#553, a sleep of
Pyjo.Steps in its tests on an event loop of libuv, whose timer may be
due a millisecond early, and pyjoyment/Pyjoyment#554, the data of a
connection that the daemon has removed discarded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Pyjo.Reactor.Winloop: a timer not due before its time

libuv counts a timer in whole milliseconds from uv_now, its clock
rounded down, and Winloop rounds the delay to them, as uvloop does, so a
timer, as asyncio.sleep, was due up to a millisecond before its time,
where the loop of asyncio is never early: a sleep of 2 ms took 1.5 ms.
call_later of _Loop, which call_at of Winloop calls too, rounds the
delay up to a millisecond and adds the one that the clock loses, as
_Loop of Pyjo.Reactor.Uvloop does, after MagicStack/uvloop#755.

The subtest of Pyjo-Reactor-Uvloop measures with time.perf_counter,
which time.monotonic is on Windows since Python 3.13, and which counted
in the ticks of the system of 15.6 ms before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

uvloop may sleep less than n for asyncio.sleep(n)

Uh oh!

There was an error while loading. Please reload this page.

1 participant

@pctablet505
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.