Duncan Coutts pushed to branch wip/io-manager-deadlock-detection at Glasgow Haskell Compiler / GHC Commits: 05b91257 by Duncan Coutts at 2026-09-02T09:31:58+01:00 Move idle GC tracking to its own file, step 1 of 2 Add a new IdleGC.{c,h} module. Move the RecentActivity type, recent_activity variable and {get,set}RecentActivity wrappers. - - - - - 2658aba3 by Duncan Coutts at 2026-09-02T09:40:58+01:00 Move idle GC functionality to its own file, step 2 of 2 We can now fully encapsulate the recent_activity state, exposing just three hooks, and one query used in the scheduler and timer tick. This is a prelude to making changes to the implementation of tracking when to perform an idle gc. - - - - - 5d11c6ff by Duncan Coutts at 2026-09-02T19:11:54+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. - - - - - 10 changed files: - rts/Capability.c - + rts/IdleGC.c - + rts/IdleGC.h - rts/Schedule.c - rts/Schedule.h - rts/Timer.c - rts/posix/Poll.c - rts/posix/Timeout.c - rts/posix/Timeout.h - rts/rts.cabal Changes: ===================================== rts/Capability.c ===================================== @@ -31,6 +31,7 @@ #include "sm/OSMem.h" #include "sm/BlockAlloc.h" // for countBlocks() #include "IOManager.h" +#include "IdleGC.h" #include <string.h> @@ -88,7 +89,7 @@ STATIC_INLINE bool globalWorkToDo (void) { return getSchedState() >= SCHED_INTERRUPTING - || getRecentActivity() == ACTIVITY_INACTIVE; // need to check for deadlock + || isIdleGcPending(); } #endif ===================================== rts/IdleGC.c ===================================== @@ -0,0 +1,279 @@ +/* ----------------------------------------------------------------------------- + * + * (c) The GHC Team, 1995-2005 + * + * Idle GC: tracking of when to perform a GC during idle time. + * + * ---------------------------------------------------------------------------*/ + +#include "rts/PosixSource.h" +#include "Rts.h" + +#include "IdleGC.h" +#include "Timer.h" +#include "Schedule.h" + +/* + Note [GC During Idle Time] + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + A major GC can be performed during idle time (i.e. when no Haskell threads + are ready to run). This can be beneficial for two reasons. First, running + the GC during idle time makes it less likely that a GC will be triggered when + the process is active, increasing apparent responsiveness. Second, these idle + time GCs allow finalizers to run, preventing resources from being held + indefinitely when the process is otherwise idle for extended periods. + + There are two runtime RTS options to control idle time GC timing. The primary + control is set by `-I<n>`, which specifies the minimum period of time the + process must be idle before a GC is automatically triggered. It defaults to + 0.3 seconds for the threaded runtime and 0 (which disables idle GC entirely) + for the non-threaded runtime. For certain workflows, the 0.3 second delay for + the threaded RTS may be too small. If an application must process an extended + burst of short-lived requests occurring a couple of times a second, it may go + idle for >0.3 seconds frequently, resulting in potentially dozens of major + GCs triggered every minute (and resulting heavy CPU load) while the burst + lasts. Setting the `-I<n>` value higher will prevent this flurry of major GCs, + though there is a danger that setting the value too high will prevent automatic + GCs entirely, if the process never gets a chance to go idle for long enough to + meet the larger threshold. + + In this case, the second control, set by `-Iw<n>` may be helpful. For example, + setting `-I0.3 -Iw30` triggers automatic GCs after only 0.3 seconds of idle + time, but subject to a minimum delay *between* automatic GCs of at least 30 + seconds. This is likely to work well for applications that must process a + nearly constant stream of frequent, short-lived requests, ensuring that + automatic GCs are triggered promptly when the process goes idle while limiting + the overall frequency of such GCs. (The default is `-Iw0`, meaning no limit on + frequency of GCs.) + + Automatic GC timing is implemented below using two count-down timers. The + `idle_ticks_to_gc` timer counts down the `-I<n>` setting: it is initialized + when the process goes idle and counts down idle time before an automatic GC + becomes possible. The `inter_gc_ticks_to_gc` counts down the `-Iw<n>` setting: + it is initialized when an automatic GC is actually performed, and holds off the + next automatic GC until its count expires, limiting the overall frequency of + automatic GCs. Both timers must expire before an automatic GC is triggered. + + See issue #11134 for additional detail. + + See also Note [Deadlock detection]. +*/ + + +/* Flag that tracks whether we have done any execution in this time + * slice, and controls the disabling of the interval timer. + * + * The timer interrupt transitions ACTIVITY_YES into + * ACTIVITY_MAYBE_NO, waits for RtsFlags.GcFlags.idleGCDelayTime, + * and then: + * - if idle GC is on, set ACTIVITY_INACTIVE and wakeUpRts() + * - if idle GC is off, set ACTIVITY_DONE_GC and stopTimer() + * + * If the scheduler finds ACTIVITY_INACTIVE, then it sets + * ACTIVITY_DONE_GC, performs the GC and calls stopTimer(). + * + * If the scheduler finds ACTIVITY_DONE_GC and it has a thread to run, + * it enables the timer again with startTimer(). + */ +enum RecentActivity { + // the RTS is active + ACTIVITY_YES = 0, + // no activity since the last timer signal + ACTIVITY_MAYBE_NO = 1, + // RtsFlags.GcFlags.idleGCDelayTime has passed with no activity + ACTIVITY_INACTIVE = 2, + // like ACTIVITY_INACTIVE, but we've done a GC too (if idle GC is + // enabled) and the interval timer is now turned off. + ACTIVITY_DONE_GC = 3, +}; + +/* Recent activity flag. + * + * Note we must always use atomic operations for it, even in the non-threaded + * runtime, since the timer tick runs in a separate thread. + * + * We don't need an initIdleGc. All it would do is set + * recent_activity = ACTIVITY_YES; which we can do statically. + */ +StgWord recent_activity = ACTIVITY_YES; + +static inline enum RecentActivity +setRecentActivity(enum RecentActivity new_value) +{ + StgWord old = SEQ_CST_XCHG_ALWAYS((StgPtr) &recent_activity, (StgWord) new_value); + return (enum RecentActivity) old; +} + +static inline enum RecentActivity +getRecentActivity(void) +{ + return (enum RecentActivity) RELAXED_LOAD_ALWAYS(&recent_activity); +} + +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; + +/* - countdown for minimum time *between* idle GCs (set by -Iw) */ +static int inter_gc_ticks_to_gc = 0; + +/* + * Called from handle_tick(). + */ +void handleIdleGcTick(void) +{ + /* + * If we've been inactive for idleGCDelayTime (set by +RTS + * -I), tell the scheduler to wake up and do a GC, to check + * for threads that are deadlocked. However, ensure we wait + * at least interIdleGCWait (+RTS -Iw) between idle GCs. + */ + switch (getRecentActivity()) { + case ACTIVITY_YES: + setRecentActivity(ACTIVITY_MAYBE_NO); + idle_ticks_to_gc = RtsFlags.GcFlags.idleGCDelayTime / + RtsFlags.MiscFlags.tickInterval; + break; + case ACTIVITY_MAYBE_NO: + if (idle_ticks_to_gc == 0 && inter_gc_ticks_to_gc == 0) { + if (RtsFlags.GcFlags.doIdleGC) { + setRecentActivity(ACTIVITY_INACTIVE); + inter_gc_ticks_to_gc = RtsFlags.GcFlags.interIdleGCWait / + RtsFlags.MiscFlags.tickInterval; + wakeUpRts(); + // The scheduler will call pauseTimer() when it has done + // the GC. + } else { + setRecentActivity(ACTIVITY_DONE_GC); + // disable timer signals (see #1623, #5991, #9105) + // but only if we're not profiling (e.g. passed -h or -p RTS + // flags). If we are profiling we need to keep the timer active + // so that samples continue to be collected. +#if defined(PROFILING) + if (!(RtsFlags.ProfFlags.doHeapProfile + || RtsFlags.CcFlags.doCostCentres)) { + pauseTimer(); + } +#else + pauseTimer(); +#endif + } + } else { + if (idle_ticks_to_gc) idle_ticks_to_gc--; + if (inter_gc_ticks_to_gc) inter_gc_ticks_to_gc--; + } + break; + default: + break; + } +} + +#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 = -TIME_MAX; + +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()) + { + case ACTIVITY_DONE_GC: { + // ACTIVITY_DONE_GC means we turned off the timer signal to + // conserve power (see #1623). Re-enable it here. + uint32_t prev; + prev = setRecentActivity(ACTIVITY_YES); + if (prev == ACTIVITY_DONE_GC) { +#if !defined(PROFILING) + unpauseTimer(); +#endif + } + break; + } + case ACTIVITY_INACTIVE: + // If we reached ACTIVITY_INACTIVE, then don't reset it until + // we've done the GC. The thread running here might just be + // the IO manager thread that handle_tick() woke up via + // wakeUpRts(). + break; + default: + setRecentActivity(ACTIVITY_YES); + } +} + +void notifyIdleGcDone(bool force_major) +{ + switch (getRecentActivity()) { + case ACTIVITY_INACTIVE: + if (force_major) { + // We are doing a GC because the system has been idle for a + // timeslice and we need to check for deadlock. Record the + // fact that we've done a GC and turn off the timer signal; + // it will get re-enabled if we run any threads after the GC. + setRecentActivity(ACTIVITY_DONE_GC); +#if !defined(PROFILING) + pauseTimer(); +#endif +#if !defined(THREADED_IDLEGC) + last_idle_gc_time = getProcessElapsedTime(); +#endif + break; + } + // fall through... + + case ACTIVITY_MAYBE_NO: + // the GC might have taken long enough for the timer to set + // recent_activity = ACTIVITY_MAYBE_NO or ACTIVITY_INACTIVE, + // but we aren't necessarily deadlocked: + setRecentActivity(ACTIVITY_YES); + break; + + case ACTIVITY_DONE_GC: + // If we are actually active, the scheduler will reset the + // recent_activity flag and re-enable the timer. + break; + case ACTIVITY_YES: + break; + } +} ===================================== rts/IdleGC.h ===================================== @@ -0,0 +1,48 @@ +/* ----------------------------------------------------------------------------- + * + * (c) The GHC Team, 1995-2005 + * + * Idle GC: tracking of when to perform a GC during idle time. + * + * Interacts closely with the scheduler and timer tick. + * + * ---------------------------------------------------------------------------*/ + +#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. + */ +bool isIdleGcPending(void); + +/* Called from schedule() */ +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/Schedule.c ===================================== @@ -42,6 +42,7 @@ #include "Threads.h" #include "Timer.h" #include "Ticker.h" +#include "IdleGC.h" #include "ThreadPaused.h" #include "ThreadLabels.h" #include "Messages.h" @@ -84,14 +85,6 @@ uint64_t allocated_bytes_at_heapoverflow = 0; */ bool heap_overflow = false; -/* flag that tracks whether we have done any execution in this time slice. - * LOCK: currently none, perhaps we should lock (but needs to be - * updated in the fast path of the scheduler). - * - * NB. must be StgWord, we do xchg() on it. - */ -StgWord recent_activity = ACTIVITY_YES; - /* if this flag is set as well, give up execution * LOCK: none (changes monotonically) */ @@ -437,29 +430,8 @@ run_thread: dirty_TSO(cap,t); dirty_STACK(cap,t->stackobj); - switch (getRecentActivity()) - { - case ACTIVITY_DONE_GC: { - // ACTIVITY_DONE_GC means we turned off the timer signal to - // conserve power (see #1623). Re-enable it here. - uint32_t prev; - prev = setRecentActivity(ACTIVITY_YES); - if (prev == ACTIVITY_DONE_GC) { -#if !defined(PROFILING) - unpauseTimer(); -#endif - } - break; - } - case ACTIVITY_INACTIVE: - // If we reached ACTIVITY_INACTIVE, then don't reset it until - // we've done the GC. The thread running here might just be - // the IO manager thread that handle_tick() woke up via - // wakeUpRts(). - break; - default: - setRecentActivity(ACTIVITY_YES); - } + // Let the idle gc tracker know that we're running a thread again + notifyIdleGcActive(); traceEventRunThread(cap, t); @@ -986,7 +958,7 @@ static void scheduleDetectDeadlock (Capability **pcap, Task *task) { /* See Note [Deadlock detection] */ - if (getRecentActivity() == ACTIVITY_INACTIVE) { + if (isIdleGcPending()) { debugTrace(DEBUG_sched, "maybe deadlocked, forcing major GC..."); @@ -1933,35 +1905,7 @@ delete_threads_and_gc: traceSparkCounters(cap); - switch (getRecentActivity()) { - case ACTIVITY_INACTIVE: - if (force_major) { - // We are doing a GC because the system has been idle for a - // timeslice and we need to check for deadlock. Record the - // fact that we've done a GC and turn off the timer signal; - // it will get re-enabled if we run any threads after the GC. - setRecentActivity(ACTIVITY_DONE_GC); -#if !defined(PROFILING) - pauseTimer(); -#endif - break; - } - // fall through... - - case ACTIVITY_MAYBE_NO: - // the GC might have taken long enough for the timer to set - // recent_activity = ACTIVITY_MAYBE_NO or ACTIVITY_INACTIVE, - // but we aren't necessarily deadlocked: - setRecentActivity(ACTIVITY_YES); - break; - - case ACTIVITY_DONE_GC: - // If we are actually active, the scheduler will reset the - // recent_activity flag and re-enable the timer. - break; - case ACTIVITY_YES: - break; - } + notifyIdleGcDone(force_major); #if defined(THREADED_RTS) // Stable point where we can do a global check on our spark counters @@ -2788,8 +2732,6 @@ void initScheduler(void) { setSchedState(SCHED_RUNNING); - setRecentActivity(ACTIVITY_YES); - /* Initialise the mutex and condition variables used by * the scheduler. */ ===================================== rts/Schedule.h ===================================== @@ -80,58 +80,6 @@ INLINE_HEADER enum SchedState getSchedState(void) return (enum SchedState) SEQ_CST_LOAD_ALWAYS(&sched_state); } -/* - * flag that tracks whether we have done any execution in this time - * slice, and controls the disabling of the interval timer. - * - * The timer interrupt transitions ACTIVITY_YES into - * ACTIVITY_MAYBE_NO, waits for RtsFlags.GcFlags.idleGCDelayTime, - * and then: - * - if idle GC is on, set ACTIVITY_INACTIVE and wakeUpRts() - * - if idle GC is off, set ACTIVITY_DONE_GC and stopTimer() - * - * If the scheduler finds ACTIVITY_INACTIVE, then it sets - * ACTIVITY_DONE_GC, performs the GC and calls stopTimer(). - * - * If the scheduler finds ACTIVITY_DONE_GC and it has a thread to run, - * it enables the timer again with startTimer(). - */ -enum RecentActivity { - // the RTS is active - ACTIVITY_YES = 0, - // no activity since the last timer signal - ACTIVITY_MAYBE_NO = 1, - // RtsFlags.GcFlags.idleGCDelayTime has passed with no activity - ACTIVITY_INACTIVE = 2, - // like ACTIVITY_INACTIVE, but we've done a GC too (if idle GC is - // enabled) and the interval timer is now turned off. - ACTIVITY_DONE_GC = 3, -}; - -/* Recent activity flag. - * Locks required : Transition from MAYBE_NO to INACTIVE - * happens in the timer signal, so it is atomic. Transition from - * INACTIVE to DONE_GC happens under sched_mutex. No lock required - * to set it to ACTIVITY_YES. - * - * N.B. we must always use atomics here since even in the non-threaded runtime - * the timer may be provided via a signal. - */ -extern StgWord recent_activity; - -INLINE_HEADER enum RecentActivity -setRecentActivity(enum RecentActivity new_value) -{ - StgWord old = SEQ_CST_XCHG_ALWAYS((StgPtr) &recent_activity, (StgWord) new_value); - return (enum RecentActivity) old; -} - -INLINE_HEADER enum RecentActivity -getRecentActivity(void) -{ - return (enum RecentActivity) RELAXED_LOAD_ALWAYS(&recent_activity); -} - extern bool heap_overflow; #if defined(THREADED_RTS) ===================================== rts/Timer.c ===================================== @@ -22,7 +22,7 @@ #include "Timer.h" #include "Proftimer.h" -#include "Schedule.h" +#include "IdleGC.h" #include "Ticker.h" #include "Capability.h" #include "RtsSignals.h" @@ -37,57 +37,6 @@ static int ticks_to_eventlog_flush = 0; #endif -/* - Note [GC During Idle Time] - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - In the threaded RTS, a major GC can be performed during idle time (i.e., when - no Haskell computations are ready to run). This can be beneficial for two - reasons. First, running the GC during idle time makes it less likely that a GC - will be triggered when the process is active, increasing apparent - responsiveness. Second, these idle time GCs allow finalizers to run, - preventing resources from being held indefinitely when the process is otherwise - idle for extended periods. - - There are two runtime RTS options to control idle time GC timing. The primary - control is set by `-I<n>`, which specifies the minimum period of time the - process must be idle before a GC is automatically triggered. It defaults to - 0.3 seconds for the threaded runtime and 0 (which disables idle GC entirely) - for the non-threaded runtime. For certain workflows, the 0.3 second delay for - the threaded RTS may be too small. If an application must process an extended - burst of short-lived requests occurring a couple of times a second, it may go - idle for >0.3 seconds frequently, resulting in potentially dozens of major - GCs triggered every minute (and resulting heavy CPU load) while the burst - lasts. Setting the `-I<n>` value higher will prevent this flurry of major GCs, - though there is a danger that setting the value too high will prevent automatic - GCs entirely, if the process never gets a chance to go idle for long enough to - meet the larger threshold. - - In this case, the second control, set by `-Iw<n>` may be helpful. For example, - setting `-I0.3 -Iw30` triggers automatic GCs after only 0.3 seconds of idle - time, but subject to a minimum delay *between* automatic GCs of at least 30 - seconds. This is likely to work well for applications that must process a - nearly constant stream of frequent, short-lived requests, ensuring that - automatic GCs are triggered promptly when the process goes idle while limiting - the overall frequency of such GCs. (The default is `-Iw0`, meaning no limit on - frequency of GCs.) - - Automatic GC timing is implemented below using two count-down timers. The - `idle_ticks_to_gc` timer counts down the `-I<n>` setting: it is initialized - when the process goes idle and counts down idle time before an automatic GC - becomes possible. The `inter_gc_ticks_to_gc` counts down the `-Iw<n>` setting: - it is initialized when an automatic GC is actually performed, and holds off the - next automatic GC until its count expires, limiting the overall frequency of - automatic GCs. Both timers must expire before an automatic GC is triggered. - - See issue #11134 for additional detail. -*/ - -/* - countdown for minimum idle time before we start a GC (set by -I) */ -static int idle_ticks_to_gc = 0; - -/* - countdown for minimum time *between* idle GCs (set by -Iw) */ -static int inter_gc_ticks_to_gc = 0; - /* * Function: handle_tick() * @@ -119,50 +68,9 @@ handle_tick(int unused STG_UNUSED) } #endif - /* - * If we've been inactive for idleGCDelayTime (set by +RTS - * -I), tell the scheduler to wake up and do a GC, to check - * for threads that are deadlocked. However, ensure we wait - * at least interIdleGCWait (+RTS -Iw) between idle GCs. - */ - switch (getRecentActivity()) { - case ACTIVITY_YES: - setRecentActivity(ACTIVITY_MAYBE_NO); - idle_ticks_to_gc = RtsFlags.GcFlags.idleGCDelayTime / - RtsFlags.MiscFlags.tickInterval; - break; - case ACTIVITY_MAYBE_NO: - if (idle_ticks_to_gc == 0 && inter_gc_ticks_to_gc == 0) { - if (RtsFlags.GcFlags.doIdleGC) { - setRecentActivity(ACTIVITY_INACTIVE); - inter_gc_ticks_to_gc = RtsFlags.GcFlags.interIdleGCWait / - RtsFlags.MiscFlags.tickInterval; - wakeUpRts(); - // The scheduler will call pauseTimer() when it has done - // the GC. - } else { - setRecentActivity(ACTIVITY_DONE_GC); - // disable timer signals (see #1623, #5991, #9105) - // but only if we're not profiling (e.g. passed -h or -p RTS - // flags). If we are profiling we need to keep the timer active - // so that samples continue to be collected. -#if defined(PROFILING) - if (!(RtsFlags.ProfFlags.doHeapProfile - || RtsFlags.CcFlags.doCostCentres)) { - pauseTimer(); - } -#else - pauseTimer(); +#if defined(THREADED_IDLEGC) + handleIdleGcTick(); #endif - } - } else { - if (idle_ticks_to_gc) idle_ticks_to_gc--; - if (inter_gc_ticks_to_gc) inter_gc_ticks_to_gc--; - } - break; - default: - break; - } } void initTimer(void) ===================================== rts/posix/Poll.c ===================================== @@ -28,6 +28,11 @@ #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 +510,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 +549,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" ===================================== rts/rts.cabal ===================================== @@ -420,6 +420,7 @@ library HsFFI.c Inlines.c Interpreter.c + IdleGC.c IOManager.c LdvProfile.c Libdw.c View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/7b99108940a6eba52960363634ddb48... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/7b99108940a6eba52960363634ddb48... 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)