diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs index 85355aec046..01438bbe2e2 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs @@ -117,20 +117,20 @@ public async ValueTask> ExecuteBatchA var contentType = httpResponse.ContentHeaders.ContentType?.ToString() ?? "unknown"; var isSuccessful = httpResponse.IsSuccessStatusCode; - var nodeResponsesByNodeId = new Dictionary(requests.Length); + var nodeResponses = new NodeResponse[requests.Length]; var builder = ImmutableArray.CreateBuilder(requests.Length); for (var i = 0; i < requests.Length; i++) { var nodeResponse = new NodeResponse(uri, contentType, isSuccessful); - nodeResponsesByNodeId[requests[i].Node.Id] = nodeResponse; + nodeResponses[i] = nodeResponse; builder.Add(nodeResponse); } _ = ReadBatchStreamInBackgroundAsync( context, requests, - nodeResponsesByNodeId, + nodeResponses, httpResponse, cancellationToken); @@ -290,7 +290,7 @@ private static VariableBatchRequest CreateVariableBatchRequest( private async Task ReadBatchStreamInBackgroundAsync( OperationPlanContext context, ImmutableArray requests, - Dictionary nodeResponses, + NodeResponse[] nodeResponses, GraphQLHttpResponse httpResponse, CancellationToken cancellationToken) { @@ -311,15 +311,7 @@ private async Task ReadBatchStreamInBackgroundAsync( } var request = requests[requestIndex]; - - if (!nodeResponses.TryGetValue(request.Node.Id, out var nodeResponse)) - { - result.Dispose(); - throw new InvalidOperationException( - string.Format( - FusionExecutionResources.SourceSchemaHttpClient_NoResponseChannelForNode, - request.Node.Id)); - } + var nodeResponse = nodeResponses[requestIndex]; var variableIndex = ResolveVariableIndex(request, result); @@ -338,15 +330,17 @@ private async Task ReadBatchStreamInBackgroundAsync( // Stream completed successfully. Complete all channels, failing any // that never received results (fail-loud). - foreach (var (nodeId, nodeResponse) in nodeResponses) + for (var i = 0; i < nodeResponses.Length; i++) { + var nodeResponse = nodeResponses[i]; + if (!nodeResponse.HasReceivedResults) { nodeResponse.Complete( new InvalidOperationException( string.Format( FusionExecutionResources.SourceSchemaHttpClient_NoResultForNode, - nodeId))); + requests[i].Node.Id))); } else { @@ -356,9 +350,9 @@ private async Task ReadBatchStreamInBackgroundAsync( } catch (Exception ex) { - foreach (var nodeResponse in nodeResponses.Values) + for (var i = 0; i < nodeResponses.Length; i++) { - nodeResponse.Complete(ex); + nodeResponses[i].Complete(ex); } } finally @@ -499,8 +493,12 @@ private void WriteResultToChannel( ImmutableArray additionalPaths, SourceResultDocument document) { - var sourceSchemaResult = new SourceSchemaResult(path, document); - _configuration.OnSourceSchemaResult?.Invoke(context, node, sourceSchemaResult); + var sourceSchemaResult = additionalPaths.IsDefaultOrEmpty + ? new SourceSchemaResult(path, document) + : new SourceSchemaResult(path, document, additionalPaths: additionalPaths); + var onSourceSchemaResult = _configuration.OnSourceSchemaResult; + + onSourceSchemaResult?.Invoke(context, node, sourceSchemaResult); if (!nodeResponse.TryWrite(sourceSchemaResult)) { @@ -510,17 +508,15 @@ private void WriteResultToChannel( nodeResponse.HasReceivedResults = true; - foreach (var additionalPath in additionalPaths) + if (onSourceSchemaResult is null || additionalPaths.IsDefaultOrEmpty) { - var alias = sourceSchemaResult.WithPath(additionalPath); - _configuration.OnSourceSchemaResult?.Invoke(context, node, alias); + return; + } - if (!nodeResponse.TryWrite(alias)) - { - // alias does not own the document (ownsDocument: false via WithPath), - // so no disposal needed for the alias itself. - return; - } + // Preserve callback behavior for all logical result paths without enqueueing aliases. + foreach (var additionalPath in additionalPaths) + { + onSourceSchemaResult(context, node, sourceSchemaResult.WithPath(additionalPath)); } } 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 3bcf2e8f875..c863c9d05f1 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,7 +1,9 @@ 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; namespace HotChocolate.Fusion.Execution.Clients; @@ -19,12 +21,18 @@ internal sealed class SourceSchemaRequestDispatcher : ISourceSchemaScheduler , ISourceSchemaDispatcher { +#if NET9_0_OR_GREATER + private readonly Lock _sync = new(); +#else private readonly object _sync = new(); +#endif private readonly OperationPlanContext _context; private readonly ISourceSchemaClientScope _clientScope; private readonly CancellationToken _requestAborted; private readonly Dictionary _groups = []; - private readonly Dictionary _groupByNodeId = []; + private readonly List _trackedNodeIdSlots = []; + private int[] _groupByNodeIdSlots = []; + private int[] _nodeStateSlots = []; private Exception? _abortError; private bool _aborted; @@ -91,7 +99,7 @@ public ValueTask ExecuteAsync( } // we register the node to be dispatched. else if (_groups.TryGetValue(groupId, out var group) - && group.TrySubmit(request, out pendingRequest)) + && group.TrySubmit(request, _nodeStateSlots, out pendingRequest)) { if (group.TryCreateDispatch(out pendingRequests)) { @@ -104,7 +112,7 @@ public ValueTask ExecuteAsync( { abortError = new InvalidOperationException( string.Format( - FusionExecutionResources.SourceSchemaRequestDispatcher_NodeNotRegisteredInGroup, + SourceSchemaRequestDispatcher_NodeNotRegisteredInGroup, request.Node.Id, groupId)); } @@ -138,7 +146,7 @@ public void RegisterGroup(int groupId, IReadOnlyList nodeIds) if (nodeIds.Count == 0) { throw new ArgumentException( - FusionExecutionResources.SourceSchemaRequestDispatcher_RegisterGroupEmptyNodeIds, + SourceSchemaRequestDispatcher_RegisterGroupEmptyNodeIds, nameof(nodeIds)); } @@ -151,15 +159,27 @@ public void RegisterGroup(int groupId, IReadOnlyList nodeIds) if (!_groups.TryGetValue(groupId, out var group)) { - group = new GroupState(groupId); + group = new GroupState(groupId, nodeIds.Count); _groups.Add(groupId, group); } - group.Register(nodeIds); - foreach (var nodeId in nodeIds) { - _groupByNodeId[nodeId] = groupId; + EnsureNodeIdSlotCapacity(nodeId + 1); + var existingGroupId = _groupByNodeIdSlots[nodeId]; + + if (existingGroupId < 0) + { + _trackedNodeIdSlots.Add(nodeId); + group.RegisterNode(nodeId); + } + else if (existingGroupId != groupId) + { + group.RegisterNode(nodeId); + } + + _groupByNodeIdSlots[nodeId] = groupId; + _nodeStateSlots[nodeId] = 0; } } } @@ -171,7 +191,7 @@ public void RegisterGroup(int groupId, IReadOnlyList nodeIds) /// The execution node ID to skip. public void SkipNode(int nodeId) { - ImmutableArray pendingRequests = []; + ImmutableArray pendingRequests; var needsDispatch = false; lock (_sync) @@ -181,13 +201,19 @@ public void SkipNode(int nodeId) return; } - if (!_groupByNodeId.TryGetValue(nodeId, out var groupId) - || !_groups.TryGetValue(groupId, out var group)) + if ((uint)nodeId >= (uint)_groupByNodeIdSlots.Length) { return; } - group.Skip(nodeId); + var groupId = _groupByNodeIdSlots[nodeId]; + + if (groupId < 0 || !_groups.TryGetValue(groupId, out var group)) + { + return; + } + + group.Skip(nodeId, _nodeStateSlots); if (group.TryCreateDispatch(out pendingRequests)) { @@ -224,12 +250,12 @@ public void Abort(Exception? error = null) } _aborted = true; - _abortError = error ?? new OperationCanceledException(FusionExecutionResources.SourceSchemaRequestDispatcher_OperationAborted); + _abortError = error ?? new OperationCanceledException(SourceSchemaRequestDispatcher_OperationAborted); abortError = _abortError; pendingRequests = [.. _groups.Values.SelectMany(static t => t.PendingRequests)]; _groups.Clear(); - _groupByNodeId.Clear(); + ClearNodeIdSlots(); } foreach (var pendingRequest in pendingRequests) @@ -250,7 +276,7 @@ public void Reset() _aborted = false; _abortError = null; _groups.Clear(); - _groupByNodeId.Clear(); + ClearNodeIdSlots(); } } @@ -331,16 +357,23 @@ private async ValueTask DispatchBatchAsync( { try { + var requests = new SourceSchemaClientRequest[pendingRequests.Length]; + + for (var i = 0; i < pendingRequests.Length; i++) + { + requests[i] = pendingRequests[i].Request; + } + var responses = await client.ExecuteBatchAsync( _context, - [.. pendingRequests.Select(t => t.Request)], + ImmutableCollectionsMarshal.AsImmutableArray(requests), _requestAborted) .ConfigureAwait(false); if (responses.Length != pendingRequests.Length) { throw new InvalidOperationException( - FusionExecutionResources.SourceSchemaRequestDispatcher_BatchResponseCountMismatch); + SourceSchemaRequestDispatcher_BatchResponseCountMismatch); } for (var i = 0; i < pendingRequests.Length; i++) @@ -371,7 +404,7 @@ [.. pendingRequests.Select(t => t.Request)], } private Exception CreateAbortException() - => _abortError ?? new OperationCanceledException(FusionExecutionResources.SourceSchemaRequestDispatcher_OperationAborted); + => _abortError ?? new OperationCanceledException(SourceSchemaRequestDispatcher_OperationAborted); private void RemoveGroup(GroupState group) { @@ -379,15 +412,67 @@ private void RemoveGroup(GroupState group) foreach (var nodeId in group.NodeIds) { - _groupByNodeId.Remove(nodeId); + if ((uint)nodeId < (uint)_groupByNodeIdSlots.Length) + { + _groupByNodeIdSlots[nodeId] = -1; + _nodeStateSlots[nodeId] = -1; + } + } + } + + private void ClearNodeIdSlots() + { + if (_trackedNodeIdSlots.Count == 0) + { + return; + } + + foreach (var nodeId in _trackedNodeIdSlots) + { + if ((uint)nodeId < (uint)_groupByNodeIdSlots.Length) + { + _groupByNodeIdSlots[nodeId] = -1; + _nodeStateSlots[nodeId] = -1; + } + } + + _trackedNodeIdSlots.Clear(); + } + + private void EnsureNodeIdSlotCapacity(int minCapacity) + { + if (_groupByNodeIdSlots.Length >= minCapacity) + { + return; + } + + var newCapacity = _groupByNodeIdSlots.Length == 0 ? 8 : _groupByNodeIdSlots.Length; + + while (newCapacity < minCapacity) + { + newCapacity *= 2; + } + + var groupByNodeIdSlots = new int[newCapacity]; + var nodeStateSlots = new int[newCapacity]; + Array.Fill(groupByNodeIdSlots, -1); + Array.Fill(nodeStateSlots, -1); + + if (_groupByNodeIdSlots.Length > 0) + { + Array.Copy(_groupByNodeIdSlots, groupByNodeIdSlots, _groupByNodeIdSlots.Length); + Array.Copy(_nodeStateSlots, nodeStateSlots, _nodeStateSlots.Length); } + + _groupByNodeIdSlots = groupByNodeIdSlots; + _nodeStateSlots = nodeStateSlots; } - private sealed class GroupState(int id) + private sealed class GroupState(int id, int initialCapacity) { - private readonly HashSet _nodeIds = []; - private readonly HashSet _remainingNodeIds = []; - private readonly Dictionary _pendingRequests = []; + private readonly List _nodeIds = new(initialCapacity); + private readonly Dictionary _pendingRequests = new(initialCapacity); + private int _remainingNodes; private bool _dispatchCreated; public int Id { get; } = id; @@ -396,36 +481,35 @@ private sealed class GroupState(int id) public IEnumerable PendingRequests => _pendingRequests.Values; - public void Register(IReadOnlyList nodeIds) + public void RegisterNode(int nodeId) { - foreach (var nodeId in nodeIds) - { - _nodeIds.Add(nodeId); - _remainingNodeIds.Add(nodeId); - } + _nodeIds.Add(nodeId); + _remainingNodes++; } public bool TrySubmit( SourceSchemaClientRequest request, + int[] nodeStateSlots, out PendingRequest? pendingRequest) { var nodeId = request.Node.Id; - if (!_nodeIds.Contains(nodeId)) - { - pendingRequest = null; - return false; - } - if (_pendingRequests.ContainsKey(nodeId)) { throw new InvalidOperationException( string.Format( - FusionExecutionResources.SourceSchemaRequestDispatcher_DuplicateNodeSubmission, + SourceSchemaRequestDispatcher_DuplicateNodeSubmission, nodeId)); } - _remainingNodeIds.Remove(nodeId); + if ((uint)nodeId >= (uint)nodeStateSlots.Length || nodeStateSlots[nodeId] != 0) + { + pendingRequest = null; + return false; + } + + nodeStateSlots[nodeId] = 1; + _remainingNodes--; pendingRequest = new PendingRequest(request); _pendingRequests.Add(nodeId, pendingRequest); @@ -433,12 +517,18 @@ public bool TrySubmit( return true; } - public void Skip(int nodeId) - => _remainingNodeIds.Remove(nodeId); + public void Skip(int nodeId, int[] nodeStateSlots) + { + if ((uint)nodeId < (uint)nodeStateSlots.Length && nodeStateSlots[nodeId] == 0) + { + nodeStateSlots[nodeId] = 1; + _remainingNodes--; + } + } public bool TryCreateDispatch(out ImmutableArray pendingRequests) { - if (_dispatchCreated || _remainingNodeIds.Count > 0) + if (_dispatchCreated || _remainingNodes > 0) { pendingRequests = []; return false; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs index cd5c4862954..4e729741cb7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs @@ -1,7 +1,13 @@ +using System.Collections.Immutable; using HotChocolate.Fusion.Text.Json; namespace HotChocolate.Fusion.Execution.Clients; +/// +/// Represents the result returned by a source schema after executing a GraphQL request. +/// Provides access to the data, errors, and extensions sections of the +/// response and manages the lifetime of the underlying result document. +/// public sealed class SourceSchemaResult : IDisposable { private static ReadOnlySpan DataProperty => "data"u8; @@ -11,11 +17,20 @@ public sealed class SourceSchemaResult : IDisposable private readonly bool _ownsDocument; private bool _errorsParsed; + /// + /// Creates a new that takes ownership of the given document + /// and will dispose it when this result is disposed. + /// + /// The path in the Fusion result where this result will be merged. + /// The raw response document from the source schema. + /// Whether this is the final message in a streaming response. + /// Any additional paths where this result should also be merged. public SourceSchemaResult( Path path, SourceResultDocument document, - FinalMessage final = FinalMessage.Undefined) - : this(path, document, final, ownsDocument: true) + FinalMessage final = FinalMessage.Undefined, + ImmutableArray additionalPaths = default) + : this(path, document, final, ownsDocument: true, additionalPaths) { } @@ -23,19 +38,34 @@ private SourceSchemaResult( Path path, SourceResultDocument document, FinalMessage final, - bool ownsDocument) + bool ownsDocument, + ImmutableArray additionalPaths) { ArgumentNullException.ThrowIfNull(path); ArgumentNullException.ThrowIfNull(document); _document = document; _ownsDocument = ownsDocument; + AdditionalPaths = additionalPaths.IsDefault ? [] : additionalPaths; Path = path; Final = final; } + /// + /// The primary path in the composite result into which this source schema result will be merged. + /// public Path Path { get; } + /// + /// Additional paths where this result should also be merged, used when a single source + /// schema response satisfies multiple selection sets at different locations. + /// + public ImmutableArray AdditionalPaths { get; } + + /// + /// The data element of the source schema response, or an empty element if the + /// response did not include a data property. + /// public SourceResultElement Data { get @@ -45,6 +75,10 @@ public SourceResultElement Data } } + /// + /// The parsed errors from the source schema response, or null if there were none. + /// Parsed lazily on first access. + /// public SourceSchemaErrors? Errors { get @@ -70,8 +104,15 @@ internal SourceResultElement RawErrors } } + /// + /// Returns true if the source schema response contains an errors property. + /// public bool HasErrors => _document.Root.TryGetProperty(ErrorsProperty, out _); + /// + /// The extensions element of the source schema response, or an empty element if + /// the response did not include an extensions property. + /// public SourceResultElement Extensions { get @@ -81,10 +122,22 @@ public SourceResultElement Extensions } } + /// + /// Indicates whether this result represents the final message in a streaming response. + /// public FinalMessage Final { get; } - internal SourceSchemaResult WithPath(Path path) => new(path, _document, Final, ownsDocument: false); + /// + /// Creates a copy of this result associated with a different path, without taking ownership + /// of the underlying document. Used internally when the same result needs to be referenced + /// at a different location in the composite result. + /// + internal SourceSchemaResult WithPath(Path path) + => new(path, _document, Final, ownsDocument: false, additionalPaths: []); + /// + /// Disposes the underlying result document if this instance owns it. + /// public void Dispose() { if (_ownsDocument) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs index 604b60e5cb2..9a0de27ecdd 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs @@ -3,32 +3,44 @@ using System.Runtime.CompilerServices; using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Language; -using HotChocolate.Utilities; namespace HotChocolate.Fusion.Execution; internal sealed class ExecutionState(bool collectTelemetry, CancellationTokenSource cts) { - private readonly List _stack = []; - - private readonly List _backlog = []; - - private readonly HashSet _completed = []; + private const byte NodeStateNone = 0; + private const byte NodeStateBacklog = 1; + private const byte NodeStateSkipped = 2; + private readonly List _stack = []; + private readonly List _ready = []; + private readonly List _trackedNodeStateSlots = []; + private readonly List _trackedDependencySlots = []; private readonly ConcurrentQueue _completedResults = new(); + private byte[] _nodeStates = []; + private int[] _remainingDependencies = []; + private int _backlogCount; private int _activeNodes; public readonly OrderedDictionary Traces = []; - public readonly AsyncAutoResetEvent Signal = new(); public void FillBacklog(OperationPlan plan) { + _ready.Clear(); + _backlogCount = 0; + + ResetNodeStates(); + ResetRemainingDependencies(); + switch (plan.Operation.Definition.Operation) { case OperationType.Query: - _backlog.AddRange(plan.AllNodes); + foreach (var node in plan.AllNodes) + { + AddToBacklog(node); + } break; case OperationType.Mutation: @@ -41,13 +53,20 @@ public void FillBacklog(OperationPlan plan) continue; } - _backlog.Add(node); + AddToBacklog(node); } break; case OperationType.Subscription: - _backlog.AddRange(plan.AllNodes); - _backlog.Remove(plan.RootNodes.Single()); + var root = plan.RootNodes.Single(); + + foreach (var node in plan.AllNodes) + { + if (!ReferenceEquals(node, root)) + { + AddToBacklog(node); + } + } // The root node of a subscription is started outside the state. // We cater to this fact and fix the state by stating with am active node count of 1. @@ -62,8 +81,12 @@ public void FillBacklog(OperationPlan plan) public void Reset() { _stack.Clear(); - _backlog.Clear(); - _completed.Clear(); + _ready.Clear(); + _backlogCount = 0; + + ResetNodeStates(); + ResetRemainingDependencies(); + _completedResults.Clear(); _activeNodes = 0; @@ -72,7 +95,7 @@ public void Reset() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsProcessing() => _backlog.Count > 0 || Volatile.Read(ref _activeNodes) > 0; + public bool IsProcessing() => _backlogCount > 0 || Volatile.Read(ref _activeNodes) > 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasActiveNodes() => Volatile.Read(ref _activeNodes) > 0; @@ -81,7 +104,13 @@ public void Reset() public void StartNode(OperationPlanContext context, ExecutionNode node, CancellationToken cancellationToken) { Interlocked.Increment(ref _activeNodes); - _backlog.Remove(node); + + if ((uint)node.Id < (uint)_remainingDependencies.Length) + { + _remainingDependencies[node.Id] = -1; + } + + RemoveFromBacklog(node.Id, NodeStateNone); _ = node.ExecuteAsync(context, cancellationToken); } @@ -133,8 +162,6 @@ public void CompleteNode( if (result.Status is ExecutionStatus.Success or ExecutionStatus.PartialSuccess) { - _completed.Add(node); - if (result.DependentsToExecute.Length > 0) { foreach (var dependent in node.Dependents) @@ -145,6 +172,31 @@ public void CompleteNode( } } } + + foreach (var dependent in node.Dependents) + { + if ((uint)dependent.Id >= (uint)_remainingDependencies.Length) + { + continue; + } + + var remainingDependencies = _remainingDependencies[dependent.Id]; + + if (remainingDependencies <= 0) + { + continue; + } + + if (remainingDependencies == 1) + { + _remainingDependencies[dependent.Id] = 0; + _ready.Add(dependent); + } + else if (remainingDependencies > 1) + { + _remainingDependencies[dependent.Id] = remainingDependencies - 1; + } + } } if (result.Status is ExecutionStatus.Skipped or ExecutionStatus.Failed) @@ -162,9 +214,13 @@ public void SkipNode(OperationPlanContext context, ExecutionNode node) { context.SourceSchemaDispatcher.SkipNode(current.Id); - if (_backlog.Remove(current) + if ((uint)current.Id < (uint)_remainingDependencies.Length) + { + _remainingDependencies[current.Id] = -1; + } + + if (RemoveFromBacklog(current.Id, NodeStateSkipped) && collectTelemetry - && !_completed.Contains(current) && !Traces.ContainsKey(current.Id)) { Traces.Add( @@ -178,11 +234,17 @@ public void SkipNode(OperationPlanContext context, ExecutionNode node) }); } - foreach (var enqueuedNode in _backlog) + foreach (var dependent in current.Dependents) { - if (enqueuedNode.Dependencies.Contains(current)) + if ((uint)dependent.Id >= (uint)_remainingDependencies.Length + || _remainingDependencies[dependent.Id] < 0) + { + continue; + } + + if (IsInBacklog(dependent.Id)) { - _stack.Push(enqueuedNode); + _stack.Push(dependent); } } } @@ -192,36 +254,176 @@ public bool EnqueueNextNodes(OperationPlanContext context, CancellationToken can { _stack.Clear(); - foreach (var node in _backlog) + if (_ready.Count == 0) + { + return false; + } + + var isSorted = true; + var previousId = int.MinValue; + + foreach (var node in _ready) { - if (CanExecuteNode(node)) + if ((uint)node.Id < (uint)_remainingDependencies.Length + && _remainingDependencies[node.Id] == 0) { _stack.Push(node); + + if (node.Id < previousId) + { + isSorted = false; + } + + previousId = node.Id; } } + _ready.Clear(); + + if (_stack.Count == 0) + { + return false; + } + + if (!isSorted && _stack.Count > 1) + { + _stack.Sort(static (a, b) => a.Id.CompareTo(b.Id)); + } + foreach (var node in _stack) { StartNode(context, node, cancellationToken); } - return _stack.Count > 0; + return true; } - private bool CanExecuteNode(ExecutionNode node) + private void EnsureDependencyCapacity(int minCapacity) { - var dependenciesFulfilled = true; + if (_remainingDependencies.Length >= minCapacity) + { + return; + } + + var newCapacity = _remainingDependencies.Length == 0 ? 8 : _remainingDependencies.Length; - foreach (var dependency in node.Dependencies) + while (newCapacity < minCapacity) { - if (_completed.Contains(dependency)) - { - continue; - } + newCapacity *= 2; + } + + var dependencies = new int[newCapacity]; + Array.Fill(dependencies, -1); + + if (_remainingDependencies.Length > 0) + { + Array.Copy(_remainingDependencies, dependencies, _remainingDependencies.Length); + } + + _remainingDependencies = dependencies; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsInBacklog(int nodeId) + => (uint)nodeId < (uint)_nodeStates.Length + && _nodeStates[nodeId] == NodeStateBacklog; + + private void AddToBacklog(ExecutionNode node) + { + var nodeId = node.Id; + + if ((uint)nodeId >= (uint)_nodeStates.Length) + { + EnsureNodeStateCapacity(nodeId + 1); + } + + if (_nodeStates[nodeId] == NodeStateBacklog) + { + return; + } + + if (_nodeStates[nodeId] == NodeStateNone) + { + _trackedNodeStateSlots.Add(nodeId); + } + + _nodeStates[nodeId] = NodeStateBacklog; + _backlogCount++; + + var remainingDependencies = node.Dependencies.Length; + EnsureDependencyCapacity(nodeId + 1); + _remainingDependencies[nodeId] = remainingDependencies; + _trackedDependencySlots.Add(nodeId); + + if (remainingDependencies == 0) + { + _ready.Add(node); + } + } + + private bool RemoveFromBacklog(int nodeId, byte targetState) + { + if (!IsInBacklog(nodeId)) + { + return false; + } - dependenciesFulfilled = false; + _nodeStates[nodeId] = targetState; + _backlogCount--; + return true; + } + + private void ResetNodeStates() + { + if (_trackedNodeStateSlots.Count == 0) + { + return; + } + + foreach (var slot in _trackedNodeStateSlots) + { + _nodeStates[slot] = NodeStateNone; + } + + _trackedNodeStateSlots.Clear(); + } + + private void ResetRemainingDependencies() + { + if (_trackedDependencySlots.Count == 0) + { + return; + } + + foreach (var slot in _trackedDependencySlots) + { + _remainingDependencies[slot] = -1; + } + + _trackedDependencySlots.Clear(); + } + + private void EnsureNodeStateCapacity(int minCapacity) + { + if (_nodeStates.Length >= minCapacity) + { + return; + } + + var newCapacity = _nodeStates.Length == 0 ? 8 : _nodeStates.Length; + + while (newCapacity < minCapacity) + { + newCapacity *= 2; + } + + var nodeStates = new byte[newCapacity]; + + if (_nodeStates.Length > 0) + { + Array.Copy(_nodeStates, nodeStates, _nodeStates.Length); } - return dependenciesFulfilled; + _nodeStates = nodeStates; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationPlan.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationPlan.cs index 20646611c6d..cabbffe0071 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationPlan.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationPlan.cs @@ -1,4 +1,3 @@ -using System.Collections.Frozen; using System.Collections.Immutable; using System.Security.Cryptography; using HotChocolate.Buffers; @@ -14,7 +13,8 @@ namespace HotChocolate.Fusion.Execution.Nodes; public sealed record OperationPlan { private static readonly JsonOperationPlanFormatter s_formatter = new(); - private readonly FrozenDictionary _nodes = FrozenDictionary.Empty; + private readonly ExecutionNode?[] _nodesById = []; + private readonly ImmutableArray _batchingGroups; private OperationPlan( string id, @@ -30,7 +30,8 @@ private OperationPlan( AllNodes = allNodes; SearchSpace = searchSpace; ExpandedNodes = expandedNodes; - _nodes = allNodes.ToFrozenDictionary(t => t.Id); + _nodesById = CreateNodeLookup(allNodes); + _batchingGroups = CreateBatchingGroups(allNodes); } /// @@ -74,6 +75,13 @@ public IReadOnlyList VariableDefinitions /// public int ExpandedNodes { get; } + /// + /// The batching groups derived from the execution nodes in this plan. Each group contains + /// the IDs of nodes that belong to the same batch and should be executed together. + /// + internal ImmutableArray BatchingGroups + => _batchingGroups; + /// /// Retrieves an execution node by its unique identifier. /// @@ -81,7 +89,15 @@ public IReadOnlyList VariableDefinitions /// The execution node with the specified identifier. /// Thrown when no node with the specified ID exists. public ExecutionNode GetNodeById(int id) - => _nodes[id]; + { + if ((uint)id < (uint)_nodesById.Length + && _nodesById[id] is { } node) + { + return node; + } + + throw new KeyNotFoundException(); + } /// /// Creates a new operation plan with the specified identifier. @@ -153,4 +169,77 @@ public static OperationPlan Create( return new OperationPlan(id, operation, rootNodes, allNodes, searchSpace, expandedNodes); } + + private static ImmutableArray CreateBatchingGroups( + ImmutableArray allNodes) + { + Dictionary>? groups = null; + + foreach (var executionNode in allNodes) + { + var groupId = executionNode switch + { + OperationExecutionNode n => n.BatchingGroupId, + OperationBatchExecutionNode n => n.BatchingGroupId, + _ => null + }; + + if (groupId is null) + { + continue; + } + + groups ??= []; + + if (!groups.TryGetValue(groupId.Value, out var nodeIds)) + { + nodeIds = []; + groups.Add(groupId.Value, nodeIds); + } + + nodeIds.Add(executionNode.Id); + } + + if (groups is null) + { + return []; + } + + var registrations = ImmutableArray.CreateBuilder(groups.Count); + + foreach (var (groupId, nodeIds) in groups) + { + registrations.Add(new BatchingGroupRegistration(groupId, [.. nodeIds])); + } + + return registrations.MoveToImmutable(); + } + + private static ExecutionNode?[] CreateNodeLookup(ImmutableArray allNodes) + { + if (allNodes.IsDefaultOrEmpty) + { + return []; + } + + var maxId = 0; + + foreach (var node in allNodes) + { + maxId = Math.Max(maxId, node.Id); + } + + var nodesById = new ExecutionNode?[maxId + 1]; + + foreach (var node in allNodes) + { + nodesById[node.Id] = node; + } + + return nodesById; + } + + internal readonly record struct BatchingGroupRegistration( + int GroupId, + int[] NodeIds); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs index 985e9281d61..5b71ef1cb7c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs @@ -328,42 +328,9 @@ private static async IAsyncEnumerable CreateSubscriptionEnumera private static void RegisterBatchingGroups(OperationPlanContext context, OperationPlan plan) { - Dictionary>? groups = null; - - foreach (var executionNode in plan.AllNodes) - { - var groupId = executionNode switch - { - OperationExecutionNode n => n.BatchingGroupId, - OperationBatchExecutionNode n => n.BatchingGroupId, - _ => null - }; - - if (groupId is null) - { - continue; - } - - groups ??= []; - - if (!groups.TryGetValue(groupId.Value, out var nodeIds)) - { - nodeIds = []; - groups.Add(groupId.Value, nodeIds); - } - - nodeIds.Add(executionNode.Id); - } - - if (groups is null) - { - return; - } - - foreach (var (groupId, nodeIds) in groups) + foreach (var group in plan.BatchingGroups) { - nodeIds.TrimExcess(); - context.SourceSchemaDispatcher.RegisterGroup(groupId, nodeIds); + context.SourceSchemaDispatcher.RegisterGroup(group.GroupId, group.NodeIds); } } } 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 00caae77665..fe580a90a4a 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 @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json; @@ -29,6 +30,9 @@ internal sealed class FetchResultStore : IDisposable private readonly ErrorHandlingMode _errorHandlingMode; private readonly ulong _includeFlags; private readonly ConcurrentStack _memory = []; + private readonly List _collectTargetCurrent = []; + private readonly List _collectTargetNext = []; + private readonly List _collectTargetCombined = []; private CompositeResultDocument _result; private ValueCompletion _valueCompletion; private List? _errors; @@ -102,44 +106,64 @@ public bool AddPartialResults( var dataElements = ArrayPool.Shared.Rent(results.Length); var errorTries = ArrayPool.Shared.Rent(results.Length); - var dataElementsSpan = dataElements.AsSpan()[..results.Length]; - var errorTriesSpan = errorTries.AsSpan()[..results.Length]; + var dataElementsSpan = dataElements.AsSpan(0, results.Length); + var errorTriesSpan = errorTries.AsSpan(0, results.Length); + List? rootErrors = null; try { - ref var result = ref MemoryMarshal.GetReference(results); - ref var dataElement = ref MemoryMarshal.GetReference(dataElementsSpan); - ref var errorTrie = ref MemoryMarshal.GetReference(errorTriesSpan); - ref var end = ref Unsafe.Add(ref result, results.Length); - - while (Unsafe.IsAddressLessThan(ref result, ref end)) + for (var i = 0; i < results.Length; i++) { - // we need to track the result objects as they used rented memory. + var result = results[i]; + + // we need to track the result objects as they use rented memory. _memory.Push(result); var errors = result.Errors; - if (errors?.RootErrors is { Length: > 0 } rootErrors) + if (errors?.RootErrors is { Length: > 0 } rootErrorsFromResult) { - lock (_lock) - { - _errors ??= []; - _errors.AddRange(rootErrors); - } + rootErrors ??= []; + rootErrors.AddRange(rootErrorsFromResult); + } + + dataElementsSpan[i] = GetDataElement(sourcePath, result.Data); + errorTriesSpan[i] = GetErrorTrie(sourcePath, errors?.Trie); + } + + lock (_lock) + { + if (rootErrors is not null) + { + _errors ??= []; + _errors.AddRange(rootErrors); } - dataElement = GetDataElement(sourcePath, result.Data); - errorTrie = GetErrorTrie(sourcePath, errors?.Trie); + var resultData = _result.Data; - result = ref Unsafe.Add(ref result, 1)!; - dataElement = ref Unsafe.Add(ref dataElement, 1); - errorTrie = ref Unsafe.Add(ref errorTrie, 1)!; + for (var i = 0; i < results.Length; i++) + { + var result = results[i]; + + if (!SaveSafeResult( + resultData, + result.Path, + result.AdditionalPaths.AsSpan(), + dataElementsSpan[i], + errorTriesSpan[i], + responseNames)) + { + return false; + } + } } - return SaveSafe(results, dataElementsSpan, errorTriesSpan, responseNames); + return true; } finally { + dataElementsSpan.Clear(); + errorTriesSpan.Clear(); ArrayPool.Shared.Return(dataElements); ArrayPool.Shared.Return(errorTries); } @@ -223,54 +247,62 @@ public bool AddErrors(IError error, ReadOnlySpan responseNames, params R return true; } - private bool SaveSafe( - ReadOnlySpan results, - ReadOnlySpan dataElements, - ReadOnlySpan errorTries, + private bool SaveSafeResult( + CompositeResultElement resultData, + Path path, + ReadOnlySpan additionalPaths, + SourceResultElement dataElement, + ErrorTrie? errorTrie, ReadOnlySpan responseNames) { - lock (_lock) + if (!SaveSafeResult(resultData, path, dataElement, errorTrie, responseNames)) { - ref var result = ref MemoryMarshal.GetReference(results); - ref var data = ref MemoryMarshal.GetReference(dataElements); - ref var errorTrie = ref MemoryMarshal.GetReference(errorTries); - ref var end = ref Unsafe.Add(ref result, results.Length); - var resultData = _result.Data; + return false; + } - while (Unsafe.IsAddressLessThan(ref result, ref end)) + for (var i = 0; i < additionalPaths.Length; i++) + { + if (!SaveSafeResult(resultData, additionalPaths[i], dataElement, errorTrie, responseNames)) { - if (resultData.IsNullOrInvalidated) - { - return false; - } + return false; + } + } - var element = result.Path.IsRoot ? resultData : GetStartObjectResult(result.Path); - if (element.IsNullOrInvalidated) - { - goto SaveSafe_Next; - } + return true; + } - var canExecutionContinue = - _valueCompletion.BuildResult( - data, - element, - errorTrie, - responseNames); + private bool SaveSafeResult( + CompositeResultElement resultData, + Path path, + SourceResultElement dataElement, + ErrorTrie? errorTrie, + ReadOnlySpan responseNames) + { + if (resultData.IsNullOrInvalidated) + { + return false; + } - if (!canExecutionContinue) - { - resultData.Invalidate(); - return false; - } + var element = path.IsRoot ? resultData : GetStartObjectResult(path); + if (element.IsNullOrInvalidated) + { + return true; + } -SaveSafe_Next: - result = ref Unsafe.Add(ref result, 1)!; - data = ref Unsafe.Add(ref data, 1); - errorTrie = ref Unsafe.Add(ref errorTrie, 1)!; - } + var canExecutionContinue = + _valueCompletion.BuildResult( + dataElement, + element, + errorTrie, + responseNames); + + if (canExecutionContinue) + { + return true; } - return true; + resultData.Invalidate(); + return false; } public ImmutableArray CreateVariableValueSets( @@ -325,7 +357,8 @@ public ImmutableArray CreateVariableValueSets( lock (_lock) { - var combined = new List(); + var combined = _collectTargetCombined; + combined.Clear(); foreach (var selectionSet in selectionSets) { @@ -349,8 +382,11 @@ public ImmutableArray CreateVariableValueSets( // Caller must hold _lock for reading. private List? CollectTargetElements(SelectionPath selectionSet) { - var current = new List { _result.Data }; - var next = new List(); + var current = _collectTargetCurrent; + var next = _collectTargetNext; + current.Clear(); + next.Clear(); + current.Add(_result.Data); for (var i = 0; i < selectionSet.Segments.Length; i++) { @@ -383,7 +419,7 @@ public ImmutableArray CreateVariableValueSets( if (valueKind is JsonValueKind.Array) { - next.AddRange(UnrollLists(value)); + AppendUnrolledLists(value, next); continue; } @@ -398,7 +434,9 @@ public ImmutableArray CreateVariableValueSets( } } - (next, current) = (current, next); + var temp = current; + current = next; + next = temp; next.Clear(); if (current.Count == 0) @@ -416,6 +454,42 @@ private ImmutableArray BuildVariableValueSets( ReadOnlySpan requiredData) { PooledArrayWriter? buffer = null; + + if (requestVariables.Count == 0) + { + var fastPathResult = requiredData.Length switch + { + 1 => BuildVariableValueSetsSingleRequirement( + elements, + requiredData[0], + ref buffer), + + 2 => BuildVariableValueSetsTwoRequirements( + elements, + requiredData[0], + requiredData[1], + ref buffer), + + 3 => BuildVariableValueSetsThreeRequirements( + elements, + requiredData[0], + requiredData[1], + requiredData[2], + ref buffer), + _ => default + }; + + if (!fastPathResult.IsDefault) + { + if (buffer is not null) + { + _memory.Push(buffer); + } + + return fastPathResult; + } + } + VariableValues[]? variableValueSets = null; Dictionary? seen = null; List?[]? additionalPaths = null; @@ -452,33 +526,476 @@ private ImmutableArray BuildVariableValueSets( variableValueSets[nextIndex++] = new VariableValues(result.Path, variables); } - if (additionalPaths is not null) + if (buffer is not null) { - for (var i = 0; i < nextIndex; i++) + _memory.Push(buffer); + } + + return FinalizeVariableValueSets(variableValueSets, additionalPaths, nextIndex); + } + + private ImmutableArray BuildVariableValueSetsSingleRequirement( + List elements, + OperationRequirement requirement, + ref PooledArrayWriter? buffer) + { + if (TryGetSimpleRequirementFieldName(requirement.Map, out var fieldName)) + { + return BuildVariableValueSetsSingleRequirementFastPath( + elements, + requirement, + fieldName, + ref buffer); + } + + return BuildVariableValueSetsSingleRequirementSlowPath(elements, requirement, ref buffer); + } + + private ImmutableArray BuildVariableValueSetsSingleRequirementFastPath( + List elements, + OperationRequirement requirement, + string fieldName, + ref PooledArrayWriter? buffer) + { + VariableValues[]? variableValueSets = null; + Dictionary? seen = null; + List?[]? additionalPaths = null; + var nextIndex = 0; + + foreach (var result in elements) + { + if (!result.TryGetProperty(fieldName, out var value) + || value.ValueKind is JsonValueKind.Undefined) { - if (additionalPaths[i] is { } paths) + continue; + } + + if (value.ValueKind is JsonValueKind.Null && requirement.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + var mappedValue = ResultDataMapper.MapLeafValue(value, ref buffer); + variableValueSets ??= new VariableValues[elements.Count]; + + if (nextIndex > 0) + { + seen ??= new Dictionary(SingleValueNodeComparer.Instance) { - variableValueSets![i] = variableValueSets[i] with - { - AdditionalPaths = [.. paths] - }; + [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; } + + variableValueSets[nextIndex++] = new VariableValues( + result.Path, + new ObjectValueNode([ + new ObjectFieldNode( + requirement.Key, + mappedValue) + ])); } - if (variableValueSets?.Length > 0) + return FinalizeVariableValueSets(variableValueSets, additionalPaths, nextIndex); + } + + private ImmutableArray BuildVariableValueSetsSingleRequirementSlowPath( + List elements, + OperationRequirement requirement, + ref PooledArrayWriter? buffer) + { + VariableValues[]? variableValueSets = null; + Dictionary? seen = null; + List?[]? additionalPaths = null; + var nextIndex = 0; + + foreach (var result in elements) { - Array.Resize(ref variableValueSets, nextIndex); + var value = ResultDataMapper.Map(result, requirement.Map, _schema, ref buffer); + + if (value is null) + { + continue; + } + + if (value.Kind == SyntaxKind.NullValue && requirement.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + variableValueSets ??= new VariableValues[elements.Count]; + + if (nextIndex > 0) + { + seen ??= new Dictionary(SingleValueNodeComparer.Instance) + { + [variableValueSets[0].Values.Fields[0].Value] = 0 + }; + + if (seen.TryGetValue(value, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + seen[value] = nextIndex; + } + + variableValueSets[nextIndex++] = new VariableValues( + result.Path, + new ObjectValueNode([new ObjectFieldNode(requirement.Key, value)])); } - if (buffer is not null) + return FinalizeVariableValueSets(variableValueSets, additionalPaths, nextIndex); + } + + private ImmutableArray BuildVariableValueSetsTwoRequirements( + List elements, + OperationRequirement requirement1, + OperationRequirement requirement2, + ref PooledArrayWriter? buffer) + { + if (TryGetSimpleRequirementFieldName(requirement1.Map, out var fieldName1) + && TryGetSimpleRequirementFieldName(requirement2.Map, out var fieldName2)) { - _memory.Push(buffer); + return BuildVariableValueSetsTwoRequirementsFastPath( + elements, + requirement1, + fieldName1, + requirement2, + fieldName2, + ref buffer); + } + + return BuildVariableValueSetsTwoRequirementsSlowPath( + elements, + requirement1, + requirement2, + ref buffer); + } + + private ImmutableArray BuildVariableValueSetsTwoRequirementsFastPath( + List elements, + OperationRequirement requirement1, + string fieldName1, + OperationRequirement requirement2, + string fieldName2, + ref PooledArrayWriter? buffer) + { + VariableValues[]? variableValueSets = null; + Dictionary? seen = null; + List?[]? additionalPaths = null; + var nextIndex = 0; + + foreach (var result in elements) + { + if (!result.TryGetProperty(fieldName1, out var value1) + || value1.ValueKind is JsonValueKind.Undefined + || value1.ValueKind is JsonValueKind.Null + && requirement1.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + if (!result.TryGetProperty(fieldName2, out var value2) + || value2.ValueKind is JsonValueKind.Undefined + || value2.ValueKind is JsonValueKind.Null + && requirement2.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + var mappedValue1 = ResultDataMapper.MapLeafValue(value1, ref buffer); + var mappedValue2 = ResultDataMapper.MapLeafValue(value2, ref buffer); + variableValueSets ??= new VariableValues[elements.Count]; + var key = new TwoValueNodeTuple(mappedValue1, mappedValue2); + + if (nextIndex > 0) + { + seen ??= new Dictionary(TwoValueNodeTupleComparer.Instance) + { + [new TwoValueNodeTuple( + variableValueSets[0].Values.Fields[0].Value, + variableValueSets[0].Values.Fields[1].Value)] = 0 + }; + + if (seen.TryGetValue(key, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + seen[key] = nextIndex; + } + + variableValueSets[nextIndex++] = new VariableValues( + result.Path, + new ObjectValueNode([ + new ObjectFieldNode(requirement1.Key, mappedValue1), + new ObjectFieldNode(requirement2.Key, mappedValue2) + ])); + } + + return FinalizeVariableValueSets(variableValueSets, additionalPaths, nextIndex); + } + + private ImmutableArray BuildVariableValueSetsTwoRequirementsSlowPath( + List elements, + OperationRequirement requirement1, + OperationRequirement requirement2, + ref PooledArrayWriter? buffer) + { + VariableValues[]? variableValueSets = null; + Dictionary? seen = null; + List?[]? additionalPaths = null; + var nextIndex = 0; + + foreach (var result in elements) + { + var value1 = ResultDataMapper.Map(result, requirement1.Map, _schema, ref buffer); + + if (value1 is null + || value1.Kind == SyntaxKind.NullValue + && requirement1.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + var value2 = ResultDataMapper.Map(result, requirement2.Map, _schema, ref buffer); + + if (value2 is null + || value2.Kind == SyntaxKind.NullValue + && requirement2.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + variableValueSets ??= new VariableValues[elements.Count]; + var key = new TwoValueNodeTuple(value1, value2); + + if (nextIndex > 0) + { + seen ??= new Dictionary(TwoValueNodeTupleComparer.Instance) + { + [new TwoValueNodeTuple( + variableValueSets[0].Values.Fields[0].Value, + variableValueSets[0].Values.Fields[1].Value)] = 0 + }; + + if (seen.TryGetValue(key, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + seen[key] = nextIndex; + } + + variableValueSets[nextIndex++] = new VariableValues( + result.Path, + new ObjectValueNode([ + new ObjectFieldNode(requirement1.Key, value1), + new ObjectFieldNode(requirement2.Key, value2) + ])); + } + + return FinalizeVariableValueSets(variableValueSets, additionalPaths, nextIndex); + } + + private ImmutableArray BuildVariableValueSetsThreeRequirements( + List elements, + OperationRequirement requirement1, + OperationRequirement requirement2, + OperationRequirement requirement3, + ref PooledArrayWriter? buffer) + { + if (TryGetSimpleRequirementFieldName(requirement1.Map, out var fieldName1) + && TryGetSimpleRequirementFieldName(requirement2.Map, out var fieldName2) + && TryGetSimpleRequirementFieldName(requirement3.Map, out var fieldName3)) + { + return BuildVariableValueSetsThreeRequirementsFastPath( + elements, + requirement1, + fieldName1, + requirement2, + fieldName2, + requirement3, + fieldName3, + ref buffer); + } + + return BuildVariableValueSetsThreeRequirementsSlowPath( + elements, + requirement1, + requirement2, + requirement3, + ref buffer); + } + + private ImmutableArray BuildVariableValueSetsThreeRequirementsFastPath( + List elements, + OperationRequirement requirement1, + string fieldName1, + OperationRequirement requirement2, + string fieldName2, + OperationRequirement requirement3, + string fieldName3, + ref PooledArrayWriter? buffer) + { + VariableValues[]? variableValueSets = null; + Dictionary? seen = null; + List?[]? additionalPaths = null; + var nextIndex = 0; + + foreach (var result in elements) + { + if (!result.TryGetProperty(fieldName1, out var value1) + || value1.ValueKind is JsonValueKind.Undefined + || value1.ValueKind is JsonValueKind.Null + && requirement1.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + if (!result.TryGetProperty(fieldName2, out var value2) + || value2.ValueKind is JsonValueKind.Undefined + || value2.ValueKind is JsonValueKind.Null + && requirement2.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + if (!result.TryGetProperty(fieldName3, out var value3) + || value3.ValueKind is JsonValueKind.Undefined + || value3.ValueKind is JsonValueKind.Null + && requirement3.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + var mappedValue1 = ResultDataMapper.MapLeafValue(value1, ref buffer); + var mappedValue2 = ResultDataMapper.MapLeafValue(value2, ref buffer); + var mappedValue3 = ResultDataMapper.MapLeafValue(value3, ref buffer); + variableValueSets ??= new VariableValues[elements.Count]; + var key = new ThreeValueNodeTuple(mappedValue1, mappedValue2, mappedValue3); + + if (nextIndex > 0) + { + seen ??= new Dictionary(ThreeValueNodeTupleComparer.Instance) + { + [new ThreeValueNodeTuple( + variableValueSets[0].Values.Fields[0].Value, + variableValueSets[0].Values.Fields[1].Value, + variableValueSets[0].Values.Fields[2].Value)] = 0 + }; + + if (seen.TryGetValue(key, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + seen[key] = nextIndex; + } + + variableValueSets[nextIndex++] = new VariableValues( + result.Path, + new ObjectValueNode([ + new ObjectFieldNode(requirement1.Key, mappedValue1), + new ObjectFieldNode(requirement2.Key, mappedValue2), + new ObjectFieldNode(requirement3.Key, mappedValue3) + ])); } - return variableValueSets is not null - ? ImmutableCollectionsMarshal.AsImmutableArray(variableValueSets) - : []; + return FinalizeVariableValueSets(variableValueSets, additionalPaths, nextIndex); + } + + private ImmutableArray BuildVariableValueSetsThreeRequirementsSlowPath( + List elements, + OperationRequirement requirement1, + OperationRequirement requirement2, + OperationRequirement requirement3, + ref PooledArrayWriter? buffer) + { + VariableValues[]? variableValueSets = null; + Dictionary? seen = null; + List?[]? additionalPaths = null; + var nextIndex = 0; + + foreach (var result in elements) + { + var value1 = ResultDataMapper.Map(result, requirement1.Map, _schema, ref buffer); + + if (value1 is null + || value1.Kind == SyntaxKind.NullValue + && requirement1.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + var value2 = ResultDataMapper.Map(result, requirement2.Map, _schema, ref buffer); + + if (value2 is null + || value2.Kind == SyntaxKind.NullValue + && requirement2.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + var value3 = ResultDataMapper.Map(result, requirement3.Map, _schema, ref buffer); + + if (value3 is null + || value3.Kind == SyntaxKind.NullValue + && requirement3.Type.Kind == SyntaxKind.NonNullType) + { + continue; + } + + variableValueSets ??= new VariableValues[elements.Count]; + var key = new ThreeValueNodeTuple(value1, value2, value3); + + if (nextIndex > 0) + { + seen ??= new Dictionary(ThreeValueNodeTupleComparer.Instance) + { + [new ThreeValueNodeTuple( + variableValueSets[0].Values.Fields[0].Value, + variableValueSets[0].Values.Fields[1].Value, + variableValueSets[0].Values.Fields[2].Value)] = 0 + }; + + if (seen.TryGetValue(key, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + seen[key] = nextIndex; + } + + variableValueSets[nextIndex++] = new VariableValues( + result.Path, + new ObjectValueNode([ + new ObjectFieldNode(requirement1.Key, value1), + new ObjectFieldNode(requirement2.Key, value2), + new ObjectFieldNode(requirement3.Key, value3) + ])); + } + + return FinalizeVariableValueSets(variableValueSets, additionalPaths, nextIndex); } private ObjectValueNode? MapRequirements( @@ -487,8 +1004,20 @@ private ImmutableArray BuildVariableValueSets( ReadOnlySpan requirements, ref PooledArrayWriter? buffer) { - var fields = new List(forwardedVariables.Count + requirements.Length); - fields.AddRange(forwardedVariables); + var fieldCount = forwardedVariables.Count + requirements.Length; + + if (fieldCount == 0) + { + return new ObjectValueNode([]); + } + + var fields = new ObjectFieldNode[fieldCount]; + var index = 0; + + for (var i = 0; i < forwardedVariables.Count; i++) + { + fields[index++] = forwardedVariables[i]; + } foreach (var requirement in requirements) { @@ -504,7 +1033,7 @@ private ImmutableArray BuildVariableValueSets( return null; } - fields.Add(field); + fields[index++] = field; } return new ObjectValueNode(fields); @@ -520,7 +1049,31 @@ private ImmutableArray BuildVariableValueSets( return value is null ? null : new ObjectFieldNode(key, value); } - private static IEnumerable UnrollLists(CompositeResultElement list) + private static bool TryGetSimpleRequirementFieldName( + IValueSelectionNode map, + [NotNullWhen(true)] out string? fieldName) + { + if (map is PathNode + { + TypeName: null, + PathSegment: + { + TypeName: null, + PathSegment: null + } pathSegment + }) + { + fieldName = pathSegment.FieldName.Value; + return true; + } + + fieldName = null; + return false; + } + + private static void AppendUnrolledLists( + CompositeResultElement list, + List destination) { foreach (var element in list.EnumerateArray()) { @@ -533,14 +1086,11 @@ private static IEnumerable UnrollLists(CompositeResultEl if (elementValueKind is JsonValueKind.Array) { - foreach (var nestedElement in UnrollLists(element)) - { - yield return nestedElement; - } + AppendUnrolledLists(element, destination); } else { - yield return element; + destination.Add(element); } } } @@ -685,4 +1235,84 @@ public void Dispose() memory.Dispose(); } } + + private sealed class SingleValueNodeComparer : IEqualityComparer + { + public static SingleValueNodeComparer Instance { get; } = new(); + + public bool Equals(IValueNode? x, IValueNode? y) + => SyntaxComparer.BySyntax.Equals(x, y); + + public int GetHashCode(IValueNode obj) + => SyntaxComparer.BySyntax.GetHashCode(obj); + } + + private static ImmutableArray FinalizeVariableValueSets( + VariableValues[]? variableValueSets, + List?[]? additionalPaths, + int nextIndex) + { + if (variableValueSets is null || nextIndex == 0) + { + return []; + } + + if (additionalPaths is not null) + { + for (var i = 0; i < nextIndex; i++) + { + if (additionalPaths[i] is { } paths) + { + variableValueSets[i] = variableValueSets[i] with + { + AdditionalPaths = [.. paths] + }; + } + } + } + + if (variableValueSets.Length != nextIndex) + { + Array.Resize(ref variableValueSets, nextIndex); + } + + return ImmutableCollectionsMarshal.AsImmutableArray(variableValueSets); + } + + private readonly record struct TwoValueNodeTuple(IValueNode Value1, IValueNode Value2); + + private readonly record struct ThreeValueNodeTuple( + IValueNode Value1, + IValueNode Value2, + IValueNode Value3); + + private sealed class TwoValueNodeTupleComparer : IEqualityComparer + { + public static TwoValueNodeTupleComparer Instance { get; } = new(); + + public bool Equals(TwoValueNodeTuple x, TwoValueNodeTuple y) + => SyntaxComparer.BySyntax.Equals(x.Value1, y.Value1) + && SyntaxComparer.BySyntax.Equals(x.Value2, y.Value2); + + public int GetHashCode(TwoValueNodeTuple obj) + => HashCode.Combine( + SyntaxComparer.BySyntax.GetHashCode(obj.Value1), + SyntaxComparer.BySyntax.GetHashCode(obj.Value2)); + } + + private sealed class ThreeValueNodeTupleComparer : IEqualityComparer + { + public static ThreeValueNodeTupleComparer Instance { get; } = new(); + + public bool Equals(ThreeValueNodeTuple x, ThreeValueNodeTuple y) + => SyntaxComparer.BySyntax.Equals(x.Value1, y.Value1) + && SyntaxComparer.BySyntax.Equals(x.Value2, y.Value2) + && SyntaxComparer.BySyntax.Equals(x.Value3, y.Value3); + + public int GetHashCode(ThreeValueNodeTuple obj) + => HashCode.Combine( + SyntaxComparer.BySyntax.GetHashCode(obj.Value1), + SyntaxComparer.BySyntax.GetHashCode(obj.Value2), + SyntaxComparer.BySyntax.GetHashCode(obj.Value3)); + } } 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 60e07be9efc..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 @@ -79,32 +79,65 @@ internal static class ResultDataMapper // system we would need to also cover raw field results. if (result.Selection is { IsLeaf: true }) { - if (resultValueKind is JsonValueKind.Array) - { - var items = new List(); - context.Writer ??= new PooledArrayWriter(); - var parser = new JsonValueParser(buffer: context.Writer); + return MapLeafValue(result, ref context.Writer); + } - foreach (var item in result.EnumerateArray()) - { - if (item.ValueKind is JsonValueKind.Null) - { - items.Add(NullValueNode.Default); - continue; - } + throw new InvalidSelectionMapPathException(node); + } - items.Add(parser.Parse(item.GetRawValue(includeQuotes: true))); - } + internal static IValueNode MapLeafValue( + CompositeResultElement value, + ref PooledArrayWriter? writer) + { + if (value.ValueKind is JsonValueKind.Array) + { + var items = new List(value.GetArrayLength()); + var parser = default(JsonValueParser); + var parserInitialized = false; - return new ListValueNode(items); + foreach (var item in value.EnumerateArray()) + { + items.Add(ParseLeafValue(item, ref writer, ref parser, ref parserInitialized)); } - context.Writer ??= new PooledArrayWriter(); - var scalarParser = new JsonValueParser(buffer: context.Writer); - return scalarParser.Parse(result.GetRawValue(includeQuotes: true)); + return new ListValueNode(items); } - throw new InvalidSelectionMapPathException(node); + var scalarParser = default(JsonValueParser); + var scalarParserInitialized = false; + return ParseLeafValue(value, ref writer, ref scalarParser, ref scalarParserInitialized); + } + + private static IValueNode ParseLeafValue( + CompositeResultElement value, + ref PooledArrayWriter? writer, + ref JsonValueParser parser, + ref bool parserInitialized) + { + switch (value.ValueKind) + { + case JsonValueKind.Null: + return NullValueNode.Default; + + case JsonValueKind.True: + return BooleanValueNode.True; + + case JsonValueKind.False: + return BooleanValueNode.False; + + case JsonValueKind.String: + return new StringValueNode(value.AssertString()); + + default: + writer ??= new PooledArrayWriter(); + if (!parserInitialized) + { + parser = new JsonValueParser(buffer: writer); + parserInitialized = true; + } + + return parser.Parse(value.GetRawValue(includeQuotes: true)); + } } private static IValueNode? Visit(ObjectValueSelectionNode node, Context context) @@ -117,7 +150,7 @@ internal static class ResultDataMapper throw new InvalidOperationException("Only object results are supported."); } - var fields = new List(); + var fields = new List(node.Fields.Length); foreach (var field in node.Fields) { @@ -133,7 +166,6 @@ internal static class ResultDataMapper fields.Add(new ObjectFieldNode(field.Name.Value, value)); } - fields.Capacity = fields.Count; return new ObjectValueNode(fields); } @@ -165,7 +197,7 @@ internal static class ResultDataMapper return null; } - var items = new List(); + var items = new List(result.GetArrayLength()); foreach (var item in result.EnumerateArray()) { diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap index c375ff8f2d5..88d2aba887c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap @@ -112,7 +112,7 @@ }, { "Key": "exception.stacktrace", - "Value": "System.Net.Http.HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error).\n at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()\n at HotChocolate.Fusion.Transport.Http.GraphQLHttpResponse.ReadAsResultAsync(CancellationToken cancellationToken) in GraphQLHttpResponse.cs:line 150\n at HotChocolate.Fusion.Execution.Clients.SourceSchemaHttpClient.Response.ReadAsResultStreamAsync(CancellationToken cancellationToken)+MoveNext() in SourceSchemaHttpClient.cs:line 581\n at HotChocolate.Fusion.Execution.Clients.SourceSchemaHttpClient.Response.ReadAsResultStreamAsync(CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource.GetResult()\n at HotChocolate.Fusion.Execution.Nodes.OperationExecutionNode.OnExecuteAsync(OperationPlanContext context, CancellationToken cancellationToken) in OperationExecutionNode.cs:line 158\n at HotChocolate.Fusion.Execution.Nodes.OperationExecutionNode.OnExecuteAsync(OperationPlanContext context, CancellationToken cancellationToken) in OperationExecutionNode.cs:line 158" + "Value": "System.Net.Http.HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error).\n at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()\n at HotChocolate.Fusion.Transport.Http.GraphQLHttpResponse.ReadAsResultAsync(CancellationToken cancellationToken) in GraphQLHttpResponse.cs:line 150\n at HotChocolate.Fusion.Execution.Clients.SourceSchemaHttpClient.Response.ReadAsResultStreamAsync(CancellationToken cancellationToken)+MoveNext() in SourceSchemaHttpClient.cs:line 577\n at HotChocolate.Fusion.Execution.Clients.SourceSchemaHttpClient.Response.ReadAsResultStreamAsync(CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource.GetResult()\n at HotChocolate.Fusion.Execution.Nodes.OperationExecutionNode.OnExecuteAsync(OperationPlanContext context, CancellationToken cancellationToken) in OperationExecutionNode.cs:line 158\n at HotChocolate.Fusion.Execution.Nodes.OperationExecutionNode.OnExecuteAsync(OperationPlanContext context, CancellationToken cancellationToken) in OperationExecutionNode.cs:line 158" }, { "Key": "exception.message",