Skip to content

Harden runtime correctness and performance - #45

Merged
JovaniPink merged 1 commit into
masterfrom
runtime-correctness-performance-hardening
Aug 30, 2026
Merged

Harden runtime correctness and performance#45
JovaniPink merged 1 commit into
masterfrom
runtime-correctness-performance-hardening

Conversation

@JovaniPink

Copy link
Copy Markdown
Owner

Summary

This PR fixes confirmed runtime lifecycle, snapshot isolation, timer resource,
and transition performance defects found during a fresh audit of master at
18d6ea972a1dafb4cae29af6b2c6d1aaaf38b3ef.

It preserves the public Machine(config, ...) boundary, run-to-completion
ordering, immutable structural snapshot fields, XState-shaped configuration,
and zero runtime dependencies.

What changed

Correctness and lifecycle

  • Serialize independent copies of snapshot value, context, and output data.
  • Add optional typed context serializer and deserializer callbacks for custom
    context types such as immutable dataclasses.
  • Use the immutable snapshot configuration as the transition authority so
    mutating the derived state.value cannot change execution.
  • Prevent actor stop from racing late invocation reconciliation and leaving a
    running child.
  • Serialize child creation with parent lifecycle changes and avoid registering
    a child spawned after its parent stopped.
  • Make sync and async interpreter stop terminal. A stopped interpreter cannot
    restart and later events remain dropped.
  • Make async stop wait for owned timer task cancellation to settle.
  • Prevent later interpreter-owned actions from scheduling new work when stop
    occurs during an awaited async action.

Performance and resources

  • Skip trace-only microstep context snapshots unless a trace helper or an
    interpreter inspector requests them.
  • Reuse each transition exit set during parallel conflict resolution.
  • Transition from state.configuration instead of rebuilding configuration
    from state.value.
  • Replace one threading.Timer per timeout with one on-demand daemon scheduler
    thread per ThreadClock. The worker exits after the final timer fires or is
    canceled.

Typing and maintainability

  • Complete full strict MyPy coverage for all 23 source files.
  • Add concrete generic task, registry, timeout, and guard mapping annotations.
  • Add regression tests for each corrected behavior and update public guidance
    and the unreleased changelog.

Compatibility

  • No package dependency, distribution name, import path, or machine
    configuration boundary changes.
  • Snapshot context codec parameters are optional keyword-only additions.
  • Trace helpers and inspectors retain their existing snapshots and trace
    ordering.
  • Stopped direct interpreters are now terminal, matching stopped actor behavior.
    Code that intentionally restarted the same interpreter must create a new
    interpreter instead.
  • ThreadClock still gives each clock its own worker; it does not introduce a
    process-global scheduler.
  • An async action already awaiting at stop is not forcibly canceled. It may
    finish in its caller's send task, but later runtime actions are skipped.

Performance evidence

The temporary standard-library-only harness ran outside the repository on
macOS without coverage instrumentation. Each result aggregates seven fresh
processes with three warmups and 21 samples per process. It records median,
p95, process median absolute deviation, operations per second, peak
tracemalloc memory, and task or thread counts. Each case verifies its expected
snapshot before timing.

Representative median results:

Workload CPython 3.13 before CPython 3.13 after CPython 3.14 before CPython 3.14 after
Ignored transition 23.59 us 15.02 us 23.97 us 12.84 us
Eventless chain, 1,000 87.94 ms 11.40 ms 87.29 ms 11.19 ms
Dictionary context, 1,000 630.83 us 322.73 us 274.98 us 143.24 us
Parallel conflicts, 64 regions 44.05 ms 3.35 ms 43.49 ms 3.09 ms
Sync interpreter, no subscribers 31.01 us 20.75 us 30.68 us 18.38 us
Async interpreter, 100 callers 39.19 us 30.45 us 38.30 us 26.65 us
Invoke reconciliation, 100 children 5.77 ms 4.60 ms 5.76 ms 4.12 ms
Thread timer lifecycle, 50 timers 2.31 ms, 50 threads 0.134 ms, 1 thread 2.44 ms, 50 threads 0.122 ms, 1 thread
Snapshot round trip, context 1,000 19.62 us 647.25 us 21.08 us 266.53 us

The snapshot round-trip regression is intentional and material. The old path
returned aliased data and restored shallow context copies, so its timing did
not include the isolation promised by persistence. The new path performs the
required defensive copies. That cost scales with context size and is documented
rather than hidden behind a score.

Profiling after the change confirms that default context deepcopy remains the
dominant cost for large mutable dictionary contexts. Exit-set computation is
now once per enabled transition during conflict filtering, trace-only context
copies are absent from ordinary transitions, and 50 real timers use one worker.

These are local macOS measurements, not a service SLO or competitor ranking.
Ubuntu CI proves functionality, not equivalent performance. Cross-platform
performance remains unverified.

The harness and JSON profiles remain in /private/tmp and are not tracked.
Generated dist/ artifacts are ignored and are not part of this PR.

Validation

Exact branch commit: 10b6d3f5248cba365c70bb501c70955e398b79a9

  • poetry run python scripts/release_preflight.py v0.7.1 --target-ref HEAD --master-ref HEAD
    • 496 primary tests passed.
    • 57 configured SCXML cases passed.
    • Ruff format and lint passed.
    • Repository MyPy passed for 23 source files.
    • Metadata and lock checks passed.
    • The sdist and wheel built.
    • Isolated wheel validation and actor smoke passed.
  • poetry run mypy --strict src/xstate/ passed for all 23 source files.
  • poetry run python -m pytest tests/ --ignore=tests/test_scxml.py --cov --cov-report=term
    passed with 91.34 percent branch coverage against the 90 percent gate.
  • A separate CPython 3.13 isolated environment installed the wheel with no
    dependencies and passed a live actor transition and snapshot serialization
    smoke test.
  • The complete performance matrix and cProfile plus tracemalloc probes passed
    on CPython 3.13.1 and 3.14.7 at the exact branch commit.
  • git diff --check passed, added lines are ASCII, and the worktree was clean
    before push.

Risk and rollback

The highest-risk changes are actor lifecycle locking, the shared timer worker,
terminal interpreter stop behavior, and conflict filtering. Targeted regression
tests cover their failure mechanisms, and the complete SCXML suite protects
run-to-completion behavior.

If rollback is required, revert this single commit. There are no data migrations,
new dependencies, publication changes, or external state changes.

Review focus

  • Lock ordering between parent stop, child stop, spawn, and invocation
    reconciliation.
  • Timer heap cleanup and worker exit after cancellation.
  • Trace equivalence when inspectors are enabled and trace work is skipped when
    they are absent.
  • The explicit snapshot isolation versus serialization cost tradeoff.
  • Terminal stop behavior for direct sync and async interpreter users.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T16:29:57.342155Z 10b6d3f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@JovaniPink
JovaniPink merged commit f9fbef1 into master Aug 30, 2026
4 checks passed
@JovaniPink
JovaniPink deleted the runtime-correctness-performance-hardening branch August 30, 2026 16:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10b6d3f524

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/xstate/actor.py
# Match XState: a stopped actor does not restart.
return self
effective = initial_state if initial_state is not None else self._snapshot
self._backend.start(cast(Any, effective))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Release the lifecycle lock before starting actor logic

When an invoked from_callback implementation starts a worker, waits for it, and that worker calls send_back, startup deadlocks: the parent holds _lifecycle_lock throughout invocation reconciliation and child.start(), while the worker blocks in parent.send() waiting for the same lock. This callback pattern completed before this change, so lifecycle state should be reserved under the lock without holding it across backend startup or other user-provided actor logic.

AGENTS.md reference: AGENTS.md:L70-L71

Useful? React with 👍 / 👎.

Comment on lines +173 to +176
current_task = asyncio.current_task()
pending = [task for task in tasks if task is not current_task]
if pending:
await asyncio.gather(*pending, return_exceptions=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid cancelling the task that is calling stop

When await service.stop() is called from an action reached through a delayed send, and at least one other timer is pending, the current delayed-send task is cancelled here and the cancellation is delivered at the new await asyncio.gather(...). Consequently stop() never returns normally and the remainder of the user's action is skipped; excluding the current task only from pending is insufficient because it was already cancelled. Exclude the current task from cancellation as well.

AGENTS.md reference: AGENTS.md:L68-L69

Useful? React with 👍 / 👎.

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