diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.Pooling.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.Pooling.cs index 289daa1347d..75bc97e6a18 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.Pooling.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.Pooling.cs @@ -1,10 +1,12 @@ using System.Buffers; using System.Diagnostics; +using System.Text.Json; using HotChocolate.Buffers; using HotChocolate.Execution; using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Fusion.Text.Json; using HotChocolate.Language; +using HotChocolate.Text.Json; namespace HotChocolate.Fusion.Execution.Results; @@ -36,6 +38,11 @@ public void Initialize( _deferFlags = deferFlags; _disposed = false; + _variableWriter?.Dispose(); + _variableWriter = new ArenaBufferWriter(arena); + _jsonWriter = new JsonWriter(_variableWriter, new JsonWriterOptions { Indented = false }); + _variableDedupTable.SetWriter(_variableWriter); + _pathPool ??= new PathSegmentLocalPool(pathSegmentLocalPoolCapacity); _result = new CompositeResultDocument(arena, operation, includeFlags, deferFlags, _pathPool); @@ -74,6 +81,11 @@ public void Reset(IMemoryArena arena) _arena = arena; + _variableWriter.Dispose(); + _variableWriter = new ArenaBufferWriter(arena); + _jsonWriter.Reset(_variableWriter); + _variableDedupTable.SetWriter(_variableWriter); + _result = new CompositeResultDocument( _arena, _operation, @@ -116,18 +128,22 @@ internal void Clean(int maxCollectTargetRetainLength, int maxDictionaryRetainCap _errors?.Clear(); _pocketedErrors?.Clear(); - // reset variable writer (returns excess chunks, keeps the first) - _variableWriter.Clean(); + _variableWriter?.Dispose(); + _variableWriter = default!; + _jsonWriter = default!; // clear collect target arrays to unroot CompositeResultDocument references; // if they grew too large during a burst, swap them for smaller ones. TrimOrClearBuffer(ref _collectTargetA, maxCollectTargetRetainLength); TrimOrClearBuffer(ref _collectTargetB, maxCollectTargetRetainLength); TrimOrClearBuffer(ref _collectTargetCombined, maxCollectTargetRetainLength); + TrimOrClearBuffer(ref _collectTargetPathSegments, maxCollectTargetRetainLength); + _collectTargetPathSegmentCount = 0; // clear dictionaries/hashsets; drop oversized ones. TrimOrClear(ref _seenPaths, maxDictionaryRetainCapacity, ReferenceEqualityComparer.Instance); _variableDedupTable.Clear(); + _variableDedupTable.ClearWriter(); // null out per-request references _result = default!; @@ -138,12 +154,12 @@ internal void Clean(int maxCollectTargetRetainLength, int maxDictionaryRetainCap _arena = default!; } - private static void TrimOrClearBuffer(ref CompositeResultElement[] buffer, int maxRetainLength) + private static void TrimOrClearBuffer(ref T[] buffer, int maxRetainLength) { if (buffer.Length > maxRetainLength) { - ArrayPool.Shared.Return(buffer, clearArray: true); - buffer = ArrayPool.Shared.Rent(64); + ArrayPool.Shared.Return(buffer, clearArray: true); + buffer = ArrayPool.Shared.Rent(64); } else { @@ -174,9 +190,7 @@ private static void TrimOrClear( { if (dict.Count > maxRetainCapacity) { - dict = comparer is null - ? new Dictionary() - : new Dictionary(comparer); + dict = comparer is null ? [] : [with(comparer)]; } else { 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 e48c05c4bf1..638a3c82621 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -38,18 +38,20 @@ internal sealed partial class FetchResultStore : IDisposable private readonly object _lock = new(); #endif private readonly List _memory = []; - private readonly ChunkedArrayWriter _variableWriter = new(); - private readonly JsonWriter _jsonWriter; - private readonly VariableDedupTable _variableDedupTable; + private ArenaBufferWriter _variableWriter = default!; + private JsonWriter _jsonWriter = default!; + private readonly VariableDedupTable _variableDedupTable = new(); private ISchemaDefinition _schema = default!; private IErrorHandler _errorHandler = default!; private Operation _operation = default!; private ErrorHandlingMode _errorHandlingMode; private ulong _includeFlags; private ulong _deferFlags; - private CompositeResultElement[] _collectTargetA = ArrayPool.Shared.Rent(64); - private CompositeResultElement[] _collectTargetB = ArrayPool.Shared.Rent(64); - private CompositeResultElement[] _collectTargetCombined = ArrayPool.Shared.Rent(64); + private CollectedTargetElement[] _collectTargetA = ArrayPool.Shared.Rent(64); + private CollectedTargetElement[] _collectTargetB = ArrayPool.Shared.Rent(64); + private CollectedTargetElement[] _collectTargetCombined = ArrayPool.Shared.Rent(64); + private CollectedPathSegment[] _collectTargetPathSegments = ArrayPool.Shared.Rent(64); + private int _collectTargetPathSegmentCount; private PathSegmentLocalPool _pathPool = default!; private IMemoryArena _arena = default!; private HashSet _seenPaths = new(ReferenceEqualityComparer.Instance); @@ -59,11 +61,7 @@ internal sealed partial class FetchResultStore : IDisposable private Dictionary? _pocketedErrors; private bool _disposed; - internal FetchResultStore() - { - _jsonWriter = new JsonWriter(_variableWriter, new JsonWriterOptions { Indented = false }); - _variableDedupTable = new VariableDedupTable(_variableWriter); - } + internal FetchResultStore() { } public CompositeResultDocument Result => _result; @@ -686,6 +684,8 @@ public ImmutableArray CreateVariableValueSets( lock (_lock) { + _collectTargetPathSegmentCount = 0; + var elements = CollectTargetElements(selectionSet); if (elements.IsEmpty) @@ -720,6 +720,8 @@ public ImmutableArray CreateVariableValueSets( lock (_lock) { + _collectTargetPathSegmentCount = 0; + var combinedCount = 0; foreach (var selectionSet in selectionSets) @@ -787,29 +789,35 @@ internal ImmutableArray CreateVariableValueSetsFromSnapshot( } // Caller must hold _lock for reading. - private ReadOnlySpan CollectTargetElements(SelectionPath selectionSet) + private ReadOnlySpan CollectTargetElements(SelectionPath selectionSet) { var current = _collectTargetA; var currentCount = 0; var next = _collectTargetB; var nextCount = 0; - current[currentCount++] = _result.Data; + current[currentCount++] = new CollectedTargetElement(_result.Data, pathSegmentIndex: -1); for (var i = 0; i < selectionSet.Length; i++) { var segment = selectionSet[i]; + if (segment.Kind is SelectionPathSegmentKind.Root) + { + continue; + } + if (segment.Kind is SelectionPathSegmentKind.InlineFragment) { for (var j = 0; j < currentCount; j++) { - var element = current[j]; + var target = current[j]; + var element = target.Element; if (element.TryGetProperty(IntrospectionFieldNames.TypeNameSpan, out var value) && value.ValueKind is JsonValueKind.String && value.TextEqualsHelper(segment.Name, isPropertyName: false)) { - AddToBuffer(ref next, ref nextCount, element); + AddToBuffer(ref next, ref nextCount, target); } } } @@ -817,7 +825,8 @@ private ReadOnlySpan CollectTargetElements(SelectionPath { for (var j = 0; j < currentCount; j++) { - var element = current[j]; + var target = current[j]; + var element = target.Element; if (!element.TryGetProperty(segment.Name, out var value)) { continue; @@ -832,13 +841,18 @@ private ReadOnlySpan CollectTargetElements(SelectionPath if (valueKind is JsonValueKind.Array) { - AppendUnrolledLists(value, ref next, ref nextCount); + var fieldPath = AppendFieldPath(target.PathSegmentIndex, value); + AppendUnrolledLists(value, fieldPath, ref next, ref nextCount); continue; } if (valueKind is JsonValueKind.Object) { - AddToBuffer(ref next, ref nextCount, value); + var fieldPath = AppendFieldPath(target.PathSegmentIndex, value); + AddToBuffer( + ref next, + ref nextCount, + new CollectedTargetElement(value, fieldPath)); continue; } @@ -926,7 +940,7 @@ private ImmutableArray BuildVariableValueSetsFromSnapshot( } private ImmutableArray BuildVariableValueSets( - ReadOnlySpan elements, + ReadOnlySpan elements, IReadOnlyList requestVariables, ReadOnlySpan requiredData) { @@ -972,8 +986,9 @@ private ImmutableArray BuildVariableValueSets( var additionalPaths = new AdditionalPathAccumulator(); var nextIndex = 0; - foreach (var result in elements) + foreach (var target in elements) { + var result = target.Element; variableValueSets ??= s_variableValuePool.Rent(elements.Length); _jsonWriter.Reset(_variableWriter); @@ -1011,7 +1026,7 @@ private ImmutableArray BuildVariableValueSets( _jsonWriter.WriteEndObject(); var entry = TryCreateVariableValues( - result.CompactPath, startPosition, ref additionalPaths, nextIndex); + target, startPosition, ref additionalPaths, nextIndex); if (entry is null) { @@ -1034,7 +1049,7 @@ private ImmutableArray BuildVariableValueSets( } private ImmutableArray BuildVariableValueSetsSingleRequirement( - ReadOnlySpan elements, + ReadOnlySpan elements, OperationRequirement requirement) { if (TryGetSimpleRequirementFieldName(requirement.Map, out var fieldName)) @@ -1046,7 +1061,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirement( } private ImmutableArray BuildVariableValueSetsSingleRequirementFastPath( - ReadOnlySpan elements, + ReadOnlySpan elements, OperationRequirement requirement, string fieldName) { @@ -1057,7 +1072,8 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa for (var i = 0; i < elements.Length; i++) { - var result = elements[i]; + var target = elements[i]; + var result = target.Element; if (!result.TryGetProperty(fieldName, out var value)) { @@ -1089,7 +1105,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa // we try to create a VariableValues object, // if that fails the variables already were created and we move on. - var entry = TryCreateVariableValues(result.CompactPath, startPosition, ref additionalPaths, nextIndex); + var entry = TryCreateVariableValues(target, startPosition, ref additionalPaths, nextIndex); if (entry is null) { @@ -1104,15 +1120,16 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa } private ImmutableArray BuildVariableValueSetsSingleRequirementSlowPath( - ReadOnlySpan elements, + ReadOnlySpan elements, OperationRequirement requirement) { VariableValues[]? variableValueSets = null; var additionalPaths = new AdditionalPathAccumulator(); var nextIndex = 0; - foreach (var result in elements) + foreach (var target in elements) { + var result = target.Element; variableValueSets ??= s_variableValuePool.Rent(elements.Length); _jsonWriter.Reset(_variableWriter); @@ -1129,7 +1146,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementSl _jsonWriter.WriteEndObject(); var entry = TryCreateVariableValues( - result.CompactPath, startPosition, ref additionalPaths, nextIndex); + target, startPosition, ref additionalPaths, nextIndex); if (entry is null) { @@ -1144,7 +1161,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementSl } private ImmutableArray BuildVariableValueSetsTwoRequirements( - ReadOnlySpan elements, + ReadOnlySpan elements, OperationRequirement requirement1, OperationRequirement requirement2) { @@ -1166,7 +1183,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirements( } private ImmutableArray BuildVariableValueSetsTwoRequirementsFastPath( - ReadOnlySpan elements, + ReadOnlySpan elements, OperationRequirement requirement1, string fieldName1, OperationRequirement requirement2, @@ -1176,8 +1193,9 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast var additionalPaths = new AdditionalPathAccumulator(); var nextIndex = 0; - foreach (var result in elements) + foreach (var target in elements) { + var result = target.Element; if (!result.TryGetProperty(fieldName1, out var value1) || value1.ValueKind is JsonValueKind.Undefined || (value1.ValueKind is JsonValueKind.Null @@ -1206,7 +1224,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast _jsonWriter.WriteEndObject(); var entry = TryCreateVariableValues( - result.CompactPath, startPosition, ref additionalPaths, nextIndex); + target, startPosition, ref additionalPaths, nextIndex); if (entry is null) { @@ -1221,7 +1239,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast } private ImmutableArray BuildVariableValueSetsTwoRequirementsSlowPath( - ReadOnlySpan elements, + ReadOnlySpan elements, OperationRequirement requirement1, OperationRequirement requirement2) { @@ -1229,8 +1247,9 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsSlow var additionalPaths = new AdditionalPathAccumulator(); var nextIndex = 0; - foreach (var result in elements) + foreach (var target in elements) { + var result = target.Element; variableValueSets ??= s_variableValuePool.Rent(elements.Length); _jsonWriter.Reset(_variableWriter); @@ -1255,7 +1274,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsSlow _jsonWriter.WriteEndObject(); - var entry = TryCreateVariableValues(result.CompactPath, startPosition, ref additionalPaths, nextIndex); + var entry = TryCreateVariableValues(target, startPosition, ref additionalPaths, nextIndex); if (entry is null) { @@ -1270,7 +1289,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsSlow } private ImmutableArray BuildVariableValueSetsThreeRequirements( - ReadOnlySpan elements, + ReadOnlySpan elements, OperationRequirement requirement1, OperationRequirement requirement2, OperationRequirement requirement3) @@ -1297,7 +1316,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirements( } private ImmutableArray BuildVariableValueSetsThreeRequirementsFastPath( - ReadOnlySpan elements, + ReadOnlySpan elements, OperationRequirement requirement1, string fieldName1, OperationRequirement requirement2, @@ -1309,8 +1328,9 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsFa var additionalPaths = new AdditionalPathAccumulator(); var nextIndex = 0; - foreach (var result in elements) + foreach (var target in elements) { + var result = target.Element; if (!result.TryGetProperty(fieldName1, out var value1) || value1.ValueKind is JsonValueKind.Undefined || (value1.ValueKind is JsonValueKind.Null @@ -1348,7 +1368,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsFa WriteCompositeResultValue(value3); _jsonWriter.WriteEndObject(); - var entry = TryCreateVariableValues(result.CompactPath, startPosition, ref additionalPaths, nextIndex); + var entry = TryCreateVariableValues(target, startPosition, ref additionalPaths, nextIndex); if (entry is null) { @@ -1363,7 +1383,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsFa } private ImmutableArray BuildVariableValueSetsThreeRequirementsSlowPath( - ReadOnlySpan elements, + ReadOnlySpan elements, OperationRequirement requirement1, OperationRequirement requirement2, OperationRequirement requirement3) @@ -1372,8 +1392,9 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsSl var additionalPaths = new AdditionalPathAccumulator(); var nextIndex = 0; - foreach (var result in elements) + foreach (var target in elements) { + var result = target.Element; variableValueSets ??= s_variableValuePool.Rent(elements.Length); _jsonWriter.Reset(_variableWriter); @@ -1406,7 +1427,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsSl _jsonWriter.WriteEndObject(); - var entry = TryCreateVariableValues(result.CompactPath, startPosition, ref additionalPaths, nextIndex); + var entry = TryCreateVariableValues(target, startPosition, ref additionalPaths, nextIndex); if (entry is null) { @@ -1442,6 +1463,17 @@ private static bool TryGetSimpleRequirementFieldName( return false; } + private VariableValues? TryCreateVariableValues( + CollectedTargetElement target, + int startPosition, + ref AdditionalPathAccumulator additionalPaths, + int nextIndex) + => TryCreateVariableValues( + CreateCollectedCompactPath(target.PathSegmentIndex), + startPosition, + ref additionalPaths, + nextIndex); + private VariableValues? TryCreateVariableValues( CompactPath path, int startPosition, @@ -1729,36 +1761,125 @@ private static CompactPathSegment ImportAdditionalPaths(CompactPathSegment sourc return new CompactPathSegment(copy, 0, copy.Length); } - private static void AppendUnrolledLists( + private int AppendFieldPath( + int parentPathSegmentIndex, + CompositeResultElement fieldValue) + { + var selection = fieldValue.Selection + ?? throw new InvalidOperationException( + "Cannot collect a target path for a field without selection metadata."); + + return AppendCollectedPathSegment(parentPathSegmentIndex, selection.Id); + } + + private int AppendCollectedPathSegment(int parentPathSegmentIndex, int segment) + { + EnsureCapacity( + ref _collectTargetPathSegments, + _collectTargetPathSegmentCount + 1, + _collectTargetPathSegmentCount); + + var index = _collectTargetPathSegmentCount++; + _collectTargetPathSegments[index] = new CollectedPathSegment(parentPathSegmentIndex, segment); + return index; + } + + private CompactPath CreateCollectedCompactPath(int pathSegmentIndex) + { + if (pathSegmentIndex < 0) + { + return CompactPath.Root; + } + + Span segments = stackalloc int[32]; + int[]? rented = null; + var count = 0; + + try + { + var current = pathSegmentIndex; + + while (current >= 0) + { + if (count == segments.Length) + { + var newSegments = ArrayPool.Shared.Rent(segments.Length * 2); + segments[..count].CopyTo(newSegments); + + if (rented is not null) + { + ArrayPool.Shared.Return(rented); + } + + rented = newSegments; + segments = newSegments; + } + + var node = _collectTargetPathSegments[current]; + segments[count++] = node.Segment; + current = node.ParentIndex; + } + + Span buffer = stackalloc int[32]; + var builder = new CompactPathBuilder(buffer, _pathPool); + + for (var i = count - 1; i >= 0; i--) + { + builder.Append(segments[i]); + } + + return builder.ToPath(); + } + finally + { + if (rented is not null) + { + ArrayPool.Shared.Return(rented); + } + } + } + + private void AppendUnrolledLists( CompositeResultElement list, - ref CompositeResultElement[] destination, + int listPathSegmentIndex, + ref CollectedTargetElement[] destination, ref int destinationCount) { + var index = 0; + foreach (var element in list.EnumerateArray()) { var elementValueKind = element.ValueKind; if (elementValueKind is JsonValueKind.Null or JsonValueKind.Undefined) { + index++; continue; } + var elementPathSegmentIndex = AppendCollectedPathSegment(listPathSegmentIndex, ~index); + if (elementValueKind is JsonValueKind.Array) { - AppendUnrolledLists(element, ref destination, ref destinationCount); - } - else - { - AddToBuffer(ref destination, ref destinationCount, element); + AppendUnrolledLists(element, elementPathSegmentIndex, ref destination, ref destinationCount); + index++; + continue; } + + AddToBuffer( + ref destination, + ref destinationCount, + new CollectedTargetElement(element, elementPathSegmentIndex)); + + index++; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void AddToBuffer( - ref CompositeResultElement[] buffer, + ref CollectedTargetElement[] buffer, ref int count, - CompositeResultElement value) + CollectedTargetElement value) { if (count == buffer.Length) { @@ -1769,25 +1890,25 @@ private static void AddToBuffer( } private static void GrowBuffer( - ref CompositeResultElement[] buffer, + ref CollectedTargetElement[] buffer, int count) { - var newBuffer = ArrayPool.Shared.Rent(buffer.Length * 2); + var newBuffer = ArrayPool.Shared.Rent(buffer.Length * 2); buffer.AsSpan(0, count).CopyTo(newBuffer); - ArrayPool.Shared.Return(buffer, clearArray: true); + ArrayPool.Shared.Return(buffer, clearArray: true); buffer = newBuffer; } - private static void EnsureCapacity( - ref CompositeResultElement[] buffer, + private static void EnsureCapacity( + ref T[] buffer, int required, int count) { if (required > buffer.Length) { - var newBuffer = ArrayPool.Shared.Rent(required); + var newBuffer = ArrayPool.Shared.Rent(required); buffer.AsSpan(0, count).CopyTo(newBuffer); - ArrayPool.Shared.Return(buffer, clearArray: true); + ArrayPool.Shared.Return(buffer, clearArray: true); buffer = newBuffer; } } @@ -2132,9 +2253,10 @@ public void Dispose() _disposed = true; - ArrayPool.Shared.Return(_collectTargetA, clearArray: true); - ArrayPool.Shared.Return(_collectTargetB, clearArray: true); - ArrayPool.Shared.Return(_collectTargetCombined, clearArray: true); + ArrayPool.Shared.Return(_collectTargetA, clearArray: true); + ArrayPool.Shared.Return(_collectTargetB, clearArray: true); + ArrayPool.Shared.Return(_collectTargetCombined, clearArray: true); + ArrayPool.Shared.Return(_collectTargetPathSegments, clearArray: true); foreach (var memory in _memory) { @@ -2143,7 +2265,8 @@ public void Dispose() _memory.Clear(); - _variableWriter.Dispose(); + _variableWriter?.Dispose(); + _variableDedupTable.Dispose(); _pathPool?.Dispose(); } @@ -2210,16 +2333,44 @@ private static ImmutableArray FinalizeVariableValueSets( return ImmutableCollectionsMarshal.AsImmutableArray(result); } - private sealed class VariableDedupTable(ChunkedArrayWriter writer) : IDisposable + private readonly struct CollectedTargetElement( + CompositeResultElement element, + int pathSegmentIndex) + { + public CompositeResultElement Element { get; } = element; + + public int PathSegmentIndex { get; } = pathSegmentIndex; + } + + private readonly struct CollectedPathSegment( + int parentIndex, + int segment) + { + public int ParentIndex { get; } = parentIndex; + + public int Segment { get; } = segment; + } + + private sealed class VariableDedupTable : IDisposable { private const int DefaultBucketSize = 4; private const int DefaultBucketCount = 16; - private readonly ChunkedArrayWriter _writer = writer; + private ArenaBufferWriter _writer = default!; private Entry[] _table = ArrayPool.Shared.Rent(DefaultBucketCount * DefaultBucketSize); private int _bucketCount = DefaultBucketCount; private readonly int _bucketSize = DefaultBucketSize; + public void SetWriter(ArenaBufferWriter writer) + { + _writer = writer; + } + + public void ClearWriter() + { + _writer = default!; + } + public void Initialize(int capacity) { _bucketCount = NextPowerOfTwo(Math.Max(capacity, DefaultBucketCount)); @@ -2298,6 +2449,7 @@ public void Dispose() { ArrayPool.Shared.Return(_table); _table = []; + _writer = default!; } private void Grow() diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/ValueCompletion.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/ValueCompletion.cs index 52fa7593a42..d892a4822cb 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/ValueCompletion.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/ValueCompletion.cs @@ -78,9 +78,19 @@ public bool BuildResult( InitializeTargetObject(source, target); } + var hasObjectScope = target.TryGetObjectScope(out var objectScope); + foreach (var property in source.EnumerateObject()) { - if (!target.TryGetProperty(property.NameSpan, out var resultField)) + CompositeResultElement resultField; + if (hasObjectScope) + { + if (!objectScope.TryGetProperty(property.NameSpan, out resultField)) + { + continue; + } + } + else if (!target.TryGetProperty(property.NameSpan, out resultField)) { continue; } @@ -812,9 +822,19 @@ private bool TryCompleteObjectValue( target.SetObjectValue(objectSelectionSet); } + var hasObjectScope = target.TryGetObjectScope(out var objectScope); + foreach (var property in source.EnumerateObject()) { - if (!target.TryGetProperty(property.NameSpan, out var targetProperty)) + CompositeResultElement targetProperty; + if (hasObjectScope) + { + if (!objectScope.TryGetProperty(property.NameSpan, out targetProperty)) + { + continue; + } + } + else if (!target.TryGetProperty(property.NameSpan, out targetProperty)) { continue; } diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/JsonSegment.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/JsonSegment.cs index 71a10beb5bd..5b34ae61bdf 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/JsonSegment.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/JsonSegment.cs @@ -1,16 +1,17 @@ using System.Buffers; using HotChocolate.Buffers; +using HotChocolate.Fusion.Text.Json; using HotChocolate.Text.Json; namespace HotChocolate.Fusion; public readonly struct JsonSegment : IEquatable { - private readonly ChunkedArrayWriter _memory; + private readonly IJsonSegmentSource? _memory; private readonly int _location; private readonly int _length; - private JsonSegment(ChunkedArrayWriter memory, int location, int length) + private JsonSegment(IJsonSegmentSource memory, int location, int length) { _memory = memory; _location = location; @@ -32,21 +33,21 @@ public ReadOnlySequence AsSequence() var start = _location; var length = _length; - var first = _memory.Read(ref start, ref length); + var first = Read(ref start, ref length); if (length == 0) { - // Single chunk — common case, no allocation for segment chain. + // Single chunk, common case with no allocation for segment chain. return new ReadOnlySequence(first.ToArray()); } - // Multi-chunk — build a ReadOnlySequence from linked segments. + // Multi-chunk, build a ReadOnlySequence from linked segments. var firstSegment = new MemorySegment(first.ToArray()); var lastSegment = firstSegment; do { - lastSegment = lastSegment.Append(_memory.Read(ref start, ref length)); + lastSegment = lastSegment.Append(Read(ref start, ref length)); } while (length > 0); @@ -62,21 +63,21 @@ public void WriteTo(JsonWriter writer) var start = _location; var length = _length; - var first = _memory.Read(ref start, ref length); + var first = Read(ref start, ref length); if (length == 0) { - // Single chunk — common case. + // Single chunk, common case. writer.WriteRawValue(first); return; } - // Multi-chunk — write start, continuations, then set separator flag. + // Multi-chunk, write start, continuations, then set separator flag. writer.WriteRawValueStart(first); do { - writer.WriteRawValueContinuation(_memory.Read(ref start, ref length)); + writer.WriteRawValueContinuation(Read(ref start, ref length)); } while (length > 0); @@ -90,25 +91,34 @@ public bool Equals(JsonSegment other) return other.IsEmpty; } - if (other.IsEmpty || !ReferenceEquals(_memory, other._memory)) + if (other.IsEmpty) { return false; } - return _memory.SequenceEqual(_location, other._location, _length); + return ReferenceEquals(_memory, other._memory) + && _memory!.SequenceEqual(_location, other._location, _length); } public override bool Equals(object? obj) => obj is JsonSegment other && Equals(other); public override int GetHashCode() - => IsEmpty ? 0 : _memory.GetHashCode(_location, _length); + => IsEmpty + ? 0 + : _memory!.GetHashCode(_location, _length); public static JsonSegment Empty => default; internal static JsonSegment Create(ChunkedArrayWriter memory, int location, int length) => new(memory, location, length); + internal static JsonSegment Create(ArenaBufferWriter memory, int location, int length) + => new(memory, location, length); + + private ReadOnlySpan Read(ref int start, ref int length) + => _memory!.Read(ref start, ref length); + private sealed class MemorySegment : ReadOnlySequenceSegment { public MemorySegment(ReadOnlyMemory memory) diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/ArenaBufferWriter.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/ArenaBufferWriter.cs index f4f9f069330..3be4275d9db 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/ArenaBufferWriter.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/ArenaBufferWriter.cs @@ -1,5 +1,9 @@ using System.Buffers; using System.Runtime.CompilerServices; +#if NET8_0_OR_GREATER +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +#endif using HotChocolate.Buffers; namespace HotChocolate.Fusion.Text.Json; @@ -11,14 +15,20 @@ namespace HotChocolate.Fusion.Text.Json; /// without /// any further copy. /// -internal sealed class ArenaBufferWriter : IBufferWriter, IDisposable +internal sealed class ArenaBufferWriter : IBufferWriter, IJsonSegmentSource, IDisposable { + private const int DataOffsetBits = 17; + private const int DataOffsetMask = (1 << DataOffsetBits) - 1; + private const int DefaultScratchSize = 128; + private const int SimdThreshold = 64; + private readonly IMemoryArena _arena; private MemorySegment[] _segments; private int _usedChunks; private int _currentChunk; private int _currentChunkBytes; private int _currentChunkOffset; + private int _position; private byte[] _currentBuffer; private int _currentBase; private byte[] _scratch = []; @@ -39,6 +49,16 @@ public ArenaBufferWriter(IMemoryArena arena) _currentBase = segment.Offset; } + /// + /// Gets the current gap-free write position. + /// + public int Position => _position; + + /// + /// Gets the total number of bytes written. + /// + public int Length => _position; + /// /// Gets the table of filled segments. Every segment except the last is full; the last holds /// bytes. @@ -58,6 +78,7 @@ public ArenaBufferWriter(IMemoryArena arena) /// public Span GetSpan(int sizeHint = 0) { + var size = sizeHint < 1 ? DefaultScratchSize : sizeHint; var remaining = _currentChunkBytes - _currentChunkOffset; _advanceFromScratch = false; @@ -69,26 +90,27 @@ public Span GetSpan(int sizeHint = 0) // The hint of zero asks for at least one byte; the current chunk always has room because a // full chunk rolled over above. - if (sizeHint <= remaining) + if (size <= remaining) { return _currentBuffer.AsSpan(_currentBase + _currentChunkOffset, remaining); } // The requested size exceeds the remaining space in this chunk. Hand out a scratch buffer // and copy it into the segments gap-free on Advance, so the written layout never has holes. - if (sizeHint > _scratch.Length) + if (size > _scratch.Length) { ReturnScratch(); - _scratch = ArrayPool.Shared.Rent(sizeHint); + _scratch = ArrayPool.Shared.Rent(size); } _advanceFromScratch = true; - return _scratch.AsSpan(0, sizeHint); + return _scratch.AsSpan(0, size); } /// public Memory GetMemory(int sizeHint = 0) { + var size = sizeHint < 1 ? DefaultScratchSize : sizeHint; var remaining = _currentChunkBytes - _currentChunkOffset; _advanceFromScratch = false; @@ -98,19 +120,19 @@ public Memory GetMemory(int sizeHint = 0) remaining = _currentChunkBytes; } - if (sizeHint <= remaining) + if (size <= remaining) { return _currentBuffer.AsMemory(_currentBase + _currentChunkOffset, remaining); } - if (sizeHint > _scratch.Length) + if (size > _scratch.Length) { ReturnScratch(); - _scratch = ArrayPool.Shared.Rent(sizeHint); + _scratch = ArrayPool.Shared.Rent(size); } _advanceFromScratch = true; - return _scratch.AsMemory(0, sizeHint); + return _scratch.AsMemory(0, size); } /// @@ -119,10 +141,12 @@ public void Advance(int count) if (!_advanceFromScratch) { _currentChunkOffset += count; + _position += count; return; } _advanceFromScratch = false; + var advanced = count; var source = _scratch.AsSpan(0, count); while (source.Length > 0) @@ -140,6 +164,276 @@ public void Advance(int count) _currentChunkOffset += take; source = source[take..]; } + + _position += advanced; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan Read(ref int start, ref int length) + { + GetChunkOffset(start, out var chunkIndex, out var offsetInChunk); + var available = SourceResultDocument.GetDataChunkSize(chunkIndex) - offsetInChunk; + + if (available >= length) + { + var span = SegmentSpan(chunkIndex, offsetInChunk, length); + length = 0; + return span; + } + + start += available; + length -= available; + return SegmentSpan(chunkIndex, offsetInChunk, available); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool SequenceEqual(int locationA, int locationB, int length) + { + if (locationA == locationB || length == 0) + { + return true; + } + + GetChunkOffset(locationA, out var chunkA, out var offsetA); + GetChunkOffset(locationB, out var chunkB, out var offsetB); + + var availA = SourceResultDocument.GetDataChunkSize(chunkA) - offsetA; + var availB = SourceResultDocument.GetDataChunkSize(chunkB) - offsetB; + + if (availA >= length && availB >= length) + { + return SegmentSpan(chunkA, offsetA, length).SequenceEqual( + SegmentSpan(chunkB, offsetB, length)); + } + + return SequenceEqualMultiChunk(chunkA, offsetA, chunkB, offsetB, length); + } + + private bool SequenceEqualMultiChunk( + int chunkA, + int offsetA, + int chunkB, + int offsetB, + int remaining) + { + while (remaining > 0) + { + var availA = SourceResultDocument.GetDataChunkSize(chunkA) - offsetA; + var availB = SourceResultDocument.GetDataChunkSize(chunkB) - offsetB; + var toCompare = Math.Min(remaining, Math.Min(availA, availB)); + + if (!SegmentSpan(chunkA, offsetA, toCompare).SequenceEqual( + SegmentSpan(chunkB, offsetB, toCompare))) + { + return false; + } + + remaining -= toCompare; + offsetA += toCompare; + offsetB += toCompare; + + if (offsetA >= SourceResultDocument.GetDataChunkSize(chunkA)) + { + chunkA++; + offsetA = 0; + } + + if (offsetB >= SourceResultDocument.GetDataChunkSize(chunkB)) + { + chunkB++; + offsetB = 0; + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetHashCode(int location, int length) + { + if (length == 0) + { + return 0; + } + + GetChunkOffset(location, out var chunkIndex, out var offsetInChunk); + var availableInChunk = SourceResultDocument.GetDataChunkSize(chunkIndex) - offsetInChunk; + + if (availableInChunk >= length) + { + return (int)(ComputeHashCore(0u, SegmentSpan(chunkIndex, offsetInChunk, length)) & 0x7FFFFFFF); + } + + var hash = 0u; + + while (length > 0) + { + var toHash = Math.Min(length, SourceResultDocument.GetDataChunkSize(chunkIndex) - offsetInChunk); + hash = ComputeHashCore(hash, SegmentSpan(chunkIndex, offsetInChunk, toHash)); + length -= toHash; + chunkIndex++; + offsetInChunk = 0; + } + + return (int)(hash & 0x7FFFFFFF); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ResetTo(int position) + { + GetChunkOffset(position, out _currentChunk, out _currentChunkOffset); + var segment = _segments[_currentChunk]; + _currentBuffer = segment.Buffer; + _currentBase = segment.Offset; + _currentChunkBytes = segment.Length; + _position = position; + _advanceFromScratch = false; + } + + public void Reset() + { + ResetTo(0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint ComputeHashCore(uint hash, ReadOnlySpan bytes) + { +#if NET8_0_OR_GREATER + if (bytes.Length >= SimdThreshold) + { + return ComputeHashSimd(hash, bytes); + } +#endif + unchecked + { + foreach (var b in bytes) + { + hash = (hash * 31) + b; + } + } + + return hash; + } + +#if NET8_0_OR_GREATER + private static uint ComputeHashSimd(uint hash, ReadOnlySpan bytes) + { + unchecked + { + const uint pow31_1 = 31; + const uint pow31_2 = 31 * 31; + const uint pow31_3 = 31 * 31 * 31; + const uint pow31_4 = 31 * 31 * 31 * 31; + const uint pow31_5 = pow31_4 * 31; + const uint pow31_6 = pow31_5 * 31; + const uint pow31_7 = pow31_6 * 31; + const uint pow31_8 = pow31_7 * 31; + + ref var src = ref MemoryMarshal.GetReference(bytes); + var i = 0; + + if (Vector256.IsHardwareAccelerated && bytes.Length >= 64) + { + var acc = Vector256.Zero; + var mul = Vector256.Create(pow31_8); + var simdEnd = bytes.Length & ~7; + + for (; i < simdEnd; i += 8) + { + var raw = Vector128.CreateScalarUnsafe( + Unsafe.ReadUnaligned(ref Unsafe.Add(ref src, i))).AsByte(); + var (loShort, _) = Vector128.Widen(raw); + var (lo32, hi32) = Vector128.Widen(loShort); + var wide = Vector256.Create(lo32, hi32); + + acc = (acc * mul) + wide; + } + + var finalPow = Vector256.Create( + pow31_7, pow31_6, pow31_5, pow31_4, + pow31_3, pow31_2, pow31_1, 1u); + acc *= finalPow; + + var sum128 = acc.GetLower() + acc.GetUpper(); + var t = sum128 + Vector128.Shuffle(sum128, Vector128.Create(2u, 3u, 0u, 1u)); + var simdResult = (t + Vector128.Shuffle(t, Vector128.Create(1u, 0u, 3u, 2u))) + .ToScalar(); + + hash = (hash * Pow31(simdEnd)) + simdResult; + } + + if (Vector128.IsHardwareAccelerated && bytes.Length - i >= 4) + { + var acc = Vector128.Zero; + var mul = Vector128.Create(pow31_4); + var simdEnd = i + ((bytes.Length - i) & ~3); + var simdStart = i; + + for (; i < simdEnd; i += 4) + { + var raw = Vector128.CreateScalarUnsafe( + Unsafe.ReadUnaligned(ref Unsafe.Add(ref src, i))).AsByte(); + var (loShort, _) = Vector128.Widen(raw); + var (wide, _) = Vector128.Widen(loShort); + + acc = (acc * mul) + wide; + } + + var finalPow = Vector128.Create(pow31_3, pow31_2, pow31_1, 1u); + acc *= finalPow; + + var t = acc + Vector128.Shuffle(acc, Vector128.Create(2u, 3u, 0u, 1u)); + var simdResult = (t + Vector128.Shuffle(t, Vector128.Create(1u, 0u, 3u, 2u))) + .ToScalar(); + + hash = (hash * Pow31(simdEnd - simdStart)) + simdResult; + } + + for (; i < bytes.Length; i++) + { + hash = (hash * 31) + Unsafe.Add(ref src, i); + } + + return hash; + } + } + + private static uint Pow31(int n) + { + unchecked + { + var result = 1u; + var b = 31u; + + while (n > 0) + { + if ((n & 1) != 0) + { + result *= b; + } + + b *= b; + n >>= 1; + } + + return result; + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ReadOnlySpan SegmentSpan(int chunkIndex, int offsetInChunk, int length) + { + var segment = _segments[chunkIndex]; + return segment.Buffer.AsSpan(segment.Offset + offsetInChunk, length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void GetChunkOffset(int position, out int chunkIndex, out int offsetInChunk) + { + var packed = SourceResultDocument.LinearToPacked(position); + chunkIndex = packed >>> DataOffsetBits; + offsetInChunk = packed & DataOffsetMask; } public void Dispose() diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.TryGetProperty.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.TryGetProperty.cs index 7c926b07f8c..dd9f13c451f 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.TryGetProperty.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultDocument.TryGetProperty.cs @@ -1,5 +1,7 @@ using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; +using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Text.Json; namespace HotChocolate.Fusion.Text.Json; @@ -162,6 +164,28 @@ internal bool TryGetNamedPropertyValue( out value); } + internal bool TryGetObjectScope( + Cursor startCursor, + out CompositeResultObjectScope scope) + { + ObjectDisposedException.ThrowIf(_disposed != 0, this); + + var row = _metaDb.GetValue(ref startCursor); + CheckExpectedType(ElementTokenType.StartObject, row.TokenType); + + if (row.OperationReferenceType is OperationReferenceType.SelectionSet) + { + var selectionSetId = row.OperationReferenceId; + var selectionSet = _operation.GetSelectionSetById(selectionSetId); + + scope = new CompositeResultObjectScope(this, startCursor, selectionSet, selectionSetId); + return true; + } + + scope = default; + return false; + } + private bool TryGetNamedPropertyValue( Cursor startCursor, Cursor endCursor, @@ -274,3 +298,40 @@ internal Cursor GetEndCursor(Cursor cursor) return cursor + _metaDb.GetNumberOfRows(cursor); } } + +internal readonly ref struct CompositeResultObjectScope +{ + private readonly CompositeResultDocument _document; + private readonly CompositeResultDocument.Cursor _startObject; + private readonly SelectionSet _selectionSet; + private readonly int _selectionSetId; + + internal CompositeResultObjectScope( + CompositeResultDocument document, + CompositeResultDocument.Cursor startObject, + SelectionSet selectionSet, + int selectionSetId) + { + _document = document; + _startObject = startObject; + _selectionSet = selectionSet; + _selectionSetId = selectionSetId; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetProperty( + ReadOnlySpan name, + out CompositeResultElement value) + { + if (_selectionSet.TryGetSelection(name, out var selection)) + { + var propertyIndex = selection.Id - _selectionSetId - 1; + var cursor = _startObject + (propertyIndex * 2) + 2; + value = new CompositeResultElement(_document, cursor); + return true; + } + + value = default; + return false; + } +} diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultElement.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultElement.cs index d4ce0195afe..d0db6fd6c33 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultElement.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/CompositeResultElement.cs @@ -432,6 +432,13 @@ public bool TryGetProperty(ReadOnlySpan utf8PropertyName, out CompositeRes return _parent.TryGetNamedPropertyValue(_cursor, utf8PropertyName, out value); } + internal bool TryGetObjectScope(out CompositeResultObjectScope scope) + { + CheckValidInstance(); + + return _parent.TryGetObjectScope(_cursor, out scope); + } + internal CompositeResultElement GetPropertyBySelectionId(int selectionId) { CheckValidInstance(); diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/OperationPlanContextRoutingTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/OperationPlanContextRoutingTests.cs index 38f5e9ba9cd..d1de3520e7a 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/OperationPlanContextRoutingTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/OperationPlanContextRoutingTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using HotChocolate.Buffers; using HotChocolate.Execution; +using HotChocolate.Execution.Errors; using HotChocolate.Fusion.Execution.Clients; using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Fusion.Execution.Results; @@ -294,6 +295,7 @@ private sealed class RoutingTestFixture : IAsyncDisposable private readonly IVariableValueCollection _variables; private readonly List _rentedContexts = []; private readonly List _stores = []; + private readonly List _arenas = []; private readonly List _ctsList = []; private readonly List<(ObjectPool Pool, PooledRequestContext Context)> _requestContexts = []; @@ -400,7 +402,18 @@ public void SetRequirements( params ObjectFieldNode[] fields) { var sourceStore = new FetchResultStore(); + var sourceArena = new MemoryArena(); + sourceStore.Initialize( + sourceArena, + _executor.Schema, + DefaultErrorHandler.Default, + _operationPlan.Operation, + ErrorHandlingMode.Propagate, + includeFlags: 0, + deferFlags: 0, + pathSegmentLocalPoolCapacity: 16); _stores.Add(sourceStore); + _arenas.Add(sourceArena); var entry = sourceStore.CreateVariableValueSets(CompactPath.Root, fields); context.SetRequirements([entry], keys); @@ -423,6 +436,11 @@ public async ValueTask DisposeAsync() store.Dispose(); } + foreach (var arena in _arenas) + { + arena.Dispose(); + } + foreach (var cts in _ctsList) { cts.Dispose(); diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs index 1f11723f5d6..53d0af56579 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs @@ -116,12 +116,80 @@ public void Reset_Should_ClearAccumulatedErrors_When_ReusedForNextEvent() static error => Assert.Equal("event 2", error.Message)); } + [Fact] + public void CreateVariableValueSets_Should_PreserveArrayElementPaths_When_TargetElementsDeduplicate() + { + // arrange + var schema = ComposeSchema( + """ + # name: test + type Query { + products: [Product] + } + + type Product { + id: ID + name: String + } + """); + var plan = PlanOperation(schema, "{ products { id name } }"); + var node = (OperationExecutionNode)plan.RootNodes[0]; + + Assert.True(plan.Operation.RootSelectionSet.TryGetSelection("products", out var productsSelection)); + + using var resultArena = new MemoryArena(); + using var sourceArena = new MemoryArena(); + using var store = new FetchResultStore(); + store.Initialize( + resultArena, + schema, + DefaultErrorHandler.Default, + plan.Operation, + ErrorHandlingMode.Propagate, + includeFlags: 0, + deferFlags: 0, + pathSegmentLocalPoolCapacity: 16); + + var payload = + """ + { + "products": [ + { "id": "1", "name": "same" }, + null, + { "id": "2", "name": "same" } + ] + } + """u8.ToArray(); + var source = SourceResultDocument.Parse(sourceArena, payload, payload.Length); + if (!store.AddPartialResults(source, node.ResultSelectionSet)) + { + throw new InvalidOperationException("The source result could not be merged."); + } + + // act + var result = store.CreateVariableValueSets( + SelectionPath.Root.AppendField("products"), + requestVariables: [], + requiredData: [Requirement("__fusion_1_name", "name")]); + + // assert + var entry = Assert.Single(result); + Assert.Equal(Path(productsSelection!.Id, ~0), entry.Path); + Assert.Equal([Path(productsSelection.Id, ~2)], entry.AdditionalPaths.AsSpan().ToArray()); + Normalize(entry.Values).MatchInlineSnapshot( + """ + {"__fusion_1_name":"same"} + """); + } + [Fact] public void CreateVariableValueSetsFromSnapshot_Should_MergeForwardedVariables_When_RequirementsAreImported() { // arrange - using var source = new FetchResultStore(); - using var target = new FetchResultStore(); + using var sourceArena = new MemoryArena(); + using var targetArena = new MemoryArena(); + using var source = CreateVariableStore(sourceArena); + using var target = CreateVariableStore(targetArena); var imported = CreateVariableValues( source, @@ -150,8 +218,10 @@ public void CreateVariableValueSetsFromSnapshot_Should_MergeForwardedVariables_W public void CreateVariableValueSetsFromSnapshot_Should_PreserveAdditionalPaths_When_FilteredRequirementsDeduplicate() { // arrange - using var source = new FetchResultStore(); - using var target = new FetchResultStore(); + using var sourceArena = new MemoryArena(); + using var targetArena = new MemoryArena(); + using var source = CreateVariableStore(sourceArena); + using var target = CreateVariableStore(targetArena); var primaryPath = Path(1); var primaryAdditionalPath = Path(2); @@ -197,8 +267,10 @@ public void CreateVariableValueSetsFromSnapshot_Should_PreserveAdditionalPaths_W public void CreateVariableValueSetsFromSnapshot_Should_CopyCompositeValues_When_ImportedRequirementIsNested() { // arrange - using var source = new FetchResultStore(); - using var target = new FetchResultStore(); + using var sourceArena = new MemoryArena(); + using var targetArena = new MemoryArena(); + using var source = CreateVariableStore(sourceArena); + using var target = CreateVariableStore(targetArena); var imported = CreateVariableValues( source, @@ -228,8 +300,10 @@ public void CreateVariableValueSetsFromSnapshot_Should_CopyCompositeValues_When_ public void CreateVariableValueSetsFromSnapshot_Should_Throw_When_RequirementWasNotImported() { // arrange - using var source = new FetchResultStore(); - using var target = new FetchResultStore(); + using var sourceArena = new MemoryArena(); + using var targetArena = new MemoryArena(); + using var source = CreateVariableStore(sourceArena); + using var target = CreateVariableStore(targetArena); var imported = CreateVariableValues( source, @@ -254,8 +328,10 @@ public void CreateVariableValueSetsFromSnapshot_Should_Throw_When_RequirementWas public void CreateVariableValueSetsFromSnapshot_Should_CopyCompositeArrayValues_When_ImportedRequirementIsArray() { // arrange - using var source = new FetchResultStore(); - using var target = new FetchResultStore(); + using var sourceArena = new MemoryArena(); + using var targetArena = new MemoryArena(); + using var source = CreateVariableStore(sourceArena); + using var target = CreateVariableStore(targetArena); var imported = CreateVariableValues( source, @@ -289,11 +365,13 @@ public void CreateVariableValueSetsFromSnapshot_Should_CopyCompositeArrayValues_ public void CreateVariableValueSetsFromSnapshot_Should_CopyValueAcrossChunks_When_ValueSpansChunkBoundary() { // arrange - using var source = new FetchResultStore(); - using var target = new FetchResultStore(); + using var sourceArena = new MemoryArena(); + using var targetArena = new MemoryArena(); + using var source = CreateVariableStore(sourceArena); + using var target = CreateVariableStore(targetArena); - // The value alone exceeds one 128KB ChunkedArrayWriter chunk so the - // imported entry's JsonSegment spans more than one chunk. + // The value alone exceeds the arena data ramp so the imported entry's JsonSegment spans + // more than one chunk. var largeValue = new string('a', 200_000); var imported = CreateVariableValues( source, @@ -317,12 +395,14 @@ public void CreateVariableValueSetsFromSnapshot_Should_CopyValueAcrossChunks_Whe public void CreateVariableValueSetsFromSnapshot_Should_MatchPropertyName_When_PropertyNameSpansChunkBoundary() { // arrange - using var source = new FetchResultStore(); - using var target = new FetchResultStore(); + using var sourceArena = new MemoryArena(); + using var targetArena = new MemoryArena(); + using var source = CreateVariableStore(sourceArena); + using var target = CreateVariableStore(targetArena); - // Pad the source writer so the next entry's property name straddles - // a 128KB ChunkedArrayWriter chunk boundary. - PadSourceWriterTo(source, position: 131_065); + // Pad the source writer so the next entry's property name straddles an arena chunk + // boundary. + PadSourceWriterTo(source, position: SourceResultDocument.GetDataChunkSize(0) - 7); var imported = CreateVariableValues( source, @@ -348,8 +428,10 @@ public void CreateVariableValueSetsFromSnapshot_Should_MatchPropertyName_When_Pr public void CreateVariableValueSetsFromSnapshot_Should_EmitInRequestedOrder_When_ImportedSnapshotOrderDiffers() { // arrange - using var source = new FetchResultStore(); - using var target = new FetchResultStore(); + using var sourceArena = new MemoryArena(); + using var targetArena = new MemoryArena(); + using var source = CreateVariableStore(sourceArena); + using var target = CreateVariableStore(targetArena); // Imported snapshot has properties in order a, b, c. var imported = CreateVariableValues( @@ -383,7 +465,8 @@ public void CreateVariableValueSetsFromSnapshot_Should_EmitInRequestedOrder_When public void CreateVariableValueSetsFromSnapshot_Should_ReturnEmpty_When_ImportedEntriesIsEmpty() { // arrange - using var target = new FetchResultStore(); + using var targetArena = new MemoryArena(); + using var target = CreateVariableStore(targetArena); // act var result = target.CreateVariableValueSetsFromSnapshot( @@ -400,8 +483,10 @@ public void CreateVariableValueSetsFromSnapshot_Should_ReturnEmpty_When_Imported public void CreateVariableValueSetsFromSnapshot_Should_SkipEmptyEntries_When_EntryValuesIsEmpty() { // arrange - using var source = new FetchResultStore(); - using var target = new FetchResultStore(); + using var sourceArena = new MemoryArena(); + using var targetArena = new MemoryArena(); + using var source = CreateVariableStore(sourceArena); + using var target = CreateVariableStore(targetArena); var realEntry = CreateVariableValues( source, @@ -424,6 +509,29 @@ public void CreateVariableValueSetsFromSnapshot_Should_SkipEmptyEntries_When_Ent """); } + private FetchResultStore CreateVariableStore(IMemoryArena arena) + { + var schema = ComposeSchema( + """ + # name: test + type Query { + field: String + } + """); + var plan = PlanOperation(schema, "{ field }"); + var store = new FetchResultStore(); + store.Initialize( + arena, + schema, + DefaultErrorHandler.Default, + plan.Operation, + ErrorHandlingMode.Propagate, + includeFlags: 0, + deferFlags: 0, + pathSegmentLocalPoolCapacity: 16); + return store; + } + private static VariableValues CreateVariableValues( FetchResultStore store, CompactPath path, @@ -442,11 +550,14 @@ private static ObjectFieldNode Field(string name, IValueNode value) => new(name, value); private static OperationRequirement Requirement(string key) + => Requirement(key, key); + + private static OperationRequirement Requirement(string key, string fieldName) => new( key, new NamedTypeNode("String"), SelectionPath.Root, - new PathNode(new PathSegmentNode(new FusionNameNode(key)))); + new PathNode(new PathSegmentNode(new FusionNameNode(fieldName)))); private static HashSet ImportedKeys(params string[] keys) => new(keys, StringComparer.Ordinal); diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/ArenaBufferWriterTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/ArenaBufferWriterTests.cs index ade7cd9de58..f7db03e7c54 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/ArenaBufferWriterTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/ArenaBufferWriterTests.cs @@ -44,6 +44,65 @@ public void GetSpan_Should_RoundTripScratchBytes_When_RequestSpansChunks() Assert.Equal(Encoding.UTF8.GetString(json), document.Root.GetRawText()); } + [Fact] + public void GetHashCode_Should_MatchScalar_When_UsingSimdAndGeometricChunks() + { + // Arrange + var lengths = new[] + { + 0, + 1, + 63, + 64, + 65, + 512, + SourceResultDocument.GetDataChunkSize(0) + 17 + }; + var mismatches = new List(); + + // Act + foreach (var length in lengths) + { + var data = new byte[length]; + var rng = new Random(length); + rng.NextBytes(data); + + using var arena = new MemoryArena(); + using var writer = CreateWriterWithData(arena, data); + var expected = ScalarHash(data); + var actual = writer.GetHashCode(0, data.Length); + + if (actual != expected) + { + mismatches.Add($"{length}: expected {expected}, actual {actual}"); + } + } + + // Assert + Assert.Empty(mismatches); + } + + private static ArenaBufferWriter CreateWriterWithData(MemoryArena arena, byte[] data) + { + var writer = new ArenaBufferWriter(arena); + var span = writer.GetSpan(data.Length); + data.CopyTo(span); + writer.Advance(data.Length); + return writer; + } + + private static int ScalarHash(byte[] data) + { + var hash = 0u; + + foreach (var b in data) + { + hash = (hash * 31) + b; + } + + return (int)(hash & 0x7FFFFFFF); + } + private static string CreateAsciiPattern(int length) { var chars = new char[length]; diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/CompositeResultDocumentTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/CompositeResultDocumentTests.cs index 1bd7554d4ad..dc7e12bce0c 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/CompositeResultDocumentTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Text/Json/CompositeResultDocumentTests.cs @@ -80,6 +80,55 @@ fragment Product on Product { Assert.Equal(JsonValueKind.Object, productBySlug.ValueKind); } + [Fact] + public void TryGetObjectScope_Should_ReturnFalseAndFallback_When_ObjectHasNoSelectionSet() + { + // arrange + var schema = CreateCompositeSchema(); + + var plan = PlanOperation( + schema, + """ + { + productBySlug(slug: "1") { + ... Product + } + } + + fragment Product on Product { + id + name + } + """); + + var compositeResult = new CompositeResultDocument(CommonTestExtensions.CreateArena(), plan.Operation, 0); + var operation = compositeResult.Data.Operation; + + var productBySlug = compositeResult.Data.GetProperty("productBySlug"); + var productBySlugSelection = productBySlug.AssertSelection(); + var selectionSet = operation.GetSelectionSet(productBySlugSelection); + productBySlug.SetObjectValue(selectionSet); + + var objectStart = compositeResult.GetStartCursor(productBySlug.Cursor); + var row = compositeResult._metaDb.Get(objectStart); + compositeResult._metaDb.Replace( + objectStart, + ElementTokenType.StartObject, + sizeOrLength: row.SizeOrLength, + parentRow: row.Parent, + numberOfRows: row.NumberOfRows, + flags: row.Flags); + + // act + var hasScope = productBySlug.TryGetObjectScope(out _); + var hasProperty = productBySlug.TryGetProperty("id"u8, out var id); + + // assert + Assert.False(hasScope); + Assert.True(hasProperty); + Assert.Equal("id", id.GetPropertyName()); + } + [Fact] public void Add_SourceResult_Leaf_Value() { diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/ChunkedArrayWriter.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/ChunkedArrayWriter.cs index 53d61ff703e..b627cdeca45 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Buffers/ChunkedArrayWriter.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/ChunkedArrayWriter.cs @@ -13,7 +13,7 @@ namespace HotChocolate.Buffers; /// Uses cursor-based addressing where a single location maps to a chunk index /// and offset via shift and mask operations on . /// -internal sealed class ChunkedArrayWriter : IBufferWriter, IDisposable +internal sealed class ChunkedArrayWriter : IBufferWriter, IJsonSegmentSource, IDisposable { private const int BufferSize = JsonMemory.BufferSize; private const int BufferMask = BufferSize - 1; diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/IJsonSegmentSource.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/IJsonSegmentSource.cs new file mode 100644 index 00000000000..92aa8f33f80 --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/IJsonSegmentSource.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Buffers; + +internal interface IJsonSegmentSource +{ + ReadOnlySpan Read(ref int start, ref int length); + + bool SequenceEqual(int locationA, int locationB, int length); + + int GetHashCode(int location, int length); +}