[Git][ghc/ghc][wip/io-manager-deadlock-detection] Add idle GC mode for !HAVE_PREEMPTION
Duncan Coutts pushed to branch wip/io-manager-deadlock-detection at Glasgow Haskell Compiler / GHC Commits: e3f31b36 by Duncan Coutts at 2026-09-04T10:51:13+01:00 Add idle GC mode for !HAVE_PREEMPTION We have to do idle GC (and thus deadlock detection) differently when we do not have the ticker and the ability to interrupt the I/O manager when it's blocking. - - - - - 6 changed files: - rts/IdleGC.c - rts/IdleGC.h - rts/Timer.c - rts/posix/Poll.c - rts/posix/Timeout.c - rts/posix/Timeout.h Changes: ===================================== rts/IdleGC.c ===================================== @@ -55,6 +55,8 @@ automatic GCs. Both timers must expire before an automatic GC is triggered. See issue #11134 for additional detail. + + See also Note [Deadlock detection]. */ @@ -113,6 +115,8 @@ bool isIdleGcPending(void) return (getRecentActivity() == ACTIVITY_INACTIVE); } +#if defined(THREADED_IDLEGC) + /* - countdown for minimum idle time before we start a GC (set by -I) */ static int idle_ticks_to_gc = 0; @@ -170,6 +174,47 @@ void handleIdleGcTick(void) } } +#else // !defined(THREADED_IDLEGC) + +/* Note [Idle GC without preemption] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +On non-threaded platforms without pre-emption, e.g. wasm, we have to use a +different design for scheduling idle GC (and thus deadlock detection, see +Note [Deadlock detection]). + +Instead of a ticker thread that interrupts the idle capability after some delay, +we arrange for the I/O manager to only sleep for a maximum of the idle GC delay +time. The function getNextIdleGcDelayTime() returns this time, accounting for +the interIdleGCWait. + +Then if that timeout is reached the I/O manager calls notifyIdleGcIdle(), +which schedules an idle GC (by seting the recent activity state). The I/O +manager must then behave as if it had been interrupted (as happens with the +threaded idle GC design). +*/ + +static Time last_idle_gc_time = 0; + +Time getNextIdleGcDelayTime() +{ + Time now = getProcessElapsedTime(); + Time delay = + stg_max (last_idle_gc_time + RtsFlags.GcFlags.interIdleGCWait - now, + RtsFlags.GcFlags.idleGCDelayTime); + ASSERT(delay >= 0); + return delay; +} + +void notifyIdleGcIdle(bool deadlocked) +{ + if (RtsFlags.GcFlags.doIdleGC || deadlocked) { + setRecentActivity(ACTIVITY_INACTIVE); + } +} + +#endif // defined(THREADED_IDLEGC) + void notifyIdleGcActive(void) { switch (getRecentActivity()) @@ -209,6 +254,9 @@ void notifyIdleGcDone(bool force_major) setRecentActivity(ACTIVITY_DONE_GC); #if !defined(PROFILING) pauseTimer(); +#endif +#if !defined(THREADED_IDLEGC) + last_idle_gc_time = getProcessElapsedTime(); #endif break; } ===================================== rts/IdleGC.h ===================================== @@ -10,6 +10,15 @@ #include "BeginPrivate.h" +/* See Note [GC During Idle Time] */ + +#if defined(HAVE_PREEMPTION) +#define THREADED_IDLEGC +#else +/* See Note [Idle GC without preemption] */ +#endif + + /* Are we in a state where we are want an idle GC to occur? * Used in Capability globalWorkToDo() and Schedule scheduleDetectDeadlock. */ @@ -21,7 +30,19 @@ void notifyIdleGcActive(void); /* Called from schedule(), specifically scheduleDoGC() */ void notifyIdleGcDone(bool force_major); +#if defined(THREADED_IDLEGC) + /* Called from handle_tick() */ void handleIdleGcTick(void); +#else // !defined(THREADED_IDLEGC) + +/* Called from I/O managers before waiting */ +Time getNextIdleGcDelayTime(void); + +/* Called from I/O managers after waiting */ +void notifyIdleGcIdle(bool deadlocked); + +#endif + #include "EndPrivate.h" ===================================== rts/Timer.c ===================================== @@ -68,7 +68,9 @@ handle_tick(int unused STG_UNUSED) } #endif +#if defined(THREADED_IDLEGC) handleIdleGcTick(); +#endif } void initTimer(void) ===================================== rts/posix/Poll.c ===================================== @@ -22,12 +22,18 @@ #include "rts/Time.h" #include "RaiseAsync.h" #include "Trace.h" +#include "IdleGC.h" #include "Poll.h" #include "RtsSignals.h" #include <limits.h> #include <errno.h> + +#if !defined(THREADED_IDLEGC) +#undef HAVE_DECL_PPOLL // for simplicity, always use poll when !THREADED_IDLEGC +#endif + #if defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1 /* We prefer the ppoll() function if available since it allows more precise * timeouts. It is not in the posix standard however and some platforms @@ -505,6 +511,17 @@ bool awaitCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) int timeout_ms = timeoutInMilliseconds(iomgr, wait, now); #endif +#if !defined(THREADED_IDLEGC) + /* Without threaded idle GC, poll() does not get interrupted for an + * idle GC. Instead we just limit our wait time to the idle gc delay + * time, and if that timeout is reached then we schedule an idle GC + * (see handleTimeoutForIdleGc below). + */ + bool any_pending_io = nfds > 0; + int idlegc_status; + adjustTimeoutForIdleGc(any_pending_io, &timeout_ms, &idlegc_status); +#endif + /* Check for I/O readiness, possibly waiting. */ #if defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1 int res = ppoll(poll_table, nfds, timeout_ns, NULL); @@ -533,6 +550,9 @@ bool awaitCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) #else ASSERT(timeout_ms != -1); #endif +#if !defined(THREADED_IDLEGC) + handleIdleGcTimeout(idlegc_status, &interrupt); +#endif } else if (res > 0) { int ncompletions = res; ===================================== rts/posix/Timeout.c ===================================== @@ -17,6 +17,7 @@ #include "Timeout.h" #include "IOManagerInternals.h" #include "TimeoutQueue.h" +#include "IdleGC.h" #include <limits.h> @@ -135,7 +136,8 @@ static void notifyTimeoutCompletion(CapIOManager *iomgr, StgTimeout *timeout) /* poll() expect a timeout in milliseconds, with special * values of -1 for indefinite wait, and 0 for no waiting. */ -#if !(defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1) +#if !(defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1) \ + || !defined(THREADED_IDLEGC) int timeoutInMilliseconds(CapIOManager *iomgr, bool wait, Time now) { if (!wait) { @@ -220,5 +222,56 @@ struct timespec *timeoutInNanoseconds(CapIOManager *iomgr, bool wait, } #endif +#if !defined(THREADED_IDLEGC) +enum { + timeout_is_idlegc_delay = 1 << 0, + system_must_be_deadlocked = 1 << 1 +}; + +/* Utilities for handling the non-threaded idle GC variation. + * + * See Note [Idle GC without preemption] + * + * Before calling poll()/select() etc, adjust the timeout to account for the + * idle GC delay time. + * + * If the timeout occurs, call handleIdleGcTimeout, which will schedule an idle + * GC if the timeout was reached. + * + * The idlegc_status must be passed from adjustTimeoutForIdleGc to + * handleIdleGcTimeout. + */ +void adjustTimeoutForIdleGc(bool any_pending_io, + int *timeout_ms /* in/out */, + int *idlegc_status /* out */) +{ + int idlegc_delay_ms = TimeToMS(getNextIdleGcDelayTime()); + + if (RTS_UNLIKELY(!any_pending_io && *timeout_ms == -1)) { + /* We must be deadlocked. Schedule an idle GC unconditionally. */ + *timeout_ms = 0; /* Cause poll() to return res == 0 immediately. */ + *idlegc_status = timeout_is_idlegc_delay + & system_must_be_deadlocked; + + } else if (RTS_UNLIKELY(RtsFlags.GcFlags.doIdleGC && + (idlegc_delay_ms < *timeout_ms || *timeout_ms == -1))) { + /* The idle GC delay will be the next timeout. */ + *timeout_ms = idlegc_delay_ms; + *idlegc_status = timeout_is_idlegc_delay; + + } else { + *idlegc_status = 0; + } +} + +void handleIdleGcTimeout(int idlegc_status, bool *interrupt) +{ + if (idlegc_status & timeout_is_idlegc_delay) { + notifyIdleGcIdle(idlegc_status & system_must_be_deadlocked); + *interrupt = true; + } +} +#endif + #endif // defined(IOMGR_ENABLED_POLL) ===================================== rts/posix/Timeout.h ===================================== @@ -33,7 +33,8 @@ void processTimeoutCompletions(CapIOManager *iomgr, Time now); * timeout in milliseconds, with special values of -1 for indefinite wait, * and 0 for no waiting. */ -#if !(defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1) +#if !(defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1) \ + || !defined(THREADED_IDLEGC) int timeoutInMilliseconds(CapIOManager *iomgr, bool wait, Time now); #endif @@ -46,5 +47,17 @@ struct timespec *timeoutInNanoseconds(CapIOManager *iomgr, bool wait, Time now, struct timespec *tv); #endif +#if !defined(THREADED_IDLEGC) +/* Utilities for handling the non-threaded idle GC variation. + * + * See Note [Idle GC without preemption] + */ +void adjustTimeoutForIdleGc(bool any_pending_io, + int *timeout_ms /* in/out */, + int *idlegc_status /* out */); + +void handleIdleGcTimeout(int idlegc_status, bool *interrupt); +#endif + #include "EndPrivate.h" View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/e3f31b3679ae1cc600189f1c99bea5ff... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/e3f31b3679ae1cc600189f1c99bea5ff... You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
participants (1)
-
Duncan Coutts (@dcoutts)