Skip to content

Fix infinite WaitTimer spin when the delayed-callback timer is disarmed (UINT64_MAX)#1001

Merged
jasonsandlin merged 1 commit into
mainfrom
user/pkacha/fix-timer-uint64max-spin
Jul 9, 2026
Merged

Fix infinite WaitTimer spin when the delayed-callback timer is disarmed (UINT64_MAX)#1001
jasonsandlin merged 1 commit into
mainfrom
user/pkacha/fix-timer-uint64max-spin

Conversation

@PushpadantK

Copy link
Copy Markdown
Collaborator

Problem

An intermittent hang caused by an infinite spin in the XTaskQueue delayed-callback timer.

Root cause

#983 ("Centralize timer-arming into three helpers", 385cc9a) removed the if (dueTime == UINT64_MAX) return; guard from the top of TaskQueuePortImpl::SubmitPendingCallbacks and routed the now < dueTime path through the new RearmTimerIfDueTimeUnchanged() helper, which calls m_timer.Start(dueTime) unconditionally. Only ArmTimerIfEarlier() retained a UINT64_MAX guard.

UINT64_MAX is the "nothing armed" sentinel for m_timerDue. When a timer callback fires while the timer is disarmed:

  1. SubmitPendingCallbacks reads dueTime = UINT64_MAX, sees now < UINT64_MAX (always true), and calls RearmTimerIfDueTimeUnchanged(UINT64_MAX)m_timer.Start(UINT64_MAX).
  2. WaitTimerImpl::Start builds Deadline(Deadline::duration(UINT64_MAX)). With the STL backend's signed-int64 duration rep, UINT64_MAX-1 → a deadline 1 ns before the steady_clock epoch (perpetually in the past).
  3. The timer thread fires immediately, re-enters SubmitPendingCallbacks, re-arms the sentinel, and fires again — an infinite fire/re-arm spin.

Because every iteration takes the now < dueTime re-arm branch, PromoteReadyPendingCallbacks is never reached, so all real pending delayed callbacks are starved (stranded in m_pendingList). On PS5 this manifested as an intermittent hang: a delayed websocket send-completion poll was never promoted, so the send never completed.

Fix

  • Restore the pre-#983 UINT64_MAX guard at the top of SubmitPendingCallbacks.
  • Add the same guard to RearmTimerIfDueTimeUnchanged (mirrors the existing guard in ArmTimerIfEarlier).

The disarm sentinel can no longer be programmed as a real deadline.

Validation

Reproduced on PS5 (STL wait-timer backend) with tracing that showed the timer firing ~307k times on the UINT64_MAX sentinel while real callbacks sat unpromoted. With the guard, the spin is gone: the timer-fire count returns to normal, the guard hits exactly at the sentinel, and promotions resume. A 29‑scenario GameSave PSX device suite passed with 0 failures, including every previously-hanging teardown scenario.

…ed (UINT64_MAX)

PR #983 removed the `if (dueTime == UINT64_MAX) return;` guard from the top of
TaskQueuePortImpl::SubmitPendingCallbacks and routed the early (now < dueTime)
path through the new RearmTimerIfDueTimeUnchanged() helper, which calls
m_timer.Start(dueTime) unconditionally. Only ArmTimerIfEarlier() kept a
UINT64_MAX guard.

UINT64_MAX is the "nothing armed" sentinel for m_timerDue. When a timer callback
fires while the timer is disarmed, SubmitPendingCallbacks re-arms the sentinel:
WaitTimerImpl::Start(UINT64_MAX) builds Deadline(Deadline::duration(UINT64_MAX)),
and because the STL backend's duration rep is a signed int64, UINT64_MAX becomes
-1 -- a deadline 1ns before the steady_clock epoch, i.e. perpetually in the past.
The wait-timer thread then fires immediately, calls SubmitPendingCallbacks, sees
m_timerDue == UINT64_MAX (now < UINT64_MAX is always true), re-arms the sentinel,
and fires again: an infinite fire/re-arm spin that pins the timer thread and
starves every real pending delayed callback (they sit unpromoted in m_pendingList
because PromoteReadyPendingCallbacks is never reached). Observed on PS5 as an
intermittent hang where a delayed websocket send-completion poll is never
promoted, so the operation never completes.

Fix: restore the pre-#983 sentinel guard at the top of SubmitPendingCallbacks and
add the same guard to RearmTimerIfDueTimeUnchanged, so the disarm sentinel can
never be programmed as a real deadline.
@jasonsandlin jasonsandlin merged commit 1fea36d into main Jul 9, 2026
19 checks passed
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.

2 participants