Skip to content

Commit bc5a190

Browse files
authored
[Fusion] Deduplicate Requirements (#9116)
1 parent b90a8ba commit bc5a190

18 files changed

+227
-248
lines changed

src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ internal sealed partial class DeferExecutionCoordinator
88
private bool _isInitialized;
99
#endif
1010

11+
[Conditional("DEBUG")]
12+
private void AssertInitialized()
13+
{
14+
#if DEBUG
15+
Debug.Assert(_isInitialized);
16+
#endif
17+
}
18+
1119
/// <summary>
1220
/// Initializes the coordinator for a new execution cycle.
1321
/// Must be called before any other operations when leased from a pool.

src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public DeferExecutionCoordinator()
4848
/// </summary>
4949
public int Branch(int currentBranchId, Path path, DeferUsage deferUsage)
5050
{
51-
Debug.Assert(_isInitialized);
51+
AssertInitialized();
5252

5353
var key = new DeferredBranchKey(path, deferUsage, currentBranchId);
5454

@@ -74,7 +74,7 @@ public int Branch(int currentBranchId, Path path, DeferUsage deferUsage)
7474
/// </summary>
7575
public void EnqueueResult(OperationResult result)
7676
{
77-
Debug.Assert(_isInitialized);
77+
AssertInitialized();
7878

7979
lock (_sync)
8080
{
@@ -89,7 +89,7 @@ public void EnqueueResult(OperationResult result)
8989
/// </summary>
9090
public void EnqueueResult(OperationResult result, int branchId)
9191
{
92-
Debug.Assert(_isInitialized);
92+
AssertInitialized();
9393

9494
lock (_sync)
9595
{
@@ -110,7 +110,7 @@ public void EnqueueResult(OperationResult result, int branchId)
110110
public async IAsyncEnumerable<OperationResult> ReadResultsAsync(
111111
[EnumeratorCancellation] CancellationToken cancellationToken = default)
112112
{
113-
Debug.Assert(_isInitialized);
113+
AssertInitialized();
114114

115115
List<OperationResult>? snapshot = null;
116116
await using var registration = cancellationToken.Register(_signal.Set);

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,20 @@ public override async IAsyncEnumerable<SourceSchemaResult> ReadAsResultStreamAsy
210210
case 1:
211211
{
212212
var result = await response.ReadAsResultAsync(cancellationToken);
213-
var sourceSchemaResult = new SourceSchemaResult(variables[0].Path, result);
213+
var variable = variables[0];
214+
var sourceSchemaResult = new SourceSchemaResult(variable.Path, result);
214215

215216
configuration.OnSourceSchemaResult?.Invoke(context, node, sourceSchemaResult);
216217

217218
yield return sourceSchemaResult;
219+
220+
foreach (var additionalPath in variable.AdditionalPaths)
221+
{
222+
var alias = sourceSchemaResult.WithPath(additionalPath);
223+
configuration.OnSourceSchemaResult?.Invoke(context, node, alias);
224+
yield return alias;
225+
}
226+
218227
break;
219228
}
220229

@@ -228,14 +237,20 @@ public override async IAsyncEnumerable<SourceSchemaResult> ReadAsResultStreamAsy
228237
await foreach (var result in response.ReadAsResultStreamAsync()
229238
.WithCancellation(cancellationToken))
230239
{
231-
var (path, _) = variables[requestIndex];
232-
233-
var sourceSchemaResult = new SourceSchemaResult(path, result);
240+
var variable = variables[requestIndex];
241+
var sourceSchemaResult = new SourceSchemaResult(variable.Path, result);
234242

235243
configuration.OnSourceSchemaResult?.Invoke(context, node, sourceSchemaResult);
236244

237245
yield return sourceSchemaResult;
238246

247+
foreach (var additionalPath in variable.AdditionalPaths)
248+
{
249+
var alias = sourceSchemaResult.WithPath(additionalPath);
250+
configuration.OnSourceSchemaResult?.Invoke(context, node, alias);
251+
yield return alias;
252+
}
253+
239254
requestIndex++;
240255
}
241256
}
@@ -253,23 +268,47 @@ public override async IAsyncEnumerable<SourceSchemaResult> ReadAsResultStreamAsy
253268
}
254269

255270
var index = variableIndex.GetInt32();
256-
var (path, _) = variables[index];
257-
var sourceSchemaResult = new SourceSchemaResult(path, result);
271+
var variable = variables[index];
272+
var sourceSchemaResult = new SourceSchemaResult(variable.Path, result);
258273

259274
configuration.OnSourceSchemaResult?.Invoke(context, node, sourceSchemaResult);
260275

261276
yield return sourceSchemaResult;
277+
278+
foreach (var additionalPath in variable.AdditionalPaths)
279+
{
280+
var alias = sourceSchemaResult.WithPath(additionalPath);
281+
configuration.OnSourceSchemaResult?.Invoke(context, node, alias);
282+
yield return alias;
283+
}
262284
}
263285
}
264286

265287
if (errorResult is not null)
266288
{
267289
yield return errorResult;
268290

291+
foreach (var additionalPath in variables[0].AdditionalPaths)
292+
{
293+
var alias = errorResult.WithPath(additionalPath);
294+
configuration.OnSourceSchemaResult?.Invoke(context, node, alias);
295+
yield return alias;
296+
}
297+
269298
for (var i = 1; i < variables.Length; i++)
270299
{
271-
var (path, _) = variables[i];
272-
yield return new SourceSchemaResult(path, SourceResultDocument.CreateEmptyObject());
300+
var variable = variables[i];
301+
var sourceSchemaResult = new SourceSchemaResult(
302+
variable.Path,
303+
SourceResultDocument.CreateEmptyObject());
304+
yield return sourceSchemaResult;
305+
306+
foreach (var additionalPath in variable.AdditionalPaths)
307+
{
308+
var alias = sourceSchemaResult.WithPath(additionalPath);
309+
configuration.OnSourceSchemaResult?.Invoke(context, node, alias);
310+
yield return alias;
311+
}
273312
}
274313
}
275314

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,27 @@ public sealed class SourceSchemaResult : IDisposable
88
private static ReadOnlySpan<byte> ErrorsProperty => "errors"u8;
99
private static ReadOnlySpan<byte> ExtensionsProperty => "extensions"u8;
1010
private readonly SourceResultDocument _document;
11+
private readonly bool _ownsDocument;
1112

1213
public SourceSchemaResult(
1314
Path path,
1415
SourceResultDocument document,
1516
FinalMessage final = FinalMessage.Undefined)
17+
: this(path, document, final, ownsDocument: true)
18+
{
19+
}
20+
21+
private SourceSchemaResult(
22+
Path path,
23+
SourceResultDocument document,
24+
FinalMessage final,
25+
bool ownsDocument)
1626
{
1727
ArgumentNullException.ThrowIfNull(path);
1828
ArgumentNullException.ThrowIfNull(document);
1929

2030
_document = document;
31+
_ownsDocument = ownsDocument;
2132
Path = path;
2233
Final = final;
2334
}
@@ -60,5 +71,13 @@ public SourceResultElement Extensions
6071

6172
public FinalMessage Final { get; }
6273

63-
public void Dispose() => _document.Dispose();
74+
internal SourceSchemaResult WithPath(Path path) => new(path, _document, Final, ownsDocument: false);
75+
76+
public void Dispose()
77+
{
78+
if (_ownsDocument)
79+
{
80+
_document.Dispose();
81+
}
82+
}
6483
}

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,14 @@ protected override async ValueTask<ExecutionStatus> OnExecuteAsync(
133133
context.TrackSourceSchemaClientResponse(this, response);
134134

135135
// we read the responses from the response stream.
136-
bufferLength = Math.Max(variables.Length, 1);
136+
var totalPathCount = variables.Length;
137+
138+
for (var i = 0; i < variables.Length; i++)
139+
{
140+
totalPathCount += variables[i].AdditionalPaths.Length;
141+
}
142+
143+
bufferLength = Math.Max(totalPathCount, 1);
137144
buffer = ArrayPool<SourceSchemaResult>.Shared.Rent(bufferLength);
138145

139146
await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken))
@@ -264,14 +271,27 @@ private static void AddErrors(
264271
}
265272
else
266273
{
267-
var pathBufferLength = variables.Length;
274+
var pathBufferLength = 0;
275+
276+
for (var i = 0; i < variables.Length; i++)
277+
{
278+
pathBufferLength += 1 + variables[i].AdditionalPaths.Length;
279+
}
280+
268281
var pathBuffer = ArrayPool<Path>.Shared.Rent(pathBufferLength);
269282

270283
try
271284
{
285+
var pathBufferIndex = 0;
286+
272287
for (var i = 0; i < variables.Length; i++)
273288
{
274-
pathBuffer[i] = variables[i].Path;
289+
pathBuffer[pathBufferIndex++] = variables[i].Path;
290+
291+
foreach (var additionalPath in variables[i].AdditionalPaths)
292+
{
293+
pathBuffer[pathBufferIndex++] = additionalPath;
294+
}
275295
}
276296

277297
context.AddErrors(error, responseNames, pathBuffer.AsSpan(0, pathBufferLength));

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ public ImmutableArray<VariableValues> CreateVariableValueSets(
362362

363363
PooledArrayWriter? buffer = null;
364364
VariableValues[]? variableValueSets = null;
365+
Dictionary<ObjectValueNode, int>? seen = null;
366+
List<Path>?[]? additionalPaths = null;
365367
var nextIndex = 0;
366368

367369
foreach (var result in current)
@@ -372,10 +374,44 @@ public ImmutableArray<VariableValues> CreateVariableValueSets(
372374
requiredData,
373375
ref buffer);
374376

375-
if (variables is not null)
377+
if (variables is null)
376378
{
377-
variableValueSets ??= new VariableValues[current.Count];
378-
variableValueSets[nextIndex++] = new VariableValues(result.Path, variables);
379+
continue;
380+
}
381+
382+
variableValueSets ??= new VariableValues[current.Count];
383+
384+
if (nextIndex > 0)
385+
{
386+
seen ??= new Dictionary<ObjectValueNode, int>(VariableValueComparer.Instance)
387+
{
388+
[variableValueSets[0].Values] = 0
389+
};
390+
391+
if (seen.TryGetValue(variables, out var existingIndex))
392+
{
393+
additionalPaths ??= new List<Path>?[current.Count];
394+
(additionalPaths[existingIndex] ??= []).Add(result.Path);
395+
continue;
396+
}
397+
398+
seen[variables] = nextIndex;
399+
}
400+
401+
variableValueSets[nextIndex++] = new VariableValues(result.Path, variables);
402+
}
403+
404+
if (additionalPaths is not null)
405+
{
406+
for (var i = 0; i < nextIndex; i++)
407+
{
408+
if (additionalPaths[i] is { } paths)
409+
{
410+
variableValueSets![i] = variableValueSets[i] with
411+
{
412+
AdditionalPaths = [.. paths]
413+
};
414+
}
379415
}
380416
}
381417

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using HotChocolate.Language;
2+
3+
namespace HotChocolate.Fusion.Execution.Results;
4+
5+
internal sealed class VariableValueComparer : IEqualityComparer<ObjectValueNode>
6+
{
7+
public static readonly VariableValueComparer Instance = new();
8+
9+
public bool Equals(ObjectValueNode? x, ObjectValueNode? y)
10+
{
11+
if (ReferenceEquals(x, y))
12+
{
13+
return true;
14+
}
15+
16+
if (x is null || y is null)
17+
{
18+
return false;
19+
}
20+
21+
var xFields = x.Fields;
22+
var yFields = y.Fields;
23+
24+
if (xFields.Count != yFields.Count)
25+
{
26+
return false;
27+
}
28+
29+
// MapRequirements always creates fields in deterministic order.
30+
// We only compare values, as names are equivalent for a given operation node.
31+
for (var i = 0; i < xFields.Count; i++)
32+
{
33+
if (!SyntaxComparer.BySyntax.Equals(xFields[i].Value, yFields[i].Value))
34+
{
35+
return false;
36+
}
37+
}
38+
39+
return true;
40+
}
41+
42+
public int GetHashCode(ObjectValueNode obj)
43+
{
44+
ArgumentNullException.ThrowIfNull(obj);
45+
46+
var hashCode = new HashCode();
47+
48+
// MapRequirements creates a deterministic field order, so no field sorting is needed.
49+
for (var i = 0; i < obj.Fields.Count; i++)
50+
{
51+
hashCode.Add(SyntaxComparer.BySyntax.GetHashCode(obj.Fields[i].Value));
52+
}
53+
54+
return hashCode.ToHashCode();
55+
}
56+
}
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
using System.Collections.Immutable;
12
using HotChocolate.Language;
23

34
namespace HotChocolate.Fusion.Execution;
45

5-
public sealed record VariableValues(Path Path, ObjectValueNode Values);
6+
public sealed record VariableValues(Path Path, ObjectValueNode Values)
7+
{
8+
/// <summary>
9+
/// Gets the additional paths that share the same variable values as the primary <see cref="Path"/>.
10+
/// </summary>
11+
public ImmutableArray<Path> AdditionalPaths { get; init; } = [];
12+
}

src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/BookStoreTests.Fetch_Books_From_SourceSchema1_And_Authors_From_SourceSchema2.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,6 @@ sourceSchemas:
223223
{
224224
"__fusion_1_id": 1
225225
},
226-
{
227-
"__fusion_1_id": 2
228-
},
229-
{
230-
"__fusion_1_id": 2
231-
},
232226
{
233227
"__fusion_1_id": 2
234228
}

src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/BookStoreTests.Fetch_Books_With_Variable_First_And_First_Omitted.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,6 @@ sourceSchemas:
231231
{
232232
"__fusion_1_id": 1
233233
},
234-
{
235-
"__fusion_1_id": 2
236-
},
237-
{
238-
"__fusion_1_id": 2
239-
},
240234
{
241235
"__fusion_1_id": 2
242236
}

0 commit comments

Comments
 (0)