Skip to content

perf(coro): share direct-await execution contexts - #1035

Merged
Coldwings merged 2 commits into
mainfrom
perf/share-direct-await-context
Aug 13, 2026
Merged

perf(coro): share direct-await execution contexts#1035
Coldwings merged 2 commits into
mainfrom
perf/share-direct-await-context

Conversation

@Coldwings

Copy link
Copy Markdown
Owner

Description

Share one task_execution_context across ordinary directly awaited Elio task
frames, so transparent coroutine composition no longer allocates a context and
parent-cancellation callback per frame. The actual first await or runtime
handoff is authoritative; task creation ancestry is not.

task_scope() remains an explicit structured-cancellation boundary. Caller
cancellation propagates into it, scope cancellation does not poison its caller
after join, and the final user-affinity value flows back without transferring
active I/O pins.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Performance improvement (optimization that improves speed/memory usage)
  • Documentation (changes to documentation, comments, or examples)
  • Refactoring (code changes that neither fix bugs nor add features)
  • Tests (adding or modifying tests)
  • Build/CI (changes to build system, CI configuration, or dependencies)

Related Issues

Closes #1034
Related to #1032

Changes Made

Core Changes

  • Defer execution-context allocation for nested unstarted Elio tasks and bind
    transparent children to the actual Elio awaiter's context.
  • Materialize distinct contexts at scheduler/spawn/foreign-promise boundaries;
    keep raw try_spawn ownership safe if materialization throws.
  • Remove ordinary direct-await parent callback registration and affinity
    copy-back bookkeeping.
  • Preserve task_scope() isolation with one-way parent cancellation and final
    user-affinity propagation.
  • Add a direct-await chain microbenchmark, allocation/ownership/cancellation
    regressions, raw-resume coverage, and documentation across the API contract,
    reference, concepts, migration, performance, and changelog pages.

API Changes

Before: each directly awaited Elio task frame owned a distinct execution
context, linked cancellation from its parent, and copied affinity at await
boundaries.

After: ordinary directly awaited Elio frames share the logical-vthread root
context. Independent roots, foreign promises, and task_scope() use distinct
contexts.

Migration Guide

Use spawn()/go() when a helper requires a distinct cancellation or affinity
domain. A token retained from a completed transparent child now names the
surrounding logical vthread. Low-level integrations that inspect or raw-resume
a nested unstarted task must first materialize an independent context; normal
task awaits and runtime handoffs do this automatically.

Testing

Unit Tests

  • Added new tests for the changes
  • Updated existing tests if needed
  • All tests pass locally

Integration Tests

  • Tested with existing examples
  • Tested RPC deadline/session reuse and scheduler affinity regressions

Sanitizer Testing

  • Tested with ASAN (AddressSanitizer)
  • Tested with TSAN (ThreadSanitizer)
  • No new warnings or errors

Test Results

Normal: 812 test cases, 12,222 assertions passed
ASAN full: 811 test cases, 12,220 assertions passed; no sanitizer diagnostics
TSAN full: 810 passed, 1 conditionally skipped, 12,215 assertions passed; no sanitizer diagnostics
Final scope-boundary tests: 2 cases, 9 assertions passed under ASAN and TSAN
RPC near-deadline regression: 3/3 repeated runs passed after scope isolation

Pinned Release direct-await benchmark, five interleaved baseline/current pairs:

8-frame direct await: 141.80 ns/frame -> 46.45 ns/frame (-67.25%)

Checklist

Code Quality

  • My code follows the project's code style
  • I have added/updated comments for complex logic
  • I have removed any debug code, TODOs, or commented-out code
  • My changes generate no new warnings

Documentation

  • I have updated documentation
  • I have added a benchmark example
  • I have updated API documentation

Testing

  • I have added tests that prove the optimization and boundaries
  • New and existing unit tests pass locally with my changes
  • I have tested with ASAN and TSAN

Compatibility

  • I have documented the breaking behavioral changes
  • I have considered the impact on existing users
  • I have updated CHANGELOG.md

Performance

  • I have considered the performance impact
  • I have added benchmarks for performance-critical changes

Additional Notes

The common transparent direct-await path still contains no new allocation. A
rare raw-resumed deferred root materializes its context on the first nested
direct await to preserve low-level safety.

Reviewer Guidance

Areas requiring special attention:

  • First-start context binding across direct await, scheduler admission, spawn,
    task-group, foreign-promise, and raw-handle paths.
  • task_scope() cancellation isolation versus parent propagation and affinity
    continuity.
  • Context and cancellation-token lifetime after child frame completion.

Questions for reviewers:

  • Are any independent initial-admission paths missing context materialization?
  • Is the task_scope() exception to transparent sharing sufficiently explicit
    in code and documentation?

Copilot AI balanced review requested due to automatic review settings August 13, 2026 03:42

Copilot AI 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.

Pull request overview

Optimizes direct coroutine composition by sharing execution contexts while preserving isolation at runtime and structured-cancellation boundaries.

Changes:

  • Defers and shares contexts for direct Elio task awaits.
  • Preserves independent contexts for spawning, foreign promises, and task_scope().
  • Adds regression tests, benchmarks, and migration documentation.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
CHANGELOG.md Documents the breaking context semantics.
examples/microbench.cpp Benchmarks direct-await chains.
include/elio/coro/promise_base.hpp Implements deferred context binding and isolation.
include/elio/coro/task.hpp Shares contexts across direct awaits.
include/elio/coro/task_execution_context.hpp Updates logical-vthread context behavior.
include/elio/coro/task_group.hpp Isolates task_scope() contexts.
include/elio/runtime/scheduler.hpp Materializes contexts during scheduler admission.
include/elio/runtime/spawn.hpp Materializes contexts for spawn failures.
tests/unit/test_io.cpp Adapts raw-resume I/O coverage.
tests/unit/test_task_execution_context.cpp Tests sharing, allocation, and ownership.
tests/unit/test_task_group.cpp Tests scope cancellation isolation.
wiki/API-Contracts.md Updates public behavioral contracts.
wiki/API-Reference.md Documents context and scope semantics.
wiki/Core-Concepts.md Explains logical-vthread context sharing.
wiki/Migrating-to-0.6.md Provides migration guidance.
wiki/Performance-Tuning.md Describes allocation improvements.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread include/elio/runtime/scheduler.hpp
@Coldwings

Copy link
Copy Markdown
Owner Author

@copilot Please review the current PR head only. Do not make code changes.

Copilot AI commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

@copilot Please review the current PR head only. Do not make code changes.

Review of current PR head (commits 1134423 + 0222175). No code changes made.


Summary

The two commits together correctly implement context sharing for direct-await chains and address the original frame-ownership safety concern on context-materialization failure. The following is a read-only assessment of the current head.


scheduler::try_spawn / scheduler::spawn — ownership on allocation failure ✅

try_spawn calls ensure_independent_execution_context() inside the try_initial_admission_ lambda. The helper acquires a mutex via RAII, so an allocation exception propagates cleanly through the lock guard without taking any action on the handle. The handle is left live, matching the documented borrowed-handle contract.

spawn wraps try_spawn in a try/catch, destroys the handle before rethrowing, and also destroys on a false return. Both branches are correct.

object_cache::ensure_sweep_running — sweep-start retry ✅

ensure_independent_execution_context() is now called while t (the task<void>) still owns the frame, before task_access::release. If that throws:

  • t goes out of scope and its destructor reclaims the frame.
  • started_guard fires and restores sweep_started_ = false, allowing the next cache access to retry.

After release, the raw handle is unguarded. If try_spawn throws it is destroyed in the catch block; if it returns false it is destroyed in the else branch. All three failure modes are clean.

Context-sharing logic — await_suspend

For an Elio-to-Elio direct await, await_suspend calls parent->ensure_independent_execution_context() on the actual awaiter before binding. This materializes a context only when the parent itself is a raw-resumed deferred root without one. The normal composition path (parent already has a context) executes the if (!execution_context_) check and returns immediately — no allocation.

task_scope() isolation: bind_isolated_direct_await_context correctly materializes independent contexts for both parent and child, copies affinity without activating I/O pins, and registers one-way cancellation from parent. propagate_isolated_direct_await_policy_to_parent on await_resume flows user affinity back without transferring context ownership.

Test coverage ✅

test_task_execution_context.cpp tests:

  • try_spawn borrowed-handle: handle remains live after throw; caller-side destroy() confirms the frame was not leaked.
  • spawn consuming-handle: owned_observer.expired() after throw confirms the frame was destroyed before rethrowing.

test_object_cache.cpp tests:

  • Allocation failure on first sweep start: exception surfaces to the coroutine caller as std::bad_alloc, sweep_started_ is reset, and a subsequent cache.get completes normally. This exercises the full retry path.

Documentation ✅

API-Contracts.md — adds "Context materialization can allocate and propagate allocation failure from co_await before the child starts." This is accurate: allocation only occurs for deferred-root, foreign-promise, or task_scope() boundaries; normal Elio-to-Elio awaits are allocation-free.

API-Reference.md — the scheduler section correctly documents the asymmetric ownership semantics: try_spawn leaves a borrowed handle live on exception; spawn/spawn_to destroy the consumed handle before rethrowing.


Minor observations (no action required)

  • fail_next_task_execution_context_allocation_for_test is an inline atomic<bool> guarded by #ifdef ELIO_RUNTIME_TEST_HOOKS. The exchange in make_task_execution_context uses acq_rel, which is consistent with the release stores in the tests. ✅
  • The test for the borrowed-handle path calls borrowed_handle.destroy() explicitly after the failed try_spawn. This is correct by contract and the test commentary makes the responsibility clear. ✅
  • try_initial_admission_ holds shutdown_mutex_ during the lambda. Because ensure_independent_execution_context is a heap allocation with no scheduler interaction, this does not introduce a deadlock risk. ✅

No high-confidence bugs found in the current head.

@Coldwings
Coldwings merged commit 15f1d10 into main Aug 13, 2026
11 checks passed
@Coldwings
Coldwings deleted the perf/share-direct-await-context branch August 13, 2026 04:38
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.

[Performance] Share execution context across direct Elio task awaits

3 participants