Skip to content

Stop a child from its own invocation's turn without hanging - #248

Merged
SandroMaglione merged 1 commit into
mainfrom
stop-child-from-invocation-turn
Oct 23, 2026
Merged

SandroMaglione merged 1 commit into
mainfrom
stop-child-from-invocation-turn

Conversation

@SandroMaglione

Copy link
Copy Markdown
Member

Summary

  • Since 0.45.0, a child's worker starts in the turn of the invocation that completes. If the child's onDone sends to its parent and the parent's transition stops that child, the stop asked to interrupt the worker from inside the invocation fiber. The invocation fiber then waited for the child's worker, and the worker was waiting for the invocation fiber to exit, so both machines hung, including scope finalization.
  • startWorker now forks the requested interruption instead of awaiting it in the sender's turn. Whoever requested the stop already awaits completion.
  • Adds a regression test covering synchronous and asynchronous invocations, with generic and compiled parents. The asynchronous cases hang on main.

Changeset

  • Added or updated for a library or package-metadata change
  • Not required because this PR does not change a publishable package

Validation

  • pnpm check
  • Automated type-performance measurement passed or was not required
  • Automated runtime- and memory-performance measurement passed or was not required

pnpm perf:runtime also completed locally. A consumer app's machine tests that hung on 0.45.0 pass with this build: 357/357.

🤖 Generated with Claude Code

Since 0.45.0 an idle machine's worker starts in the sender's turn. When a
child's invocation completed, its onDone ran inside the invocation fiber,
and a parent that stopped the child in response requested an interrupt
there. The invocation fiber then waited for the child's worker, which was
waiting for the invocation fiber to exit. The interruption now runs in its
own fiber, while whoever requested the stop awaits completion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Type performance

Measured with TypeScript 6.0.3 and skipLibCheck=true.

Scenario Base PR Difference
Effect only 55 55 0 (0.0%)
Import effect-machine 55 55 0 (0.0%)
Machine.state (3 children) 2,664 2,664 0 (0.0%)
Machine.make (3 states, 2 events) 8,932 8,932 0 (0.0%)
machine.handle (3 states, 2 transitions) 39,449 39,449 0 (0.0%)
declared transition (10 named branches) 60,402 60,402 0 (0.0%)
atomic construction, guards, and reentry 85,710 85,710 0 (0.0%)
registered invocation (input-taking Effect) 39,819 39,819 0 (0.0%)
machine.handle (depth 24) 194,531 194,531 0 (0.0%)
machine.handle (wide depth 16) 218,238 218,238 0 (0.0%)
machine.handle (parallel/history/choice) 91,642 91,642 0 (0.0%)
machine definition (3 independent implementations) 99,173 99,173 0 (0.0%)
machine exact input/output/error/services 82,941 82,941 0 (0.0%)
execution adapter readiness 71,428 71,428 0 (0.0%)

Marginal instantiations are measured against the matching setup without that API call:

Scenario Base PR Difference
Import effect-machine 0 0 0
Machine.state (3 children) 2,609 2,609 0 (0.0%)
Machine.make (3 states, 2 events) 6,260 6,260 0 (0.0%)
machine.handle (3 states, 2 transitions) 30,517 30,517 0 (0.0%)
declared transition (10 named branches) 51,849 51,849 0 (0.0%)
atomic construction, guards, and reentry 70,370 70,370 0 (0.0%)
registered invocation (input-taking Effect) 32,682 32,682 0 (0.0%)
machine.handle (depth 24) 177,222 177,222 0 (0.0%)
machine.handle (wide depth 16) 201,707 201,707 0 (0.0%)
machine.handle (parallel/history/choice) 79,144 79,144 0 (0.0%)
machine definition (3 independent implementations) 88,492 88,492 0 (0.0%)
machine exact input/output/error/services 70,732 70,732 0 (0.0%)
execution adapter readiness 31,922 31,922 0 (0.0%)
Check times (informational)
Scenario Base PR
Effect only 0.02s 0.02s
Import effect-machine 0.02s 0.02s
Machine.state (3 children) 0.05s 0.06s
Machine.make (3 states, 2 events) 0.08s 0.09s
machine.handle (3 states, 2 transitions) 0.19s 0.19s
declared transition (10 named branches) 0.23s 0.23s
atomic construction, guards, and reentry 0.31s 0.32s
registered invocation (input-taking Effect) 0.21s 0.19s
machine.handle (depth 24) 0.39s 0.37s
machine.handle (wide depth 16) 0.42s 0.38s
machine.handle (parallel/history/choice) 0.35s 0.32s
machine definition (3 independent implementations) 0.34s 0.33s
machine exact input/output/error/services 0.33s 0.33s
execution adapter readiness 0.28s 0.27s

Type instantiations are the comparison metric. Check time varies with runner load and is informational only.

@github-actions

Copy link
Copy Markdown
Contributor

Runtime performance

Median of 5 independent benchmark processes on AMD EPYC 9V45 96-Core Processor with Node v24.21.0.

Pull request baseline

Scenario Effect Machine
Plan counter transitions 187,719 transitions/s
Drain burst with terminal fence 663,024 increments/s
Drain burst with a change observer 625,433 increments/s
Lookup and send to one child 545,340 increments/s
Start and stop a machine 198,926 machines/s
Start and stop a parent with one child 61,675 families/s
Complete an Effect invocation with tracing enabled 26,756 invocations/s
Complete an Effect invocation with tracing disabled 29,481 invocations/s
Plan transitions through a compound state 170,125 transitions/s
Plan transitions through parallel regions 136,273 transitions/s
Drain burst through a compound state 546,437 events/s
Drain burst through two parallel regions 511,291 events/s
Drain a compound-state burst with a change observer 518,622 events/s

Process runtime reference points

Scenario Effect Machine
Start and stop a raw generic process 53,425 processes/s
Start and stop a raw compiled process 121,921 processes/s
Memory profile Effect Machine
Idle machine 1.9 KiB
Raw generic managed process 11.3 KiB
Raw compiled process 3.1 KiB
Two independent idle machines 3.5 KiB
Idle parent with one child 6.0 KiB
Parent with observed child registry 9.5 KiB
Parent with observed invoked child snapshots 6.9 KiB

Effect Machine change from base

Metric Base Base variability PR PR variability Difference
Plan counter transitions 192,567 transitions/s 0.1% MAD 187,719 transitions/s 0.6% MAD -2.5%
Drain burst with terminal fence 681,598 increments/s 0.5% MAD 663,024 increments/s 0.6% MAD -2.7%
Drain burst with a change observer 628,850 increments/s 0.7% MAD 625,433 increments/s 0.9% MAD -0.5%
Lookup and send to one child 556,368 increments/s 1.0% MAD 545,340 increments/s 2.0% MAD -2.0%
Start and stop a machine 198,886 machines/s 0.4% MAD 198,926 machines/s 0.8% MAD +0.0%
Start and stop a parent with one child 61,828 families/s 0.4% MAD 61,675 families/s 0.1% MAD -0.2%
Complete an Effect invocation with tracing enabled 26,784 invocations/s 2.1% MAD 26,756 invocations/s 1.5% MAD -0.1%
Complete an Effect invocation with tracing disabled 30,112 invocations/s 1.0% MAD 29,481 invocations/s 0.9% MAD -2.1%
Plan transitions through a compound state 170,328 transitions/s 0.1% MAD 170,125 transitions/s 1.1% MAD -0.1%
Plan transitions through parallel regions 136,766 transitions/s 1.2% MAD 136,273 transitions/s 1.1% MAD -0.4%
Drain burst through a compound state 550,509 events/s 0.5% MAD 546,437 events/s 0.4% MAD -0.7%
Drain burst through two parallel regions 506,994 events/s 0.5% MAD 511,291 events/s 0.9% MAD +0.8%
Drain a compound-state burst with a change observer 517,848 events/s 0.6% MAD 518,622 events/s 1.0% MAD +0.1%
Idle machine heap per unit 1.9 KiB 1.6% MAD 1.9 KiB 1.3% MAD +2.2%
Raw generic managed process heap per unit 11.2 KiB 0.0% MAD 11.3 KiB 1.4% MAD +0.7%
Raw compiled process heap per unit 3.1 KiB 0.1% MAD 3.1 KiB 0.1% MAD +0.1%
Two independent idle machines heap per unit 3.5 KiB 0.0% MAD 3.5 KiB 0.1% MAD -0.6%
Idle parent with one child heap per unit 6.0 KiB 0.1% MAD 6.0 KiB 0.0% MAD -0.0%
Parent with observed child registry heap per unit 9.5 KiB 0.0% MAD 9.5 KiB 0.0% MAD +0.0%
Parent with observed invoked child snapshots heap per unit 6.9 KiB 0.1% MAD 6.9 KiB 0.2% MAD +0.3%

Process runtime reference change from base

Metric Base Base variability PR PR variability Difference
Start and stop a raw generic process 52,554 processes/s 1.1% MAD 53,425 processes/s 1.2% MAD +1.7%
Start and stop a raw compiled process 120,019 processes/s 0.5% MAD 121,921 processes/s 0.1% MAD +1.6%

Regression guard

No large, noise-adjusted throughput or heap regressions detected.

Versions and interpretation
  • Effect Machine: 0.45.0

Higher throughput is better; lower heap is better. Variability is the median absolute deviation across independent processes, relative to their median. Small differences on shared GitHub-hosted hardware remain informational; the required guard rejects only large changes beyond the measured noise allowance.

@SandroMaglione
SandroMaglione merged commit 6739e91 into main Oct 23, 2026
9 checks passed
@SandroMaglione
SandroMaglione deleted the stop-child-from-invocation-turn branch October 23, 2026 09:46
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