diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs index 45ae3f879e3..0c66513ef4b 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs @@ -24,6 +24,10 @@ namespace HotChocolate.Transport.Http; /// public sealed class DefaultGraphQLHttpClient : GraphQLHttpClient { +#if FUSION + private const string JsonUtf8ContentType = $"{ContentType.Json}; charset=utf-8"; +#endif + private readonly HttpClient _http; private readonly bool _disposeInnerClient; @@ -148,11 +152,22 @@ private static HttpRequestMessage CreateRequestMessage( Method = method }; +#if FUSION + if (request.AcceptHeaderValue is not null) + { + message.Headers.TryAddWithoutValidation("Accept", request.AcceptHeaderValue); + } + else + { +#endif message.Headers.Accept.Clear(); foreach (var contentType in request.Accept) { message.Headers.Accept.Add(contentType); } +#if FUSION + } +#endif if (method == GraphQLHttpMethod.Post) { @@ -192,7 +207,12 @@ private static ByteArrayContent CreatePostContent( var internalBuffer = PooledArrayWriterMarshal.GetUnderlyingBuffer(arrayWriter); var content = new ByteArrayContent(internalBuffer, 0, arrayWriter.Length); +#if FUSION + content.Headers.ContentType = null; + content.Headers.TryAddWithoutValidation("Content-Type", JsonUtf8ContentType); +#else content.Headers.ContentType = new MediaTypeHeaderValue(ContentType.Json, "utf-8"); +#endif return content; } @@ -215,11 +235,21 @@ private static HttpContent CreateMultipartContent( var form = new MultipartFormDataContent(); var operation = new ByteArrayContent(buffer, start, arrayWriter.Length - start); +#if FUSION + operation.Headers.ContentType = null; + operation.Headers.TryAddWithoutValidation("Content-Type", JsonUtf8ContentType); +#else operation.Headers.ContentType = new MediaTypeHeaderValue(ContentType.Json, "utf-8"); +#endif form.Add(operation, "operations"); var fileMap = new ByteArrayContent(buffer, 0, start); +#if FUSION + fileMap.Headers.ContentType = null; + fileMap.Headers.TryAddWithoutValidation("Content-Type", JsonUtf8ContentType); +#else fileMap.Headers.ContentType = new MediaTypeHeaderValue(ContentType.Json, "utf-8"); +#endif form.Add(fileMap, "map"); foreach (var fileInfo in fileInfos) diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpRequest.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpRequest.cs index f603a6344e7..cb5c9b5dc46 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpRequest.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpRequest.cs @@ -157,6 +157,14 @@ public GraphQLHttpRequest(OperationBatchRequest body, Uri? requestUri = null) /// public ImmutableArray Accept { get; set; } = DefaultAcceptContentTypes; +#if FUSION + /// + /// Gets or sets a pre-formatted Accept header value to avoid per-request allocations. + /// When set, this is used instead of the typed array. + /// + public string? AcceptHeaderValue { get; set; } +#endif + /// /// Gets or sets a hook that can alter the before it is sent. /// diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs index 80ac858282f..c29dc4e4dfb 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs @@ -24,6 +24,15 @@ namespace HotChocolate.Transport.Http; public sealed class GraphQLHttpResponse : IDisposable { #if FUSION + private const string ContentTypeHeaderName = "Content-Type"; + private const string CharsetPrefix = "charset="; + private const string Utf8 = "utf-8"; + private const string JsonUtf8ContentType = $"{ContentType.Json}; charset={Utf8}"; + private const string GraphQLUtf8ContentType = $"{ContentType.GraphQL}; charset={Utf8}"; + private const string EventStreamUtf8ContentType = $"{ContentType.EventStream}; charset={Utf8}"; + private const string GraphQLJsonLineUtf8ContentType = $"{ContentType.GraphQLJsonLine}; charset={Utf8}"; + private const string JsonLineUtf8ContentType = $"{ContentType.JsonLine}; charset={Utf8}"; + private static readonly StreamPipeReaderOptions s_options = new( pool: MemoryPool.Shared, bufferSize: 4096, @@ -93,6 +102,139 @@ public GraphQLHttpResponse(HttpResponseMessage message) /// public HttpContentHeaders ContentHeaders => _message.Content.Headers; +#if FUSION + /// + /// Gets the raw Content-Type header value without parsing into . + /// + public string? RawContentType + { + get + { + if (_message.Content.Headers.NonValidated.TryGetValues(ContentTypeHeaderName, out var values)) + { + var enumerator = values.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return null; + } + + var mediaType = enumerator.Current; + + // Some handlers may emit media type and charset as separate values. + // Normalize known UTF-8 combinations back to shared constants. + if (enumerator.MoveNext() + && TryNormalizeKnownUtf8ContentType(mediaType.AsSpan(), enumerator.Current.AsSpan(), out var normalized)) + { + return normalized; + } + + return mediaType; + } + + return null; + } + } + + private static bool TryNormalizeKnownUtf8ContentType( + ReadOnlySpan mediaType, + ReadOnlySpan charset, + out string contentType) + { + if (!IsUtf8(charset)) + { + contentType = null!; + return false; + } + + mediaType = NormalizeMediaType(mediaType); + + if (mediaType.Equals(ContentType.GraphQL, StringComparison.OrdinalIgnoreCase)) + { + contentType = GraphQLUtf8ContentType; + return true; + } + + if (mediaType.Equals(ContentType.JsonLine, StringComparison.OrdinalIgnoreCase)) + { + contentType = JsonLineUtf8ContentType; + return true; + } + + if (mediaType.Equals(ContentType.Json, StringComparison.OrdinalIgnoreCase)) + { + contentType = JsonUtf8ContentType; + return true; + } + + if (mediaType.Equals(ContentType.EventStream, StringComparison.OrdinalIgnoreCase)) + { + contentType = EventStreamUtf8ContentType; + return true; + } + + if (mediaType.Equals(ContentType.GraphQLJsonLine, StringComparison.OrdinalIgnoreCase)) + { + contentType = GraphQLJsonLineUtf8ContentType; + return true; + } + + contentType = null!; + return false; + } + + private static bool IsUtf8(ReadOnlySpan value) + { + value = TrimWhiteSpace(value); + + if (value.Length > 0 && value[0] == ';') + { + value = TrimWhiteSpace(value[1..]); + } + + if (value.StartsWith(CharsetPrefix, StringComparison.OrdinalIgnoreCase)) + { + value = TrimWhiteSpace(value[CharsetPrefix.Length..]); + } + + if (value.Length > 1 && value[0] == '"' && value[^1] == '"') + { + value = TrimWhiteSpace(value[1..^1]); + } + + return value.Equals(Utf8, StringComparison.OrdinalIgnoreCase); + } + + private static ReadOnlySpan NormalizeMediaType(ReadOnlySpan mediaType) + { + mediaType = TrimWhiteSpace(mediaType); + + if (mediaType.Length > 0 && mediaType[^1] == ';') + { + mediaType = TrimWhiteSpace(mediaType[..^1]); + } + + return mediaType; + } + + private static ReadOnlySpan TrimWhiteSpace(ReadOnlySpan value) + { + var start = 0; + var end = value.Length - 1; + + while (start <= end && char.IsWhiteSpace(value[start])) + { + start++; + } + + while (end >= start && char.IsWhiteSpace(value[end])) + { + end--; + } + + return value[start..(end + 1)]; + } +#endif + /// /// Gets the collection of trailing headers included in an HTTP response. /// diff --git a/src/HotChocolate/Fusion/benchmarks/k6/start-gateway.sh b/src/HotChocolate/Fusion/benchmarks/k6/start-gateway.sh old mode 100644 new mode 100755 diff --git a/src/HotChocolate/Fusion/benchmarks/k6/start-source-schemas.sh b/src/HotChocolate/Fusion/benchmarks/k6/start-source-schemas.sh old mode 100644 new mode 100755 diff --git a/src/HotChocolate/Fusion/benchmarks/k6/stop-services.sh b/src/HotChocolate/Fusion/benchmarks/k6/stop-services.sh old mode 100644 new mode 100755 diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs index 01438bbe2e2..2ddcd462d8e 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs @@ -114,7 +114,7 @@ public async ValueTask> ExecuteBatchA var httpResponse = await _client.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); var uri = httpRequest.Uri ?? new Uri("http://unknown"); - var contentType = httpResponse.ContentHeaders.ContentType?.ToString() ?? "unknown"; + var contentType = httpResponse.RawContentType ?? "unknown"; var isSuccessful = httpResponse.IsSuccessStatusCode; var nodeResponses = new NodeResponse[requests.Length]; @@ -145,9 +145,9 @@ public async ValueTask> ExecuteBatchA private GraphQLHttpRequest CreateHttpRequest( SourceSchemaClientRequest originalRequest) { - var defaultAccept = originalRequest.OperationType is OperationType.Subscription - ? _configuration.SubscriptionAcceptHeaderValues - : _configuration.DefaultAcceptHeaderValues; + var defaultAcceptHeader = originalRequest.OperationType is OperationType.Subscription + ? _configuration.SubscriptionAcceptHeaderValue + : _configuration.DefaultAcceptHeaderValue; var operationSourceText = originalRequest.OperationSourceText; switch (originalRequest.Variables.Length) @@ -156,7 +156,7 @@ private GraphQLHttpRequest CreateHttpRequest( return new GraphQLHttpRequest(CreateSingleRequest(operationSourceText)) { Uri = _configuration.BaseAddress, - Accept = defaultAccept + AcceptHeaderValue = defaultAcceptHeader }; case 1: @@ -164,7 +164,7 @@ private GraphQLHttpRequest CreateHttpRequest( return new GraphQLHttpRequest(CreateSingleRequest(operationSourceText, variableValues)) { Uri = _configuration.BaseAddress, - Accept = defaultAccept, + AcceptHeaderValue = defaultAcceptHeader, EnableFileUploads = originalRequest.RequiresFileUpload }; @@ -174,7 +174,7 @@ private GraphQLHttpRequest CreateHttpRequest( return new GraphQLHttpRequest(CreateOperationBatchRequest(operationSourceText, originalRequest)) { Uri = _configuration.BaseAddress, - Accept = _configuration.BatchingAcceptHeaderValues, + AcceptHeaderValue = _configuration.BatchingAcceptHeaderValue, EnableFileUploads = originalRequest.RequiresFileUpload }; } @@ -182,7 +182,7 @@ private GraphQLHttpRequest CreateHttpRequest( return new GraphQLHttpRequest(CreateVariableBatchRequest(operationSourceText, originalRequest)) { Uri = _configuration.BaseAddress, - Accept = _configuration.BatchingAcceptHeaderValues, + AcceptHeaderValue = _configuration.BatchingAcceptHeaderValue, EnableFileUploads = originalRequest.RequiresFileUpload }; } @@ -214,7 +214,7 @@ private GraphQLHttpRequest CreateHttpBatchRequest( return new GraphQLHttpRequest(new OperationBatchRequest(batchRequests)) { Uri = _configuration.BaseAddress, - Accept = _configuration.BatchingAcceptHeaderValues, + AcceptHeaderValue = _configuration.BatchingAcceptHeaderValue, EnableFileUploads = enableFileUploads }; } @@ -547,7 +547,7 @@ private sealed class Response( { public override Uri Uri => request.Uri ?? new Uri("http://unknown"); - public override string ContentType => response.ContentHeaders.ContentType?.ToString() ?? "unknown"; + public override string ContentType => response.RawContentType ?? "unknown"; public override bool IsSuccessful => response.IsSuccessStatusCode; diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs index e8275a9b313..8908547957f 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs @@ -135,6 +135,9 @@ public SourceSchemaHttpClientConfiguration( BatchingMode = batchingMode; DefaultAcceptHeaderValues = defaultAcceptHeaderValues ?? AcceptContentTypes.Default; + DefaultAcceptHeaderValue = defaultAcceptHeaderValues.HasValue + ? AcceptContentTypes.FormatAcceptHeader(defaultAcceptHeaderValues.Value) + : AcceptContentTypes.DefaultHeader; if (batchingMode.HasFlag(SourceSchemaHttpClientBatchingMode.VariableBatching)) { @@ -144,15 +147,22 @@ public SourceSchemaHttpClientConfiguration( if (batchingAcceptHeaderValues.HasValue) { BatchingAcceptHeaderValues = batchingAcceptHeaderValues.Value; + BatchingAcceptHeaderValue = AcceptContentTypes.FormatAcceptHeader(batchingAcceptHeaderValues.Value); } else { BatchingAcceptHeaderValues = batchingMode == SourceSchemaHttpClientBatchingMode.ApolloRequestBatching ? AcceptContentTypes.ApolloRequestBatching : AcceptContentTypes.VariableBatching; + BatchingAcceptHeaderValue = batchingMode == SourceSchemaHttpClientBatchingMode.ApolloRequestBatching + ? AcceptContentTypes.ApolloRequestBatchingHeader + : AcceptContentTypes.VariableBatchingHeader; } SubscriptionAcceptHeaderValues = subscriptionAcceptHeaderValues ?? AcceptContentTypes.Subscription; + SubscriptionAcceptHeaderValue = subscriptionAcceptHeaderValues.HasValue + ? AcceptContentTypes.FormatAcceptHeader(subscriptionAcceptHeaderValues.Value) + : AcceptContentTypes.SubscriptionHeader; OnBeforeSend = onBeforeSend; OnAfterReceive = onAfterReceive; @@ -189,16 +199,31 @@ public SourceSchemaHttpClientConfiguration( /// public ImmutableArray DefaultAcceptHeaderValues { get; } + /// + /// Gets a pre-formatted Accept header string for single, non-Subscription GraphQL requests. + /// + public string DefaultAcceptHeaderValue { get; } + /// /// Gets the Accept header values sent in case of a batching request. /// public ImmutableArray BatchingAcceptHeaderValues { get; } + /// + /// Gets a pre-formatted Accept header string for batching requests. + /// + public string BatchingAcceptHeaderValue { get; } + /// /// Gets the Accept header values sent in case of a subscription. /// public ImmutableArray SubscriptionAcceptHeaderValues { get; } + /// + /// Gets a pre-formatted Accept header string for subscriptions. + /// + public string SubscriptionAcceptHeaderValue { get; } + /// /// Called before the request is sent. /// @@ -242,5 +267,16 @@ private static class AcceptContentTypes new("application/jsonl") { CharSet = "utf-8" }, new("text/event-stream") { CharSet = "utf-8" } ]; + + public static string DefaultHeader { get; } = FormatAcceptHeader(Default); + + public static string VariableBatchingHeader { get; } = FormatAcceptHeader(VariableBatching); + + public static string ApolloRequestBatchingHeader { get; } = FormatAcceptHeader(ApolloRequestBatching); + + public static string SubscriptionHeader { get; } = FormatAcceptHeader(Subscription); + + public static string FormatAcceptHeader(ImmutableArray values) + => string.Join(", ", values); } } diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 931ecfa84c2..91b5524efe1 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -30,9 +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 CompositeResultElement[] _collectTargetA = ArrayPool.Shared.Rent(64); + private CompositeResultElement[] _collectTargetB = ArrayPool.Shared.Rent(64); + private CompositeResultElement[] _collectTargetCombined = ArrayPool.Shared.Rent(64); private CompositeResultDocument _result; private ValueCompletion _valueCompletion; private List? _errors; @@ -440,7 +440,7 @@ public ImmutableArray CreateVariableValueSets( { var elements = CollectTargetElements(selectionSet); - if (elements is null) + if (elements.IsEmpty) { return []; } @@ -472,36 +472,44 @@ public ImmutableArray CreateVariableValueSets( lock (_lock) { - var combined = _collectTargetCombined; - combined.Clear(); + var combinedCount = 0; foreach (var selectionSet in selectionSets) { var elements = CollectTargetElements(selectionSet); - if (elements is not null) + if (!elements.IsEmpty) { - combined.AddRange(elements); + EnsureCapacity( + ref _collectTargetCombined, + combinedCount + elements.Length, + combinedCount); + elements.CopyTo(_collectTargetCombined.AsSpan(combinedCount)); + combinedCount += elements.Length; } } - if (combined.Count == 0) + if (combinedCount == 0) { return []; } - return BuildVariableValueSets(combined, requestVariables, requiredData); + return BuildVariableValueSets( + _collectTargetCombined.AsSpan(0, combinedCount), + requestVariables, + requiredData); } } // Caller must hold _lock for reading. - private List? CollectTargetElements(SelectionPath selectionSet) + private ReadOnlySpan CollectTargetElements(SelectionPath selectionSet) { - var current = _collectTargetCurrent; - var next = _collectTargetNext; - current.Clear(); - next.Clear(); - current.Add(_result.Data); + var current = _collectTargetA; + var currentCount = 0; + var next = _collectTargetB; + var nextCount = 0; + + current[currentCount++] = _result.Data; for (var i = 0; i < selectionSet.Length; i++) { @@ -509,20 +517,22 @@ public ImmutableArray CreateVariableValueSets( if (segment.Kind is SelectionPathSegmentKind.InlineFragment) { - foreach (var element in current) + for (var j = 0; j < currentCount; j++) { + var element = current[j]; if (element.TryGetProperty(IntrospectionFieldNames.TypeNameSpan, out var value) && value.ValueKind is JsonValueKind.String && value.TextEqualsHelper(segment.Name, isPropertyName: false)) { - next.Add(element); + AddToBuffer(ref next, ref nextCount, element); } } } else if (segment.Kind is SelectionPathSegmentKind.Field) { - foreach (var element in current) + for (var j = 0; j < currentCount; j++) { + var element = current[j]; if (!element.TryGetProperty(segment.Name, out var value)) { continue; @@ -537,13 +547,13 @@ public ImmutableArray CreateVariableValueSets( if (valueKind is JsonValueKind.Array) { - AppendUnrolledLists(value, next); + AppendUnrolledLists(value, ref next, ref nextCount); continue; } if (valueKind is JsonValueKind.Object) { - next.Add(value); + AddToBuffer(ref next, ref nextCount, value); continue; } @@ -552,20 +562,26 @@ public ImmutableArray CreateVariableValueSets( } } - (next, current) = (current, next); - next.Clear(); + (current, next) = (next, current); + (currentCount, nextCount) = (nextCount, 0); - if (current.Count == 0) + if (currentCount == 0) { - return null; + // Store potentially grown arrays back. + _collectTargetA = current; + _collectTargetB = next; + return ReadOnlySpan.Empty; } } - return current; + // Store potentially grown arrays back. + _collectTargetA = current; + _collectTargetB = next; + return current.AsSpan(0, currentCount); } private ImmutableArray BuildVariableValueSets( - List elements, + ReadOnlySpan elements, IReadOnlyList requestVariables, ReadOnlySpan requiredData) { @@ -620,18 +636,18 @@ private ImmutableArray BuildVariableValueSets( continue; } - variableValueSets ??= new VariableValues[elements.Count]; + variableValueSets ??= new VariableValues[elements.Length]; if (nextIndex > 0) { - seen ??= new Dictionary(elements.Count, VariableValueComparer.Instance) + seen ??= new Dictionary(elements.Length, VariableValueComparer.Instance) { [variableValueSets[0].Values] = 0 }; if (seen.TryGetValue(variables, out var existingIndex)) { - additionalPaths ??= new List?[elements.Count]; + additionalPaths ??= new List?[elements.Length]; (additionalPaths[existingIndex] ??= []).Add(result.Path); continue; } @@ -651,7 +667,7 @@ private ImmutableArray BuildVariableValueSets( } private ImmutableArray BuildVariableValueSetsSingleRequirement( - List elements, + ReadOnlySpan elements, OperationRequirement requirement, ref PooledArrayWriter? buffer) { @@ -668,7 +684,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirement( } private ImmutableArray BuildVariableValueSetsSingleRequirementFastPath( - List elements, + ReadOnlySpan elements, OperationRequirement requirement, string fieldName, ref PooledArrayWriter? buffer) @@ -680,7 +696,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa var nextIndex = 0; var isNonNullRequirement = requirement.Type.Kind is SyntaxKind.NonNullType; - for (var i = 0; i < elements.Count; i++) + for (var i = 0; i < elements.Length; i++) { var result = elements[i]; @@ -701,7 +717,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa continue; } - variableValueSets ??= new VariableValues[elements.Count]; + variableValueSets ??= new VariableValues[elements.Length]; IValueNode mappedValue; if (valueKind is JsonValueKind.String) @@ -711,13 +727,13 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa if (seenStrings is not null && seenStrings.TryGetValue(stringValue, out var existingIndex)) { - additionalPaths ??= new List?[elements.Count]; + additionalPaths ??= new List?[elements.Length]; (additionalPaths[existingIndex] ??= []).Add(result.Path); continue; } mappedValue = ResultDataMapper.GetStringValueNode(stringValue); - seenStrings ??= new Dictionary(elements.Count, StringComparer.Ordinal); + seenStrings ??= new Dictionary(elements.Length, StringComparer.Ordinal); seenStrings[stringValue] = nextIndex; } else @@ -727,12 +743,12 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa if (seen is not null && seen.TryGetValue(mappedValue, out var existingIndex)) { - additionalPaths ??= new List?[elements.Count]; + additionalPaths ??= new List?[elements.Length]; (additionalPaths[existingIndex] ??= []).Add(result.Path); continue; } - seen ??= new Dictionary(elements.Count, SingleValueNodeComparer.Instance); + seen ??= new Dictionary(elements.Length, SingleValueNodeComparer.Instance); seen[mappedValue] = nextIndex; } @@ -749,7 +765,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa } private ImmutableArray BuildVariableValueSetsSingleRequirementSlowPath( - List elements, + ReadOnlySpan elements, OperationRequirement requirement, ref PooledArrayWriter? buffer) { @@ -772,18 +788,18 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementSl continue; } - variableValueSets ??= new VariableValues[elements.Count]; + variableValueSets ??= new VariableValues[elements.Length]; if (nextIndex > 0) { - seen ??= new Dictionary(elements.Count, SingleValueNodeComparer.Instance) + seen ??= new Dictionary(elements.Length, SingleValueNodeComparer.Instance) { [variableValueSets[0].Values.Fields[0].Value] = 0 }; if (seen.TryGetValue(value, out var existingIndex)) { - additionalPaths ??= new List?[elements.Count]; + additionalPaths ??= new List?[elements.Length]; (additionalPaths[existingIndex] ??= []).Add(result.Path); continue; } @@ -800,7 +816,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementSl } private ImmutableArray BuildVariableValueSetsTwoRequirements( - List elements, + ReadOnlySpan elements, OperationRequirement requirement1, OperationRequirement requirement2, ref PooledArrayWriter? buffer) @@ -825,7 +841,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirements( } private ImmutableArray BuildVariableValueSetsTwoRequirementsFastPath( - List elements, + ReadOnlySpan elements, OperationRequirement requirement1, string fieldName1, OperationRequirement requirement2, @@ -857,12 +873,12 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast var mappedValue1 = MapRequirementLeafValue(value1, ref buffer); var mappedValue2 = MapRequirementLeafValue(value2, ref buffer); - variableValueSets ??= new VariableValues[elements.Count]; + variableValueSets ??= new VariableValues[elements.Length]; var key = new TwoValueNodeTuple(mappedValue1, mappedValue2); if (nextIndex > 0) { - seen ??= new Dictionary(elements.Count, TwoValueNodeTupleComparer.Instance) + seen ??= new Dictionary(elements.Length, TwoValueNodeTupleComparer.Instance) { [new TwoValueNodeTuple( variableValueSets[0].Values.Fields[0].Value, @@ -871,7 +887,7 @@ [new TwoValueNodeTuple( if (seen.TryGetValue(key, out var existingIndex)) { - additionalPaths ??= new List?[elements.Count]; + additionalPaths ??= new List?[elements.Length]; (additionalPaths[existingIndex] ??= []).Add(result.Path); continue; } @@ -891,7 +907,7 @@ [new TwoValueNodeTuple( } private ImmutableArray BuildVariableValueSetsTwoRequirementsSlowPath( - List elements, + ReadOnlySpan elements, OperationRequirement requirement1, OperationRequirement requirement2, ref PooledArrayWriter? buffer) @@ -921,12 +937,12 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsSlow continue; } - variableValueSets ??= new VariableValues[elements.Count]; + variableValueSets ??= new VariableValues[elements.Length]; var key = new TwoValueNodeTuple(value1, value2); if (nextIndex > 0) { - seen ??= new Dictionary(elements.Count, TwoValueNodeTupleComparer.Instance) + seen ??= new Dictionary(elements.Length, TwoValueNodeTupleComparer.Instance) { [new TwoValueNodeTuple( variableValueSets[0].Values.Fields[0].Value, @@ -935,7 +951,7 @@ [new TwoValueNodeTuple( if (seen.TryGetValue(key, out var existingIndex)) { - additionalPaths ??= new List?[elements.Count]; + additionalPaths ??= new List?[elements.Length]; (additionalPaths[existingIndex] ??= []).Add(result.Path); continue; } @@ -955,7 +971,7 @@ [new TwoValueNodeTuple( } private ImmutableArray BuildVariableValueSetsThreeRequirements( - List elements, + ReadOnlySpan elements, OperationRequirement requirement1, OperationRequirement requirement2, OperationRequirement requirement3, @@ -985,7 +1001,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirements( } private ImmutableArray BuildVariableValueSetsThreeRequirementsFastPath( - List elements, + ReadOnlySpan elements, OperationRequirement requirement1, string fieldName1, OperationRequirement requirement2, @@ -1028,12 +1044,12 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsFa var mappedValue1 = MapRequirementLeafValue(value1, ref buffer); var mappedValue2 = MapRequirementLeafValue(value2, ref buffer); var mappedValue3 = MapRequirementLeafValue(value3, ref buffer); - variableValueSets ??= new VariableValues[elements.Count]; + variableValueSets ??= new VariableValues[elements.Length]; var key = new ThreeValueNodeTuple(mappedValue1, mappedValue2, mappedValue3); if (nextIndex > 0) { - seen ??= new Dictionary(elements.Count, ThreeValueNodeTupleComparer.Instance) + seen ??= new Dictionary(elements.Length, ThreeValueNodeTupleComparer.Instance) { [new ThreeValueNodeTuple( variableValueSets[0].Values.Fields[0].Value, @@ -1043,7 +1059,7 @@ [new ThreeValueNodeTuple( if (seen.TryGetValue(key, out var existingIndex)) { - additionalPaths ??= new List?[elements.Count]; + additionalPaths ??= new List?[elements.Length]; (additionalPaths[existingIndex] ??= []).Add(result.Path); continue; } @@ -1064,7 +1080,7 @@ [new ThreeValueNodeTuple( } private ImmutableArray BuildVariableValueSetsThreeRequirementsSlowPath( - List elements, + ReadOnlySpan elements, OperationRequirement requirement1, OperationRequirement requirement2, OperationRequirement requirement3, @@ -1080,8 +1096,8 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsSl var value1 = ResultDataMapper.Map(result, requirement1.Map, _schema, ref buffer); if (value1 is null - || value1.Kind == SyntaxKind.NullValue - && requirement1.Type.Kind == SyntaxKind.NonNullType) + || (value1.Kind == SyntaxKind.NullValue + && requirement1.Type.Kind == SyntaxKind.NonNullType)) { continue; } @@ -1089,8 +1105,8 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsSl var value2 = ResultDataMapper.Map(result, requirement2.Map, _schema, ref buffer); if (value2 is null - || value2.Kind == SyntaxKind.NullValue - && requirement2.Type.Kind == SyntaxKind.NonNullType) + || (value2.Kind == SyntaxKind.NullValue + && requirement2.Type.Kind == SyntaxKind.NonNullType)) { continue; } @@ -1098,18 +1114,18 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsSl var value3 = ResultDataMapper.Map(result, requirement3.Map, _schema, ref buffer); if (value3 is null - || value3.Kind == SyntaxKind.NullValue - && requirement3.Type.Kind == SyntaxKind.NonNullType) + || (value3.Kind == SyntaxKind.NullValue + && requirement3.Type.Kind == SyntaxKind.NonNullType)) { continue; } - variableValueSets ??= new VariableValues[elements.Count]; + variableValueSets ??= new VariableValues[elements.Length]; var key = new ThreeValueNodeTuple(value1, value2, value3); if (nextIndex > 0) { - seen ??= new Dictionary(elements.Count, ThreeValueNodeTupleComparer.Instance) + seen ??= new Dictionary(elements.Length, ThreeValueNodeTupleComparer.Instance) { [new ThreeValueNodeTuple( variableValueSets[0].Values.Fields[0].Value, @@ -1119,7 +1135,7 @@ [new ThreeValueNodeTuple( if (seen.TryGetValue(key, out var existingIndex)) { - additionalPaths ??= new List?[elements.Count]; + additionalPaths ??= new List?[elements.Length]; (additionalPaths[existingIndex] ??= []).Add(result.Path); continue; } @@ -1222,7 +1238,8 @@ private static IValueNode MapRequirementLeafValue( private static void AppendUnrolledLists( CompositeResultElement list, - List destination) + ref CompositeResultElement[] destination, + ref int destinationCount) { foreach (var element in list.EnumerateArray()) { @@ -1235,15 +1252,53 @@ private static void AppendUnrolledLists( if (elementValueKind is JsonValueKind.Array) { - AppendUnrolledLists(element, destination); + AppendUnrolledLists(element, ref destination, ref destinationCount); } else { - destination.Add(element); + AddToBuffer(ref destination, ref destinationCount, element); } } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddToBuffer( + ref CompositeResultElement[] buffer, + ref int count, + CompositeResultElement value) + { + if (count == buffer.Length) + { + GrowBuffer(ref buffer, count); + } + + buffer[count++] = value; + } + + private static void GrowBuffer( + ref CompositeResultElement[] buffer, + int count) + { + var newBuffer = ArrayPool.Shared.Rent(buffer.Length * 2); + buffer.AsSpan(0, count).CopyTo(newBuffer); + ArrayPool.Shared.Return(buffer, clearArray: true); + buffer = newBuffer; + } + + private static void EnsureCapacity( + ref CompositeResultElement[] buffer, + int required, + int count) + { + if (required > buffer.Length) + { + var newBuffer = ArrayPool.Shared.Rent(required); + buffer.AsSpan(0, count).CopyTo(newBuffer); + ArrayPool.Shared.Return(buffer, clearArray: true); + buffer = newBuffer; + } + } + public PooledArrayWriter CreateRentedBuffer() { ObjectDisposedException.ThrowIf(_disposed, this); @@ -1375,6 +1430,10 @@ public void Dispose() _disposed = true; + ArrayPool.Shared.Return(_collectTargetA, clearArray: true); + ArrayPool.Shared.Return(_collectTargetB, clearArray: true); + ArrayPool.Shared.Return(_collectTargetCombined, clearArray: true); + while (_memory.TryPop(out var memory)) { memory.Dispose(); diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.Cursor.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.Cursor.cs index 2d183fac77f..35927ff65c3 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.Cursor.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.Cursor.cs @@ -120,8 +120,11 @@ public Cursor AddRows(int delta) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Cursor WithRow(int row) => From(Chunk, row); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int ToIndex() => (Chunk * RowsPerChunk) + Row; + public int Index + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (Chunk * RowsPerChunk) + Row; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int ToTotalBytes() => (Chunk * ChunkBytes) + ByteOffset; diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.cs index b89108b3adb..1112eb3e30c 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.cs @@ -125,6 +125,7 @@ internal Path CreatePath(Cursor current) return Path.Root; } + var cursorIndex = current.Index; Span chain = stackalloc Cursor[64]; var c = current; var written = 0; @@ -169,7 +170,7 @@ internal Path CreatePath(Cursor current) if (parentTokenType is ElementTokenType.StartArray) { // arrayIndex = abs(child) - (abs(parent) + 1) - var absChild = c.Chunk * Cursor.RowsPerChunk + c.Row; + var absChild = (c.Chunk * Cursor.RowsPerChunk) + c.Row; var absParent = parentCursor.Chunk * Cursor.RowsPerChunk + parentCursor.Row; var arrayIndex = absChild - (absParent + 1); path = path.Append(arrayIndex); @@ -392,7 +393,7 @@ internal void AssignCompositeValue(CompositeResultElement target, CompositeResul _metaDb.Replace( cursor: target.Cursor, tokenType: ElementTokenType.Reference, - location: value.Cursor.ToIndex(), + location: value.Cursor.Index, parentRow: _metaDb.GetParent(target.Cursor)); } @@ -451,7 +452,7 @@ internal void AssignNullValue(CompositeResultElement target) private Cursor WriteStartObject(Cursor parent, int selectionSetId = 0) { var flags = ElementFlags.None; - var parentRow = ToIndex(parent); + var parentRow = parent.Index; if (parentRow < 0) { @@ -480,7 +481,7 @@ private void WriteEndObject(Cursor startObjectCursor, int length) private Cursor WriteStartArray(Cursor parent, int length = 0) { var flags = ElementFlags.None; - var parentRow = ToIndex(parent); + var parentRow = parent.Index; if (parentRow < 0) { @@ -521,14 +522,14 @@ private void WriteEmptyProperty(Cursor parent, Selection selection) var prop = _metaDb.Append( ElementTokenType.PropertyName, - parentRow: ToIndex(parent), + parentRow: parent.Index, operationReferenceId: selection.Id, operationReferenceType: OperationReferenceType.Selection, flags: flags); _metaDb.Append( ElementTokenType.None, - parentRow: ToIndex(prop)); + parentRow: prop.Index); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -536,12 +537,9 @@ private void WriteEmptyValue(Cursor parent) { _metaDb.Append( ElementTokenType.None, - parentRow: ToIndex(parent)); + parentRow: parent.Index); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int ToIndex(Cursor c) => (c.Chunk * Cursor.RowsPerChunk) + c.Row; - private static void CheckExpectedType(ElementTokenType expected, ElementTokenType actual) { if (expected != actual) diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap index d33e8a023b2..4a93931c9fa 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap +++ b/src/HotChocolate/Fusion/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 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 159\n at HotChocolate.Fusion.Execution.Nodes.OperationExecutionNode.OnExecuteAsync(OperationPlanContext context, CancellationToken cancellationToken) in OperationExecutionNode.cs:line 159" + "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 292\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 159\n at HotChocolate.Fusion.Execution.Nodes.OperationExecutionNode.OnExecuteAsync(OperationPlanContext context, CancellationToken cancellationToken) in OperationExecutionNode.cs:line 159" }, { "Key": "exception.message", diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/CompositeResultDocumentMetaDbTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/CompositeResultDocumentMetaDbTests.cs index 3a58e295102..a2c39cf6d70 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/CompositeResultDocumentMetaDbTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/CompositeResultDocumentMetaDbTests.cs @@ -13,7 +13,7 @@ public void CreateForEstimatedRows_WithSmallEstimate_CreatesValidMetaDb() using var metaDb = MetaDb.CreateForEstimatedRows(10); // Assert - Assert.Equal(0, metaDb.NextCursor.ToIndex()); + Assert.Equal(0, metaDb.NextCursor.Index); } [Fact] @@ -30,7 +30,7 @@ public void Append_SingleRow_ReturnsCorrectIndex() flags: ElementFlags.None); // Assert - Assert.Equal(0, cursor.ToIndex()); + Assert.Equal(0, cursor.Index); Assert.Equal(20, _metaDb.NextCursor.ToTotalBytes()); } @@ -43,9 +43,9 @@ public void Append_MultipleRows_ReturnsSequentialIndices() var index3 = _metaDb.Append(ElementTokenType.EndObject); // Assert - Assert.Equal(0, index1.ToIndex()); - Assert.Equal(1, index2.ToIndex()); - Assert.Equal(2, index3.ToIndex()); + Assert.Equal(0, index1.Index); + Assert.Equal(1, index2.Index); + Assert.Equal(2, index3.Index); Assert.Equal(60, _metaDb.NextCursor.ToTotalBytes()); }