Skip to content

Let GCs run while the watchdog sleeps (Julia 1.12+) - #123

Merged
davidanthoff merged 1 commit into
mainfrom
watchdog-gc-safe-sleep
Sep 25, 2026
Merged

davidanthoff merged 1 commit into
mainfrom
watchdog-gc-safe-sleep

Conversation

@davidanthoff

Copy link
Copy Markdown
Member

Summary

On Julia 1.12 and later, the test server's watchdog thread now sleeps in a GC-safe region. Stop-the-world collections in a test process no longer have to wait for it to wake up.

Why

_watchdog_loop sleeps with Libc.systemsleep(0.05), which is a plain ccall. The thread therefore stays GC-unsafe for the whole sleep. Every collection in the test process, including the frequent young ones during an item, waits until the watchdog wakes and reaches its GC.safepoint().

  • How much: 25–50 ms per GC on Windows and 50–190 ms per GC on GitHub macOS runners.
  • Why nobody saw it: none of this time shows up in the GC time reported for an item, because total_time excludes time-to-safepoint.

Changes

  • New _watchdog_sleep(seconds):
    • Julia ≥ 1.12: @ccall gc_safe=true to usleep on Unix and to libuv's uv_sleep on Windows.
    • Older Julia: Libc.systemsleep, unchanged. @static ensures older versions never expand the new form. The file loads and sleeps correctly on Julia 1.0 through 1.13.
  • Why uv_sleep on Windows: @ccall can't express stdcall, and calling Win32 Sleep as cdecl would corrupt the stack on 32-bit Windows. uv_sleep is a cdecl function exported by the Julia runtime, and on Windows it is exactly Sleep(msec). It doesn't touch the event loop.
  • The GC.safepoint() in the loop stays, because older Julia still depends on it. Comments are updated.
  • CHANGELOG entry under Fixed.

Measurements

Base.gc_num().total_time_to_safepoint per GC.gc(true), averaged over 5 collections. Setup: --threads=1,2, a 200 MB heap, the real watchdog.jl loop running.

Julia Before After
1.11.9 x64 23.8 ms unchanged (fallback path)
1.12.7 x64 22.3 ms 0.0 ms
1.13.0 x64 23.7 ms 0.0 ms
1.12.7 x86 (32-bit Windows) 23.4 ms 0.0 ms

The Unix usleep path was not run locally, since I only have Windows. It's the same call Libc.systemsleep makes, with gc_safe=true added, and CI runs it on Linux and macOS.

Testing

test_worker_lifecycle.jl, test_timeout.jl and test_shutdown.jl pass on Julia 1.12 (89/89). They include "GC between test items does not deadlock the test process" and the hang-diagnostics timeout item. The Julia 1.0/1.6/1.11/1.12 platform items also pass.

Related

Found during the arm64 GC investigation, alongside #121 and #122.

🤖 Generated with Claude Code

The watchdog thread polled with `Libc.systemsleep`, a plain `ccall`, so it
stayed GC-unsafe for the whole 50 ms sleep and every stop-the-world GC in the
test process waited for it to wake up: 25-50 ms of extra time-to-safepoint
per GC on Windows, 50-190 ms on GitHub macOS runners, none of it counted in
the GC time reported for test items.

`_watchdog_loop` now sleeps through `_watchdog_sleep`, which on Julia >= 1.12
uses `@ccall gc_safe=true`: `usleep` on Unix and libuv's `uv_sleep` on Windows.
`@ccall` cannot express `stdcall`, which Win32 `Sleep` needs on 32-bit
Windows; `uv_sleep` is a cdecl wrapper around `Sleep` exported by the Julia
runtime. Older Julia keeps `Libc.systemsleep`, behind `@static` so the new
macro form is never expanded there. The explicit `GC.safepoint()` stays.

Measured with a 200 MB heap and 5x GC.gc(true): time-to-safepoint per GC
drops from ~23 ms mean to 0 on 1.12.7 and 1.13.0 (x64) and on 1.12.7 (x86);
1.11 is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@davidanthoff
davidanthoff merged commit 84006d1 into main Sep 25, 2026
37 of 40 checks passed
@davidanthoff
davidanthoff deleted the watchdog-gc-safe-sleep branch September 25, 2026 15:17
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