Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
@@ -0,0 +1,130 @@
using System.Runtime.InteropServices;
using HotChocolate.Fusion.Text.Json;
using HotChocolate.Types;
using DbRow = HotChocolate.Fusion.Text.Json.CompositeResultDocument.DbRow;
using ElementFlags = HotChocolate.Fusion.Text.Json.CompositeResultDocument.ElementFlags;
using OperationReferenceType = HotChocolate.Fusion.Text.Json.CompositeResultDocument.OperationReferenceType;

namespace HotChocolate.Fusion.Execution.Nodes;

/// <summary>
/// The prepared result row block for one object instance of a selection set: StartObject, one
/// PropertyName and value row per selection, EndObject. The rows carry only selection-invariant
/// state, so one template serves every request that executes the declaring operation.
/// Per-instance state (parent pointers and per-request exclusion) is stamped when the block is
/// applied to a result document.
/// </summary>
internal readonly struct ObjectTemplate
{
private readonly byte[] _rows;
private readonly int[] _conditionalSelections;

private ObjectTemplate(byte[] rows, int[] conditionalSelections)
{
_rows = rows;
_conditionalSelections = conditionalSelections;
}

/// <summary>
/// Gets the prepared row block.
/// </summary>
public ReadOnlySpan<byte> Rows => _rows;

/// <summary>
/// Gets the indices of the selections whose exclusion depends on the request's
/// skip/include or defer state. The canonical rows never carry
/// <see cref="ElementFlags.IsExcluded"/>; the flag is applied per
/// request for these selections only.
/// </summary>
public ReadOnlySpan<int> ConditionalSelections => _conditionalSelections;

public static ObjectTemplate Create(SelectionSet selectionSet)
{
var selections = selectionSet.Selections;
var rows = new byte[((selections.Length * 2) + 2) * DbRow.Size];
var conditionalCount = 0;

foreach (var selection in selections)
{
if (selection.IsConditional || selection.CanBeDeferred)
{
conditionalCount++;
}
}

var conditionalSelections = conditionalCount == 0 ? [] : new int[conditionalCount];
conditionalCount = 0;

var startObjectRow = new DbRow(
ElementTokenType.StartObject,
sizeOrLength: selections.Length,
operationReferenceId: selectionSet.Id,
operationReferenceType: OperationReferenceType.SelectionSet,
numberOfRows: (selections.Length * 2) + 1);
MemoryMarshal.Write(rows, in startObjectRow);
var offset = DbRow.Size;

for (var i = 0; i < selections.Length; i++)
{
var selection = selections[i];

if (selection.IsConditional || selection.CanBeDeferred)
{
conditionalSelections[conditionalCount++] = i;
}

var propertyRow = new DbRow(
ElementTokenType.PropertyName,
operationReferenceId: selection.Id,
operationReferenceType: OperationReferenceType.Selection,
flags: GetPropertyFlags(selection));
MemoryMarshal.Write(rows.AsSpan(offset), in propertyRow);
offset += DbRow.Size;

// __typename resolves to the concrete type of this selection set. Synthesizing
// it here means payloads that never echo __typename (such as broker event
// streams) still report it, while a subgraph that does echo it simply overwrites
// the slot with the identical value. The value row stores the selection-set id
// as an inline string reference, so the interned type name is resolved (and
// quoted) lazily when read.
var valueRow =
selection.Field.IsIntrospectionField
&& selection.Field.Name == IntrospectionFieldNames.TypeName
? new DbRow(ElementTokenType.String, location: selectionSet.Id)
: new DbRow(ElementTokenType.None);
MemoryMarshal.Write(rows.AsSpan(offset), in valueRow);
offset += DbRow.Size;
}

var endObjectRow = new DbRow(ElementTokenType.EndObject);
MemoryMarshal.Write(rows.AsSpan(offset), in endObjectRow);

return new ObjectTemplate(rows, conditionalSelections);
}

/// <summary>
/// Computes the selection-invariant flags of a property row. Skip/include and defer
/// state is per request and is never part of the canonical rows.
/// </summary>
private static ElementFlags GetPropertyFlags(Selection selection)
{
var flags = ElementFlags.None;

if (selection.IsInternal)
{
flags = ElementFlags.IsInternal;
}

if (selection.Type.Kind is not TypeKind.NonNull)
{
flags |= ElementFlags.IsNullable;
}

if (selection.IsEnumValue)
{
flags |= ElementFlags.IsEnumValue;
}

return flags;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ internal void Seal(SelectionSet selectionSet)

public bool IsDeferred(ulong deferFlags) => (_deferMask & deferFlags) != 0;

/// <summary>
/// Gets a value indicating whether this selection can be deferred for some request.
/// </summary>
internal bool CanBeDeferred => _deferMask != 0;

/// <summary>
/// Returns the active delivery groups for this selection after resolving
/// each occurrence to its nearest active ancestor and pruning child groups
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public sealed class SelectionSet : ISelectionSet
private readonly Selection[] _selections;
private readonly FrozenDictionary<string, Selection> _responseNameLookup;
private readonly SelectionLookup _utf8ResponseNameLookup;
private ObjectTemplate _objectTemplate;
private bool _isSealed;

public SelectionSet(
Expand Down Expand Up @@ -87,6 +88,13 @@ public SelectionSet(
/// </summary>
public bool HasIncrementalParts { get; }

/// <summary>
/// Gets the prepared result row block shared by every object instance of this selection
/// set. It is built when the selection set is sealed and reused across all requests of
/// the declaring operation.
/// </summary>
internal ObjectTemplate ObjectTemplate => _objectTemplate;

IEnumerable<ISelection> ISelectionSet.GetSelections() => _selections;

/// <summary>
Expand Down Expand Up @@ -266,5 +274,7 @@ internal void Seal(Operation operation)
{
selection.Seal(this);
}

_objectTemplate = ObjectTemplate.Create(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,192 @@ ref Unsafe.Add(ref region, i * DbRow.Size),
}
}

/// <summary>
/// Appends a pre-built object row block (StartObject, one PropertyName and value row per
/// selection, EndObject) and stamps the per-instance parent pointers: the StartObject
/// row points at the caller's parent, each PropertyName row at the StartObject row, and
/// each value row at its PropertyName row. <paramref name="excludedRowOffsets"/> lists
/// the block-relative offsets of the property rows to mark
/// <see cref="ElementFlags.IsExcluded"/>, in ascending order; the flag is stamped while
/// the parent pointers are written. Returns the cursor of the StartObject row.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Cursor AppendObjectBlock(
ReadOnlySpan<byte> template,
int parentRow,
ReadOnlySpan<int> excludedRowOffsets)
{
Debug.Assert(parentRow is >= 0 and <= 0x1FFFFFFF);
Debug.Assert(template.Length >= DbRow.Size * 2);
Debug.Assert(template.Length % DbRow.Size == 0);

var next = _next;
var byteOffset = next.ByteOffset;
var chunks = _chunks;

// Fast path: the whole block fits into the current chunk, so the packed cursor
// value advances by plain integer increments and no chunk carry can occur while
// stamping the parent pointers.
if (byteOffset + template.Length <= _chunkBytes
&& (uint)next.Chunk < (uint)chunks.Length
&& chunks[next.Chunk] is { Buffer: { } buffer } segment)
{
// The capacity guard above is only meaningful together with the rented-buffer
// check: after a cursor carry into an unrented chunk, _chunkBytes still holds
// the previous chunk's capacity and only ReserveRow refreshes it.
Debug.Assert(_chunkBytes == 1 << (10 + (int)Cursor.ChunkSizeFor(next.Chunk)));

ref var dest = ref Unsafe.Add(
ref MemoryMarshal.GetArrayDataReference(buffer),
segment.Offset + byteOffset);

Unsafe.CopyBlockUnaligned(
ref dest,
ref MemoryMarshal.GetReference(template),
(uint)template.Length);

// Stamp the StartObject row's parent; the trailing EndObject row keeps its
// template parent of zero.
Unsafe.WriteUnaligned(ref dest, parentRow);

var rowCount = template.Length / DbRow.Size;
var startValue = next.Value;

// Excluded offsets are ascending and always odd (property rows), so the loop
// visits every one of them; a sentinel merge stamps the flag into the int right
// next to the parent pointer that was just written.
var excludedIndex = 0;
var nextExcluded = excludedRowOffsets.IsEmpty ? int.MaxValue : excludedRowOffsets[0];

for (var rowOffset = 1; rowOffset < rowCount - 1; rowOffset += 2)
{
Unsafe.WriteUnaligned(
ref Unsafe.Add(ref dest, rowOffset * DbRow.Size),
startValue);
Unsafe.WriteUnaligned(
ref Unsafe.Add(ref dest, (rowOffset + 1) * DbRow.Size),
startValue + rowOffset);

if (rowOffset == nextExcluded)
{
ref var flagsRef = ref Unsafe.Add(
ref dest,
(rowOffset * DbRow.Size) + DbRow.SelectionAndFlagsOffset);
Unsafe.WriteUnaligned(
ref flagsRef,
Unsafe.ReadUnaligned<int>(ref flagsRef)
| ((int)ElementFlags.IsExcluded << DbRow.FlagsShift));
excludedIndex++;
nextExcluded = excludedIndex < excludedRowOffsets.Length ? excludedRowOffsets[excludedIndex] : int.MaxValue;
}
}

Debug.Assert(excludedIndex == excludedRowOffsets.Length);

_next = next + rowCount;
return next;
}

return AppendObjectBlockSpanning(template, parentRow, excludedRowOffsets);
}

// Applies a block that does not fit into the current chunk, copying whole-row segments
// and rolling chunks through ReserveRow exactly like the per-row appenders. Parent
// pointers cannot be stamped with plain integer adds across a chunk carry, so each
// segment stamps with its own base cursor value; the only cross-segment reference is a
// value row whose PropertyName row closed the previous segment, which is stamped from
// that segment's last cursor value.
private Cursor AppendObjectBlockSpanning(
ReadOnlySpan<byte> template,
int parentRow,
ReadOnlySpan<int> excludedRowOffsets)
{
var totalRows = template.Length / DbRow.Size;
var startObjectCursor = default(Cursor);
var startObjectValue = 0;
var previousRowValue = 0;
var rowIndex = 0;

// The excluded offsets are block-relative, ascending, and always odd (property
// rows), so the segment walks visit them in order and a single sentinel cursor
// spans all segments. A property row that is the last row of a segment is stamped
// by that segment's walk; the value row opening the next segment is even and never
// matches. The StartObject and EndObject rows are skipped below, and no excluded
// offset can name them: the largest property offset is totalRows - 3.
var excludedIndex = 0;
var nextExcluded = excludedRowOffsets.IsEmpty ? int.MaxValue : excludedRowOffsets[0];

while (rowIndex < totalRows)
{
var (chunk, byteOffset, cursor) = ReserveRow();
var segmentRows = Math.Min(
(_chunkBytes - byteOffset) / DbRow.Size,
totalRows - rowIndex);
var segmentBase = cursor.Value;

ref var dest = ref Unsafe.Add(
ref MemoryMarshal.GetArrayDataReference(chunk.Buffer),
chunk.Offset + byteOffset);

Unsafe.CopyBlockUnaligned(
ref dest,
ref Unsafe.Add(
ref MemoryMarshal.GetReference(template),
rowIndex * DbRow.Size),
(uint)(segmentRows * DbRow.Size));

if (rowIndex == 0)
{
startObjectCursor = cursor;
startObjectValue = segmentBase;
Unsafe.WriteUnaligned(ref dest, parentRow);
}

for (var i = 0; i < segmentRows; i++)
{
var blockRow = rowIndex + i;

if (blockRow == 0 || blockRow == totalRows - 1)
{
// The StartObject row is stamped above; the EndObject row keeps its
// template parent of zero.
continue;
}

var parentValue = (blockRow & 1) == 1
? startObjectValue
: i == 0
? previousRowValue
: segmentBase + i - 1;

Unsafe.WriteUnaligned(
ref Unsafe.Add(ref dest, i * DbRow.Size),
parentValue);

if (blockRow == nextExcluded)
{
ref var flagsRef = ref Unsafe.Add(
ref dest,
(i * DbRow.Size) + DbRow.SelectionAndFlagsOffset);
Unsafe.WriteUnaligned(
ref flagsRef,
Unsafe.ReadUnaligned<int>(ref flagsRef)
| ((int)ElementFlags.IsExcluded << DbRow.FlagsShift));
excludedIndex++;
nextExcluded = excludedIndex < excludedRowOffsets.Length ? excludedRowOffsets[excludedIndex] : int.MaxValue;
}
}

previousRowValue = segmentBase + segmentRows - 1;
rowIndex += segmentRows;
_next = cursor + segmentRows;
}

Debug.Assert(excludedIndex == excludedRowOffsets.Length);

return startObjectCursor;
}

/// <summary>
/// Reserves the next row slot, advancing to a new chunk if necessary. Does not
/// advance <see cref="_next"/>; the caller updates it after writing the row.
Expand Down
Loading
Loading