Skip to content

perf(grain): move runtime access to grain context - #10117

Open
ReubenBond wants to merge 4 commits into
dotnet:mainfrom
ReubenBond:rebond/lazy-grain-runtime-access
Open

perf(grain): move runtime access to grain context#10117
ReubenBond wants to merge 4 commits into
dotnet:mainfrom
ReubenBond:rebond/lazy-grain-runtime-access

Conversation

@ReubenBond

@ReubenBond ReubenBond commented May 17, 2026

Copy link
Copy Markdown
Member

Reason

Each Grain instance retained an IGrainRuntime reference even though its activation context already owns access to the configured runtime. Keeping runtime access on the grain duplicated state across every activation.

Solution

Add IGrainContext.GrainRuntime and make Grain delegate runtime access to its context. Runtime contexts expose the configured shared runtime directly, while the default interface implementation supports custom contexts through an IGrainRuntime component or activation-service registration.

The protected Grain(IGrainContext, IGrainRuntime?) constructor now installs the supplied test runtime through IGrainContext.GrainRuntime. ActivationData and GrainTypeSharedContext use dedicated, safely published override fields so normal runtime access performs field reads rather than component dictionary or DI lookups. Stateless-worker contexts delegate to the same shared runtime.

Required runtime access retains the deterministic InvalidOperationException when no runtime is available. Optional direct-construction behavior remains compatible: ServiceProvider can be null and RuntimeIdentity returns an empty string.

Measured tradeoff

BenchmarkDotNet on .NET 10 measured the eager grain shape at 32 bytes and the context-owned shape at 24 bytes. Retained managed memory scaled from approximately 40 to 32 bytes per array-held grain, confirming an 8-byte grain-object reduction: about 8 MB per million live grain objects, 24 MB at three million, and 48 MB at six million.

Activations whose ActivationDataExtra is allocated also carry an 8-byte optional runtime-override slot, and each grain type has one additional shared override slot. Consequently, the full object-graph saving is approximately 8 × (grains without extras) - 8 × grain types; stateless-worker activations always have extras, so their total retained-memory saving is effectively zero.

Construction improved from 19.24 ns to 10.90 ns when runtime was never accessed and from 18.85 ns to 9.80 ns including first access. Cached ordinary access measured 0.97 ns versus 1.48 ns; the stateless/extras path measured 0.97 ns versus 1.87 ns. All paths remain allocation-free after construction.

OrleansTestKit migration

Current OrleansTestKit versions reflect the removed Grain.Runtime backing field. TestKit must instead set context.GrainRuntime (or register an IGrainRuntime component/service) before grain creation and remove the backing-field reflection. The new context API includes a setter specifically for this test override seam.

Focused coverage exercises component and DI fallback precedence, standard mock contexts, explicit runtime injection, concurrent access, unavailable-runtime diagnostics, direct construction, lifecycle callbacks, field removal, and the existing stateless-worker runtime path.

@ReubenBond ReubenBond changed the title Lazily resolve grain runtime perf(grain): lazily resolve grain runtime May 29, 2026
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from 72c7709 to 91ec0aa Compare August 21, 2026 21:43
Copilot AI lite review requested due to automatic review settings August 21, 2026 21:43

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-grain instance overhead by deferring IGrainRuntime resolution until first use, retrieving it via grain-context component lookup (with an activation-services fallback) and caching the result. This aligns runtime resolution across activation types while avoiding eagerly retaining a runtime reference for grains which never use runtime-backed helpers.

Changes:

  • Updated Grain to lazily resolve and cache IGrainRuntime on first access instead of resolving in the constructor.
  • Exposed IGrainRuntime via GrainTypeSharedContext’s component lookup to support context-based runtime resolution.
  • Added focused regression tests covering lazy resolution, caching, explicit injection, and the activation-services fallback path.
Show a summary per file
File Description
src/Orleans.Core.Abstractions/Core/Grain.cs Switches IGrainRuntime handling to lazy resolution with per-instance caching.
src/Orleans.Runtime/Catalog/GrainTypeSharedContext.cs Makes IGrainRuntime available through shared-context GetComponent lookups.
test/Orleans.Core.Tests/Runtime/GrainRuntimeResolutionTests.cs Adds regression coverage for lazy runtime resolution behavior and fallbacks.

Review details

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

  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

@ReubenBond
ReubenBond marked this pull request as ready for review August 21, 2026 22:31
Copilot AI review requested due to automatic review settings August 23, 2026 16:15
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from 91ec0aa to b9f31b6 Compare August 23, 2026 16:15

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread test/Orleans.Core.Tests/Runtime/GrainRuntimeResolutionTests.cs Outdated
Copilot AI review requested due to automatic review settings August 23, 2026 16:29
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from b9f31b6 to 4df63b3 Compare August 23, 2026 16:29

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/Orleans.Core.Abstractions/Core/Grain.cs Outdated
Comment thread src/Orleans.Core.Abstractions/Core/Grain.cs Outdated
Copilot AI review requested due to automatic review settings August 23, 2026 16:41
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from 4df63b3 to e7eb8d3 Compare August 23, 2026 16:41

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ReubenBond

Copy link
Copy Markdown
Member Author

The current head has deterministic Functional test failures in existing scheduler tests. OrleansTaskSchedulerAdvancedTests_Set2.ActivationSched_Test1_Bounce and ActivationSched_Test1 construct a grain outside the Orleans activation path and now throw from Grain.ResolveGrainRuntime():

InvalidOperationException: Grain was created outside of the Orleans creation process and no runtime was specified.

Failed run: https://github.com/dotnet/orleans/actions/runs/32652451640

The lazy runtime change needs to preserve the supported scheduler-test/manual-grain setup or the affected test fixture must explicitly provide the intended runtime.

Copilot AI review requested due to automatic review settings August 23, 2026 17:25
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from e7eb8d3 to 0742784 Compare August 23, 2026 17:25

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/Orleans.Core.Abstractions/Core/Grain.cs Outdated
Copilot AI review requested due to automatic review settings August 23, 2026 17:36
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from 0742784 to 91c87a1 Compare August 23, 2026 17:36

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/Orleans.Core.Abstractions/Core/Grain.cs Outdated
@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Code coverage

77.51% line coverage - 97,363 / 125,614 lines

Coverage details

Copilot AI review requested due to automatic review settings August 23, 2026 17:58
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from 91c87a1 to 89af829 Compare August 23, 2026 17:58

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.

Review details

Suppressed comments (1)

src/Orleans.Core.Abstractions/Core/Grain.cs:36

  • The <exception> XML doc for GrainFactory says "activation process"/"no runtime was provided", but the thrown exception message (and tests) use "creation process"/"no runtime was specified". Aligning the documentation with the deterministic exception text will avoid confusion for consumers reading the API docs.
    /// <exception cref="InvalidOperationException">
    /// The grain was created outside of the Orleans activation process and no runtime was provided.
    /// </exception>
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread test/Orleans.Core.Tests/Runtime/GrainRuntimeResolutionTests.cs Outdated
Copilot AI review requested due to automatic review settings August 23, 2026 18:08
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from 89af829 to 0aa10e4 Compare August 23, 2026 18:08

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/Orleans.Core.Abstractions/Core/Grain.cs Outdated
Copilot AI review requested due to automatic review settings August 23, 2026 18:28
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from 0aa10e4 to ce03418 Compare August 23, 2026 18:28

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings August 24, 2026 02:29
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from ce03418 to 9b48c26 Compare August 24, 2026 02:29
@ReubenBond ReubenBond changed the title perf(grain): lazily resolve grain runtime perf(grain): move runtime access to grain context Aug 24, 2026

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.

Review details

Suppressed comments (1)

src/Orleans.Core.Abstractions/Core/Grain.cs:41

  • ServiceProvider is declared as non-nullable IServiceProvider, but its XML doc ("Null if this grain is not associated with a Runtime") and the new DirectConstruction_WithoutRuntime_PreservesOptionalRuntimeBehavior test both require it to be able to return null. Consider updating the nullability contract (eg, IServiceProvider? or [return: MaybeNull]) so implementers/consumers get correct nullable flow analysis, and update the generated API surface accordingly.
    /// <summary>
    /// Gets the IServiceProvider managed by the runtime. Null if this grain is not associated with a Runtime, such as when created directly for unit testing.
    /// </summary>
    protected internal IServiceProvider ServiceProvider => GrainContext?.ActivationServices ?? GrainContext?.GrainRuntime?.ServiceProvider!;

  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Move IGrainRuntime ownership from each Grain instance to IGrainContext, retaining component and service fallbacks for custom test contexts. Runtime activations expose the configured shared runtime directly and support explicit component overrides without dictionary lookups. This removes one reference from each Grain object while preserving direct-construction behavior and unit-test injection.
Copilot AI review requested due to automatic review settings August 28, 2026 23:28
@ReubenBond
ReubenBond force-pushed the rebond/lazy-grain-runtime-access branch from 9b48c26 to 36b45b8 Compare August 28, 2026 23:28

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.

Copilot review overview

Review tier: Lite
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity src/​Orleans.Runtime/​Catalog/​ActivationData.csActivationData.GrainRuntime reads _extras using a plain read. Since the runtime override can be…
Suppressed comments (1)

src/Orleans.Runtime/Catalog/ActivationData.cs:397

  • In SetComponent, the runtime override path assigns _extras = new() with a normal write and then relies on readers seeing it without synchronization. If GrainRuntime is meant to support concurrent override updates, publish the _extras reference with Volatile.Write (and prefer using a local extras variable) so readers using Volatile.Read(ref _extras) reliably observe the override container.
            if (componentType == typeof(IGrainRuntime))
            {
                if (_extras is null)
                {
                    if (instance is null)
                    {
                        return;
                    }

                    _extras = new();
                }

                Volatile.Write(ref _extras.GrainRuntime, (IGrainRuntime?)instance);
                return;
            }

Comment thread src/Orleans.Runtime/Catalog/ActivationData.cs Outdated
Copilot AI review requested due to automatic review settings August 29, 2026 09:52

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.

Copilot review overview

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 High severity

New issues introduced by this change (1)
Severity Finding
High severity src/​Orleans.Core.Abstractions/​Core/​Grain.csServiceProvider is declared as non-nullable IServiceProvider, but the implementation can return…
Issues resolved since last review (1)
Severity Finding
Medium severity src/​Orleans.Runtime/​Catalog/​ActivationData.csActivationData.GrainRuntime reads _extras using a plain read. Since the runtime override can be… View resolved comment

Comment thread src/Orleans.Core.Abstractions/Core/Grain.cs Outdated
Copilot AI review requested due to automatic review settings August 29, 2026 11:02

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.

Copilot review overview

🔵 Needs a closer look

Review tier: Lite
Findings: None

Issues resolved since last review (1)
Severity Finding
High severity src/​Orleans.Core.Abstractions/​Core/​Grain.csServiceProvider is declared as non-nullable IServiceProvider, but the implementation can return… View resolved comment

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