From 615289d3ade4776378fab9f5772809a22534a3b5 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 25 Feb 2026 22:40:01 +0000 Subject: [PATCH 01/36] Fusion: reduce forwarded variable allocation overhead --- .../Fusion.Execution/Execution/OperationPlanContext.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs index 7d74b5a67c7..5c80d6785c3 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs @@ -369,15 +369,15 @@ internal OperationResult Complete(bool reusable = false) return operationResult; } - private List GetPathThroughVariables( + private IReadOnlyList GetPathThroughVariables( ReadOnlySpan forwardedVariables) { if (Variables.IsEmpty || forwardedVariables.Length == 0) { - return []; + return Array.Empty(); } - var variables = new List(); + var variables = new List(forwardedVariables.Length); foreach (var variableName in forwardedVariables) { @@ -400,7 +400,9 @@ private List GetPathThroughVariables( } } - return variables; + return variables.Count == 0 + ? Array.Empty() + : variables; } public ISourceSchemaClient GetClient(string schemaName, OperationType operationType) From 55cdf5f9a860d3457e2c2df93d649b2c891f0f56 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 25 Feb 2026 22:44:24 +0000 Subject: [PATCH 02/36] Fusion: reduce dispatcher batching submission overhead --- .../Clients/SourceSchemaRequestDispatcher.cs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs index c863c9d05f1..90cb20f7a7a 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs @@ -21,6 +21,11 @@ internal sealed class SourceSchemaRequestDispatcher : ISourceSchemaScheduler , ISourceSchemaDispatcher { + private const int NodeStateUnregistered = -1; + private const int NodeStatePending = 0; + private const int NodeStateSubmitted = 1; + private const int NodeStateSkipped = 2; + #if NET9_0_OR_GREATER private readonly Lock _sync = new(); #else @@ -179,7 +184,7 @@ public void RegisterGroup(int groupId, IReadOnlyList nodeIds) } _groupByNodeIdSlots[nodeId] = groupId; - _nodeStateSlots[nodeId] = 0; + _nodeStateSlots[nodeId] = NodeStatePending; } } } @@ -415,7 +420,7 @@ private void RemoveGroup(GroupState group) if ((uint)nodeId < (uint)_groupByNodeIdSlots.Length) { _groupByNodeIdSlots[nodeId] = -1; - _nodeStateSlots[nodeId] = -1; + _nodeStateSlots[nodeId] = NodeStateUnregistered; } } } @@ -432,7 +437,7 @@ private void ClearNodeIdSlots() if ((uint)nodeId < (uint)_groupByNodeIdSlots.Length) { _groupByNodeIdSlots[nodeId] = -1; - _nodeStateSlots[nodeId] = -1; + _nodeStateSlots[nodeId] = NodeStateUnregistered; } } @@ -456,7 +461,7 @@ private void EnsureNodeIdSlotCapacity(int minCapacity) var groupByNodeIdSlots = new int[newCapacity]; var nodeStateSlots = new int[newCapacity]; Array.Fill(groupByNodeIdSlots, -1); - Array.Fill(nodeStateSlots, -1); + Array.Fill(nodeStateSlots, NodeStateUnregistered); if (_groupByNodeIdSlots.Length > 0) { @@ -471,7 +476,7 @@ private void EnsureNodeIdSlotCapacity(int minCapacity) private sealed class GroupState(int id, int initialCapacity) { private readonly List _nodeIds = new(initialCapacity); - private readonly Dictionary _pendingRequests = new(initialCapacity); + private readonly List _pendingRequests = new(initialCapacity); private int _remainingNodes; private bool _dispatchCreated; @@ -479,7 +484,7 @@ private sealed class GroupState(int id, int initialCapacity) public IEnumerable NodeIds => _nodeIds; - public IEnumerable PendingRequests => _pendingRequests.Values; + public IEnumerable PendingRequests => _pendingRequests; public void RegisterNode(int nodeId) { @@ -493,8 +498,12 @@ public bool TrySubmit( out PendingRequest? pendingRequest) { var nodeId = request.Node.Id; + var nodeState = + (uint)nodeId < (uint)nodeStateSlots.Length + ? nodeStateSlots[nodeId] + : NodeStateUnregistered; - if (_pendingRequests.ContainsKey(nodeId)) + if (nodeState == NodeStateSubmitted) { throw new InvalidOperationException( string.Format( @@ -502,26 +511,27 @@ public bool TrySubmit( nodeId)); } - if ((uint)nodeId >= (uint)nodeStateSlots.Length || nodeStateSlots[nodeId] != 0) + if (nodeState != NodeStatePending) { pendingRequest = null; return false; } - nodeStateSlots[nodeId] = 1; + nodeStateSlots[nodeId] = NodeStateSubmitted; _remainingNodes--; pendingRequest = new PendingRequest(request); - _pendingRequests.Add(nodeId, pendingRequest); + _pendingRequests.Add(pendingRequest); return true; } public void Skip(int nodeId, int[] nodeStateSlots) { - if ((uint)nodeId < (uint)nodeStateSlots.Length && nodeStateSlots[nodeId] == 0) + if ((uint)nodeId < (uint)nodeStateSlots.Length + && nodeStateSlots[nodeId] == NodeStatePending) { - nodeStateSlots[nodeId] = 1; + nodeStateSlots[nodeId] = NodeStateSkipped; _remainingNodes--; } } @@ -542,7 +552,7 @@ public bool TryCreateDispatch(out ImmutableArray pendingRequests return true; } - pendingRequests = [.. _pendingRequests.Values]; + pendingRequests = [.. _pendingRequests]; return true; } } From 68e3f559abe2e27e808921643f60fdc3d076c8d7 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 25 Feb 2026 22:47:38 +0000 Subject: [PATCH 03/36] Fusion: fast-path single partial result merge --- .../Execution/Results/FetchResultStore.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index fe580a90a4a..11af87c3c21 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -104,6 +104,11 @@ public bool AddPartialResults( nameof(results)); } + if (results.Length == 1) + { + return AddSinglePartialResult(sourcePath, results[0], responseNames); + } + var dataElements = ArrayPool.Shared.Rent(results.Length); var errorTries = ArrayPool.Shared.Rent(results.Length); var dataElementsSpan = dataElements.AsSpan(0, results.Length); @@ -169,6 +174,35 @@ public bool AddPartialResults( } } + private bool AddSinglePartialResult( + SelectionPath sourcePath, + SourceSchemaResult result, + ReadOnlySpan responseNames) + { + _memory.Push(result); + + var errors = result.Errors; + var dataElement = GetDataElement(sourcePath, result.Data); + var errorTrie = GetErrorTrie(sourcePath, errors?.Trie); + + lock (_lock) + { + if (errors?.RootErrors is { Length: > 0 } rootErrors) + { + _errors ??= []; + _errors.AddRange(rootErrors); + } + + return SaveSafeResult( + _result.Data, + result.Path, + result.AdditionalPaths.AsSpan(), + dataElement, + errorTrie, + responseNames); + } + } + /// /// Adds partial root data to the result document. /// From fe5966b72811119f0af474c25c4dfe2eb6ac6bbb Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 25 Feb 2026 23:29:38 +0000 Subject: [PATCH 04/36] Fusion: avoid duplicate error lookups in operation result streams --- .../Execution/Nodes/OperationBatchExecutionNode.cs | 4 +++- .../Execution/Nodes/OperationExecutionNode.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs index c1dbbba1b4e..dff79d411e3 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs @@ -152,7 +152,9 @@ protected override async ValueTask OnExecuteAsync( { buffer[index++] = result; - if (result.HasErrors) + // Parsing errors here allows the result store to reuse the cached value + // and avoids a second document lookup per result. + if (result.Errors is not null) { hasSomeErrors = true; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs index 83ee1f473bb..a06cc1e621d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs @@ -159,7 +159,9 @@ protected override async ValueTask OnExecuteAsync( { buffer[index++] = result; - if (result.HasErrors) + // Parsing errors here allows the result store to reuse the cached value + // and avoids a second document lookup per result. + if (result.Errors is not null) { hasSomeErrors = true; } From 4ad7bcdf5f4a4572f4bf890f415db650f3d7b814 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 25 Feb 2026 23:32:59 +0000 Subject: [PATCH 05/36] Fusion: skip error merge overhead when source results are clean --- .../Nodes/OperationBatchExecutionNode.cs | 6 +- .../Execution/Nodes/OperationExecutionNode.cs | 6 +- .../Execution/OperationPlanContext.cs | 6 +- .../Execution/Results/FetchResultStore.cs | 81 +++++++++++++++++++ 4 files changed, 95 insertions(+), 4 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs index dff79d411e3..1f69eaf4a55 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs @@ -191,7 +191,11 @@ protected override async ValueTask OnExecuteAsync( try { - context.AddPartialResults(_source, buffer.AsSpan(0, index), _responseNames); + context.AddPartialResults( + _source, + buffer.AsSpan(0, index), + _responseNames, + hasSomeErrors); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs index a06cc1e621d..69495e8138c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs @@ -198,7 +198,11 @@ protected override async ValueTask OnExecuteAsync( try { - context.AddPartialResults(_source, buffer.AsSpan(0, index), _responseNames); + context.AddPartialResults( + _source, + buffer.AsSpan(0, index), + _responseNames, + hasSomeErrors); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs index 5c80d6785c3..e7b6234b9fd 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs @@ -260,9 +260,11 @@ internal ImmutableArray CreateVariableValueSets( internal void AddPartialResults( SelectionPath sourcePath, ReadOnlySpan results, - ReadOnlySpan responseNames) + ReadOnlySpan responseNames, + bool containsErrors = true) { - var canExecutionContinue = _resultStore.AddPartialResults(sourcePath, results, responseNames); + var canExecutionContinue = + _resultStore.AddPartialResults(sourcePath, results, responseNames, containsErrors); if (!canExecutionContinue) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 11af87c3c21..b4d74147c84 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -93,6 +93,13 @@ public bool AddPartialResults( SelectionPath sourcePath, ReadOnlySpan results, ReadOnlySpan responseNames) + => AddPartialResults(sourcePath, results, responseNames, containsErrors: true); + + public bool AddPartialResults( + SelectionPath sourcePath, + ReadOnlySpan results, + ReadOnlySpan responseNames, + bool containsErrors) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(sourcePath); @@ -104,6 +111,13 @@ public bool AddPartialResults( nameof(results)); } + if (!containsErrors) + { + return results.Length == 1 + ? AddSinglePartialResultNoErrors(sourcePath, results[0], responseNames) + : AddPartialResultsNoErrors(sourcePath, results, responseNames); + } + if (results.Length == 1) { return AddSinglePartialResult(sourcePath, results[0], responseNames); @@ -174,6 +188,53 @@ public bool AddPartialResults( } } + private bool AddPartialResultsNoErrors( + SelectionPath sourcePath, + ReadOnlySpan results, + ReadOnlySpan responseNames) + { + var dataElements = ArrayPool.Shared.Rent(results.Length); + var dataElementsSpan = dataElements.AsSpan(0, results.Length); + + try + { + for (var i = 0; i < results.Length; i++) + { + var result = results[i]; + _memory.Push(result); + dataElementsSpan[i] = GetDataElement(sourcePath, result.Data); + } + + lock (_lock) + { + var resultData = _result.Data; + + for (var i = 0; i < results.Length; i++) + { + var result = results[i]; + + if (!SaveSafeResult( + resultData, + result.Path, + result.AdditionalPaths.AsSpan(), + dataElementsSpan[i], + errorTrie: null, + responseNames)) + { + return false; + } + } + } + + return true; + } + finally + { + dataElementsSpan.Clear(); + ArrayPool.Shared.Return(dataElements); + } + } + private bool AddSinglePartialResult( SelectionPath sourcePath, SourceSchemaResult result, @@ -203,6 +264,26 @@ private bool AddSinglePartialResult( } } + private bool AddSinglePartialResultNoErrors( + SelectionPath sourcePath, + SourceSchemaResult result, + ReadOnlySpan responseNames) + { + _memory.Push(result); + var dataElement = GetDataElement(sourcePath, result.Data); + + lock (_lock) + { + return SaveSafeResult( + _result.Data, + result.Path, + result.AdditionalPaths.AsSpan(), + dataElement, + errorTrie: null, + responseNames); + } + } + /// /// Adds partial root data to the result document. /// From b208de12a140f79725da146820d36b1bcee99291 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 09:30:42 +0000 Subject: [PATCH 06/36] Fusion: avoid pooled buffers for single source responses --- .../Nodes/OperationBatchExecutionNode.cs | 83 +++++++++++++++---- .../Execution/Nodes/OperationExecutionNode.cs | 83 +++++++++++++++---- .../Execution/OperationPlanContext.cs | 15 ++++ .../Execution/Results/FetchResultStore.cs | 17 +++- 4 files changed, 161 insertions(+), 37 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs index 1f69eaf4a55..76f9fce0223 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs @@ -127,6 +127,7 @@ protected override async ValueTask OnExecuteAsync( var index = 0; var bufferLength = 0; SourceSchemaResult[]? buffer = null; + SourceSchemaResult? singleResult = null; var hasSomeErrors = false; try @@ -138,19 +139,29 @@ protected override async ValueTask OnExecuteAsync( context.TrackSourceSchemaClientResponse(this, response); // we read the responses from the response stream. - var totalPathCount = variables.Length; - - for (var i = 0; i < variables.Length; i++) - { - totalPathCount += variables[i].AdditionalPaths.Length; - } - - bufferLength = Math.Max(totalPathCount, 1); - buffer = ArrayPool.Shared.Rent(bufferLength); - await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { - buffer[index++] = result; + if (buffer is null) + { + if (singleResult is null) + { + singleResult = result; + index = 1; + } + else + { + bufferLength = Math.Max(GetExpectedResultCount(variables), 2); + buffer = ArrayPool.Shared.Rent(bufferLength); + buffer[0] = singleResult; + buffer[1] = result; + singleResult = null; + index = 2; + } + } + else + { + buffer[index++] = result; + } // Parsing errors here allows the result store to reuse the cached value // and avoids a second document lookup per result. @@ -184,6 +195,10 @@ protected override async ValueTask OnExecuteAsync( buffer.AsSpan(0, index).Clear(); ArrayPool.Shared.Return(buffer); } + else + { + singleResult?.Dispose(); + } AddErrors(context, exception, variables, _responseNames); return ExecutionStatus.Failed; @@ -191,11 +206,28 @@ protected override async ValueTask OnExecuteAsync( try { - context.AddPartialResults( - _source, - buffer.AsSpan(0, index), - _responseNames, - hasSomeErrors); + if (buffer is null) + { + if (singleResult is null) + { + throw new InvalidOperationException("Expected at least one source schema result."); + } + + context.AddPartialResult( + _source, + singleResult, + _responseNames, + hasSomeErrors); + singleResult = null; + } + else + { + context.AddPartialResults( + _source, + buffer.AsSpan(0, index), + _responseNames, + hasSomeErrors); + } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -212,8 +244,11 @@ protected override async ValueTask OnExecuteAsync( } finally { - buffer.AsSpan(0, index).Clear(); - ArrayPool.Shared.Return(buffer); + if (buffer is not null) + { + buffer.AsSpan(0, index).Clear(); + ArrayPool.Shared.Return(buffer); + } } return hasSomeErrors ? ExecutionStatus.PartialSuccess : ExecutionStatus.Success; @@ -271,4 +306,16 @@ private static void AddErrors( } } } + + private static int GetExpectedResultCount(ImmutableArray variables) + { + var totalPathCount = variables.Length; + + for (var i = 0; i < variables.Length; i++) + { + totalPathCount += variables[i].AdditionalPaths.Length; + } + + return totalPathCount; + } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs index 69495e8138c..5fd23e79494 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs @@ -134,6 +134,7 @@ protected override async ValueTask OnExecuteAsync( var index = 0; var bufferLength = 0; SourceSchemaResult[]? buffer = null; + SourceSchemaResult? singleResult = null; var hasSomeErrors = false; try @@ -145,19 +146,29 @@ protected override async ValueTask OnExecuteAsync( context.TrackSourceSchemaClientResponse(this, response); // we read the responses from the response stream. - var totalPathCount = variables.Length; - - for (var i = 0; i < variables.Length; i++) - { - totalPathCount += variables[i].AdditionalPaths.Length; - } - - bufferLength = Math.Max(totalPathCount, 1); - buffer = ArrayPool.Shared.Rent(bufferLength); - await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { - buffer[index++] = result; + if (buffer is null) + { + if (singleResult is null) + { + singleResult = result; + index = 1; + } + else + { + bufferLength = Math.Max(GetExpectedResultCount(variables), 2); + buffer = ArrayPool.Shared.Rent(bufferLength); + buffer[0] = singleResult; + buffer[1] = result; + singleResult = null; + index = 2; + } + } + else + { + buffer[index++] = result; + } // Parsing errors here allows the result store to reuse the cached value // and avoids a second document lookup per result. @@ -191,6 +202,10 @@ protected override async ValueTask OnExecuteAsync( buffer.AsSpan(0, index).Clear(); ArrayPool.Shared.Return(buffer); } + else + { + singleResult?.Dispose(); + } AddErrors(context, exception, variables, _responseNames); return ExecutionStatus.Failed; @@ -198,11 +213,28 @@ protected override async ValueTask OnExecuteAsync( try { - context.AddPartialResults( - _source, - buffer.AsSpan(0, index), - _responseNames, - hasSomeErrors); + if (buffer is null) + { + if (singleResult is null) + { + throw new InvalidOperationException("Expected at least one source schema result."); + } + + context.AddPartialResult( + _source, + singleResult, + _responseNames, + hasSomeErrors); + singleResult = null; + } + else + { + context.AddPartialResults( + _source, + buffer.AsSpan(0, index), + _responseNames, + hasSomeErrors); + } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -219,8 +251,11 @@ protected override async ValueTask OnExecuteAsync( } finally { - buffer.AsSpan(0, index).Clear(); - ArrayPool.Shared.Return(buffer); + if (buffer is not null) + { + buffer.AsSpan(0, index).Clear(); + ArrayPool.Shared.Return(buffer); + } } return hasSomeErrors ? ExecutionStatus.PartialSuccess : ExecutionStatus.Success; @@ -324,6 +359,18 @@ private static void AddErrors( } } + private static int GetExpectedResultCount(ImmutableArray variables) + { + var totalPathCount = variables.Length; + + for (var i = 0; i < variables.Length; i++) + { + totalPathCount += variables[i].AdditionalPaths.Length; + } + + return totalPathCount; + } + private sealed class SubscriptionEnumerable : IAsyncEnumerable { private readonly OperationPlanContext _context; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs index e7b6234b9fd..9bcf6c5b0ca 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs @@ -272,6 +272,21 @@ internal void AddPartialResults( } } + internal void AddPartialResult( + SelectionPath sourcePath, + SourceSchemaResult result, + ReadOnlySpan responseNames, + bool containsErrors = true) + { + var canExecutionContinue = + _resultStore.AddPartialResult(sourcePath, result, responseNames, containsErrors); + + if (!canExecutionContinue) + { + ExecutionState.CancelProcessing(); + } + } + internal void AddPartialResults(SourceResultDocument result, ReadOnlySpan responseNames) { var canExecutionContinue = _resultStore.AddPartialResults(result, responseNames); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index b4d74147c84..443f9d8bf5b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -95,6 +95,21 @@ public bool AddPartialResults( ReadOnlySpan responseNames) => AddPartialResults(sourcePath, results, responseNames, containsErrors: true); + public bool AddPartialResult( + SelectionPath sourcePath, + SourceSchemaResult result, + ReadOnlySpan responseNames, + bool containsErrors = true) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(sourcePath); + ArgumentNullException.ThrowIfNull(result); + + return containsErrors + ? AddSinglePartialResult(sourcePath, result, responseNames) + : AddSinglePartialResultNoErrors(sourcePath, result, responseNames); + } + public bool AddPartialResults( SelectionPath sourcePath, ReadOnlySpan results, @@ -120,7 +135,7 @@ public bool AddPartialResults( if (results.Length == 1) { - return AddSinglePartialResult(sourcePath, results[0], responseNames); + return AddPartialResult(sourcePath, results[0], responseNames); } var dataElements = ArrayPool.Shared.Rent(results.Length); From eb9eacd461a3fb24a0e0bd8afe431d5959787560 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 09:38:59 +0000 Subject: [PATCH 07/36] Revert "Fusion: avoid pooled buffers for single source responses" This reverts commit b208de12a140f79725da146820d36b1bcee99291. --- .../Nodes/OperationBatchExecutionNode.cs | 83 ++++--------------- .../Execution/Nodes/OperationExecutionNode.cs | 83 ++++--------------- .../Execution/OperationPlanContext.cs | 15 ---- .../Execution/Results/FetchResultStore.cs | 17 +--- 4 files changed, 37 insertions(+), 161 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs index 76f9fce0223..1f69eaf4a55 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs @@ -127,7 +127,6 @@ protected override async ValueTask OnExecuteAsync( var index = 0; var bufferLength = 0; SourceSchemaResult[]? buffer = null; - SourceSchemaResult? singleResult = null; var hasSomeErrors = false; try @@ -139,29 +138,19 @@ protected override async ValueTask OnExecuteAsync( context.TrackSourceSchemaClientResponse(this, response); // we read the responses from the response stream. + var totalPathCount = variables.Length; + + for (var i = 0; i < variables.Length; i++) + { + totalPathCount += variables[i].AdditionalPaths.Length; + } + + bufferLength = Math.Max(totalPathCount, 1); + buffer = ArrayPool.Shared.Rent(bufferLength); + await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { - if (buffer is null) - { - if (singleResult is null) - { - singleResult = result; - index = 1; - } - else - { - bufferLength = Math.Max(GetExpectedResultCount(variables), 2); - buffer = ArrayPool.Shared.Rent(bufferLength); - buffer[0] = singleResult; - buffer[1] = result; - singleResult = null; - index = 2; - } - } - else - { - buffer[index++] = result; - } + buffer[index++] = result; // Parsing errors here allows the result store to reuse the cached value // and avoids a second document lookup per result. @@ -195,10 +184,6 @@ protected override async ValueTask OnExecuteAsync( buffer.AsSpan(0, index).Clear(); ArrayPool.Shared.Return(buffer); } - else - { - singleResult?.Dispose(); - } AddErrors(context, exception, variables, _responseNames); return ExecutionStatus.Failed; @@ -206,28 +191,11 @@ protected override async ValueTask OnExecuteAsync( try { - if (buffer is null) - { - if (singleResult is null) - { - throw new InvalidOperationException("Expected at least one source schema result."); - } - - context.AddPartialResult( - _source, - singleResult, - _responseNames, - hasSomeErrors); - singleResult = null; - } - else - { - context.AddPartialResults( - _source, - buffer.AsSpan(0, index), - _responseNames, - hasSomeErrors); - } + context.AddPartialResults( + _source, + buffer.AsSpan(0, index), + _responseNames, + hasSomeErrors); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -244,11 +212,8 @@ protected override async ValueTask OnExecuteAsync( } finally { - if (buffer is not null) - { - buffer.AsSpan(0, index).Clear(); - ArrayPool.Shared.Return(buffer); - } + buffer.AsSpan(0, index).Clear(); + ArrayPool.Shared.Return(buffer); } return hasSomeErrors ? ExecutionStatus.PartialSuccess : ExecutionStatus.Success; @@ -306,16 +271,4 @@ private static void AddErrors( } } } - - private static int GetExpectedResultCount(ImmutableArray variables) - { - var totalPathCount = variables.Length; - - for (var i = 0; i < variables.Length; i++) - { - totalPathCount += variables[i].AdditionalPaths.Length; - } - - return totalPathCount; - } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs index 5fd23e79494..69495e8138c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs @@ -134,7 +134,6 @@ protected override async ValueTask OnExecuteAsync( var index = 0; var bufferLength = 0; SourceSchemaResult[]? buffer = null; - SourceSchemaResult? singleResult = null; var hasSomeErrors = false; try @@ -146,29 +145,19 @@ protected override async ValueTask OnExecuteAsync( context.TrackSourceSchemaClientResponse(this, response); // we read the responses from the response stream. + var totalPathCount = variables.Length; + + for (var i = 0; i < variables.Length; i++) + { + totalPathCount += variables[i].AdditionalPaths.Length; + } + + bufferLength = Math.Max(totalPathCount, 1); + buffer = ArrayPool.Shared.Rent(bufferLength); + await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { - if (buffer is null) - { - if (singleResult is null) - { - singleResult = result; - index = 1; - } - else - { - bufferLength = Math.Max(GetExpectedResultCount(variables), 2); - buffer = ArrayPool.Shared.Rent(bufferLength); - buffer[0] = singleResult; - buffer[1] = result; - singleResult = null; - index = 2; - } - } - else - { - buffer[index++] = result; - } + buffer[index++] = result; // Parsing errors here allows the result store to reuse the cached value // and avoids a second document lookup per result. @@ -202,10 +191,6 @@ protected override async ValueTask OnExecuteAsync( buffer.AsSpan(0, index).Clear(); ArrayPool.Shared.Return(buffer); } - else - { - singleResult?.Dispose(); - } AddErrors(context, exception, variables, _responseNames); return ExecutionStatus.Failed; @@ -213,28 +198,11 @@ protected override async ValueTask OnExecuteAsync( try { - if (buffer is null) - { - if (singleResult is null) - { - throw new InvalidOperationException("Expected at least one source schema result."); - } - - context.AddPartialResult( - _source, - singleResult, - _responseNames, - hasSomeErrors); - singleResult = null; - } - else - { - context.AddPartialResults( - _source, - buffer.AsSpan(0, index), - _responseNames, - hasSomeErrors); - } + context.AddPartialResults( + _source, + buffer.AsSpan(0, index), + _responseNames, + hasSomeErrors); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -251,11 +219,8 @@ protected override async ValueTask OnExecuteAsync( } finally { - if (buffer is not null) - { - buffer.AsSpan(0, index).Clear(); - ArrayPool.Shared.Return(buffer); - } + buffer.AsSpan(0, index).Clear(); + ArrayPool.Shared.Return(buffer); } return hasSomeErrors ? ExecutionStatus.PartialSuccess : ExecutionStatus.Success; @@ -359,18 +324,6 @@ private static void AddErrors( } } - private static int GetExpectedResultCount(ImmutableArray variables) - { - var totalPathCount = variables.Length; - - for (var i = 0; i < variables.Length; i++) - { - totalPathCount += variables[i].AdditionalPaths.Length; - } - - return totalPathCount; - } - private sealed class SubscriptionEnumerable : IAsyncEnumerable { private readonly OperationPlanContext _context; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs index 9bcf6c5b0ca..e7b6234b9fd 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs @@ -272,21 +272,6 @@ internal void AddPartialResults( } } - internal void AddPartialResult( - SelectionPath sourcePath, - SourceSchemaResult result, - ReadOnlySpan responseNames, - bool containsErrors = true) - { - var canExecutionContinue = - _resultStore.AddPartialResult(sourcePath, result, responseNames, containsErrors); - - if (!canExecutionContinue) - { - ExecutionState.CancelProcessing(); - } - } - internal void AddPartialResults(SourceResultDocument result, ReadOnlySpan responseNames) { var canExecutionContinue = _resultStore.AddPartialResults(result, responseNames); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 443f9d8bf5b..b4d74147c84 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -95,21 +95,6 @@ public bool AddPartialResults( ReadOnlySpan responseNames) => AddPartialResults(sourcePath, results, responseNames, containsErrors: true); - public bool AddPartialResult( - SelectionPath sourcePath, - SourceSchemaResult result, - ReadOnlySpan responseNames, - bool containsErrors = true) - { - ObjectDisposedException.ThrowIf(_disposed, this); - ArgumentNullException.ThrowIfNull(sourcePath); - ArgumentNullException.ThrowIfNull(result); - - return containsErrors - ? AddSinglePartialResult(sourcePath, result, responseNames) - : AddSinglePartialResultNoErrors(sourcePath, result, responseNames); - } - public bool AddPartialResults( SelectionPath sourcePath, ReadOnlySpan results, @@ -135,7 +120,7 @@ public bool AddPartialResults( if (results.Length == 1) { - return AddPartialResult(sourcePath, results[0], responseNames); + return AddSinglePartialResult(sourcePath, results[0], responseNames); } var dataElements = ArrayPool.Shared.Rent(results.Length); From 224ffa63baaf39371bb35460ddcae845f8ed7531 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 09:43:46 +0000 Subject: [PATCH 08/36] Fusion: remove per-group node tracking in dispatcher --- .../Clients/SourceSchemaRequestDispatcher.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs index 90cb20f7a7a..1aa39c38d1d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs @@ -176,11 +176,11 @@ public void RegisterGroup(int groupId, IReadOnlyList nodeIds) if (existingGroupId < 0) { _trackedNodeIdSlots.Add(nodeId); - group.RegisterNode(nodeId); + group.RegisterNode(); } else if (existingGroupId != groupId) { - group.RegisterNode(nodeId); + group.RegisterNode(); } _groupByNodeIdSlots[nodeId] = groupId; @@ -414,15 +414,6 @@ private Exception CreateAbortException() private void RemoveGroup(GroupState group) { _groups.Remove(group.Id); - - foreach (var nodeId in group.NodeIds) - { - if ((uint)nodeId < (uint)_groupByNodeIdSlots.Length) - { - _groupByNodeIdSlots[nodeId] = -1; - _nodeStateSlots[nodeId] = NodeStateUnregistered; - } - } } private void ClearNodeIdSlots() @@ -475,20 +466,16 @@ private void EnsureNodeIdSlotCapacity(int minCapacity) private sealed class GroupState(int id, int initialCapacity) { - private readonly List _nodeIds = new(initialCapacity); private readonly List _pendingRequests = new(initialCapacity); private int _remainingNodes; private bool _dispatchCreated; public int Id { get; } = id; - public IEnumerable NodeIds => _nodeIds; - public IEnumerable PendingRequests => _pendingRequests; - public void RegisterNode(int nodeId) + public void RegisterNode() { - _nodeIds.Add(nodeId); _remainingNodes++; } From 0c3d145302e858a8a281551c60de2c94b5951176 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 09:57:08 +0000 Subject: [PATCH 09/36] Fusion: clear dispatcher slots from plan node ids --- .../Clients/SourceSchemaRequestDispatcher.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs index 1aa39c38d1d..1bb2f062cf2 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs @@ -164,7 +164,7 @@ public void RegisterGroup(int groupId, IReadOnlyList nodeIds) if (!_groups.TryGetValue(groupId, out var group)) { - group = new GroupState(groupId, nodeIds.Count); + group = new GroupState(groupId, nodeIds); _groups.Add(groupId, group); } @@ -414,6 +414,15 @@ private Exception CreateAbortException() private void RemoveGroup(GroupState group) { _groups.Remove(group.Id); + + foreach (var nodeId in group.NodeIds) + { + if ((uint)nodeId < (uint)_groupByNodeIdSlots.Length) + { + _groupByNodeIdSlots[nodeId] = -1; + _nodeStateSlots[nodeId] = NodeStateUnregistered; + } + } } private void ClearNodeIdSlots() @@ -464,14 +473,17 @@ private void EnsureNodeIdSlotCapacity(int minCapacity) _nodeStateSlots = nodeStateSlots; } - private sealed class GroupState(int id, int initialCapacity) + private sealed class GroupState(int id, IReadOnlyList nodeIds) { - private readonly List _pendingRequests = new(initialCapacity); + private readonly IReadOnlyList _nodeIds = nodeIds; + private readonly List _pendingRequests = new(nodeIds.Count); private int _remainingNodes; private bool _dispatchCreated; public int Id { get; } = id; + public IReadOnlyList NodeIds => _nodeIds; + public IEnumerable PendingRequests => _pendingRequests; public void RegisterNode() From ea2ed7a2cfbee1639be7a5113e97e4a1d047cbb8 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:06:13 +0000 Subject: [PATCH 10/36] Fusion: avoid temporary path nodes in result mapper --- .../Execution/Results/ResultDataMapper.cs | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index b3e101ec40d..a747247a7d1 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -154,9 +154,7 @@ private static IValueNode ParseLeafValue( foreach (var field in node.Fields) { - var value = field.ValueSelection is null - ? Visit(new PathNode(new PathSegmentNode(field.Name)), context) - : Visit(field.ValueSelection, context); + var value = Visit(field, context); if (value is null) { @@ -169,6 +167,38 @@ private static IValueNode ParseLeafValue( return new ObjectValueNode(fields); } + private static IValueNode? Visit(ObjectFieldSelectionNode field, Context context) + { + if (field.ValueSelection is { } valueSelection) + { + return Visit(valueSelection, context); + } + + if (!context.Result.TryGetProperty(field.Name.Value, out var result)) + { + return null; + } + + var resultValueKind = result.ValueKind; + + if (resultValueKind is JsonValueKind.Undefined) + { + return null; + } + + if (resultValueKind is JsonValueKind.Null) + { + return NullValueNode.Default; + } + + if (result.Selection is { IsLeaf: true }) + { + return MapLeafValue(result, ref context.Writer); + } + + throw new InvalidSelectionMapPathException(new PathNode(new PathSegmentNode(field.Name))); + } + private static IValueNode? Visit(PathObjectValueSelectionNode node, Context context) { var result = ResolvePath(context.Schema, context.Result, node.Path); From 56b618abcc0aa90f3bc9222c2aadf9a41b6cc168 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:13:10 +0000 Subject: [PATCH 11/36] Fusion: fast-path string dedupe in variable sets --- .../Execution/Results/FetchResultStore.cs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index b4d74147c84..c28afc4d20d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -674,6 +674,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa { VariableValues[]? variableValueSets = null; Dictionary? seen = null; + Dictionary? seenStrings = null; List?[]? additionalPaths = null; var nextIndex = 0; @@ -690,23 +691,38 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa continue; } - var mappedValue = ResultDataMapper.MapLeafValue(value, ref buffer); variableValueSets ??= new VariableValues[elements.Count]; + IValueNode mappedValue; - if (nextIndex > 0) + if (value.ValueKind is JsonValueKind.String) { - seen ??= new Dictionary(SingleValueNodeComparer.Instance) + var stringValue = value.AssertString(); + + if (seenStrings is not null + && seenStrings.TryGetValue(stringValue, out var existingIndex)) { - [variableValueSets[0].Values.Fields[0].Value] = 0 - }; + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + mappedValue = new StringValueNode(stringValue); + seenStrings ??= new Dictionary(StringComparer.Ordinal); + seenStrings[stringValue] = nextIndex; + } + else + { + mappedValue = ResultDataMapper.MapLeafValue(value, ref buffer); - if (seen.TryGetValue(mappedValue, out var existingIndex)) + if (seen is not null + && seen.TryGetValue(mappedValue, out var existingIndex)) { additionalPaths ??= new List?[elements.Count]; (additionalPaths[existingIndex] ??= []).Add(result.Path); continue; } + seen ??= new Dictionary(SingleValueNodeComparer.Instance); seen[mappedValue] = nextIndex; } From 03ffb256a9d455b822908dad616689922e07085e Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:14:32 +0000 Subject: [PATCH 12/36] Revert "Fusion: avoid temporary path nodes in result mapper" This reverts commit ea2ed7a2cfbee1639be7a5113e97e4a1d047cbb8. --- .../Execution/Results/ResultDataMapper.cs | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index a747247a7d1..b3e101ec40d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -154,7 +154,9 @@ private static IValueNode ParseLeafValue( foreach (var field in node.Fields) { - var value = Visit(field, context); + var value = field.ValueSelection is null + ? Visit(new PathNode(new PathSegmentNode(field.Name)), context) + : Visit(field.ValueSelection, context); if (value is null) { @@ -167,38 +169,6 @@ private static IValueNode ParseLeafValue( return new ObjectValueNode(fields); } - private static IValueNode? Visit(ObjectFieldSelectionNode field, Context context) - { - if (field.ValueSelection is { } valueSelection) - { - return Visit(valueSelection, context); - } - - if (!context.Result.TryGetProperty(field.Name.Value, out var result)) - { - return null; - } - - var resultValueKind = result.ValueKind; - - if (resultValueKind is JsonValueKind.Undefined) - { - return null; - } - - if (resultValueKind is JsonValueKind.Null) - { - return NullValueNode.Default; - } - - if (result.Selection is { IsLeaf: true }) - { - return MapLeafValue(result, ref context.Writer); - } - - throw new InvalidSelectionMapPathException(new PathNode(new PathSegmentNode(field.Name))); - } - private static IValueNode? Visit(PathObjectValueSelectionNode node, Context context) { var result = ResolvePath(context.Schema, context.Result, node.Path); From d5ae89761adbdfccd94f9a131ab0635ff209fe11 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:14:32 +0000 Subject: [PATCH 13/36] Revert "Fusion: clear dispatcher slots from plan node ids" This reverts commit 0c3d145302e858a8a281551c60de2c94b5951176. --- .../Clients/SourceSchemaRequestDispatcher.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs index 1bb2f062cf2..1aa39c38d1d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs @@ -164,7 +164,7 @@ public void RegisterGroup(int groupId, IReadOnlyList nodeIds) if (!_groups.TryGetValue(groupId, out var group)) { - group = new GroupState(groupId, nodeIds); + group = new GroupState(groupId, nodeIds.Count); _groups.Add(groupId, group); } @@ -414,15 +414,6 @@ private Exception CreateAbortException() private void RemoveGroup(GroupState group) { _groups.Remove(group.Id); - - foreach (var nodeId in group.NodeIds) - { - if ((uint)nodeId < (uint)_groupByNodeIdSlots.Length) - { - _groupByNodeIdSlots[nodeId] = -1; - _nodeStateSlots[nodeId] = NodeStateUnregistered; - } - } } private void ClearNodeIdSlots() @@ -473,17 +464,14 @@ private void EnsureNodeIdSlotCapacity(int minCapacity) _nodeStateSlots = nodeStateSlots; } - private sealed class GroupState(int id, IReadOnlyList nodeIds) + private sealed class GroupState(int id, int initialCapacity) { - private readonly IReadOnlyList _nodeIds = nodeIds; - private readonly List _pendingRequests = new(nodeIds.Count); + private readonly List _pendingRequests = new(initialCapacity); private int _remainingNodes; private bool _dispatchCreated; public int Id { get; } = id; - public IReadOnlyList NodeIds => _nodeIds; - public IEnumerable PendingRequests => _pendingRequests; public void RegisterNode() From 52442f1c6cae083150a61618a8ba532df73926b3 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:14:32 +0000 Subject: [PATCH 14/36] Revert "Fusion: remove per-group node tracking in dispatcher" This reverts commit 224ffa63baaf39371bb35460ddcae845f8ed7531. --- .../Clients/SourceSchemaRequestDispatcher.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs index 1aa39c38d1d..90cb20f7a7a 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs @@ -176,11 +176,11 @@ public void RegisterGroup(int groupId, IReadOnlyList nodeIds) if (existingGroupId < 0) { _trackedNodeIdSlots.Add(nodeId); - group.RegisterNode(); + group.RegisterNode(nodeId); } else if (existingGroupId != groupId) { - group.RegisterNode(); + group.RegisterNode(nodeId); } _groupByNodeIdSlots[nodeId] = groupId; @@ -414,6 +414,15 @@ private Exception CreateAbortException() private void RemoveGroup(GroupState group) { _groups.Remove(group.Id); + + foreach (var nodeId in group.NodeIds) + { + if ((uint)nodeId < (uint)_groupByNodeIdSlots.Length) + { + _groupByNodeIdSlots[nodeId] = -1; + _nodeStateSlots[nodeId] = NodeStateUnregistered; + } + } } private void ClearNodeIdSlots() @@ -466,16 +475,20 @@ private void EnsureNodeIdSlotCapacity(int minCapacity) private sealed class GroupState(int id, int initialCapacity) { + private readonly List _nodeIds = new(initialCapacity); private readonly List _pendingRequests = new(initialCapacity); private int _remainingNodes; private bool _dispatchCreated; public int Id { get; } = id; + public IEnumerable NodeIds => _nodeIds; + public IEnumerable PendingRequests => _pendingRequests; - public void RegisterNode() + public void RegisterNode(int nodeId) { + _nodeIds.Add(nodeId); _remainingNodes++; } From 1abbd3d2a0669b2919669e603b16edccddccb5fc Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:26:42 +0000 Subject: [PATCH 15/36] Fusion: fast-path integer leaf value mapping --- .../Execution/Results/ResultDataMapper.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index b3e101ec40d..1f0dba10d23 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -128,6 +128,14 @@ private static IValueNode ParseLeafValue( case JsonValueKind.String: return new StringValueNode(value.AssertString()); + case JsonValueKind.Number: + if (value.TryGetInt64(out var intValue)) + { + return new IntValueNode(intValue); + } + + goto default; + default: writer ??= new PooledArrayWriter(); if (!parserInitialized) From 2ee203da9a874134404d63e147a603deceb77d39 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:38:53 +0000 Subject: [PATCH 16/36] Fusion: fast-path float leaf value mapping --- .../Execution/Results/ResultDataMapper.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index 1f0dba10d23..06f59f40891 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -129,11 +129,33 @@ private static IValueNode ParseLeafValue( return new StringValueNode(value.AssertString()); case JsonValueKind.Number: - if (value.TryGetInt64(out var intValue)) + var rawNumber = value.GetRawValue(includeQuotes: false); + var dot = rawNumber.IndexOf((byte)'.'); + var exponent = rawNumber.IndexOfAny((byte)'e', (byte)'E'); + + if (dot < 0 && exponent < 0 && value.TryGetInt64(out var intValue)) { return new IntValueNode(intValue); } + // Preserve float formatting by copying the original UTF-8 number literal. + if (dot >= 0 || exponent >= 0) + { + writer ??= new PooledArrayWriter(); + + var start = writer.Length; + rawNumber.CopyTo(writer.GetSpan(rawNumber.Length)); + writer.Advance(rawNumber.Length); + + var format = exponent >= 0 + ? FloatFormat.Exponential + : FloatFormat.FixedPoint; + + return new FloatValueNode( + writer.GetWrittenMemorySegment(start, rawNumber.Length), + format); + } + goto default; default: From 601c854ce496ce4d11023709b070f372c4a2fca8 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:47:10 +0000 Subject: [PATCH 17/36] Revert "Fusion: fast-path float leaf value mapping" This reverts commit 2ee203da9a874134404d63e147a603deceb77d39. --- .../Execution/Results/ResultDataMapper.cs | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index 06f59f40891..1f0dba10d23 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -129,33 +129,11 @@ private static IValueNode ParseLeafValue( return new StringValueNode(value.AssertString()); case JsonValueKind.Number: - var rawNumber = value.GetRawValue(includeQuotes: false); - var dot = rawNumber.IndexOf((byte)'.'); - var exponent = rawNumber.IndexOfAny((byte)'e', (byte)'E'); - - if (dot < 0 && exponent < 0 && value.TryGetInt64(out var intValue)) + if (value.TryGetInt64(out var intValue)) { return new IntValueNode(intValue); } - // Preserve float formatting by copying the original UTF-8 number literal. - if (dot >= 0 || exponent >= 0) - { - writer ??= new PooledArrayWriter(); - - var start = writer.Length; - rawNumber.CopyTo(writer.GetSpan(rawNumber.Length)); - writer.Advance(rawNumber.Length); - - var format = exponent >= 0 - ? FloatFormat.Exponential - : FloatFormat.FixedPoint; - - return new FloatValueNode( - writer.GetWrittenMemorySegment(start, rawNumber.Length), - format); - } - goto default; default: From d2dc9aafd4c65cc9aa07cb619b8ce24e5fd0a360 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:49:16 +0000 Subject: [PATCH 18/36] Fusion: avoid path allocations in object field mapping --- .../Execution/Results/ResultDataMapper.cs | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index 1f0dba10d23..9ec20552c14 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -162,9 +162,7 @@ private static IValueNode ParseLeafValue( foreach (var field in node.Fields) { - var value = field.ValueSelection is null - ? Visit(new PathNode(new PathSegmentNode(field.Name)), context) - : Visit(field.ValueSelection, context); + var value = Visit(field, context); if (value is null) { @@ -177,6 +175,38 @@ private static IValueNode ParseLeafValue( return new ObjectValueNode(fields); } + private static IValueNode? Visit(ObjectFieldSelectionNode field, Context context) + { + if (field.ValueSelection is { } valueSelection) + { + return Visit(valueSelection, context); + } + + if (!context.Result.TryGetProperty(field.Name.Value, out var result)) + { + return null; + } + + var resultValueKind = result.ValueKind; + + if (resultValueKind is JsonValueKind.Undefined) + { + return null; + } + + if (resultValueKind is JsonValueKind.Null) + { + return NullValueNode.Default; + } + + if (result.Selection is { IsLeaf: true }) + { + return MapLeafValue(result, ref context.Writer); + } + + throw new InvalidSelectionMapPathException(new PathNode(new PathSegmentNode(field.Name))); + } + private static IValueNode? Visit(PathObjectValueSelectionNode node, Context context) { var result = ResolvePath(context.Schema, context.Result, node.Path); From 0ade67d9ac3966eeff993ce441cf720d7eeeab16 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 10:58:40 +0000 Subject: [PATCH 19/36] Revert "Fusion: avoid path allocations in object field mapping" This reverts commit d2dc9aafd4c65cc9aa07cb619b8ce24e5fd0a360. --- .../Execution/Results/ResultDataMapper.cs | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index 9ec20552c14..1f0dba10d23 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -162,7 +162,9 @@ private static IValueNode ParseLeafValue( foreach (var field in node.Fields) { - var value = Visit(field, context); + var value = field.ValueSelection is null + ? Visit(new PathNode(new PathSegmentNode(field.Name)), context) + : Visit(field.ValueSelection, context); if (value is null) { @@ -175,38 +177,6 @@ private static IValueNode ParseLeafValue( return new ObjectValueNode(fields); } - private static IValueNode? Visit(ObjectFieldSelectionNode field, Context context) - { - if (field.ValueSelection is { } valueSelection) - { - return Visit(valueSelection, context); - } - - if (!context.Result.TryGetProperty(field.Name.Value, out var result)) - { - return null; - } - - var resultValueKind = result.ValueKind; - - if (resultValueKind is JsonValueKind.Undefined) - { - return null; - } - - if (resultValueKind is JsonValueKind.Null) - { - return NullValueNode.Default; - } - - if (result.Selection is { IsLeaf: true }) - { - return MapLeafValue(result, ref context.Writer); - } - - throw new InvalidSelectionMapPathException(new PathNode(new PathSegmentNode(field.Name))); - } - private static IValueNode? Visit(PathObjectValueSelectionNode node, Context context) { var result = ResolvePath(context.Schema, context.Result, node.Path); From baad6e742c33ff027677400a50ed8350d239d7ee Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 11:04:44 +0000 Subject: [PATCH 20/36] Fusion: cache small int value nodes in result mapping --- .../Execution/Results/FetchResultStore.cs | 15 ++++++++++ .../Execution/Results/ResultDataMapper.cs | 28 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index c28afc4d20d..0896a5ed3c0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -675,6 +675,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa VariableValues[]? variableValueSets = null; Dictionary? seen = null; Dictionary? seenStrings = null; + Dictionary? seenIntegers = null; List?[]? additionalPaths = null; var nextIndex = 0; @@ -710,6 +711,20 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa seenStrings ??= new Dictionary(StringComparer.Ordinal); seenStrings[stringValue] = nextIndex; } + else if (value.ValueKind is JsonValueKind.Number && value.TryGetInt64(out var intValue)) + { + if (seenIntegers is not null + && seenIntegers.TryGetValue(intValue, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + mappedValue = ResultDataMapper.GetIntValueNode(intValue); + seenIntegers ??= []; + seenIntegers[intValue] = nextIndex; + } else { mappedValue = ResultDataMapper.MapLeafValue(value, ref buffer); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index 1f0dba10d23..378b5788107 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -9,6 +9,10 @@ namespace HotChocolate.Fusion.Execution.Results; internal static class ResultDataMapper { + private const int CachedIntMin = -128; + private const int CachedIntMax = 4096; + private static readonly IntValueNode[] s_cachedIntValues = CreateCachedIntValues(); + public static IValueNode? Map( CompositeResultElement result, IValueSelectionNode valueSelection, @@ -131,7 +135,7 @@ private static IValueNode ParseLeafValue( case JsonValueKind.Number: if (value.TryGetInt64(out var intValue)) { - return new IntValueNode(intValue); + return GetIntValueNode(intValue); } goto default; @@ -148,6 +152,28 @@ private static IValueNode ParseLeafValue( } } + internal static IntValueNode GetIntValueNode(long value) + { + if (value >= CachedIntMin && value <= CachedIntMax) + { + return s_cachedIntValues[value - CachedIntMin]; + } + + return new IntValueNode(value); + } + + private static IntValueNode[] CreateCachedIntValues() + { + var values = new IntValueNode[CachedIntMax - CachedIntMin + 1]; + + for (var i = 0; i < values.Length; i++) + { + values[i] = new IntValueNode(CachedIntMin + i); + } + + return values; + } + private static IValueNode? Visit(ObjectValueSelectionNode node, Context context) { var result = context.Result; From 2bf197bb638345420348328f4a3a577f2920bc03 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 11:13:41 +0000 Subject: [PATCH 21/36] Revert "Fusion: cache small int value nodes in result mapping" This reverts commit baad6e742c33ff027677400a50ed8350d239d7ee. --- .../Execution/Results/FetchResultStore.cs | 15 ---------- .../Execution/Results/ResultDataMapper.cs | 28 +------------------ 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 0896a5ed3c0..c28afc4d20d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -675,7 +675,6 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa VariableValues[]? variableValueSets = null; Dictionary? seen = null; Dictionary? seenStrings = null; - Dictionary? seenIntegers = null; List?[]? additionalPaths = null; var nextIndex = 0; @@ -711,20 +710,6 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa seenStrings ??= new Dictionary(StringComparer.Ordinal); seenStrings[stringValue] = nextIndex; } - else if (value.ValueKind is JsonValueKind.Number && value.TryGetInt64(out var intValue)) - { - if (seenIntegers is not null - && seenIntegers.TryGetValue(intValue, out var existingIndex)) - { - additionalPaths ??= new List?[elements.Count]; - (additionalPaths[existingIndex] ??= []).Add(result.Path); - continue; - } - - mappedValue = ResultDataMapper.GetIntValueNode(intValue); - seenIntegers ??= []; - seenIntegers[intValue] = nextIndex; - } else { mappedValue = ResultDataMapper.MapLeafValue(value, ref buffer); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index 378b5788107..1f0dba10d23 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -9,10 +9,6 @@ namespace HotChocolate.Fusion.Execution.Results; internal static class ResultDataMapper { - private const int CachedIntMin = -128; - private const int CachedIntMax = 4096; - private static readonly IntValueNode[] s_cachedIntValues = CreateCachedIntValues(); - public static IValueNode? Map( CompositeResultElement result, IValueSelectionNode valueSelection, @@ -135,7 +131,7 @@ private static IValueNode ParseLeafValue( case JsonValueKind.Number: if (value.TryGetInt64(out var intValue)) { - return GetIntValueNode(intValue); + return new IntValueNode(intValue); } goto default; @@ -152,28 +148,6 @@ private static IValueNode ParseLeafValue( } } - internal static IntValueNode GetIntValueNode(long value) - { - if (value >= CachedIntMin && value <= CachedIntMax) - { - return s_cachedIntValues[value - CachedIntMin]; - } - - return new IntValueNode(value); - } - - private static IntValueNode[] CreateCachedIntValues() - { - var values = new IntValueNode[CachedIntMax - CachedIntMin + 1]; - - for (var i = 0; i < values.Length; i++) - { - values[i] = new IntValueNode(CachedIntMin + i); - } - - return values; - } - private static IValueNode? Visit(ObjectValueSelectionNode node, Context context) { var result = context.Result; From 75dde74e83708aecb4435c310ee0c09bfa598ca1 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 11:15:45 +0000 Subject: [PATCH 22/36] Fusion: cache numeric string value nodes --- .../Execution/Results/FetchResultStore.cs | 2 +- .../Execution/Results/ResultDataMapper.cs | 78 ++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index c28afc4d20d..566b8877ee7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -706,7 +706,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa continue; } - mappedValue = new StringValueNode(stringValue); + mappedValue = ResultDataMapper.GetStringValueNode(stringValue); seenStrings ??= new Dictionary(StringComparer.Ordinal); seenStrings[stringValue] = nextIndex; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index 1f0dba10d23..4d1f1a4519d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json; using HotChocolate.Buffers; using HotChocolate.Fusion.Language; @@ -9,6 +11,9 @@ namespace HotChocolate.Fusion.Execution.Results; internal static class ResultDataMapper { + private const int CachedNumericStringMax = 4096; + private static readonly StringValueNode[] s_cachedNumericStrings = CreateCachedNumericStrings(); + public static IValueNode? Map( CompositeResultElement result, IValueSelectionNode valueSelection, @@ -126,7 +131,7 @@ private static IValueNode ParseLeafValue( return BooleanValueNode.False; case JsonValueKind.String: - return new StringValueNode(value.AssertString()); + return GetStringValueNode(value.AssertString()); case JsonValueKind.Number: if (value.TryGetInt64(out var intValue)) @@ -148,6 +153,77 @@ private static IValueNode ParseLeafValue( } } + internal static StringValueNode GetStringValueNode(string value) + { + if (TryGetCachedNumericString(value, out var cached)) + { + return cached; + } + + return new StringValueNode(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetCachedNumericString( + string value, + [NotNullWhen(true)] out StringValueNode? cached) + { + cached = null; + + var length = value.Length; + + if ((uint)(length - 1) > 3) + { + return false; + } + + var c0 = value[0]; + + if ((uint)(c0 - '0') > 9) + { + return false; + } + + if (length > 1 && c0 == '0') + { + return false; + } + + var parsed = c0 - '0'; + + for (var i = 1; i < length; i++) + { + var c = value[i]; + + if ((uint)(c - '0') > 9) + { + return false; + } + + parsed = (parsed * 10) + (c - '0'); + } + + if ((uint)parsed > CachedNumericStringMax) + { + return false; + } + + cached = s_cachedNumericStrings[parsed]; + return true; + } + + private static StringValueNode[] CreateCachedNumericStrings() + { + var values = new StringValueNode[CachedNumericStringMax + 1]; + + for (var i = 0; i < values.Length; i++) + { + values[i] = new StringValueNode(i.ToString()); + } + + return values; + } + private static IValueNode? Visit(ObjectValueSelectionNode node, Context context) { var result = context.Result; From 4f32f7503d90bf11ac2063a3ddc26e4ef8f21cc4 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 11:36:55 +0000 Subject: [PATCH 23/36] Fusion: fast-path two-requirement scalar tuple dedupe --- .../Execution/Results/FetchResultStore.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 566b8877ee7..687e27ebbf3 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -823,6 +823,8 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast ref PooledArrayWriter? buffer) { VariableValues[]? variableValueSets = null; + Dictionary? seenStrings = null; + Dictionary? seenIntegers = null; Dictionary? seen = null; List?[]? additionalPaths = null; var nextIndex = 0; @@ -845,6 +847,61 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast continue; } + if (value1.ValueKind is JsonValueKind.String && value2.ValueKind is JsonValueKind.String) + { + var stringValue1 = value1.AssertString(); + var stringValue2 = value2.AssertString(); + var stringKey = new TwoStringTuple(stringValue1, stringValue2); + + if (seenStrings is not null + && seenStrings.TryGetValue(stringKey, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + variableValueSets ??= new VariableValues[elements.Count]; + var insertIndex = nextIndex++; + seenStrings ??= []; + seenStrings[stringKey] = insertIndex; + variableValueSets[insertIndex] = new VariableValues( + result.Path, + new ObjectValueNode([ + new ObjectFieldNode(requirement1.Key, ResultDataMapper.GetStringValueNode(stringValue1)), + new ObjectFieldNode(requirement2.Key, ResultDataMapper.GetStringValueNode(stringValue2)) + ])); + continue; + } + + if (value1.ValueKind is JsonValueKind.Number + && value2.ValueKind is JsonValueKind.Number + && value1.TryGetInt64(out var intValue1) + && value2.TryGetInt64(out var intValue2)) + { + var intKey = new TwoLongTuple(intValue1, intValue2); + + if (seenIntegers is not null + && seenIntegers.TryGetValue(intKey, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + variableValueSets ??= new VariableValues[elements.Count]; + var insertIndex = nextIndex++; + seenIntegers ??= []; + seenIntegers[intKey] = insertIndex; + variableValueSets[insertIndex] = new VariableValues( + result.Path, + new ObjectValueNode([ + new ObjectFieldNode(requirement1.Key, new IntValueNode(intValue1)), + new ObjectFieldNode(requirement2.Key, new IntValueNode(intValue2)) + ])); + continue; + } + var mappedValue1 = ResultDataMapper.MapLeafValue(value1, ref buffer); var mappedValue2 = ResultDataMapper.MapLeafValue(value2, ref buffer); variableValueSets ??= new VariableValues[elements.Count]; @@ -1412,6 +1469,10 @@ private static ImmutableArray FinalizeVariableValueSets( private readonly record struct TwoValueNodeTuple(IValueNode Value1, IValueNode Value2); + private readonly record struct TwoLongTuple(long Value1, long Value2); + + private readonly record struct TwoStringTuple(string Value1, string Value2); + private readonly record struct ThreeValueNodeTuple( IValueNode Value1, IValueNode Value2, From 631b5ae25c7118344ee9b1490424d1367e78a094 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 11:45:21 +0000 Subject: [PATCH 24/36] Revert "Fusion: fast-path two-requirement scalar tuple dedupe" This reverts commit 4f32f7503d90bf11ac2063a3ddc26e4ef8f21cc4. --- .../Execution/Results/FetchResultStore.cs | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 687e27ebbf3..566b8877ee7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -823,8 +823,6 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast ref PooledArrayWriter? buffer) { VariableValues[]? variableValueSets = null; - Dictionary? seenStrings = null; - Dictionary? seenIntegers = null; Dictionary? seen = null; List?[]? additionalPaths = null; var nextIndex = 0; @@ -847,61 +845,6 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast continue; } - if (value1.ValueKind is JsonValueKind.String && value2.ValueKind is JsonValueKind.String) - { - var stringValue1 = value1.AssertString(); - var stringValue2 = value2.AssertString(); - var stringKey = new TwoStringTuple(stringValue1, stringValue2); - - if (seenStrings is not null - && seenStrings.TryGetValue(stringKey, out var existingIndex)) - { - additionalPaths ??= new List?[elements.Count]; - (additionalPaths[existingIndex] ??= []).Add(result.Path); - continue; - } - - variableValueSets ??= new VariableValues[elements.Count]; - var insertIndex = nextIndex++; - seenStrings ??= []; - seenStrings[stringKey] = insertIndex; - variableValueSets[insertIndex] = new VariableValues( - result.Path, - new ObjectValueNode([ - new ObjectFieldNode(requirement1.Key, ResultDataMapper.GetStringValueNode(stringValue1)), - new ObjectFieldNode(requirement2.Key, ResultDataMapper.GetStringValueNode(stringValue2)) - ])); - continue; - } - - if (value1.ValueKind is JsonValueKind.Number - && value2.ValueKind is JsonValueKind.Number - && value1.TryGetInt64(out var intValue1) - && value2.TryGetInt64(out var intValue2)) - { - var intKey = new TwoLongTuple(intValue1, intValue2); - - if (seenIntegers is not null - && seenIntegers.TryGetValue(intKey, out var existingIndex)) - { - additionalPaths ??= new List?[elements.Count]; - (additionalPaths[existingIndex] ??= []).Add(result.Path); - continue; - } - - variableValueSets ??= new VariableValues[elements.Count]; - var insertIndex = nextIndex++; - seenIntegers ??= []; - seenIntegers[intKey] = insertIndex; - variableValueSets[insertIndex] = new VariableValues( - result.Path, - new ObjectValueNode([ - new ObjectFieldNode(requirement1.Key, new IntValueNode(intValue1)), - new ObjectFieldNode(requirement2.Key, new IntValueNode(intValue2)) - ])); - continue; - } - var mappedValue1 = ResultDataMapper.MapLeafValue(value1, ref buffer); var mappedValue2 = ResultDataMapper.MapLeafValue(value2, ref buffer); variableValueSets ??= new VariableValues[elements.Count]; @@ -1469,10 +1412,6 @@ private static ImmutableArray FinalizeVariableValueSets( private readonly record struct TwoValueNodeTuple(IValueNode Value1, IValueNode Value2); - private readonly record struct TwoLongTuple(long Value1, long Value2); - - private readonly record struct TwoStringTuple(string Value1, string Value2); - private readonly record struct ThreeValueNodeTuple( IValueNode Value1, IValueNode Value2, From 0163825ad2e801eab64c81b99e117b0ba8aac0dc Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 11:47:34 +0000 Subject: [PATCH 25/36] Fusion: cache small int leaf value nodes --- .../Execution/Results/ResultDataMapper.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index 4d1f1a4519d..2c79ff1f830 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -11,7 +11,10 @@ namespace HotChocolate.Fusion.Execution.Results; internal static class ResultDataMapper { + private const int CachedIntMin = -128; + private const int CachedIntMax = 4096; private const int CachedNumericStringMax = 4096; + private static readonly IntValueNode[] s_cachedIntValues = CreateCachedIntValues(); private static readonly StringValueNode[] s_cachedNumericStrings = CreateCachedNumericStrings(); public static IValueNode? Map( @@ -136,7 +139,7 @@ private static IValueNode ParseLeafValue( case JsonValueKind.Number: if (value.TryGetInt64(out var intValue)) { - return new IntValueNode(intValue); + return GetIntValueNode(intValue); } goto default; @@ -163,6 +166,16 @@ internal static StringValueNode GetStringValueNode(string value) return new StringValueNode(value); } + internal static IntValueNode GetIntValueNode(long value) + { + if (value >= CachedIntMin && value <= CachedIntMax) + { + return s_cachedIntValues[value - CachedIntMin]; + } + + return new IntValueNode(value); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetCachedNumericString( string value, @@ -224,6 +237,18 @@ private static StringValueNode[] CreateCachedNumericStrings() return values; } + private static IntValueNode[] CreateCachedIntValues() + { + var values = new IntValueNode[CachedIntMax - CachedIntMin + 1]; + + for (var i = 0; i < values.Length; i++) + { + values[i] = new IntValueNode(CachedIntMin + i); + } + + return values; + } + private static IValueNode? Visit(ObjectValueSelectionNode node, Context context) { var result = context.Result; From 9813870e82b4f08bf68dbf337ef7b4335f51a1b3 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 11:57:53 +0000 Subject: [PATCH 26/36] Revert "Fusion: cache small int leaf value nodes" This reverts commit 0163825ad2e801eab64c81b99e117b0ba8aac0dc. --- .../Execution/Results/ResultDataMapper.cs | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs index 2c79ff1f830..4d1f1a4519d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultDataMapper.cs @@ -11,10 +11,7 @@ namespace HotChocolate.Fusion.Execution.Results; internal static class ResultDataMapper { - private const int CachedIntMin = -128; - private const int CachedIntMax = 4096; private const int CachedNumericStringMax = 4096; - private static readonly IntValueNode[] s_cachedIntValues = CreateCachedIntValues(); private static readonly StringValueNode[] s_cachedNumericStrings = CreateCachedNumericStrings(); public static IValueNode? Map( @@ -139,7 +136,7 @@ private static IValueNode ParseLeafValue( case JsonValueKind.Number: if (value.TryGetInt64(out var intValue)) { - return GetIntValueNode(intValue); + return new IntValueNode(intValue); } goto default; @@ -166,16 +163,6 @@ internal static StringValueNode GetStringValueNode(string value) return new StringValueNode(value); } - internal static IntValueNode GetIntValueNode(long value) - { - if (value >= CachedIntMin && value <= CachedIntMax) - { - return s_cachedIntValues[value - CachedIntMin]; - } - - return new IntValueNode(value); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetCachedNumericString( string value, @@ -237,18 +224,6 @@ private static StringValueNode[] CreateCachedNumericStrings() return values; } - private static IntValueNode[] CreateCachedIntValues() - { - var values = new IntValueNode[CachedIntMax - CachedIntMin + 1]; - - for (var i = 0; i < values.Length; i++) - { - values[i] = new IntValueNode(CachedIntMin + i); - } - - return values; - } - private static IValueNode? Visit(ObjectValueSelectionNode node, Context context) { var result = context.Result; From 4dd66030d227295e0c70634275d362ba80feec4e Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 12:00:01 +0000 Subject: [PATCH 27/36] Fusion: fast-path value completion when no errors --- .../Execution/Results/ValueCompletion.cs | 109 ++++++++++++++++-- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ValueCompletion.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ValueCompletion.cs index 53fb06def00..596f0a2bd61 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ValueCompletion.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ValueCompletion.cs @@ -58,6 +58,34 @@ public bool BuildResult( return BuildErrorResult(target, responseNames, error, target.Path); } + if (errorTrie is null) + { + foreach (var property in source.EnumerateObject()) + { + if (!target.TryGetProperty(property.NameSpan, out var resultField)) + { + continue; + } + + var selection = resultField.AssertSelection(); + + if (!TryCompleteValue(property.Value, resultField, null, selection, selection.Type, 0)) + { + switch (_errorHandlingMode) + { + case ErrorHandlingMode.Propagate: + var didPropagateToRoot = PropagateNullValues(resultField); + return !didPropagateToRoot; + + case ErrorHandlingMode.Halt: + return false; + } + } + } + + return true; + } + foreach (var property in source.EnumerateObject()) { if (!target.TryGetProperty(property.NameSpan, out var resultField)) @@ -66,8 +94,7 @@ public bool BuildResult( } var selection = resultField.AssertSelection(); - ErrorTrie? errorTrieForResponseName = null; - errorTrie?.TryGetValue(selection.ResponseName, out errorTrieForResponseName); + errorTrie.TryGetValue(selection.ResponseName, out var errorTrieForResponseName); if (!TryCompleteValue(property.Value, resultField, errorTrieForResponseName, selection, selection.Type, 0)) { @@ -273,13 +300,48 @@ private bool TryCompleteList( var i = 0; using var enumerator = target.EnumerateArray().GetEnumerator(); + + if (errorTrie is null) + { + foreach (var element in source.EnumerateArray()) + { + var success = enumerator.MoveNext(); + Debug.Assert(success, "The lists must have the same size."); + + if (element.IsNullOrUndefined()) + { + if (!isNullable && _errorHandlingMode is ErrorHandlingMode.Propagate or ErrorHandlingMode.Halt) + { + return false; + } + + enumerator.Current.SetNullValue(); + goto TryCompleteList_NoErrors_MoveNext; + } + + if (!HandleElement(element, enumerator.Current, null)) + { + if (!isNullable) + { + return false; + } + + enumerator.Current.SetNullValue(); + } + +TryCompleteList_NoErrors_MoveNext: + i++; + } + + return true; + } + foreach (var element in source.EnumerateArray()) { var success = enumerator.MoveNext(); Debug.Assert(success, "The lists must have the same size."); - ErrorTrie? errorTrieForIndex = null; - errorTrie?.TryGetValue(i, out errorTrieForIndex); + errorTrie.TryGetValue(i, out var errorTrieForIndex); if (errorTrieForIndex?.Error is { } error) { @@ -396,6 +458,32 @@ private bool TryCompleteObjectValue( target.SetObjectValue(selectionSet); } + if (errorTrie is null) + { + foreach (var property in source.EnumerateObject()) + { + if (!target.TryGetProperty(property.NameSpan, out var targetProperty)) + { + continue; + } + + var selection = targetProperty.AssertSelection(); + + if (!TryCompleteValue( + property.Value, + targetProperty, + null, + selection, + selection.Type, + depth)) + { + return false; + } + } + + return true; + } + foreach (var property in source.EnumerateObject()) { if (!target.TryGetProperty(property.NameSpan, out var targetProperty)) @@ -404,12 +492,15 @@ private bool TryCompleteObjectValue( } var selection = targetProperty.AssertSelection(); + errorTrie.TryGetValue(selection.ResponseName, out var errorTrieForResponseName); - ErrorTrie? errorTrieForResponseName = null; - errorTrie?.TryGetValue(selection.ResponseName, out errorTrieForResponseName); - - if (!TryCompleteValue(property.Value, - targetProperty, errorTrieForResponseName, selection, selection.Type, depth)) + if (!TryCompleteValue( + property.Value, + targetProperty, + errorTrieForResponseName, + selection, + selection.Type, + depth)) { return false; } From 76577a1c37d0e94eb3d4246cbce129b90755f2e9 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 12:08:21 +0000 Subject: [PATCH 28/36] Revert "Fusion: fast-path value completion when no errors" This reverts commit 4dd66030d227295e0c70634275d362ba80feec4e. --- .../Execution/Results/ValueCompletion.cs | 109 ++---------------- 1 file changed, 9 insertions(+), 100 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ValueCompletion.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ValueCompletion.cs index 596f0a2bd61..53fb06def00 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ValueCompletion.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ValueCompletion.cs @@ -58,34 +58,6 @@ public bool BuildResult( return BuildErrorResult(target, responseNames, error, target.Path); } - if (errorTrie is null) - { - foreach (var property in source.EnumerateObject()) - { - if (!target.TryGetProperty(property.NameSpan, out var resultField)) - { - continue; - } - - var selection = resultField.AssertSelection(); - - if (!TryCompleteValue(property.Value, resultField, null, selection, selection.Type, 0)) - { - switch (_errorHandlingMode) - { - case ErrorHandlingMode.Propagate: - var didPropagateToRoot = PropagateNullValues(resultField); - return !didPropagateToRoot; - - case ErrorHandlingMode.Halt: - return false; - } - } - } - - return true; - } - foreach (var property in source.EnumerateObject()) { if (!target.TryGetProperty(property.NameSpan, out var resultField)) @@ -94,7 +66,8 @@ public bool BuildResult( } var selection = resultField.AssertSelection(); - errorTrie.TryGetValue(selection.ResponseName, out var errorTrieForResponseName); + ErrorTrie? errorTrieForResponseName = null; + errorTrie?.TryGetValue(selection.ResponseName, out errorTrieForResponseName); if (!TryCompleteValue(property.Value, resultField, errorTrieForResponseName, selection, selection.Type, 0)) { @@ -300,48 +273,13 @@ private bool TryCompleteList( var i = 0; using var enumerator = target.EnumerateArray().GetEnumerator(); - - if (errorTrie is null) - { - foreach (var element in source.EnumerateArray()) - { - var success = enumerator.MoveNext(); - Debug.Assert(success, "The lists must have the same size."); - - if (element.IsNullOrUndefined()) - { - if (!isNullable && _errorHandlingMode is ErrorHandlingMode.Propagate or ErrorHandlingMode.Halt) - { - return false; - } - - enumerator.Current.SetNullValue(); - goto TryCompleteList_NoErrors_MoveNext; - } - - if (!HandleElement(element, enumerator.Current, null)) - { - if (!isNullable) - { - return false; - } - - enumerator.Current.SetNullValue(); - } - -TryCompleteList_NoErrors_MoveNext: - i++; - } - - return true; - } - foreach (var element in source.EnumerateArray()) { var success = enumerator.MoveNext(); Debug.Assert(success, "The lists must have the same size."); - errorTrie.TryGetValue(i, out var errorTrieForIndex); + ErrorTrie? errorTrieForIndex = null; + errorTrie?.TryGetValue(i, out errorTrieForIndex); if (errorTrieForIndex?.Error is { } error) { @@ -458,32 +396,6 @@ private bool TryCompleteObjectValue( target.SetObjectValue(selectionSet); } - if (errorTrie is null) - { - foreach (var property in source.EnumerateObject()) - { - if (!target.TryGetProperty(property.NameSpan, out var targetProperty)) - { - continue; - } - - var selection = targetProperty.AssertSelection(); - - if (!TryCompleteValue( - property.Value, - targetProperty, - null, - selection, - selection.Type, - depth)) - { - return false; - } - } - - return true; - } - foreach (var property in source.EnumerateObject()) { if (!target.TryGetProperty(property.NameSpan, out var targetProperty)) @@ -492,15 +404,12 @@ private bool TryCompleteObjectValue( } var selection = targetProperty.AssertSelection(); - errorTrie.TryGetValue(selection.ResponseName, out var errorTrieForResponseName); - if (!TryCompleteValue( - property.Value, - targetProperty, - errorTrieForResponseName, - selection, - selection.Type, - depth)) + ErrorTrie? errorTrieForResponseName = null; + errorTrie?.TryGetValue(selection.ResponseName, out errorTrieForResponseName); + + if (!TryCompleteValue(property.Value, + targetProperty, errorTrieForResponseName, selection, selection.Type, depth)) { return false; } From e3da2ee526ee8b721a3b4eeaff24f61b705e6f9c Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 12:46:23 +0000 Subject: [PATCH 29/36] Fusion: fast-path single-result source response buffering --- .../Nodes/OperationBatchExecutionNode.cs | 65 ++++++++++++++++--- .../Execution/Nodes/OperationExecutionNode.cs | 64 +++++++++++++++--- 2 files changed, 109 insertions(+), 20 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs index 1f69eaf4a55..6ba4530bd02 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Collections.Immutable; +using System.Runtime.InteropServices; using HotChocolate.Fusion.Execution.Clients; namespace HotChocolate.Fusion.Execution.Nodes; @@ -127,6 +128,7 @@ protected override async ValueTask OnExecuteAsync( var index = 0; var bufferLength = 0; SourceSchemaResult[]? buffer = null; + SourceSchemaResult? singleResult = null; var hasSomeErrors = false; try @@ -145,12 +147,26 @@ protected override async ValueTask OnExecuteAsync( totalPathCount += variables[i].AdditionalPaths.Length; } - bufferLength = Math.Max(totalPathCount, 1); - buffer = ArrayPool.Shared.Rent(bufferLength); + var initialBufferLength = Math.Max(totalPathCount, 2); await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { - buffer[index++] = result; + if (index == 0) + { + singleResult = result; + index = 1; + } + else + { + if (buffer is null) + { + bufferLength = initialBufferLength; + buffer = ArrayPool.Shared.Rent(bufferLength); + buffer[0] = singleResult!; + } + + buffer[index++] = result; + } // Parsing errors here allows the result store to reuse the cached value // and avoids a second document lookup per result. @@ -184,6 +200,10 @@ protected override async ValueTask OnExecuteAsync( buffer.AsSpan(0, index).Clear(); ArrayPool.Shared.Return(buffer); } + else if (singleResult is not null) + { + singleResult.Dispose(); + } AddErrors(context, exception, variables, _responseNames); return ExecutionStatus.Failed; @@ -191,11 +211,31 @@ protected override async ValueTask OnExecuteAsync( try { - context.AddPartialResults( - _source, - buffer.AsSpan(0, index), - _responseNames, - hasSomeErrors); + if (buffer is not null) + { + context.AddPartialResults( + _source, + buffer.AsSpan(0, index), + _responseNames, + hasSomeErrors); + } + else if (singleResult is not null) + { + var firstResult = singleResult; + context.AddPartialResults( + _source, + MemoryMarshal.CreateReadOnlySpan(ref firstResult, 1), + _responseNames, + hasSomeErrors); + } + else + { + context.AddPartialResults( + _source, + ReadOnlySpan.Empty, + _responseNames, + hasSomeErrors); + } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -212,8 +252,13 @@ protected override async ValueTask OnExecuteAsync( } finally { - buffer.AsSpan(0, index).Clear(); - ArrayPool.Shared.Return(buffer); + if (buffer is not null) + { + buffer.AsSpan(0, index).Clear(); + ArrayPool.Shared.Return(buffer); + } + + singleResult = null; } return hasSomeErrors ? ExecutionStatus.PartialSuccess : ExecutionStatus.Success; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs index 69495e8138c..49403993d1f 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs @@ -134,6 +134,7 @@ protected override async ValueTask OnExecuteAsync( var index = 0; var bufferLength = 0; SourceSchemaResult[]? buffer = null; + SourceSchemaResult? singleResult = null; var hasSomeErrors = false; try @@ -152,12 +153,26 @@ protected override async ValueTask OnExecuteAsync( totalPathCount += variables[i].AdditionalPaths.Length; } - bufferLength = Math.Max(totalPathCount, 1); - buffer = ArrayPool.Shared.Rent(bufferLength); + var initialBufferLength = Math.Max(totalPathCount, 2); await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { - buffer[index++] = result; + if (index == 0) + { + singleResult = result; + index = 1; + } + else + { + if (buffer is null) + { + bufferLength = initialBufferLength; + buffer = ArrayPool.Shared.Rent(bufferLength); + buffer[0] = singleResult!; + } + + buffer[index++] = result; + } // Parsing errors here allows the result store to reuse the cached value // and avoids a second document lookup per result. @@ -191,6 +206,10 @@ protected override async ValueTask OnExecuteAsync( buffer.AsSpan(0, index).Clear(); ArrayPool.Shared.Return(buffer); } + else if (singleResult is not null) + { + singleResult.Dispose(); + } AddErrors(context, exception, variables, _responseNames); return ExecutionStatus.Failed; @@ -198,11 +217,31 @@ protected override async ValueTask OnExecuteAsync( try { - context.AddPartialResults( - _source, - buffer.AsSpan(0, index), - _responseNames, - hasSomeErrors); + if (buffer is not null) + { + context.AddPartialResults( + _source, + buffer.AsSpan(0, index), + _responseNames, + hasSomeErrors); + } + else if (singleResult is not null) + { + var firstResult = singleResult; + context.AddPartialResults( + _source, + MemoryMarshal.CreateReadOnlySpan(ref firstResult, 1), + _responseNames, + hasSomeErrors); + } + else + { + context.AddPartialResults( + _source, + ReadOnlySpan.Empty, + _responseNames, + hasSomeErrors); + } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -219,8 +258,13 @@ protected override async ValueTask OnExecuteAsync( } finally { - buffer.AsSpan(0, index).Clear(); - ArrayPool.Shared.Return(buffer); + if (buffer is not null) + { + buffer.AsSpan(0, index).Clear(); + ArrayPool.Shared.Return(buffer); + } + + singleResult = null; } return hasSomeErrors ? ExecutionStatus.PartialSuccess : ExecutionStatus.Success; From fba80e7a88be64b8e18b13eb4127801fe12096ab Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 13:04:55 +0000 Subject: [PATCH 30/36] Fusion: defer string requirement dedupe map allocation --- .../Execution/Results/FetchResultStore.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 566b8877ee7..1de5cb217a2 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -675,6 +675,8 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa VariableValues[]? variableValueSets = null; Dictionary? seen = null; Dictionary? seenStrings = null; + string? firstStringValue = null; + var firstStringIndex = -1; List?[]? additionalPaths = null; var nextIndex = 0; @@ -706,9 +708,29 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa continue; } + if (firstStringIndex >= 0 + && string.Equals(firstStringValue, stringValue, StringComparison.Ordinal)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[firstStringIndex] ??= []).Add(result.Path); + continue; + } + mappedValue = ResultDataMapper.GetStringValueNode(stringValue); - seenStrings ??= new Dictionary(StringComparer.Ordinal); - seenStrings[stringValue] = nextIndex; + + if (firstStringIndex < 0) + { + firstStringValue = stringValue; + firstStringIndex = nextIndex; + } + else + { + seenStrings ??= new Dictionary(StringComparer.Ordinal) + { + [firstStringValue!] = firstStringIndex + }; + seenStrings[stringValue] = nextIndex; + } } else { From 4b9f7675c78bfa2898c08a0cc01c51992dd45604 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 13:14:12 +0000 Subject: [PATCH 31/36] Revert "Fusion: defer string requirement dedupe map allocation" This reverts commit fba80e7a88be64b8e18b13eb4127801fe12096ab. --- .../Execution/Results/FetchResultStore.cs | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 1de5cb217a2..566b8877ee7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -675,8 +675,6 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa VariableValues[]? variableValueSets = null; Dictionary? seen = null; Dictionary? seenStrings = null; - string? firstStringValue = null; - var firstStringIndex = -1; List?[]? additionalPaths = null; var nextIndex = 0; @@ -708,29 +706,9 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa continue; } - if (firstStringIndex >= 0 - && string.Equals(firstStringValue, stringValue, StringComparison.Ordinal)) - { - additionalPaths ??= new List?[elements.Count]; - (additionalPaths[firstStringIndex] ??= []).Add(result.Path); - continue; - } - mappedValue = ResultDataMapper.GetStringValueNode(stringValue); - - if (firstStringIndex < 0) - { - firstStringValue = stringValue; - firstStringIndex = nextIndex; - } - else - { - seenStrings ??= new Dictionary(StringComparer.Ordinal) - { - [firstStringValue!] = firstStringIndex - }; - seenStrings[stringValue] = nextIndex; - } + seenStrings ??= new Dictionary(StringComparer.Ordinal); + seenStrings[stringValue] = nextIndex; } else { From eba845909f0de267629c37bd9b6ead52a224d035 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 13:25:32 +0000 Subject: [PATCH 32/36] Fusion: lazily allocate non-string requirement dedupe map --- .../Execution/Results/FetchResultStore.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 566b8877ee7..c32a72d206a 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -714,16 +714,22 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa { mappedValue = ResultDataMapper.MapLeafValue(value, ref buffer); - if (seen is not null - && seen.TryGetValue(mappedValue, out var existingIndex)) + if (nextIndex > 0) { - additionalPaths ??= new List?[elements.Count]; - (additionalPaths[existingIndex] ??= []).Add(result.Path); - continue; - } + seen ??= new Dictionary(SingleValueNodeComparer.Instance) + { + [variableValueSets[0].Values.Fields[0].Value] = 0 + }; - seen ??= new Dictionary(SingleValueNodeComparer.Instance); - seen[mappedValue] = nextIndex; + if (seen.TryGetValue(mappedValue, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + seen[mappedValue] = nextIndex; + } } variableValueSets[nextIndex++] = new VariableValues( From ec5ce07496deda2ef251095dc946a29e14a0fa70 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 13:33:43 +0000 Subject: [PATCH 33/36] Revert "Fusion: lazily allocate non-string requirement dedupe map" This reverts commit eba845909f0de267629c37bd9b6ead52a224d035. --- .../Execution/Results/FetchResultStore.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index c32a72d206a..566b8877ee7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -714,22 +714,16 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa { mappedValue = ResultDataMapper.MapLeafValue(value, ref buffer); - if (nextIndex > 0) + if (seen is not null + && seen.TryGetValue(mappedValue, out var existingIndex)) { - seen ??= new Dictionary(SingleValueNodeComparer.Instance) - { - [variableValueSets[0].Values.Fields[0].Value] = 0 - }; - - if (seen.TryGetValue(mappedValue, out var existingIndex)) - { - additionalPaths ??= new List?[elements.Count]; - (additionalPaths[existingIndex] ??= []).Add(result.Path); - continue; - } - - seen[mappedValue] = nextIndex; + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; } + + seen ??= new Dictionary(SingleValueNodeComparer.Instance); + seen[mappedValue] = nextIndex; } variableValueSets[nextIndex++] = new VariableValues( From 3d38afc22a2a11bfc96f8a507e5d76a19155b233 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 13:36:40 +0000 Subject: [PATCH 34/36] Fusion: lazily compute batch response buffer capacity --- .../Nodes/OperationBatchExecutionNode.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs index 6ba4530bd02..8589a038d6d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs @@ -140,15 +140,6 @@ protected override async ValueTask OnExecuteAsync( context.TrackSourceSchemaClientResponse(this, response); // we read the responses from the response stream. - var totalPathCount = variables.Length; - - for (var i = 0; i < variables.Length; i++) - { - totalPathCount += variables[i].AdditionalPaths.Length; - } - - var initialBufferLength = Math.Max(totalPathCount, 2); - await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { if (index == 0) @@ -160,7 +151,14 @@ protected override async ValueTask OnExecuteAsync( { if (buffer is null) { - bufferLength = initialBufferLength; + var totalPathCount = variables.Length; + + for (var i = 0; i < variables.Length; i++) + { + totalPathCount += variables[i].AdditionalPaths.Length; + } + + bufferLength = Math.Max(totalPathCount, 2); buffer = ArrayPool.Shared.Rent(bufferLength); buffer[0] = singleResult!; } From ea50999672bcb870373889ecbd80a96fc9f57847 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 13:46:11 +0000 Subject: [PATCH 35/36] Revert "Fusion: lazily compute batch response buffer capacity" This reverts commit 3d38afc22a2a11bfc96f8a507e5d76a19155b233. --- .../Nodes/OperationBatchExecutionNode.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs index 8589a038d6d..6ba4530bd02 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs @@ -140,6 +140,15 @@ protected override async ValueTask OnExecuteAsync( context.TrackSourceSchemaClientResponse(this, response); // we read the responses from the response stream. + var totalPathCount = variables.Length; + + for (var i = 0; i < variables.Length; i++) + { + totalPathCount += variables[i].AdditionalPaths.Length; + } + + var initialBufferLength = Math.Max(totalPathCount, 2); + await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { if (index == 0) @@ -151,14 +160,7 @@ protected override async ValueTask OnExecuteAsync( { if (buffer is null) { - var totalPathCount = variables.Length; - - for (var i = 0; i < variables.Length; i++) - { - totalPathCount += variables[i].AdditionalPaths.Length; - } - - bufferLength = Math.Max(totalPathCount, 2); + bufferLength = initialBufferLength; buffer = ArrayPool.Shared.Rent(bufferLength); buffer[0] = singleResult!; } From ba532ccbc1ca35d8fd0ec5e51773314dda12c49a Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 26 Feb 2026 20:55:32 +0100 Subject: [PATCH 36/36] Polish --- .../Execution/Clients/SourceSchemaRequestDispatcher.cs | 1 - .../Execution/Nodes/OperationBatchExecutionNode.cs | 8 +++++--- .../Execution/Nodes/OperationExecutionNode.cs | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs index 90cb20f7a7a..10a17d1247d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using System.Runtime.InteropServices; -using HotChocolate.Fusion.Properties; using HotChocolate.Language; using static HotChocolate.Fusion.Execution.Clients.SourceSchemaClientCapabilities; using static HotChocolate.Fusion.Properties.FusionExecutionResources; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs index 6ba4530bd02..776f8e32be4 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs @@ -151,6 +151,8 @@ protected override async ValueTask OnExecuteAsync( await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { + // Store the first result without renting a buffer, + // since it might be the only one (e.g. a request-level error). if (index == 0) { singleResult = result; @@ -158,6 +160,8 @@ protected override async ValueTask OnExecuteAsync( } else { + // Once we see a second result, we know there are multiple, + // so we rent a buffer and move the first result into it. if (buffer is null) { bufferLength = initialBufferLength; @@ -232,7 +236,7 @@ protected override async ValueTask OnExecuteAsync( { context.AddPartialResults( _source, - ReadOnlySpan.Empty, + [], _responseNames, hasSomeErrors); } @@ -257,8 +261,6 @@ protected override async ValueTask OnExecuteAsync( buffer.AsSpan(0, index).Clear(); ArrayPool.Shared.Return(buffer); } - - singleResult = null; } return hasSomeErrors ? ExecutionStatus.PartialSuccess : ExecutionStatus.Success; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs index 49403993d1f..ddfc2aeafc9 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs @@ -157,6 +157,7 @@ protected override async ValueTask OnExecuteAsync( await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { + // If there is only one response, we skip the buffer rental. if (index == 0) { singleResult = result; @@ -164,6 +165,7 @@ protected override async ValueTask OnExecuteAsync( } else { + // If we have more than one response, we rent a buffer and move the first result into it. if (buffer is null) { bufferLength = initialBufferLength; @@ -238,7 +240,7 @@ protected override async ValueTask OnExecuteAsync( { context.AddPartialResults( _source, - ReadOnlySpan.Empty, + [], _responseNames, hasSomeErrors); } @@ -263,8 +265,6 @@ protected override async ValueTask OnExecuteAsync( buffer.AsSpan(0, index).Clear(); ArrayPool.Shared.Return(buffer); } - - singleResult = null; } return hasSomeErrors ? ExecutionStatus.PartialSuccess : ExecutionStatus.Success;