diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddlewareBase.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddlewareBase.cs index 909e952a665..3d3bb76a982 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddlewareBase.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddlewareBase.cs @@ -259,7 +259,13 @@ await session.RequestParser.ParseRequestAsync( foreach (var request in requests) { - context.Response.RegisterForDispose(request); + if (request.Variables is not null + || request.Extensions is not null + || request.VariablesMemoryOwner is not null + || request.ExtensionsMemoryOwner is not null) + { + context.Response.RegisterForDispose(request); + } } return requests; diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs index 80ac858282f..4754354ff20 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs @@ -176,6 +176,15 @@ private async ValueTask ReadAsResultInternalAsync(string? charS var chunkIndex = 0; var chunks = ArrayPool.Shared.Rent(64); + static byte[][] GrowChunkArray(byte[][] chunkArray) + { + var newChunks = ArrayPool.Shared.Rent(chunkArray.Length * 2); + Array.Copy(chunkArray, 0, newChunks, 0, chunkArray.Length); + chunkArray.AsSpan().Clear(); + ArrayPool.Shared.Return(chunkArray); + return newChunks; + } + try { while (true) @@ -219,10 +228,7 @@ private async ValueTask ReadAsResultInternalAsync(string? charS { if (chunkIndex >= chunks.Length) { - var newChunks = ArrayPool.Shared.Rent(chunks.Length * 2); - Array.Copy(chunks, 0, newChunks, 0, chunks.Length); - chunks.AsSpan().Clear(); - chunks = newChunks; + chunks = GrowChunkArray(chunks); } chunks[chunkIndex++] = currentChunk; @@ -257,10 +263,7 @@ private async ValueTask ReadAsResultInternalAsync(string? charS { if (chunkIndex >= chunks.Length) { - var newChunks = ArrayPool.Shared.Rent(chunks.Length * 2); - Array.Copy(chunks, 0, newChunks, 0, chunks.Length); - chunks.AsSpan().Clear(); - chunks = newChunks; + chunks = GrowChunkArray(chunks); } chunks[chunkIndex++] = currentChunk; @@ -277,10 +280,7 @@ private async ValueTask ReadAsResultInternalAsync(string? charS // add the final partial chunk to the list if (chunkIndex >= chunks.Length) { - var newChunks = ArrayPool.Shared.Rent(chunks.Length * 2); - Array.Copy(chunks, 0, newChunks, 0, chunks.Length); - chunks.AsSpan().Clear(); - chunks = newChunks; + chunks = GrowChunkArray(chunks); } chunks[chunkIndex++] = currentChunk; 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 01438bbe2e2..2d917de9d6c 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 @@ -191,7 +191,7 @@ private GraphQLHttpRequest CreateHttpRequest( private GraphQLHttpRequest CreateHttpBatchRequest( IReadOnlyList originalRequests) { - var batchRequests = new List(originalRequests.Count); + var batchRequests = new IOperationRequest[originalRequests.Count]; var enableFileUploads = false; for (var i = 0; i < originalRequests.Count; i++) @@ -200,15 +200,13 @@ private GraphQLHttpRequest CreateHttpBatchRequest( enableFileUploads |= sourceRequest.RequiresFileUpload; var body = CreateRequestBody(sourceRequest); - if (body is IOperationRequest operationRequest) - { - batchRequests.Add(operationRequest); - } - else + if (body is not IOperationRequest operationRequest) { throw new InvalidOperationException( $"The request body type '{body.GetType().Name}' cannot be included in an operation batch."); } + + batchRequests[i] = operationRequest; } return new GraphQLHttpRequest(new OperationBatchRequest(batchRequests)) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs index e8275a9b313..b5b045d56d2 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs @@ -218,29 +218,29 @@ private static class AcceptContentTypes { public static ImmutableArray Default { get; } = [ - new("application/graphql-response+json") { CharSet = "utf-8" }, - new("application/json") { CharSet = "utf-8" }, - new("application/jsonl") { CharSet = "utf-8" }, - new("text/event-stream") { CharSet = "utf-8" } + new("application/graphql-response+json"), + new("application/json"), + new("application/jsonl"), + new("text/event-stream") ]; public static ImmutableArray VariableBatching { get; } = [ - new("application/jsonl") { CharSet = "utf-8" }, - new("text/event-stream") { CharSet = "utf-8" }, - new("application/graphql-response+json") { CharSet = "utf-8" }, - new("application/json") { CharSet = "utf-8" } + new("application/jsonl"), + new("text/event-stream"), + new("application/graphql-response+json"), + new("application/json") ]; public static ImmutableArray ApolloRequestBatching { get; } = [ - new("application/json") { CharSet = "utf-8" } + new("application/json") ]; public static ImmutableArray Subscription { get; } = [ - new("application/jsonl") { CharSet = "utf-8" }, - new("text/event-stream") { CharSet = "utf-8" } + new("application/jsonl"), + new("text/event-stream") ]; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs index 8fd0b8d9af6..ec9c0bb438d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs @@ -50,9 +50,10 @@ public async Task ExecuteAsync( OperationPlanContext context, CancellationToken cancellationToken = default) { - var start = Stopwatch.GetTimestamp(); + var collectTelemetry = context.CollectTelemetry; + var start = collectTelemetry ? Stopwatch.GetTimestamp() : 0L; var scope = CreateScope(context); - var activity = Activity.Current; + var activity = collectTelemetry ? Activity.Current : null; ExecutionStatus status; Exception? error = null; @@ -78,15 +79,20 @@ public async Task ExecuteAsync( scope?.Dispose(); } + var duration = collectTelemetry ? Stopwatch.GetElapsedTime(start) : default; + var dependentsToExecute = context.GetDependentsToExecute(this); + var variableValueSets = collectTelemetry ? context.GetVariableValueSets(this) : []; + var transportDetails = collectTelemetry ? context.GetTransportDetails(this) : default; + var result = new ExecutionNodeResult( Id, activity, status, - Stopwatch.GetElapsedTime(start), + duration, error, - context.GetDependentsToExecute(this), - context.GetVariableValueSets(this), - context.GetTransportDetails(this)); + dependentsToExecute, + variableValueSets, + transportDetails); context.CompleteNode(result); } 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 776f8e32be4..c05f9887cf6 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,6 +1,5 @@ using System.Buffers; using System.Collections.Immutable; -using System.Runtime.InteropServices; using HotChocolate.Fusion.Execution.Clients; namespace HotChocolate.Fusion.Execution.Nodes; @@ -112,7 +111,12 @@ protected override async ValueTask OnExecuteAsync( var schemaName = _schemaName ?? context.GetDynamicSchemaName(this); - context.TrackVariableValueSets(this, variables); + var collectTelemetry = context.CollectTelemetry; + + if (collectTelemetry) + { + context.TrackVariableValueSets(this, variables); + } var request = new SourceSchemaClientRequest { @@ -137,7 +141,10 @@ protected override async ValueTask OnExecuteAsync( var response = await context.SourceSchemaScheduler .ExecuteAsync(request, cancellationToken) .ConfigureAwait(false); - context.TrackSourceSchemaClientResponse(this, response); + if (collectTelemetry) + { + context.TrackSourceSchemaClientResponse(this, response); + } // we read the responses from the response stream. var totalPathCount = variables.Length; @@ -225,10 +232,9 @@ protected override async ValueTask OnExecuteAsync( } else if (singleResult is not null) { - var firstResult = singleResult; - context.AddPartialResults( + context.AddPartialResult( _source, - MemoryMarshal.CreateReadOnlySpan(ref firstResult, 1), + singleResult, _responseNames, hasSomeErrors); } 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 ddfc2aeafc9..851da425412 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 @@ -118,7 +118,12 @@ protected override async ValueTask OnExecuteAsync( var schemaName = _schemaName ?? context.GetDynamicSchemaName(this); - context.TrackVariableValueSets(this, variables); + var collectTelemetry = context.CollectTelemetry; + + if (collectTelemetry) + { + context.TrackVariableValueSets(this, variables); + } var request = new SourceSchemaClientRequest { @@ -143,18 +148,11 @@ protected override async ValueTask OnExecuteAsync( var response = await context.SourceSchemaScheduler .ExecuteAsync(request, cancellationToken) .ConfigureAwait(false); - context.TrackSourceSchemaClientResponse(this, response); - - // we read the responses from the response stream. - var totalPathCount = variables.Length; - - for (var i = 0; i < variables.Length; i++) + if (collectTelemetry) { - totalPathCount += variables[i].AdditionalPaths.Length; + context.TrackSourceSchemaClientResponse(this, response); } - var initialBufferLength = Math.Max(totalPathCount, 2); - await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { // If there is only one response, we skip the buffer rental. @@ -168,7 +166,14 @@ protected override async ValueTask OnExecuteAsync( // If we have more than one response, we rent a buffer and move the first result into it. 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!; } @@ -229,10 +234,9 @@ protected override async ValueTask OnExecuteAsync( } else if (singleResult is not null) { - var firstResult = singleResult; - context.AddPartialResults( + context.AddPartialResult( _source, - MemoryMarshal.CreateReadOnlySpan(ref firstResult, 1), + singleResult, _responseNames, hasSomeErrors); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs index 7eb3b4f51d0..ce17946031f 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs @@ -13,6 +13,7 @@ public sealed class Selection : ISelection private readonly FieldSelectionNode[] _syntaxNodes; private readonly ulong[] _includeFlags; private readonly byte[] _utf8ResponseName; + private SelectionSetCacheEntry? _selectionSetCache; private Flags _flags; public Selection( @@ -44,6 +45,17 @@ public Selection( _flags |= Flags.Leaf; } + IType fieldType = field.Type; + while (fieldType.Kind is TypeKind.NonNull) + { + fieldType = fieldType.InnerType(); + } + + if (fieldType.Kind is TypeKind.Scalar or TypeKind.Enum) + { + _flags |= Flags.LeafValue; + } + _utf8ResponseName = Utf8StringCache.GetUtf8String(responseName); } @@ -64,6 +76,8 @@ public Selection( /// public bool IsLeaf => (_flags & Flags.Leaf) == Flags.Leaf; + internal bool IsLeafValue => (_flags & Flags.LeafValue) == Flags.LeafValue; + /// public IOutputFieldDefinition Field { get; } @@ -85,6 +99,22 @@ public Selection( internal ResolveFieldValue? Resolver => Field.Features.Get(); + internal SelectionSet GetSelectionSet(IObjectTypeDefinition typeContext) + { + ArgumentNullException.ThrowIfNull(typeContext); + + var cache = _selectionSetCache; + + if (cache is not null && ReferenceEquals(cache.TypeContext, typeContext)) + { + return cache.SelectionSet; + } + + var selectionSet = DeclaringSelectionSet.DeclaringOperation.GetSelectionSet(this, typeContext); + _selectionSetCache = new SelectionSetCacheEntry(typeContext, selectionSet); + return selectionSet; + } + IEnumerable ISelection.GetSyntaxNodes() { for (var i = 0; i < SyntaxNodes.Length; i++) @@ -169,6 +199,16 @@ private enum Flags None = 0, Internal = 1, Leaf = 2, - Sealed = 4 + Sealed = 4, + LeafValue = 8 + } + + private sealed class SelectionSetCacheEntry( + IObjectTypeDefinition typeContext, + SelectionSet selectionSet) + { + public IObjectTypeDefinition TypeContext { get; } = typeContext; + + public SelectionSet SelectionSet { get; } = selectionSet; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionPath.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionPath.cs index e599c472d16..7e9f98ab99e 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionPath.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionPath.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Text; +using HotChocolate.Fusion.Text; namespace HotChocolate.Fusion.Execution.Nodes; @@ -355,7 +356,13 @@ public override string ToString() /// /// The name of the field or type for this segment. /// The kind of segment (field or inline fragment). - public sealed record Segment(string Name, SelectionPathSegmentKind Kind); + public sealed record Segment(string Name, SelectionPathSegmentKind Kind) + { + private byte[]? _utf8Name; + + public ReadOnlySpan Utf8Name + => _utf8Name ??= Utf8StringCache.GetUtf8String(Name); + } /// /// Creates a new builder for creating instances. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs index 4b4dca60b11..dfa9f5fc08f 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using HotChocolate.Execution; using HotChocolate.Types; @@ -92,8 +93,141 @@ public bool TryGetSelection(string responseName, [NotNullWhen(true)] out Selecti /// /// Returns true if the selection was successfully resolved. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetSelection(ReadOnlySpan utf8ResponseName, [NotNullWhen(true)] out Selection? selection) - => _utf8ResponseNameLookup.TryGetSelection(utf8ResponseName, out selection); + { + // Most execution selection sets are tiny (1-3 fields), so direct + // span comparisons avoid hash computation/probing in the hot path. + var selections = _selections; + + switch (selections.Length) + { + case 1: + var candidate = selections[0]; + + if (utf8ResponseName.SequenceEqual(candidate.Utf8ResponseName)) + { + selection = candidate; + return true; + } + + selection = default; + return false; + + case 2: + var candidate0 = selections[0]; + + if (utf8ResponseName.SequenceEqual(candidate0.Utf8ResponseName)) + { + selection = candidate0; + return true; + } + + var candidate1 = selections[1]; + + if (utf8ResponseName.SequenceEqual(candidate1.Utf8ResponseName)) + { + selection = candidate1; + return true; + } + + selection = default; + return false; + + case 3: + var firstCandidate = selections[0]; + + if (utf8ResponseName.SequenceEqual(firstCandidate.Utf8ResponseName)) + { + selection = firstCandidate; + return true; + } + + var secondCandidate = selections[1]; + + if (utf8ResponseName.SequenceEqual(secondCandidate.Utf8ResponseName)) + { + selection = secondCandidate; + return true; + } + + var thirdCandidate = selections[2]; + + if (utf8ResponseName.SequenceEqual(thirdCandidate.Utf8ResponseName)) + { + selection = thirdCandidate; + return true; + } + + selection = default; + return false; + + case 4: + var firstCandidate4 = selections[0]; + + if (utf8ResponseName.SequenceEqual(firstCandidate4.Utf8ResponseName)) + { + selection = firstCandidate4; + return true; + } + + var secondCandidate4 = selections[1]; + + if (utf8ResponseName.SequenceEqual(secondCandidate4.Utf8ResponseName)) + { + selection = secondCandidate4; + return true; + } + + var thirdCandidate4 = selections[2]; + + if (utf8ResponseName.SequenceEqual(thirdCandidate4.Utf8ResponseName)) + { + selection = thirdCandidate4; + return true; + } + + var fourthCandidate4 = selections[3]; + + if (utf8ResponseName.SequenceEqual(fourthCandidate4.Utf8ResponseName)) + { + selection = fourthCandidate4; + return true; + } + + selection = default; + return false; + + } + + if (selections.Length <= 7) + { + return TryGetSelectionLinear(utf8ResponseName, selections, out selection); + } + + return _utf8ResponseNameLookup.TryGetSelection(utf8ResponseName, out selection); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetSelectionLinear( + ReadOnlySpan utf8ResponseName, + Selection[] selections, + [NotNullWhen(true)] out Selection? selection) + { + for (var i = 0; i < selections.Length; i++) + { + var candidate = selections[i]; + + if (utf8ResponseName.SequenceEqual(candidate.Utf8ResponseName)) + { + selection = candidate; + return true; + } + } + + selection = default; + return false; + } internal void Seal(Operation operation) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSetLookup.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSetLookup.cs index b52562055c6..58ea9ec7052 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSetLookup.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSetLookup.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace HotChocolate.Fusion.Execution.Nodes; @@ -84,13 +85,7 @@ public static SelectionLookup Create(SelectionSet selectionSet) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetSelection(ReadOnlySpan name, [NotNullWhen(true)] out Selection? selection) { - var table = _table.AsSpan(); - - if (table.Length == 0) - { - selection = default!; - return false; - } + var table = _table; var hashCode = ComputeHash(name, _seed); var index = hashCode & _mask; @@ -98,17 +93,18 @@ public bool TryGetSelection(ReadOnlySpan name, [NotNullWhen(true)] out Sel while (true) { ref var entry = ref table[index]; + var candidate = entry.Selection; // if we hit an empty slot, then there is no selection with the specified name. - if (entry.Selection is null) + if (candidate is null) { selection = default; return false; } - if (entry.HashCode == hashCode && name.SequenceEqual(entry.Selection.Utf8ResponseName)) + if (entry.HashCode == hashCode && name.SequenceEqual(candidate.Utf8ResponseName)) { - selection = entry.Selection; + selection = candidate; return true; } @@ -121,10 +117,11 @@ public bool TryGetSelection(ReadOnlySpan name, [NotNullWhen(true)] out Sel private static int ComputeHash(ReadOnlySpan bytes, int seed) { var hash = (uint)seed; + ref var start = ref MemoryMarshal.GetReference(bytes); - foreach (var b in bytes) + for (var i = 0; i < bytes.Length; i++) { - hash = hash * 31 + b; + hash = (hash * 31) + Unsafe.Add(ref start, i); } return (int)(hash & 0x7FFFFFFF); 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 566b8877ee7..cfe1d82691b 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 @@ -33,6 +33,7 @@ internal sealed class FetchResultStore : IDisposable private readonly List _collectTargetCurrent = []; private readonly List _collectTargetNext = []; private readonly List _collectTargetCombined = []; + private Path[] _startPathSegments = []; private CompositeResultDocument _result; private ValueCompletion _valueCompletion; private List? _errors; @@ -95,6 +96,26 @@ public bool AddPartialResults( ReadOnlySpan responseNames) => AddPartialResults(sourcePath, results, responseNames, containsErrors: true); + public bool AddPartialResult( + SelectionPath sourcePath, + SourceSchemaResult result, + ReadOnlySpan responseNames) + => AddPartialResult(sourcePath, result, responseNames, containsErrors: true); + + public bool AddPartialResult( + SelectionPath sourcePath, + SourceSchemaResult result, + ReadOnlySpan responseNames, + bool containsErrors) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(sourcePath); + + return containsErrors + ? AddSinglePartialResult(sourcePath, result, responseNames) + : AddSinglePartialResultNoErrors(sourcePath, result, responseNames); + } + public bool AddPartialResults( SelectionPath sourcePath, ReadOnlySpan results, @@ -507,20 +528,31 @@ public ImmutableArray CreateVariableValueSets( { var segment = selectionSet.Segments[i]; - foreach (var element in current) + if (segment.Kind is SelectionPathSegmentKind.InlineFragment) { - if (segment.Kind is SelectionPathSegmentKind.InlineFragment) + var currentSpan = CollectionsMarshal.AsSpan(current); + + for (var j = 0; j < currentSpan.Length; j++) { + var element = currentSpan[j]; + if (element.TryGetProperty(IntrospectionFieldNames.TypeNameSpan, out var value) && value.ValueKind is JsonValueKind.String - && value.TextEqualsHelper(segment.Name, isPropertyName: false)) + && value.ValueEquals(segment.Utf8Name)) { next.Add(element); } } - else if (segment.Kind is SelectionPathSegmentKind.Field) + } + else if (segment.Kind is SelectionPathSegmentKind.Field) + { + var currentSpan = CollectionsMarshal.AsSpan(current); + + for (var j = 0; j < currentSpan.Length; j++) { - if (!element.TryGetProperty(segment.Name, out var value)) + var element = currentSpan[j]; + + if (!element.TryGetProperty(segment.Utf8Name, out var value)) { continue; } @@ -672,6 +704,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa string fieldName, ref PooledArrayWriter? buffer) { + var requiresNonNull = requirement.Type.Kind == SyntaxKind.NonNullType; VariableValues[]? variableValueSets = null; Dictionary? seen = null; Dictionary? seenStrings = null; @@ -680,13 +713,15 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa foreach (var result in elements) { - if (!result.TryGetProperty(fieldName, out var value) - || value.ValueKind is JsonValueKind.Undefined) + if (!result.TryGetProperty(fieldName, out var value)) { continue; } - if (value.ValueKind is JsonValueKind.Null && requirement.Type.Kind == SyntaxKind.NonNullType) + var valueKind = value.ValueKind; + + if (valueKind is JsonValueKind.Undefined + || valueKind is JsonValueKind.Null && requiresNonNull) { continue; } @@ -822,6 +857,8 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast string fieldName2, ref PooledArrayWriter? buffer) { + var requiresNonNull1 = requirement1.Type.Kind == SyntaxKind.NonNullType; + var requiresNonNull2 = requirement2.Type.Kind == SyntaxKind.NonNullType; VariableValues[]? variableValueSets = null; Dictionary? seen = null; List?[]? additionalPaths = null; @@ -829,18 +866,28 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast 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) + if (!result.TryGetProperty(fieldName1, out var value1)) { continue; } - if (!result.TryGetProperty(fieldName2, out var value2) - || value2.ValueKind is JsonValueKind.Undefined - || value2.ValueKind is JsonValueKind.Null - && requirement2.Type.Kind == SyntaxKind.NonNullType) + var valueKind1 = value1.ValueKind; + + if (valueKind1 is JsonValueKind.Undefined + || valueKind1 is JsonValueKind.Null && requiresNonNull1) + { + continue; + } + + if (!result.TryGetProperty(fieldName2, out var value2)) + { + continue; + } + + var valueKind2 = value2.ValueKind; + + if (valueKind2 is JsonValueKind.Undefined + || valueKind2 is JsonValueKind.Null && requiresNonNull2) { continue; } @@ -984,6 +1031,9 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsFa string fieldName3, ref PooledArrayWriter? buffer) { + var requiresNonNull1 = requirement1.Type.Kind == SyntaxKind.NonNullType; + var requiresNonNull2 = requirement2.Type.Kind == SyntaxKind.NonNullType; + var requiresNonNull3 = requirement3.Type.Kind == SyntaxKind.NonNullType; VariableValues[]? variableValueSets = null; Dictionary? seen = null; List?[]? additionalPaths = null; @@ -991,26 +1041,41 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsFa 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) + if (!result.TryGetProperty(fieldName1, out var value1)) { continue; } - if (!result.TryGetProperty(fieldName2, out var value2) - || value2.ValueKind is JsonValueKind.Undefined - || value2.ValueKind is JsonValueKind.Null - && requirement2.Type.Kind == SyntaxKind.NonNullType) + var valueKind1 = value1.ValueKind; + + if (valueKind1 is JsonValueKind.Undefined + || valueKind1 is JsonValueKind.Null && requiresNonNull1) { continue; } - if (!result.TryGetProperty(fieldName3, out var value3) - || value3.ValueKind is JsonValueKind.Undefined - || value3.ValueKind is JsonValueKind.Null - && requirement3.Type.Kind == SyntaxKind.NonNullType) + if (!result.TryGetProperty(fieldName2, out var value2)) + { + continue; + } + + var valueKind2 = value2.ValueKind; + + if (valueKind2 is JsonValueKind.Undefined + || valueKind2 is JsonValueKind.Null && requiresNonNull2) + { + continue; + } + + if (!result.TryGetProperty(fieldName3, out var value3)) + { + continue; + } + + var valueKind3 = value3.ValueKind; + + if (valueKind3 is JsonValueKind.Undefined + || valueKind3 is JsonValueKind.Null && requiresNonNull3) { continue; } @@ -1256,7 +1321,7 @@ private static SourceResultElement GetDataElement(SelectionPath sourcePath, Sour switch (segment.Kind) { case SelectionPathSegmentKind.Root or SelectionPathSegmentKind.Field: - if (!current.TryGetProperty(segment.Name, out current)) + if (!current.TryGetProperty(segment.Utf8Name, out current)) { return default; } @@ -1265,14 +1330,8 @@ private static SourceResultElement GetDataElement(SelectionPath sourcePath, Sour case SelectionPathSegmentKind.InlineFragment: if (!current.TryGetProperty(IntrospectionFieldNames.TypeNameSpan, out var typeNameProperty) - || typeNameProperty.ValueKind != JsonValueKind.String) - { - return default; - } - - var typeName = typeNameProperty.GetString()!; - - if (typeName != segment.Name) + || typeNameProperty.ValueKind != JsonValueKind.String + || !typeNameProperty.ValueEquals(segment.Utf8Name)) { return default; } @@ -1323,33 +1382,57 @@ private CompositeResultElement GetStartResult(Path path) return _result.Data; } - var parent = path.Parent; - var element = GetStartResult(parent); - var elementKind = element.ValueKind; - - if (elementKind is JsonValueKind.Null) + var segmentCount = path.Length; + var segments = _startPathSegments; + if (segments.Length < segmentCount) { - return element; + segments = new Path[segmentCount]; + _startPathSegments = segments; } - if (elementKind is JsonValueKind.Object && path is NamePathSegment nameSegment) + var currentPath = path; + + for (var i = segmentCount - 1; i >= 0; i--) { - return element.TryGetProperty(nameSegment.Name, out var field) ? field : default; + segments[i] = currentPath; + currentPath = currentPath.Parent; } - if (elementKind is JsonValueKind.Array && path is IndexerPathSegment indexSegment) + var element = _result.Data; + + for (var i = 0; i < segmentCount; i++) { - if (element.GetArrayLength() <= indexSegment.Index) + var segment = segments[i]; + var elementKind = element.ValueKind; + + if (elementKind is JsonValueKind.Null) + { + return element; + } + + if (elementKind is JsonValueKind.Object && segment is NamePathSegment nameSegment) { - throw new InvalidOperationException( - $"The path segment '{indexSegment}' does not exist in the data."); + element = element.TryGetProperty(nameSegment.Name, out var field) ? field : default; + continue; + } + + if (elementKind is JsonValueKind.Array && segment is IndexerPathSegment indexSegment) + { + if (element.GetArrayLength() <= indexSegment.Index) + { + throw new InvalidOperationException( + $"The path segment '{indexSegment}' does not exist in the data."); + } + + element = element[indexSegment.Index]; + continue; } - return element[indexSegment.Index]; + throw new InvalidOperationException( + $"The path segment '{segment.Parent}' does not exist in the data."); } - throw new InvalidOperationException( - $"The path segment '{parent}' does not exist in the data."); + return element; } public void Dispose() 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..6f1ae8c238a 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 @@ -13,6 +13,7 @@ internal static class ResultDataMapper { private const int CachedNumericStringMax = 4096; private static readonly StringValueNode[] s_cachedNumericStrings = CreateCachedNumericStrings(); + private static readonly IntValueNode[] s_cachedIntValues = CreateCachedIntValues(); public static IValueNode? Map( CompositeResultElement result, @@ -136,6 +137,11 @@ private static IValueNode ParseLeafValue( case JsonValueKind.Number: if (value.TryGetInt64(out var intValue)) { + if ((ulong)intValue <= CachedNumericStringMax) + { + return s_cachedIntValues[(int)intValue]; + } + return new IntValueNode(intValue); } @@ -224,6 +230,18 @@ private static StringValueNode[] CreateCachedNumericStrings() return values; } + private static IntValueNode[] CreateCachedIntValues() + { + var values = new IntValueNode[CachedNumericStringMax + 1]; + + for (var i = 0; i < values.Length; i++) + { + values[i] = new IntValueNode(i); + } + + return values; + } + private static IValueNode? Visit(ObjectValueSelectionNode node, Context context) { var result = context.Result; 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..b6f19fe90a7 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 @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text.Json; using HotChocolate.Execution; @@ -58,6 +57,106 @@ public bool BuildResult( return BuildErrorResult(target, responseNames, error, target.Path); } + if (target.TryGetSelectionSet(out var targetSelectionSet)) + { + var selectionSetId = targetSelectionSet.Id; + var startCursor = target.GetStartCursor(); + + if (errorTrie is null) + { + foreach (var property in source.EnumerateObject()) + { + if (!targetSelectionSet.TryGetSelection(property.NameSpan, out var selection)) + { + continue; + } + + var resultField = target.GetSelectionProperty(selection, selectionSetId, startCursor); + var propertyValue = property.Value; + + if (!TrySetLeafValueFast(propertyValue, resultField, selection) + && !TryCompleteValue(propertyValue, 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 (!targetSelectionSet.TryGetSelection(property.NameSpan, out var selection)) + { + continue; + } + + var resultField = target.GetSelectionProperty(selection, selectionSetId, startCursor); + errorTrie.TryGetValue(selection.ResponseName, out var errorTrieForResponseName); + var propertyValue = property.Value; + + if (!TrySetLeafValueFast(propertyValue, resultField, selection) + && !TryCompleteValue( + propertyValue, + resultField, + errorTrieForResponseName, + selection, + selection.Type, + 0)) + { + switch (_errorHandlingMode) + { + case ErrorHandlingMode.Propagate: + var didPropagateToRoot = PropagateNullValues(resultField); + return !didPropagateToRoot; + + case ErrorHandlingMode.Halt: + return false; + } + } + } + + return true; + } + + if (errorTrie is null) + { + foreach (var property in source.EnumerateObject()) + { + if (!target.TryGetProperty(property.NameSpan, out var resultField)) + { + continue; + } + + var selection = resultField.AssertSelection(); + var propertyValue = property.Value; + + if (!TrySetLeafValueFast(propertyValue, resultField, selection) + && !TryCompleteValue(propertyValue, 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,10 +165,17 @@ public bool BuildResult( } var selection = resultField.AssertSelection(); - ErrorTrie? errorTrieForResponseName = null; - errorTrie?.TryGetValue(selection.ResponseName, out errorTrieForResponseName); - - if (!TryCompleteValue(property.Value, resultField, errorTrieForResponseName, selection, selection.Type, 0)) + errorTrie.TryGetValue(selection.ResponseName, out var errorTrieForResponseName); + var propertyValue = property.Value; + + if (!TrySetLeafValueFast(propertyValue, resultField, selection) + && !TryCompleteValue( + propertyValue, + resultField, + errorTrieForResponseName, + selection, + selection.Type, + 0)) { switch (_errorHandlingMode) { @@ -168,9 +274,12 @@ private bool TryCompleteValue( IType type, int depth) { + var sourceValueKind = source.ValueKind; + var isNullOrUndefined = sourceValueKind is JsonValueKind.Null or JsonValueKind.Undefined; + if (type.Kind is TypeKind.NonNull) { - if (source.IsNullOrUndefined()) + if (isNullOrUndefined) { IError error; if (errorTrie?.FindFirstError() is { } errorFromPath) @@ -205,7 +314,7 @@ private bool TryCompleteValue( type = type.InnerType(); } - if (source.IsNullOrUndefined()) + if (isNullOrUndefined) { // If the value is null, it might've been nulled due to a // down-stream null propagation. @@ -268,18 +377,144 @@ private bool TryCompleteList( var isNullable = elementType.IsNullableType(); var isLeaf = elementType.IsLeafType(); var isNested = elementType.IsListType(); + var elementKind = isNested + ? 0 + : isLeaf + ? 1 + : elementType.IsAbstractType() + ? 2 + : 3; + IObjectTypeDefinition? objectElementType = null; + SelectionSet? objectElementSelectionSet = null; + + if (elementKind is 3) + { + var namedType = elementType.NamedType(); + objectElementType = Unsafe.As(ref namedType); + objectElementSelectionSet = selection.GetSelectionSet(objectElementType); + } target.SetArrayValue(source.GetArrayLength()); + var arrayStartCursor = target.GetStartCursor(); + + if (errorTrie is null) + { + if (elementKind is 3) + { + var elementIndex = 0; + foreach (var element in source.EnumerateArray()) + { + var current = target.GetArrayElement(arrayStartCursor, elementIndex++); + + if (element.IsNullOrUndefined()) + { + if (!isNullable + && _errorHandlingMode is ErrorHandlingMode.Propagate or ErrorHandlingMode.Halt) + { + return false; + } + + current.SetNullValue(); + continue; + } + + if (!TryCompleteObjectValue( + element, + current, + null, + selection, + objectElementType!, + objectElementSelectionSet, + depth)) + { + if (!isNullable) + { + return false; + } + + current.SetNullValue(); + } + } + + return true; + } + + var j = 0; + foreach (var element in source.EnumerateArray()) + { + var current = target.GetArrayElement(arrayStartCursor, j++); + + if (element.IsNullOrUndefined()) + { + if (!isNullable && _errorHandlingMode is ErrorHandlingMode.Propagate or ErrorHandlingMode.Halt) + { + return false; + } + + current.SetNullValue(); + continue; + } + + var success = true; + + switch (elementKind) + { + case 0: + success = TryCompleteList( + element, + current, + null, + selection, + elementType, + depth); + break; + + case 1: + current.SetLeafValue(element); + break; + + case 2: + success = TryCompleteAbstractValue( + element, + current, + null, + selection, + elementType, + depth); + break; + + default: + success = TryCompleteObjectValue( + element, + current, + null, + selection, + objectElementType!, + objectElementSelectionSet, + depth); + break; + } + + if (!success) + { + if (!isNullable) + { + return false; + } + + current.SetNullValue(); + } + } + + return true; + } var i = 0; - using var enumerator = target.EnumerateArray().GetEnumerator(); foreach (var element in source.EnumerateArray()) { - var success = enumerator.MoveNext(); - Debug.Assert(success, "The lists must have the same size."); + var current = target.GetArrayElement(arrayStartCursor, i); - ErrorTrie? errorTrieForIndex = null; - errorTrie?.TryGetValue(i, out errorTrieForIndex); + errorTrie.TryGetValue(i, out var errorTrieForIndex); if (errorTrieForIndex?.Error is { } error) { @@ -304,63 +539,65 @@ private bool TryCompleteList( return false; } - enumerator.Current.SetNullValue(); - goto TryCompleteList_MoveNext; + current.SetNullValue(); + i++; + continue; + } + + var success = true; + + switch (elementKind) + { + case 0: + success = TryCompleteList( + element, + current, + errorTrieForIndex, + selection, + elementType, + depth); + break; + + case 1: + current.SetLeafValue(element); + break; + + case 2: + success = TryCompleteAbstractValue( + element, + current, + errorTrieForIndex, + selection, + elementType, + depth); + break; + + default: + success = TryCompleteObjectValue( + element, + current, + errorTrieForIndex, + selection, + objectElementType!, + objectElementSelectionSet, + depth); + break; } - if (!HandleElement(element, enumerator.Current, errorTrieForIndex)) + if (!success) { if (!isNullable) { return false; } - enumerator.Current.SetNullValue(); - goto TryCompleteList_MoveNext; + current.SetNullValue(); } -TryCompleteList_MoveNext: i++; } return true; - - bool HandleElement( - SourceResultElement sourceElement, - CompositeResultElement targetElement, - ErrorTrie? errorTrieForIndex) - { - if (isNested) - { - return TryCompleteList( - sourceElement, - targetElement, - errorTrieForIndex, - selection, - elementType, - depth); - } - - if (isLeaf) - { - targetElement.SetLeafValue(sourceElement); - return true; - } - - if (elementType.IsAbstractType()) - { - return TryCompleteAbstractValue(sourceElement, - targetElement, errorTrieForIndex, selection, elementType, depth); - } - - return TryCompleteObjectValue( - selection, - elementType, - sourceElement, - errorTrieForIndex, - depth, - targetElement); - } } private bool TryCompleteObjectValue( @@ -374,7 +611,14 @@ private bool TryCompleteObjectValue( var namedType = type.NamedType(); var objectType = Unsafe.As(ref namedType); - return TryCompleteObjectValue(source, target, errorTrie, parentSelection, objectType, depth); + return TryCompleteObjectValue( + source, + target, + errorTrie, + parentSelection, + objectType, + precomputedSelectionSet: null, + depth); } private bool TryCompleteObjectValue( @@ -383,18 +627,116 @@ private bool TryCompleteObjectValue( ErrorTrie? errorTrie, Selection parentSelection, IObjectTypeDefinition objectType, + SelectionSet? precomputedSelectionSet, int depth) { AssertDepthAllowed(ref depth); + SelectionSet? selectionSet = null; // if the property value is yet undefined we need to initialize it // with the current selection set. if (target.ValueKind is JsonValueKind.Undefined) { - var operation = parentSelection.DeclaringSelectionSet.DeclaringOperation; - var selectionSet = operation.GetSelectionSet(parentSelection, objectType); + if (precomputedSelectionSet is null) + { + precomputedSelectionSet = parentSelection.GetSelectionSet(objectType); + } + + selectionSet = precomputedSelectionSet; target.SetObjectValue(selectionSet); } + else if (target.TryGetSelectionSet(out var existingSelectionSet)) + { + selectionSet = existingSelectionSet; + } + + if (selectionSet is not null) + { + var selectionSetId = selectionSet.Id; + var startCursor = target.GetStartCursor(); + + if (errorTrie is null) + { + foreach (var property in source.EnumerateObject()) + { + if (!selectionSet.TryGetSelection(property.NameSpan, out var selection)) + { + continue; + } + + var targetProperty = target.GetSelectionProperty(selection, selectionSetId, startCursor); + var propertyValue = property.Value; + + if (!TrySetLeafValueFast(propertyValue, targetProperty, selection) + && !TryCompleteValue( + propertyValue, + targetProperty, + null, + selection, + selection.Type, + depth)) + { + return false; + } + } + + return true; + } + + foreach (var property in source.EnumerateObject()) + { + if (!selectionSet.TryGetSelection(property.NameSpan, out var selection)) + { + continue; + } + + var targetProperty = target.GetSelectionProperty(selection, selectionSetId, startCursor); + errorTrie.TryGetValue(selection.ResponseName, out var errorTrieForResponseName); + var propertyValue = property.Value; + + if (!TrySetLeafValueFast(propertyValue, targetProperty, selection) + && !TryCompleteValue( + propertyValue, + targetProperty, + errorTrieForResponseName, + selection, + selection.Type, + depth)) + { + return false; + } + } + + return true; + } + + if (errorTrie is null) + { + foreach (var property in source.EnumerateObject()) + { + if (!target.TryGetProperty(property.NameSpan, out var targetProperty)) + { + continue; + } + + var selection = targetProperty.AssertSelection(); + var propertyValue = property.Value; + + if (!TrySetLeafValueFast(propertyValue, targetProperty, selection) + && !TryCompleteValue( + propertyValue, + targetProperty, + null, + selection, + selection.Type, + depth)) + { + return false; + } + } + + return true; + } foreach (var property in source.EnumerateObject()) { @@ -404,12 +746,17 @@ private bool TryCompleteObjectValue( } var selection = targetProperty.AssertSelection(); - - ErrorTrie? errorTrieForResponseName = null; - errorTrie?.TryGetValue(selection.ResponseName, out errorTrieForResponseName); - - if (!TryCompleteValue(property.Value, - targetProperty, errorTrieForResponseName, selection, selection.Type, depth)) + errorTrie.TryGetValue(selection.ResponseName, out var errorTrieForResponseName); + var propertyValue = property.Value; + + if (!TrySetLeafValueFast(propertyValue, targetProperty, selection) + && !TryCompleteValue( + propertyValue, + targetProperty, + errorTrieForResponseName, + selection, + selection.Type, + depth)) { return false; } @@ -425,7 +772,14 @@ private bool TryCompleteAbstractValue( Selection selection, IType type, int depth) - => TryCompleteObjectValue(source, target, errorTrie, selection, GetType(type, source), depth); + => TryCompleteObjectValue( + source, + target, + errorTrie, + selection, + GetType(type, source), + precomputedSelectionSet: null, + depth); [MethodImpl(MethodImplOptions.AggressiveInlining)] private IObjectTypeDefinition GetType(IType type, SourceResultElement data) @@ -441,6 +795,21 @@ private IObjectTypeDefinition GetType(IType type, SourceResultElement data) return _schema.Types.GetType(typeName); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TrySetLeafValueFast( + SourceResultElement source, + CompositeResultElement target, + Selection selection) + { + if (selection.IsLeafValue && !source.IsNullOrUndefined()) + { + target.SetLeafValue(source); + return true; + } + + return false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AssertDepthAllowed(ref int depth) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultDocument.TryGetProperty.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultDocument.TryGetProperty.cs index a900b0c3379..e8fd2ef0251 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultDocument.TryGetProperty.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultDocument.TryGetProperty.cs @@ -1,11 +1,36 @@ using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; +using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Text.Json; namespace HotChocolate.Fusion.Text.Json; public sealed partial class CompositeResultDocument { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetSelectionSet(Cursor cursor, out SelectionSet? selectionSet) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + (cursor, var tokenType) = _metaDb.GetStartCursor(cursor); + if (tokenType is not ElementTokenType.StartObject) + { + selectionSet = null; + return false; + } + + var row = _metaDb.Get(cursor); + if (row.OperationReferenceType is not OperationReferenceType.SelectionSet) + { + selectionSet = null; + return false; + } + + selectionSet = _operation.GetSelectionSetById(row.OperationReferenceId); + return true; + } + internal bool TryGetNamedPropertyValue( Cursor startCursor, string propertyName, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultElement.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultElement.cs index 271cd4a11de..ff2c362ab49 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultElement.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultElement.cs @@ -1,5 +1,7 @@ using System.Buffers; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json; using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Text.Json; @@ -412,6 +414,40 @@ public bool TryGetProperty(ReadOnlySpan utf8PropertyName, out CompositeRes return _parent.TryGetNamedPropertyValue(_cursor, utf8PropertyName, out value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal CompositeResultDocument.Cursor GetStartCursor() + { + CheckValidInstance(); + return _parent.GetStartCursor(_cursor); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal CompositeResultElement GetArrayElement( + CompositeResultDocument.Cursor startCursor, + int index) + { + return new CompositeResultElement(_parent, startCursor + index + 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal CompositeResultElement GetSelectionProperty( + Selection selection, + int selectionSetId, + CompositeResultDocument.Cursor startCursor) + { + var propertyIndex = selection.Id - selectionSetId - 1; + var propertyRowIndex = (propertyIndex * 2) + 1; + var propertyCursor = startCursor + propertyRowIndex; + return new CompositeResultElement(_parent, propertyCursor + 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetSelectionSet([NotNullWhen(true)] out SelectionSet? selectionSet) + { + CheckValidInstance(); + return _parent.TryGetSelectionSet(_cursor, out selectionSet); + } + /// /// Gets the value as a . ///