Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ private static ImmutableArray<VariableValues> BuildFromSnapshotCurrent(
dedupTable.Initialize(importedEntries.Length);

VariableValues[]? variableValueSets = null;
var additionalPaths = new AdditionalPathAccumulator();
var additionalPaths = new AdditionalPathAccumulator(importedEntries.Length);
var nextIndex = 0;

foreach (var importedEntry in importedEntries)
Expand Down Expand Up @@ -350,7 +350,7 @@ private static ImmutableArray<VariableValues> BuildFromSnapshotPrefixBlit(
dedupTable.Initialize(importedEntries.Length);

VariableValues[]? variableValueSets = null;
var additionalPaths = new AdditionalPathAccumulator();
var additionalPaths = new AdditionalPathAccumulator(importedEntries.Length);
var nextIndex = 0;

// Rider: requirement keys are GraphQL names (always ASCII), so they are
Expand Down Expand Up @@ -736,7 +736,8 @@ private static ImmutableArray<VariableValues> FinalizeVariableValueSets(
return [];
}

additionalPaths.ApplyTo(variableValueSets, nextIndex);
// The accumulator only holds paths here, so it never reads the store or the document.
additionalPaths.ApplyTo(variableValueSets, nextIndex, store: null!, document: null!);
additionalPaths.Dispose();

var span = variableValueSets.AsSpan(0, nextIndex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,46 @@
namespace HotChocolate.Fusion.Execution.Results;

/// <summary>
/// A flat, allocation-free accumulator for additional CompactPath entries
/// that replaces per-slot List&lt;CompactPath&gt; with ArrayPool-rented buffers.
/// Stores (slotIndex, path) pairs and produces <see cref="CompactPathSegment"/>
/// per slot via counting sort in ApplyTo.
/// Collects the additional targets of variable value set slots, as paths or result elements, and
/// assigns them to their slots in the order they were added. When every target is a result element,
/// the assigned segments create their paths on first access.
/// </summary>
internal ref struct AdditionalPathAccumulator
{
private const int NoCursor = -1;
private const int MinimumCapacity = 16;

private readonly int _capacityHint;
private CompactPath[]? _paths;
private int[]? _cursors;
private int[]? _slotIndices;
private int _capacity;
private int _count;
private int _pathCount;

/// <summary>
/// Creates an accumulator that expects about <paramref name="capacityHint"/> targets.
/// </summary>
public AdditionalPathAccumulator(int capacityHint)
{
_capacityHint = capacityHint;
}

public readonly bool HasEntries => _count > 0;

public void Add(int slotIndex, CompactPath path)
{
if (_paths is null)
{
_paths = ArrayPool<CompactPath>.Shared.Rent(16);
_slotIndices = ArrayPool<int>.Shared.Rent(16);
}
else if (_count == _paths.Length)
{
Grow();
}
var index = Reserve(slotIndex);
_paths ??= ArrayPool<CompactPath>.Shared.Rent(_capacity);
_paths[index] = path;
_cursors![index] = NoCursor;
_pathCount++;
}

_paths[_count] = path;
_slotIndices![_count] = slotIndex;
_count++;
public void Add(int slotIndex, CompositeResultElement target)
{
var index = Reserve(slotIndex);
_cursors![index] = target.Cursor.Value;
}

public void AddRange(int slotIndex, ReadOnlySpan<CompactPath> paths)
Expand All @@ -42,14 +54,70 @@ public void AddRange(int slotIndex, ReadOnlySpan<CompactPath> paths)
}
}

public void ApplyTo(VariableValues[] variableValueSets, int slotCount)
public void ApplyTo(
VariableValues[] variableValueSets,
int slotCount,
FetchResultStore store,
CompositeResultDocument document)
{
if (_count == 0)
{
return;
}

var segmentBuffer = ArrayPool<CompactPathSegment>.Shared.Rent(slotCount);
var segments = segmentBuffer.AsSpan(0, slotCount);
segments.Clear();
CreateSegments(segments, store, document);

for (var slot = 0; slot < slotCount; slot++)
{
if (!segments[slot].IsDefaultOrEmpty)
{
variableValueSets[slot] = variableValueSets[slot] with { AdditionalPaths = segments[slot] };
}
}

segments.Clear();
ArrayPool<CompactPathSegment>.Shared.Return(segmentBuffer);
}

public void ApplyTo(
EntityResultPath[] resultPaths,
int slotCount,
FetchResultStore store,
CompositeResultDocument document)
{
if (_count == 0)
{
return;
}

// Count paths per slot.
var segmentBuffer = ArrayPool<CompactPathSegment>.Shared.Rent(slotCount);
var segments = segmentBuffer.AsSpan(0, slotCount);
segments.Clear();
CreateSegments(segments, store, document);

for (var slot = 0; slot < slotCount; slot++)
{
if (!segments[slot].IsDefaultOrEmpty)
{
resultPaths[slot] = resultPaths[slot] with { AdditionalPaths = segments[slot] };
}
}

segments.Clear();
ArrayPool<CompactPathSegment>.Shared.Return(segmentBuffer);
}

private readonly void CreateSegments(
Span<CompactPathSegment> segments,
FetchResultStore store,
CompositeResultDocument document)
{
var slotCount = segments.Length;

// Count targets per slot.
var counts = slotCount <= 256
? stackalloc int[slotCount]
: new int[slotCount];
Expand All @@ -70,49 +138,97 @@ public void ApplyTo(VariableValues[] variableValueSets, int slotCount)
offsets[i] = offsets[i - 1] + counts[i - 1];
}

// Scatter paths into a single shared array in sorted order.
// Scatter targets into a single shared array in sorted order.
var writePos = slotCount <= 256
? stackalloc int[slotCount]
: new int[slotCount];
offsets.CopyTo(writePos);

if (_pathCount == 0)
{
var cursors = new int[_count];

for (var i = 0; i < _count; i++)
{
cursors[writePos[_slotIndices![i]]++] = _cursors![i];
}

var source = new TargetCursorPaths(store, document, cursors);

for (var slot = 0; slot < slotCount; slot++)
{
if (counts[slot] > 0)
{
segments[slot] = new CompactPathSegment(source, offsets[slot], counts[slot]);
}
}

return;
}

var shared = new CompactPath[_count];

for (var i = 0; i < _count; i++)
{
var idx = _slotIndices![i];
shared[writePos[idx]++] = _paths![i];
shared[writePos[_slotIndices![i]]++] = GetPath(i, document);
}

// Build CompactPathSegment for each non-empty slot from contiguous slices.
for (var slot = 0; slot < slotCount; slot++)
{
if (counts[slot] == 0)
if (counts[slot] > 0)
{
continue;
segments[slot] = new CompactPathSegment(shared, offsets[slot], counts[slot]);
}
}
}

variableValueSets[slot] = variableValueSets[slot] with
{
AdditionalPaths = new CompactPathSegment(shared, offsets[slot], counts[slot])
};
private readonly CompactPath GetPath(int index, CompositeResultDocument document)
=> _cursors![index] == NoCursor
? _paths![index]
: document.CreateCompactPath(new CompositeResultDocument.Cursor(_cursors[index]));

private int Reserve(int slotIndex)
{
if (_cursors is null)
{
var capacity = Math.Max(MinimumCapacity, _capacityHint);
_cursors = ArrayPool<int>.Shared.Rent(capacity);
_slotIndices = ArrayPool<int>.Shared.Rent(capacity);
_capacity = Math.Min(_cursors.Length, _slotIndices.Length);
}
else if (_count == _capacity)
{
Grow();
}

_slotIndices![_count] = slotIndex;
return _count++;
}

private void Grow()
{
var newSize = _paths!.Length * 2;
var newSize = _capacity * 2;

var newPaths = ArrayPool<CompactPath>.Shared.Rent(newSize);
_paths.AsSpan(0, _count).CopyTo(newPaths);
_paths.AsSpan(0, _count).Clear();
ArrayPool<CompactPath>.Shared.Return(_paths);
_paths = newPaths;
if (_paths is not null)
{
var newPaths = ArrayPool<CompactPath>.Shared.Rent(newSize);
_paths.AsSpan(0, _count).CopyTo(newPaths);
_paths.AsSpan(0, _count).Clear();
ArrayPool<CompactPath>.Shared.Return(_paths);
_paths = newPaths;
}

var newCursors = ArrayPool<int>.Shared.Rent(newSize);
_cursors.AsSpan(0, _count).CopyTo(newCursors);
ArrayPool<int>.Shared.Return(_cursors!);
_cursors = newCursors;

var newIndices = ArrayPool<int>.Shared.Rent(newSize);
_slotIndices.AsSpan(0, _count).CopyTo(newIndices);
ArrayPool<int>.Shared.Return(_slotIndices!);
_slotIndices = newIndices;

_capacity = Math.Min(_cursors.Length, _slotIndices.Length);
}

public void Dispose()
Expand All @@ -124,12 +240,20 @@ public void Dispose()
_paths = null;
}

if (_cursors is not null)
{
ArrayPool<int>.Shared.Return(_cursors);
_cursors = null;
}

if (_slotIndices is not null)
{
ArrayPool<int>.Shared.Return(_slotIndices);
_slotIndices = null;
}

_capacity = 0;
_count = 0;
_pathCount = 0;
}
}
Loading
Loading