Skip to content

Windows: host timed waits end on 15.6 ms GetTickCount64 steps despite the 0.5 ms tick, and sleep_for under 1 ms does not sleep #1471

Open

Description

@popcoder565

On Windows, the std:: timed waits (condition_variable::wait_for and wait_until, this_thread::sleep_for and sleep_until) go through winpthreads. winpthreads ends a timed wait only once GetTickCount64() has advanced by the timeout. GetTickCount64 moves in 15.6 ms steps whatever the timer resolution, so the 0.5 ms tick Time.cpp sets at load does not shorten these waits.

Measured

Windows 10 (19045), MinGW-w64 GCC 15.2.0 (WinLibs r7), with the tick raised as Time.cpp does (NtSetTimerResolution(5000)), 40 calls each:

Call Median
GetTickCount64 step 15.5 ms
condition_variable::wait_for(1 ms) 15.5 ms
this_thread::sleep_for(1 ms) 15.6 ms
this_thread::sleep_for(2 ms) 15.5 ms
condition_variable::wait_until(now + 16.68 ms) returns 14.5 ms after the deadline (12.6 to 16.2)
this_thread::sleep_until(now + 16.68 ms) 30.4 ms
this_thread::sleep_for(50 µs) 0.000 ms
Sleep(1), WaitForSingleObject(event, 1), SleepConditionVariableSRW(1) 1.1 to 1.2 ms
High-resolution waitable timer, 1 ms 1.5 ms

Why

In mingw-w64's winpthreads/src/misc.c, _pthread_wait_for_single_object calls WaitForSingleObject again until GetTickCount64() reaches start + timeout. pthread_cond_timedwait (cond.c) and nanosleep (through pthread_delay_np_ms) wait through it, and libstdc++'s condition_variable and this_thread::sleep_* call those. nanosleep also converts the request to whole milliseconds (tv_nsec / POW10_6). A request under 1 ms therefore returns without waiting or yielding.

The comment above g_timerResolutionRaised in Time.cpp lists winpthreads nanosleep and std::condition_variable::wait_for among the waits the 0.5 ms tick fixes. The tick does fix Sleep and WaitFor*Object, but not these two. Guest waits are not affected, because they go through TimedWait and its high-resolution timers. PollSleep in WaitMemory.cpp and PreciseSleepUs in libc/include/PreciseWait.hpp also use high-resolution timers.

Host waits that still go through winpthreads

Each of these timed waits can end up to about 15.6 ms late on Windows:

These polls use a sleep_for under 1 ms, which returns at once, so they spin without yielding:

  • Pm4.cpp:814, WAIT_REG_MEM (50 µs)
  • Apr.cpp:588 (50 µs)
  • Aio.cpp:280 and :305 (10 µs)
  • libSceFont State.cpp:59 (30 µs)

I can send PRs for these areas separately.

Activity

  1. klosixgaming-arch commented on Oct 8, 2026

    @klosixgaming-arch
    Contributor

    This issue is related to several recent PRs I've reviewed that address Windows timing issues:

    The comprehensive analysis of winpthreads' use of GetTickCount64() is valuable. The measured data showing 15.6 ms steps despite the 0.5 ms tick resolution clearly demonstrates that the tick setting doesn't affect these waits.

    Based on my review of related PRs, I'd suggest:

    1. The approach of using high-resolution waitable timers for all timed waits (as done in TimedWait and PreciseSleepUs) is the correct long-term solution
    2. The list of host waits that still go through winpthreads is comprehensive and should be addressed systematically
    3. The spinning issue with sub-1ms sleeps is particularly problematic and should be prioritized

    This issue provides excellent documentation for anyone working on Windows timing improvements in the future.

  2. popcoder565 commented on Oct 9, 2026

    @popcoder565
    ContributorAuthor

    Status of the affected call sites, for anyone picking this up (main 6cc0503):

    🤖 Addressed by Claude Code

  3. adriadam10 commented on Oct 10, 2026

    @adriadam10
    Collaborator

    Fixed in part by #1472 (vblank pacing), #1474 (AGC completion poll), #1486 (blocking audio calls) and #1588 (empty sceNetEpollWait) with a high-resolution timer; still open: the other std:: timed waits and sub-ms sleeps (#1576, #1655, #1780 open; select and poll without sockets).

  4. justdoston commented on Oct 10, 2026

    @justdoston
    Contributor

    I'd like to take the remaining select/poll gap from @popcoder565's list: with no sockets they sleep through std::this_thread::sleep_for in whole milliseconds (select_nid_postfix in Socket.cpp, poll in Udp.cpp). #1706 and #1832, which a fix was going to follow, are both closed now. I'll post the branch here when it's ready.

    AI-assisted: yes (Claude Code).

  5. MarcZX10 commented on Oct 10, 2026

    @MarcZX10
    Contributor

    Status update (2026-10-10)

    Since the previous status note, #1551 has merged as commit 695e0b9. It replaces the sub-millisecond waits at the AGC WAIT_REG_MEM, APR wait, and Font lock backoff call sites with PreciseSleepUs, with a focused helper test. The ineffective AIO substitutions were removed after the Windows measurements in #1576.

    Remaining item from this issue Current status
    VideoOut flip wait PR #1655 is open
    AvPlayer EOF poll PR #1780 is open
    select/poll when no descriptor is a socket No open PR found; justdoston said they are investigating this gap in the latest issue comment
    sleep_for below 1 ms in the AGC/APR/Font call sites Fixed by merged #1551

    PRs #1486 and #1588 have also merged. This updates the earlier status comment, which still listed #1551 as open.

  6. justdoston commented on Oct 10, 2026

    @justdoston
    Contributor

    The select/poll part is ready: https://github.com/justdoston/AnyPS5/tree/fix/windows-select-poll-precise-sleep

    • select with no sockets sleeps with PreciseSleepUntil to its deadline (at most 100 ms per round, as before) instead of sleep_for in whole milliseconds, so it no longer rounds the timeout up to a millisecond either.
    • poll with no sockets sleeps with PreciseSleepUs.

    Both use the PreciseWait.hpp helpers from #1486 and #1588; on Linux they still sleep through std::this_thread::sleep_for.

    guest_poll, which also runs on Windows, now times 20 empty 1 ms poll calls and 20 empty 1 ms select calls: each set must take at least 20 ms and less than 200 ms, where the 15.6 ms steps measured above take about 310 ms.

    Tested: Linux full ctest (676 of 683; the 7 failures are agc_* GPU tests specific to this machine's Vulkan driver, none in sockets), with guest_poll passing. Not run on Windows: the files cross-compile with MinGW-w64 GCC 16, and under Wine 10.0 the new waits take about 1.2 ms per 1 ms call. Wine's GetTickCount64 doesn't move in 15.6 ms steps, so it can't show the old behaviour; the before/after rests on the Windows 10 numbers in this issue.

    AI-assisted: yes. An AI agent (Claude Code) did most of the work: the code and the test

  7. popcoder565 commented on Oct 10, 2026

    @popcoder565
    ContributorAuthor

    @justdoston I checked your branch fix/windows-select-poll-precise-sleep (fea3e40e) on Windows. Setup: Windows 10 (19045), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release, merged onto main ca34953.

    • With your Socket.cpp and Udp.cpp changes, guest_poll (including the new 20 × 1 ms poll and select timing) passes 3 of 3.
    • With main's Socket.cpp and Udp.cpp put back under the same test, it aborts in 3 of 3 runs (0xc0000409), so the new timing check catches the 15.6 ms steps.
    • Full ctest in that build: 719 of 719 in two of three runs. The one failure in the other run was unrelated (agc_driver, once under -j 8; reported on fix(agc): harden host import construction and mutation waits #2779, which was in the same build).

    🤖 Addressed by Claude Code

  8. MarcZX10 commented on Oct 17, 2026

    @MarcZX10
    Contributor

    I checked Doston's offered branch against current main (98c251fc6). This follow-up is already covered, so I did not open a duplicate PR:

    Current main contains these paths and tests. The offered branch is based on an older main and its GuestPoll.cpp would also revert tests added since then, so it should not be carried as-is. Issue #1471 still tracks other timed-wait call sites.

  9. MarcZX10 commented on Oct 19, 2026

    @MarcZX10
    Contributor

    A remaining guest-visible path is the Dinkumware C++ condition wait in libc/src/Threads.cpp: _Cnd_timedwait_nid_postfix uses std::condition_variable::wait_until, which reaches the winpthreads timeout path described above. The existing focused test checked only that the wait does not return early, so it did not catch the reported late timeout.

    I opened PR #7735 for this libc path. On Windows it uses the native condition variable with CRITICAL_SECTION; POSIX keeps the current implementation. The added Windows regression measures repeated 1 ms waits. The PR covers this call path only and does not close the broader issue.

  10. adriadam10 commented on Oct 20, 2026

    @adriadam10
    Collaborator

    #7735 moves the Dinkumware _Cnd_wait/_Cnd_timedwait path to native Windows condition variables, covered by guest_libc_cnd_timed_wait; the other std:: timed waits are still open.

  11. hyvvs commented on Oct 21, 2026

    @hyvvs
    Contributor

    I'm investigating another Windows timing callsite in libSceUsbd::sceUsbdHandleEventsTimeout. When the USB context is absent, it uses std::this_thread::sleep_for, which appears to have the timing behavior described here.

    I have a local patch using the existing PreciseSleepUs helper and a regression test. Preliminary Windows timing measurements and C++ syntax checks look good, but I still need to run the full library tests.

    I currently have #8729 open, so I can't submit a second PR yet under the contributor limit. Posting here to coordinate and avoid duplicate work.

  12. LeandroLP23 commented on Oct 26, 2026

    @LeandroLP23
    Contributor

    Picking up two remaining guest-visible paths, to avoid duplicate work:

    • libkernel_sys/Aio.cpp: the two timed waits (aio.changed.wait_for(lock, *wait)) for the AIO suspend/wait-complete calls. Besides the 15.6 ms steps, the timeout restarts on every wake inside the loop instead of being one deadline.
    • libSceZlib/Export.cpp: sceZlibWaitForDone's timed completion.wait_for.

    Both would move to TimedWait::Condition with a deadline from TimedWait::DeadlineNanos, as Rudp, AvPlayer and EventFlag already do, one PR per library with a repeated short-timeout timing check. I'm leaving out Signals.cpp (#11167 covers sigtimedwait; the alarm wait in the same file can follow it) and libSceUsbd (@hyvvs's, and #9339).

    AI-assisted: yes (Claude Code)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions