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
Expand Up @@ -124,6 +124,13 @@ internal void Clean(int maxCollectTargetRetainLength, int maxDictionaryRetainCap
// reset variable writer (returns excess chunks, keeps the first)
_variableWriter.Clean();

// release the forwarded variable buffer if a large input made it grow
if (_forwardedVariableWriter is { Capacity: > MaxRetainedForwardedVariableCapacity })
{
_forwardedVariableWriter.Dispose();
_forwardedVariableWriter = null;
}

// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ namespace HotChocolate.Fusion.Execution.Results;
internal sealed partial class FetchResultStore : IDisposable
{
private const int MaxRetainedDataElementStagingLength = 1024;
private const int MaxRetainedForwardedVariableCapacity = 16 * 1024;
private const int MaxStackAllocPathSegments = 32;

private static readonly ArrayPool<VariableValues> s_variableValuePool = ArrayPool<VariableValues>.Shared;
Expand All @@ -46,6 +47,7 @@ internal sealed partial class FetchResultStore : IDisposable
#endif
private readonly List<IDisposable> _memory = [];
private readonly ChunkedArrayWriter _variableWriter = new();
private PooledArrayWriter? _forwardedVariableWriter;
private readonly JsonWriter _jsonWriter;
private readonly VariableDedupTable _variableDedupTable;
private FusionSchemaDefinition _schema = default!;
Expand Down Expand Up @@ -1038,6 +1040,7 @@ private ImmutableArray<VariableValues> BuildVariableValueSetsFromSnapshot(
VariableValues[]? variableValueSets = null;
var additionalPaths = new AdditionalPathAccumulator();
var nextIndex = 0;
var forwardedVariables = SerializeForwardedVariables(requestVariables, importedEntries.Length);

foreach (var importedEntry in importedEntries)
{
Expand All @@ -1049,13 +1052,7 @@ private ImmutableArray<VariableValues> BuildVariableValueSetsFromSnapshot(
_jsonWriter.Reset(_variableWriter);
var startPosition = _variableWriter.Position;
_jsonWriter.WriteStartObject();

for (var i = 0; i < requestVariables.Count; i++)
{
var field = requestVariables[i];
_jsonWriter.WritePropertyName(field.Name.Value);
WriteValueNode(field.Value);
}
WriteForwardedVariables(requestVariables, forwardedVariables);

if (!TryWriteRequestedRequirementValues(importedEntry.Values, requiredData))
{
Expand Down Expand Up @@ -1133,6 +1130,7 @@ private ImmutableArray<VariableValues> BuildVariableValueSets(
VariableValues[]? variableValueSets = null;
var additionalPaths = new AdditionalPathAccumulator();
var nextIndex = 0;
var forwardedVariables = SerializeForwardedVariables(requestVariables, elements.Length);

foreach (var result in elements)
{
Expand All @@ -1141,14 +1139,7 @@ private ImmutableArray<VariableValues> BuildVariableValueSets(
_jsonWriter.Reset(_variableWriter);
var startPosition = _variableWriter.Position;
_jsonWriter.WriteStartObject();

// Write forwarded variables.
for (var i = 0; i < requestVariables.Count; i++)
{
var field = requestVariables[i];
_jsonWriter.WritePropertyName(field.Name.Value);
WriteValueNode(field.Value);
}
WriteForwardedVariables(requestVariables, forwardedVariables);

// Write requirement fields.
var failed = false;
Expand Down Expand Up @@ -1783,6 +1774,51 @@ private void WriteRawJsonValue(ReadOnlySequence<byte> value)
_jsonWriter.WriteRawValue(value.ToArray());
}

private ReadOnlySpan<byte> SerializeForwardedVariables(
IReadOnlyList<ObjectFieldNode> requestVariables,
int entityCount)
{
if (entityCount < 2 || requestVariables.Count == 0)
{
return [];
}

var writer = _forwardedVariableWriter ??= new PooledArrayWriter();
writer.Reset();
_jsonWriter.Reset(writer);
_jsonWriter.WriteStartObject();

for (var i = 0; i < requestVariables.Count; i++)
{
var field = requestVariables[i];
_jsonWriter.WritePropertyName(field.Name.Value);
WriteValueNode(field.Value);
}

_jsonWriter.WriteEndObject();
_jsonWriter.Reset(_variableWriter);

return writer.WrittenSpan[1..^1];
}

private void WriteForwardedVariables(
IReadOnlyList<ObjectFieldNode> requestVariables,
ReadOnlySpan<byte> serialized)
{
if (!serialized.IsEmpty)
{
_jsonWriter.WriteRawProperties(serialized);
return;
}

for (var i = 0; i < requestVariables.Count; i++)
{
var field = requestVariables[i];
_jsonWriter.WritePropertyName(field.Name.Value);
WriteValueNode(field.Value);
}
}

private void WriteValueNode(IValueNode value)
{
switch (value)
Expand Down Expand Up @@ -2498,6 +2534,7 @@ public void Dispose()

_variableDedupTable.Dispose();
_variableWriter.Dispose();
_forwardedVariableWriter?.Dispose();
_pathPool?.Dispose();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1542,6 +1542,225 @@ input FooKeyInput {
"""));
}

[Fact]
public void CreateVariableValueSets_Should_WriteForwardedVariablesForEveryEntity_When_RequestHasForwardedVariables()
{
// arrange
var schema = ComposeSchema(
"""
# name: test
type Query {
foos: [Foo]
}

type Foo {
id: ID!
}
""");

using var resultArena = new MemoryArena();
using var sourceArena = new MemoryArena();
using var store = CreateLiveStore(
schema,
"{ foos { id } }",
"""{"data":{"foos":[{"id":"1"},{"id":"2"},{"id":"3"}]}}""",
resultArena,
sourceArena);

// act
var result = store.CreateVariableValueSets(
SelectionPath.Root.AppendField("foos"),
[
Field("first", new IntValueNode(10)),
Field("filter", new ObjectValueNode(Field("name", new StringValueNode("a\"b"))))
],
[Requirement(schema, "__fusion_1_id", "id", new NamedTypeNode("ID"))]);

// assert
result.Select(entry => Normalize(entry.Values)).MatchInlineSnapshots(
[
"""
{"first":10,"filter":{"name":"a\u0022b"},"__fusion_1_id":"1"}
""",
"""
{"first":10,"filter":{"name":"a\u0022b"},"__fusion_1_id":"2"}
""",
"""
{"first":10,"filter":{"name":"a\u0022b"},"__fusion_1_id":"3"}
"""
]);
}

[Fact]
public void CreateVariableValueSets_Should_WriteForwardedVariables_When_RequestHasSingleEntity()
{
// arrange
var schema = ComposeSchema(
"""
# name: test
type Query {
foos: [Foo]
}

type Foo {
id: ID!
}
""");

using var resultArena = new MemoryArena();
using var sourceArena = new MemoryArena();
using var store = CreateLiveStore(
schema,
"{ foos { id } }",
"""{"data":{"foos":[{"id":"1"}]}}""",
resultArena,
sourceArena);

// act
var result = store.CreateVariableValueSets(
SelectionPath.Root.AppendField("foos"),
[
Field("first", new IntValueNode(10)),
Field("filter", new ObjectValueNode(Field("name", new StringValueNode("a\"b"))))
],
[Requirement(schema, "__fusion_1_id", "id", new NamedTypeNode("ID"))]);

// assert
var entry = Assert.Single(result);
Normalize(entry.Values).MatchInlineSnapshot(
"""
{"first":10,"filter":{"name":"a\u0022b"},"__fusion_1_id":"1"}
""");
}

[Fact]
public void CreateVariableValueSets_Should_WriteForwardedVariablesForRemainingEntities_When_FirstEntityFailsRequirement()
{
// arrange
var schema = ComposeSchema(
"""
# name: test
type Query {
foos: [Foo]
}

type Foo {
id: ID
}
""");

using var resultArena = new MemoryArena();
using var sourceArena = new MemoryArena();
using var store = CreateLiveStore(
schema,
"{ foos { id } }",
"""{"data":{"foos":[{"id":null},{"id":"2"},{"id":"3"}]}}""",
resultArena,
sourceArena);

// act
var result = store.CreateVariableValueSets(
SelectionPath.Root.AppendField("foos"),
[Field("first", new IntValueNode(10))],
[Requirement(schema, "__fusion_1_id", "id", new NonNullTypeNode(new NamedTypeNode("ID")))]);

// assert
result.Select(entry => Normalize(entry.Values)).MatchInlineSnapshots(
[
"""
{"first":10,"__fusion_1_id":"2"}
""",
"""
{"first":10,"__fusion_1_id":"3"}
"""
]);
}

[Fact]
public void CreateVariableValueSetsFromSnapshot_Should_WriteForwardedVariablesForEveryEntry_When_MultipleEntriesAreImported()
{
// arrange
using var source = new FetchResultStore();
using var target = new FetchResultStore();

var first = CreateVariableValues(source, Path(1), Field("__fusion_1_id", new StringValueNode("1")));
var second = CreateVariableValues(source, Path(2), Field("__fusion_1_id", new StringValueNode("2")));
var third = CreateVariableValues(source, Path(3), Field("__fusion_1_id", new StringValueNode("3")));

// act
var result = target.CreateVariableValueSetsFromSnapshot(
[first, second, third],
ImportedKeys("__fusion_1_id"),
[
Field("first", new IntValueNode(10)),
Field("filter", new ObjectValueNode(Field("name", new StringValueNode("a\"b"))))
],
[Requirement("__fusion_1_id")]);

// assert
result.Select(entry => Normalize(entry.Values)).MatchInlineSnapshots(
[
"""
{"first":10,"filter":{"name":"a\u0022b"},"__fusion_1_id":"1"}
""",
"""
{"first":10,"filter":{"name":"a\u0022b"},"__fusion_1_id":"2"}
""",
"""
{"first":10,"filter":{"name":"a\u0022b"},"__fusion_1_id":"3"}
"""
]);
}

[Fact]
public void CreateVariableValueSetsFromSnapshot_Should_WriteForwardedVariables_When_StoreIsReusedAfterLargeInputWasCleaned()
{
// arrange
using var source = new FetchResultStore();
using var target = new FetchResultStore();

var first = CreateVariableValues(source, Path(1), Field("__fusion_1_id", new StringValueNode("1")));
var second = CreateVariableValues(source, Path(2), Field("__fusion_1_id", new StringValueNode("2")));

// a forwarded value above the retained capacity grows the forwarded variable buffer, so Clean releases it
var payload = new string('a', 40_000);

var largeValues = target.CreateVariableValueSetsFromSnapshot(
[first, second],
ImportedKeys("__fusion_1_id"),
[Field("payload", new StringValueNode(payload))],
[Requirement("__fusion_1_id")])
.Select(entry => Normalize(entry.Values))
.ToArray();

target.Clean(maxCollectTargetRetainLength: 256, maxDictionaryRetainCapacity: 256);

// act
var result = target.CreateVariableValueSetsFromSnapshot(
[first, second],
ImportedKeys("__fusion_1_id"),
[Field("first", new IntValueNode(10))],
[Requirement("__fusion_1_id")]);

// assert
string[] expectedLargeValues =
[
$$"""{"payload":"{{payload}}","__fusion_1_id":"1"}""",
$$"""{"payload":"{{payload}}","__fusion_1_id":"2"}"""
];
Assert.Equal(expectedLargeValues, largeValues);

result.Select(entry => Normalize(entry.Values)).MatchInlineSnapshots(
[
"""
{"first":10,"__fusion_1_id":"1"}
""",
"""
{"first":10,"__fusion_1_id":"2"}
"""
]);
}

[Fact]
public void CreateVariableValueSets_Should_ResolveInvariantNames_When_SelectionSetOrdinalsDiffer()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<InternalsVisibleTo Include="HotChocolate.Fusion.Execution" />
<InternalsVisibleTo Include="HotChocolate.Types" />
<InternalsVisibleTo Include="HotChocolate.Subscriptions" />
<InternalsVisibleTo Include="HotChocolate.Text.Json.Tests" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
Expand Down
Loading
Loading