Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,4 @@ Directory.Build.user.props
.claude/

src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/
.work/
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dotnet test src/HotChocolate/Fusion
- No vacuous assertions (`Assert.NotNull` alone is not a test)
- If you need 8 stubs + reflection, you're at the wrong test tier
- Do not use em dash style sentences in docs, comments, or XML documentation. Use commas, periods, parentheses, or colons instead.
- XML docs should describe the contract and concepts, not internals like pooling, iteration mechanics or leak other implementation detail.

### Testing

Expand Down
13 changes: 1 addition & 12 deletions src/HotChocolate/Core/src/Execution.Abstractions/Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,11 @@ public Path Append(string name)
/// <returns>
/// Returns the new path segment.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Appending an indexer on the root segment is not allowed.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// The index must be greater than or equal to zero.
/// </exception>
public Path Append(int index)
{
if (this is RootPathSegment)
{
throw new InvalidOperationException(
"Appending an indexer on the root segment is not allowed.");
}

return new IndexerPathSegment(this, index);
}
=> new IndexerPathSegment(this, index);

/// <summary>
/// Appends another path to this path.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using BenchmarkDotNet.Attributes;
using HotChocolate.Execution;
using HotChocolate.Fusion.Execution;
using HotChocolate.Fusion.Execution.Nodes;
using HotChocolate.Fusion.Execution.Results;
using HotChocolate.Fusion.Language;
using HotChocolate.Fusion.Text.Json;
using HotChocolate.Language;
using FusionNameNode = HotChocolate.Fusion.Language.NameNode;

namespace Fusion.Execution.Benchmarks;

/// <summary>
/// Compares the cost of the defer-only snapshot variable merge path
/// (<see cref="FetchResultStore.CreateVariableValueSetsFromSnapshot"/>) against
/// the existing non-defer variable creation path. The wholesale Case D return
/// is included to confirm it stays allocation-free.
/// </summary>
[MemoryDiagnoser]
[InProcess]
public class VariableMergingBenchmark
{
private static readonly IReadOnlyList<ObjectFieldNode> s_oneForwardedVariable =
[new ObjectFieldNode("limit", new IntValueNode(10))];

private static readonly IReadOnlyList<ObjectFieldNode> s_noForwardedVariables = [];

private static readonly OperationRequirement[] s_singleRequirement =
[Requirement("__fusion_1_id")];

private static readonly HashSet<string> s_oneImportedKey =
new(["__fusion_1_id"], StringComparer.Ordinal);

private static readonly HashSet<string> s_twoImportedKeys =
new(["__fusion_1_id", "__fusion_2_sku"], StringComparer.Ordinal);

private FetchResultStore _baselineStore = null!;
private FetchResultStore _snapshotStore = null!;

private ImmutableArray<VariableValues> _singleEntrySnapshot;
private ImmutableArray<VariableValues> _subsetEntrySnapshot;
private ImmutableArray<VariableValues> _list10Snapshot;
private ImmutableArray<VariableValues> _list100Snapshot;
private ImmutableArray<VariableValues> _list1000Snapshot;

private ImmutableArray<VariableValues> _caseDSnapshot;

[GlobalSetup]
public void Setup()
{
_baselineStore = new FetchResultStore();
_snapshotStore = new FetchResultStore();

// The "source" store mints VariableValues entries that the snapshot
// merge consumes as imported parent rows. A separate store mirrors how
// a deferred incremental plan imports values from its parent before resolving.
var source = new FetchResultStore();

_singleEntrySnapshot =
[
CreateImportedEntry(
source,
CompactPath.Root,
Field("__fusion_1_id", new StringValueNode("1")))
];

_subsetEntrySnapshot =
[
CreateImportedEntry(
source,
CompactPath.Root,
Field("__fusion_1_id", new StringValueNode("1")),
Field("__fusion_2_sku", new StringValueNode("sku-1")))
];

_list10Snapshot = BuildListSnapshot(source, count: 10);
_list100Snapshot = BuildListSnapshot(source, count: 100);
_list1000Snapshot = BuildListSnapshot(source, count: 1000);

// Case D returns the full imported snapshot wholesale. Use a populated
// entry so the field read is the only cost being measured.
_caseDSnapshot = _singleEntrySnapshot;
}

[GlobalCleanup]
public void Cleanup()
{
_baselineStore.Dispose();
_snapshotStore.Dispose();
}

/// <summary>
/// Existing non-defer per-entry write path through the same
/// <see cref="FetchResultStore"/> writer used by the general overloads.
/// Acts as the per-entry baseline against which the snapshot merge cost is
/// compared. The full non-defer overload also walks the result document via
/// CollectTargetElements, which depends on a populated CompositeResultDocument
/// and is intentionally outside the scope of this benchmark.
/// </summary>
[Benchmark(Baseline = true)]
public VariableValues NonDefer_Baseline()
=> _baselineStore.CreateVariableValueSets(CompactPath.Root, s_oneForwardedVariable);

/// <summary>
/// Case D: all requested keys are imported, no forwarded variables, and
/// the requested set equals the imported set. The runtime returns the
/// imported snapshot wholesale, so the only cost is the field read.
/// </summary>
[Benchmark]
public ImmutableArray<VariableValues> Defer_CaseD_WholesaleReturn()
=> _caseDSnapshot;

/// <summary>
/// Case E with one forwarded variable and one requested requirement.
/// </summary>
[Benchmark]
public ImmutableArray<VariableValues> Defer_Snapshot_OneForwardedOneRequirement()
=> _snapshotStore.CreateVariableValueSetsFromSnapshot(
_singleEntrySnapshot,
s_oneImportedKey,
s_oneForwardedVariable,
s_singleRequirement);

/// <summary>
/// Case E strict subset filter. The imported snapshot carries two keys but
/// only one is requested, so the merge writes a smaller variable object.
/// </summary>
[Benchmark]
public ImmutableArray<VariableValues> Defer_Snapshot_StrictSubset()
=> _snapshotStore.CreateVariableValueSetsFromSnapshot(
_subsetEntrySnapshot,
s_twoImportedKeys,
s_noForwardedVariables,
s_singleRequirement);

[Benchmark]
public ImmutableArray<VariableValues> Defer_Snapshot_List_N10()
=> _snapshotStore.CreateVariableValueSetsFromSnapshot(
_list10Snapshot,
s_oneImportedKey,
s_noForwardedVariables,
s_singleRequirement);

[Benchmark]
public ImmutableArray<VariableValues> Defer_Snapshot_List_N100()
=> _snapshotStore.CreateVariableValueSetsFromSnapshot(
_list100Snapshot,
s_oneImportedKey,
s_noForwardedVariables,
s_singleRequirement);

[Benchmark]
public ImmutableArray<VariableValues> Defer_Snapshot_List_N1000()
=> _snapshotStore.CreateVariableValueSetsFromSnapshot(
_list1000Snapshot,
s_oneImportedKey,
s_noForwardedVariables,
s_singleRequirement);

private static ImmutableArray<VariableValues> BuildListSnapshot(
FetchResultStore source,
int count)
{
var builder = ImmutableArray.CreateBuilder<VariableValues>(count);

for (var i = 0; i < count; i++)
{
builder.Add(
CreateImportedEntry(
source,
Path(i),
Field("__fusion_1_id", new StringValueNode(i.ToString()))));
}

return builder.MoveToImmutable();
}

private static VariableValues CreateImportedEntry(
FetchResultStore store,
CompactPath path,
params ObjectFieldNode[] fields)
=> store.CreateVariableValueSets(path, fields);

private static ObjectFieldNode Field(string name, IValueNode value)
=> new(name, value);

private static OperationRequirement Requirement(string key)
=> new(
key,
new NamedTypeNode("String"),
SelectionPath.Root,
new PathNode(new PathSegmentNode(new FusionNameNode(key))));

private static CompactPath Path(params int[] segments)
{
var buffer = new int[segments.Length + 1];
buffer[0] = segments.Length;
segments.CopyTo(buffer.AsSpan(1));
return new CompactPath(buffer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public bool IsNodeSkipped(int nodeId)
&& (_failedOrSkippedBitset[index] & (1UL << (nodeId & 63))) != 0;
}

public void FillBacklog(OperationPlan plan)
public void FillBacklog(IOperationPlan plan)
{
_ready.Clear();
_backlogCount = 0;
Expand Down Expand Up @@ -180,7 +180,7 @@ public void CancelProcessing()
}

public void CompleteNode(
OperationPlan plan,
IOperationPlan plan,
ExecutionNode node,
ExecutionNodeResult result)
{
Expand Down Expand Up @@ -279,7 +279,7 @@ public void CompleteNode(
}
}

public void SkipNode(OperationPlan plan, ExecutionNode node)
public void SkipNode(IOperationPlan plan, ExecutionNode node)
{
_stack.Clear();
_stack.Push(node);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using HotChocolate.Execution;

namespace HotChocolate.Fusion.Execution.Nodes;

/// <summary>
/// Represents one delivery group introduced by a <c>@defer</c> directive.
/// Delivery groups form a parent chain for nested deferred fragments and are
/// referenced by selections when computing active delivery group sets.
/// </summary>
/// <param name="Label">
/// The optional label from <c>@defer(label: "...")</c>.
/// </param>
/// <param name="Parent">
/// The enclosing delivery group when this <c>@defer</c> is nested inside
/// another deferred fragment, or <c>null</c> for a top-level defer.
/// </param>
/// <param name="DeferConditionIndex">
/// The index into the <see cref="DeferConditionCollection"/> for the <c>if</c> condition
/// associated with this defer directive. This index maps to a bit position in the
/// runtime defer flags bitmask.
/// </param>
public sealed record DeliveryGroup(
string? Label,
DeliveryGroup? Parent,
byte DeferConditionIndex)
{
/// <summary>
/// A plan-stable numeric identifier for this delivery group.
/// </summary>
public int Id { get; init; } = -1;

/// <summary>
/// The selection path to the object whose selection set contains this
/// <c>@defer</c>.
/// </summary>
public SelectionPath? Path { get; init; }

/// <summary>
/// The variable name from <c>@defer(if: $var)</c>, or <c>null</c> when this
/// defer is unconditional. Runtime activation of this defer uses this variable
/// together with <see cref="DeferConditionIndex"/>.
/// </summary>
public string? IfVariable { get; init; }
}
Loading
Loading