Skip to content

Allocate child runtime resources lazily - #36

Merged
SandroMaglione merged 2 commits into
mainfrom
codex/lazy-child-runtime
Aug 7, 2026
Merged

Allocate child runtime resources lazily#36
SandroMaglione merged 2 commits into
mainfrom
codex/lazy-child-runtime

Conversation

@SandroMaglione

@SandroMaglione SandroMaglione commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

  • create the child scope and observable child registry only when child-management capabilities are first used
  • serialize first allocation against process shutdown with one synchronized lifecycle state
  • preserve immediate child lookup, race-free child change observation, dynamic spawning, and child cleanup
  • avoid holding the lifecycle lock while closing child scopes so child finalizers can unregister safely
  • run one benchmark harness against both revisions with five interleaved processes and process-level variability
  • measure observed bursts, child lookup/delivery, parent-child lifecycle, and parent-with-child memory for Effect Machine and both XState baselines

Changeset

  • Added or updated for a library or package-metadata change
  • Not required because this PR does not change src/ or package.json

Validation

  • pnpm check
  • Relevant example checks, when examples changed (no examples changed)
  • Reviewed the automated type-performance report, when the public TypeScript API or inference changed (no public type changes; local pnpm perf:types passed)
  • Reviewed the automated runtime-performance report, when runtime behavior changed

pnpm check and pnpm perf:types pass. The 181 machine runtime tests also passed five additional consecutive stress runs before the benchmark expansion.

The final local A/B used the PR harness for both revisions and the CI ordering: five interleaved full processes per revision. Relative process variability is reported as median absolute deviation.

  • ordinary planning: +0.1% (base 1.4% MAD, PR 2.0% MAD)
  • unobserved burst: +1.1% (base 0.9% MAD, PR 0.9% MAD)
  • observed burst: +1.7% (base 1.1% MAD, PR 0.5% MAD)
  • child lookup and delivery: +0.4% (base 0.8% MAD, PR 0.3% MAD)
  • no-child start/stop: +2.5% (base 1.2% MAD, PR 0.7% MAD)
  • parent-with-child start/stop: -2.6% (base 0.9% MAD, PR 0.4% MAD)
  • idle machine heap: 25.8 KiB to 25.1 KiB (-2.7%)
  • idle parent-with-child heap: 74.5 KiB to 75.2 KiB (+1.0%)

An earlier independent five-process pass measured the same memory changes, child lookup/delivery at -1.0%, and parent-with-child lifecycle at -3.6%. The combined result identifies a narrow tradeoff: machines that never use child capabilities save 2.7% idle heap and start slightly faster; machines that create a child pay about 1% more combined heap and 2.6-3.6% during family allocation/teardown, while steady-state child lookup and delivery remain flat.

The GitHub runner reproduced the result across five processes: planning +1.3%, unobserved burst +2.2%, observed burst +0.9%, child lookup/delivery -0.0%, no-child start/stop +3.0%, parent-with-child start/stop -1.5%, idle heap -2.7%, and parent-with-child heap +1.0%. All required checks pass.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Runtime performance

Median of 5 independent benchmark processes on AMD EPYC 7763 64-Core Processor with Node v24.18.0.

Pull request baseline

Scenario Effect Machine XState 5 XState 6 alpha
Plan counter transitions 27,377 transitions/s 132,804 transitions/s 123,281 transitions/s
Drain burst with terminal fence 15,655 increments/s 400,727 increments/s 111,428 increments/s
Drain burst with a change observer 14,898 increments/s 398,271 increments/s 111,663 increments/s
Lookup and send to one child 14,535 increments/s 395,440 increments/s 106,516 increments/s
Start and stop a machine 6,409 machines/s 184,502 machines/s 73,073 machines/s
Start and stop a parent with one child 2,599 families/s 67,627 families/s 26,596 families/s
Idle memory Heap per machine
Effect Machine 25.0 KiB
XState 5 3.8 KiB
XState 6 alpha 6.1 KiB

Effect Machine change from base

Metric Base PR Difference
Plan counter transitions 27,013 transitions/s 27,377 transitions/s +1.3%
Drain burst with terminal fence 15,322 increments/s 15,655 increments/s +2.2%
Drain burst with a change observer 14,761 increments/s 14,898 increments/s +0.9%
Lookup and send to one child 14,541 increments/s 14,535 increments/s -0.0%
Start and stop a machine 6,220 machines/s 6,409 machines/s +3.0%
Start and stop a parent with one child 2,639 families/s 2,599 families/s -1.5%
Idle heap per machine 25.7 KiB 25.0 KiB -2.7%
Versions and interpretation
  • Effect Machine: 0.3.0
  • XState 5: 5.32.5
  • XState 6 alpha: 6.0.0-alpha.27

Higher throughput is better; lower idle heap is better. Runtime measurements on shared GitHub-hosted hardware remain informational, so small differences should be confirmed across multiple workflow runs.

@github-actions

github-actions Bot commented Aug 7, 2026

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.defineStates (3 states) 2,965 2,965 0 (0.0%)
Machine.make (3 states, 2 events) 8,731 8,731 0 (0.0%)
machine.handle (3 states, 2 transitions) 23,634 23,634 0 (0.0%)
machine.handle (depth 8) 113,023 113,023 0 (0.0%)
machine.handle (depth 12) 125,661 125,661 0 (0.0%)
machine.handle (depth 16) 141,003 141,003 0 (0.0%)
machine.handle (depth 24) 179,799 179,799 0 (0.0%)
machine.handle (wide depth 16) 210,137 210,137 0 (0.0%)
machine.handle (parallel/history/choice) 128,876 128,876 0 (0.0%)
machine.handle (4 successive calls) 129,498 129,498 0 (0.0%)
machine exact input/output/error/services 107,371 107,371 0 (0.0%)
execution adapter readiness 122,890 122,890 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.defineStates (3 states) 2,910 2,910 0 (0.0%)
Machine.make (3 states, 2 events) 5,758 5,758 0 (0.0%)
machine.handle (3 states, 2 transitions) 14,903 14,903 0 (0.0%)
machine.handle (depth 8) 105,546 105,546 0 (0.0%)
machine.handle (depth 12) 116,984 116,984 0 (0.0%)
machine.handle (depth 16) 131,126 131,126 0 (0.0%)
machine.handle (depth 24) 167,522 167,522 0 (0.0%)
machine.handle (wide depth 16) 198,169 198,169 0 (0.0%)
machine.handle (parallel/history/choice) 110,514 110,514 0 (0.0%)
machine.handle (4 successive calls) 114,497 114,497 0 (0.0%)
machine exact input/output/error/services 95,819 95,819 0 (0.0%)
execution adapter readiness 96,968 96,968 0 (0.0%)
Check times (informational)
Scenario Base PR
Effect only 0.03s 0.03s
Import effect-machine 0.03s 0.03s
Machine.defineStates (3 states) 0.10s 0.10s
Machine.make (3 states, 2 events) 0.15s 0.15s
machine.handle (3 states, 2 transitions) 0.23s 0.22s
machine.handle (depth 8) 0.52s 0.50s
machine.handle (depth 12) 0.55s 0.56s
machine.handle (depth 16) 0.58s 0.57s
machine.handle (depth 24) 0.67s 0.66s
machine.handle (wide depth 16) 0.73s 0.74s
machine.handle (parallel/history/choice) 0.60s 0.60s
machine.handle (4 successive calls) 0.58s 0.57s
machine exact input/output/error/services 0.50s 0.51s
execution adapter readiness 0.53s 0.53s

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

@SandroMaglione
SandroMaglione marked this pull request as ready for review August 7, 2026 18:31
@SandroMaglione
SandroMaglione merged commit 3848b9b into main Aug 7, 2026
9 checks passed
@SandroMaglione
SandroMaglione deleted the codex/lazy-child-runtime branch August 7, 2026 18:31
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