Skip to content

Wake the dispatch and runspace loops as soon as a job finishes - #31

Merged
HalRiveria merged 2 commits into
mainfrom
feat/waitany-dispatch
Nov 2, 2026
Merged

HalRiveria merged 2 commits into
mainfrom
feat/waitany-dispatch

Conversation

@HalRiveria-Bot

Copy link
Copy Markdown
Collaborator

What was wrong: the dispatch loop slept 50 ms on every pass, even after a job had just finished, and only refilled the slot after the next sleep. That capped each slot at about 8 programs a second. The runspace loop's 100 ms sleep capped short workers at about 9 jobs a second per slot.

What changed:

  • The dispatch loop goes straight round after a pass where a job finished. Otherwise it waits on the exit handles of programs still running, plus the cancel token, capped at 50 ms.
  • It only waits on programs that haven't exited, so a -JobPollCompleted that keeps saying "not done" can't spin a core.
  • The runspace loop waits on its workers' handles plus the token, capped at 100 ms.
  • Above 62 running, both wait on the first 62 and notice the rest at the cap. WaitAny holds 63 handles at most on an STA thread.
  • Dispatchers sharing a SemaphoreSlim still pause after a finished job. Without that, one with a queue kept every slot until it ran out and its sibling got none.

Behavior users can notice:

  • Callbacks that run every pass (-PollCallback, -AbortPollCallback, -GroupReleaseCheck) can run many times more often when jobs finish quickly. The README now says to keep them cheap.
  • A retry queued from -JobFinalize can start within milliseconds of the failure. The README says to cap retries and not count on the gap.

Measured (PowerShell 5.1 and 7, new harness section AB):

Before After
cmd /c exit 0, -MaxConcurrent 1 (5.1) 106 ms each 19 ms each
20 ms worker, -MaxParallel 1 (5.1) 102 ms each 23 ms each
80 short programs, two dispatchers sharing 1 slot (5.1) 7.8 s 5.1 s

Two dispatchers sharing one slot alternate as before (ABABAB).

Tested: the harness passes on 5.1 and 7 (300 assertions, none skipped). The two timing tests fail on main. The turn-taking test fails against the same change without the pause. New checks also cover a lagging -JobPollCompleted, 70 programs and 70 workers at once, a job whose .Proc isn't a process, and a batch inside a worker.

🤖 Generated with Claude Code

HalRiveria and others added 2 commits November 2, 2026 17:41
The dispatch loop slept 50 ms on every pass, even one where a job had just
finished, and noticed the free slot only after another sleep. That capped each
slot at about 8 programs a second. The runspace loop's 100 ms sleep capped short
workers at about 9 jobs a second per slot.

The dispatch loop now goes straight round after a pass where a job finished, and
otherwise waits on the exit handles of programs still running plus the cancel
token, capped at 50 ms. It waits only on programs that haven't exited, so a
-JobPollCompleted that keeps saying "not done" can't spin a core. It falls back
to a sleep for a job with no real process or more than 63 running. The runspace
loop does the same with its workers' wait handles and a 100 ms cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refilling a freed slot at once let a dispatcher on a shared SemaphoreSlim keep
every slot until its queue ran out, so a sibling got none. A dispatcher on a
shared budget still pauses after a finished job.

WaitAny holds at most 63 handles on an STA thread, so waiting on 63 plus the
token threw under PowerShell 7. Both loops now wait on up to 62 plus the token,
and on those even when more are running, rather than falling back to a sleep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HalRiveria
HalRiveria merged commit e79cf89 into main Nov 2, 2026
2 checks passed
@HalRiveria
HalRiveria deleted the feat/waitany-dispatch branch November 2, 2026 23:39
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