Skip to content

perf(runtime): reduce ActivationData async loop allocations#10071

Draft
ReubenBond wants to merge 1 commit into
dotnet:mainfrom
ReubenBond:split/activationdata-async-loop-allocations
Draft

perf(runtime): reduce ActivationData async loop allocations#10071
ReubenBond wants to merge 1 commit into
dotnet:mainfrom
ReubenBond:split/activationdata-async-loop-allocations

Conversation

@ReubenBond

@ReubenBond ReubenBond commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Replaces ActivationData's signal with WorkItemGroupWaiter, a reusable IValueTaskSource, so the request loop can wait without per-iteration task allocations.
  • Extends WorkItemGroup to queue callbacks directly, including SynchronizationContext.Post/QueueAction, while preserving task scheduling behavior.
  • Schedules activation startup through the activation WorkItemGroup and updates scheduler callback signatures for nullable state.
  • Does not include the separate ActivationData locking changes.

Validation

  • git diff --check
  • conflict-marker scan
  • dotnet build src\Orleans.Runtime\Orleans.Runtime.csproj -m
  • Scheduler tests: net8/net10, 32/32 passed
  • Activation tracing tests: net8, 21/21 passed

Dependencies / notes

  • Targets main; may need review/merge coordination with the ActivationData locking PR.
  • Missing direct lost-wakeup/concurrent signal-wait coverage for WorkItemGroupWaiter.
Microsoft Reviewers: Open in CodeFlow

@ReubenBond ReubenBond changed the title Remove allocations from ActivationData core async loop Reduce ActivationData async loop allocations Apr 30, 2026
@ReubenBond ReubenBond requested a review from Copilot April 30, 2026 04:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR reduces per-iteration allocations in Orleans’ activation message loop by replacing the activation work signal with a reusable IValueTaskSource and enhancing WorkItemGroup to queue and execute non-Task callbacks directly while preserving activation scheduler behavior.

Changes:

  • Introduces WorkItemGroupWaiter (a reusable IValueTaskSource) to avoid allocating tasks per activation-loop wait.
  • Extends WorkItemGroup to queue Task, SendOrPostCallback, and Action<object?> work items and to support SynchronizationContext.Post.
  • Updates activation startup scheduling to post directly to the activation’s WorkItemGroup and adjusts scheduler callback signatures for nullable state.
Show a summary per file
File Description
src/Orleans.Runtime/Scheduler/WorkItemGroupWaiter.cs Adds reusable single-waiter async signal which schedules continuations via WorkItemGroup.
src/Orleans.Runtime/Scheduler/WorkItemGroup.cs Changes queue from Task to multi-type work items; adds SynchronizationContext support and TaskScheduler.Current plumbing.
src/Orleans.Runtime/Scheduler/TaskSchedulerUtils.cs Updates QueueAction state nullability and routes QueueWorkItem through WorkItemGroup.QueueAction.
src/Orleans.Runtime/Scheduler/IWorkItem.cs Updates ExecuteWorkItem callback signature to nullable state.
src/Orleans.Runtime/Catalog/ActivationData.cs Replaces SingleWaiterAutoResetEvent with WorkItemGroupWaiter and exposes WorkItemGroup for activation startup scheduling.
src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs Posts activation startup directly to WorkItemGroup instead of allocating a scheduled task.
src/Orleans.Core.Abstractions/Core/IGrainContext.cs Updates IWorkItemScheduler.QueueAction nullability annotations for action/state.

Copilot's findings

Comments suppressed due to low confidence (2)

src/Orleans.Runtime/Scheduler/WorkItemGroupWaiter.cs:141

  • WaitAsync() always returns new ValueTask(this, 0) with a constant token. Even with the single-waiter restriction, adding token/version support would make incorrect usage (double-await, awaiting after reset, etc) fail deterministically and aligns with the existing SingleWaiterAutoResetEvent pattern in the repo.
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ValueTask WaitAsync()
    {
        // Indicate that there is a waiter.
        var status = Interlocked.Or(ref _status, WaitingFlag);

        // If there was already a waiter, that is an error since this class is designed for use with a single waiter.
        if ((status & WaitingFlag) == WaitingFlag)
        {
            ThrowConcurrentWaitersNotSupported();
        }

        // If the event was already signaled, immediately wake the waiter.
        if ((status & SignaledFlag) == SignaledFlag)
        {
            // Reset just the status because the _continuation has not been set.
            // We know that _continuation has not been set because it is only set when
            // Signal() observes that the "Waiting" flag had been set but not the "Signaled" flag.
            ResetStatus();
            return default;
        }

        return new(this, 0);
    }

src/Orleans.Runtime/Scheduler/WorkItemGroupWaiter.cs:113

  • This new synchronization primitive is performance-critical and has subtle concurrency/lost-wakeup failure modes. There are existing scheduler-focused tests in the repo, but there’s no targeted test coverage here for key races (signal-before-wait, signal-during-OnCompleted, rapid signal/reset cycles, etc). Adding focused unit tests for WorkItemGroupWaiter would help prevent regressions.
    /// <summary>
    /// Signal the waiter.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Signal()
    {
        if ((_status & SignaledFlag) == SignaledFlag)
        {
            // The event is already signaled.
            return;
        }

        // Set the signaled flag.
        var status = Interlocked.Or(ref _status, SignaledFlag);

        // If there was a waiter and the signaled flag was unset, wake the waiter now.
        if ((status & SignaledFlag) != SignaledFlag && (status & WaitingFlag) == WaitingFlag)
        {
            // Note that in this assert we are checking the volatile _status field.
            // This is a sanity check to ensure that the signaling conditions are true:
            // that "Signaled" and "Waiting" flags are both set.
            Debug.Assert((_status & (SignaledFlag | WaitingFlag)) == (SignaledFlag | WaitingFlag));
            SignalCompletion();
        }
    }
  • Files reviewed: 7/7 changed files
  • Comments generated: 3

Comment on lines 204 to +207
/// </summary>
/// <param name="action">The work item.</param>
/// <param name="state">The state passed when invoking the item.</param>
void QueueAction(Action<object> action, object state);
void QueueAction(Action<object?> action, object? state);

public void QueueAction(Action action) => TaskScheduler.QueueAction(action);
public void QueueAction(Action<object> action, object state) => TaskScheduler.QueueAction(action, state);
public void QueueAction(Action action) => EnqueueWorkItem(new WorkItem((Action<object?>)(static state => ((Action)state!)()), action));
Comment on lines +37 to +50
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token)
{
// We only support success completion (no exception/cancellation paths)
return Volatile.Read(ref _continuation) is null ? ValueTaskSourceStatus.Pending : ValueTaskSourceStatus.Succeeded;
}

void IValueTaskSource.OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags)
{
if (continuation is null)
{
ThrowArgumentNullException();
}

// We ignore flags (FlowExecutionContext, UseSchedulingContext) because we always schedule on WorkItemGroup
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ReubenBond ReubenBond force-pushed the split/activationdata-async-loop-allocations branch from e133cfb to f0db409 Compare April 30, 2026 15:33
@ReubenBond ReubenBond changed the title Reduce ActivationData async loop allocations perf(runtime): reduce ActivationData async loop allocations May 29, 2026
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