Skip to content

Commit b263731

Browse files
authored
Add planning policy option. (#9428)
1 parent 98f9797 commit b263731

7 files changed

Lines changed: 1417 additions & 17 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace HotChocolate.Fusion.Planning;
2+
3+
/// <summary>
4+
/// Controls how aggressively structurally-identical operations are merged
5+
/// to reduce the number of downstream requests.
6+
/// </summary>
7+
public enum OperationMergePolicy
8+
{
9+
/// <summary>
10+
/// Merge only when canonical signature matches and the operations share the
11+
/// same dependency depth. This avoids any risk of over-serialization by
12+
/// ensuring merged operations were already at equivalent execution levels.
13+
/// </summary>
14+
Conservative = 0,
15+
16+
/// <summary>
17+
/// Merge when canonical signature matches and cycle-safe, but reject merges
18+
/// where the depth difference between candidates exceeds a single level.
19+
/// This provides a middle ground between request reduction and serialization risk.
20+
/// </summary>
21+
Balanced = 1,
22+
23+
/// <summary>
24+
/// Merge whenever canonical signature matches and cycle-safe, regardless of
25+
/// depth or dependency differences. This maximizes request-count reduction.
26+
/// </summary>
27+
Aggressive = 2
28+
}

src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ private OperationPlan BuildExecutionPlan(
4545
planSteps = TransformPlanSteps(planSteps, operationDefinition);
4646
IndexDependencies(planSteps, ctx);
4747
BuildExecutionNodes(planSteps, ctx, _schema, hasVariables);
48-
MergeAndBatchOperations(ctx, _options.EnableRequestGrouping);
48+
MergeAndBatchOperations(ctx, _options.EnableRequestGrouping, _options.MergePolicy);
4949
WireExecutionDependencies(ctx);
5050

5151
var rootNodes = planSteps
@@ -347,10 +347,11 @@ private static OperationExecutionNode CreateOperationExecutionNode(
347347

348348
private static void MergeAndBatchOperations(
349349
ExecutionPlanBuildContext ctx,
350-
bool enableRequestGrouping)
350+
bool enableRequestGrouping,
351+
OperationMergePolicy mergePolicy)
351352
{
352353
var nodeFieldBoundCache = new Dictionary<int, bool>();
353-
var mergeResults = MergeStructurallyIdenticalOperations(ctx, nodeFieldBoundCache);
354+
var mergeResults = MergeStructurallyIdenticalOperations(ctx, nodeFieldBoundCache, mergePolicy);
354355

355356
// Capture each node's dependency identifiers now, because the batching
356357
// step below will rewrite the dependency lookup as it merges nodes.
@@ -375,7 +376,8 @@ private static void MergeAndBatchOperations(
375376
/// </summary>
376377
private static Dictionary<int, MergeResult> MergeStructurallyIdenticalOperations(
377378
ExecutionPlanBuildContext ctx,
378-
Dictionary<int, bool> nodeFieldBoundCache)
379+
Dictionary<int, bool> nodeFieldBoundCache,
380+
OperationMergePolicy mergePolicy)
379381
{
380382
var candidates = new Dictionary<string, List<OperationExecutionNode>>(StringComparer.Ordinal);
381383

@@ -411,7 +413,8 @@ private static Dictionary<int, MergeResult> MergeStructurallyIdenticalOperations
411413
continue;
412414
}
413415

414-
foreach (var group in PartitionIntoMergeableGroups(equivalentNodes, ctx.DependenciesByStepId))
416+
foreach (var group in PartitionIntoMergeableGroups(
417+
equivalentNodes, ctx.DependenciesByStepId, mergePolicy))
415418
{
416419
if (group.Count <= 1)
417420
{
@@ -1154,12 +1157,30 @@ private static string ApplyPrefixReplacements(
11541157
/// Partitions structurally identical operations into groups that can
11551158
/// each be safely merged. Two operations cannot share a group if one
11561159
/// transitively depends on the other, because merging them would
1157-
/// create a cycle in the dependency graph.
1160+
/// create a cycle in the dependency graph. The <paramref name="mergePolicy"/>
1161+
/// further restricts which candidates may share a group based on their
1162+
/// dependency depth.
11581163
/// </summary>
11591164
private static List<List<OperationExecutionNode>> PartitionIntoMergeableGroups(
11601165
List<OperationExecutionNode> candidates,
1161-
Dictionary<int, HashSet<int>> dependenciesByStepId)
1166+
Dictionary<int, HashSet<int>> dependenciesByStepId,
1167+
OperationMergePolicy mergePolicy)
11621168
{
1169+
// Pre-compute dependency depths when the policy needs them.
1170+
Dictionary<int, int>? depthLookup = null;
1171+
1172+
if (mergePolicy is OperationMergePolicy.Conservative
1173+
or OperationMergePolicy.Balanced)
1174+
{
1175+
depthLookup = [];
1176+
var recursionStack = new HashSet<int>();
1177+
1178+
foreach (var candidate in candidates)
1179+
{
1180+
GetDependencyDepth(candidate.Id, dependenciesByStepId, depthLookup, recursionStack);
1181+
}
1182+
}
1183+
11631184
var groups = new List<List<OperationExecutionNode>>();
11641185
var visited = new HashSet<int>();
11651186

@@ -1171,22 +1192,46 @@ private static List<List<OperationExecutionNode>> PartitionIntoMergeableGroups(
11711192
{
11721193
var canJoin = true;
11731194

1174-
foreach (var existing in group)
1195+
// Policy-specific depth checks (applied before the more
1196+
// expensive transitive-reachability walk).
1197+
if (depthLookup is not null)
11751198
{
1176-
visited.Clear();
1199+
var candidateDepth = depthLookup[candidate.Id];
1200+
var referenceDepth = depthLookup[group[0].Id];
11771201

1178-
if (IsTransitivelyReachable(candidate.Id, existing.Id, dependenciesByStepId, visited))
1202+
switch (mergePolicy)
11791203
{
1180-
canJoin = false;
1181-
break;
1182-
}
1204+
case OperationMergePolicy.Conservative
1205+
when candidateDepth != referenceDepth:
1206+
canJoin = false;
1207+
break;
11831208

1184-
visited.Clear();
1209+
case OperationMergePolicy.Balanced
1210+
when Math.Abs(candidateDepth - referenceDepth) > 1:
1211+
canJoin = false;
1212+
break;
1213+
}
1214+
}
11851215

1186-
if (IsTransitivelyReachable(existing.Id, candidate.Id, dependenciesByStepId, visited))
1216+
if (canJoin)
1217+
{
1218+
foreach (var existing in group)
11871219
{
1188-
canJoin = false;
1189-
break;
1220+
visited.Clear();
1221+
1222+
if (IsTransitivelyReachable(candidate.Id, existing.Id, dependenciesByStepId, visited))
1223+
{
1224+
canJoin = false;
1225+
break;
1226+
}
1227+
1228+
visited.Clear();
1229+
1230+
if (IsTransitivelyReachable(existing.Id, candidate.Id, dependenciesByStepId, visited))
1231+
{
1232+
canJoin = false;
1233+
break;
1234+
}
11901235
}
11911236
}
11921237

src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlannerOptions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ public bool EnableRequestGrouping
5757
}
5858
} = true;
5959

60+
/// <summary>
61+
/// Gets or sets how aggressively structurally-identical operations are merged
62+
/// to reduce downstream request count. Cycle safety is always enforced regardless
63+
/// of this setting.
64+
/// </summary>
65+
public OperationMergePolicy MergePolicy
66+
{
67+
get;
68+
set
69+
{
70+
ExpectMutableOptions();
71+
field = value;
72+
}
73+
} = OperationMergePolicy.Aggressive;
74+
6075
/// <summary>
6176
/// Gets or sets the weight applied for each operation beyond the fan-out penalty threshold.
6277
/// </summary>

0 commit comments

Comments
 (0)