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 @@ -81,6 +81,7 @@ public static void TryAddExternalSetter(InterfaceType type, InterfaceTypeConfigu
{
var statements = new List<Expression>();
var variables = new List<ParameterExpression>();
var keyFields = CollectKeyFieldNames(type);

foreach (var field in type.Fields)
{
Expand All @@ -92,6 +93,14 @@ public static void TryAddExternalSetter(InterfaceType type, InterfaceTypeConfigu
var fieldType = field.Type is NonNullType nonNull ? nonNull.NullableType : field.Type;
var path = Append(pathPrefix, field.Name);

// Fields that participate in a '@key' identify the entity and are owned by the reference
// resolver. The representation echoes them back only so the entity can be located, so the
// resolver's returned values are preserved instead of being overwritten.
if (keyFields?.Contains(field.Name) == true)
{
continue;
}

if (fieldType is ListType)
{
if (property.SetMethod is not null)
Expand Down Expand Up @@ -149,6 +158,29 @@ public static void TryAddExternalSetter(InterfaceType type, InterfaceTypeConfigu
return statements.Count == 0 ? null : Block(variables, statements);
}

private static HashSet<string>? CollectKeyFieldNames(ObjectType type)
{
HashSet<string>? names = null;

foreach (var directive in type.Directives)
{
if (directive.Type.RuntimeType != typeof(KeyDirective))
{
continue;
}

foreach (var selection in directive.ToValue<KeyDirective>().Fields.Selections)
{
if (selection is FieldNode keyField)
{
(names ??= []).Add(keyField.Name.Value);
}
}
}

return names;
}

private static MethodCallExpression CreateTrySetValue(
Type runtimeType,
PropertyInfo property,
Expand Down
1 change: 1 addition & 0 deletions src/HotChocolate/Fusion/HotChocolate.Fusion.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Solution>
<Folder Name="/benchmarks/">
<Project Path="benchmarks/Composition/HotChocolate.Fusion.Composition.Benchmarks.csproj" />
<Project Path="benchmarks/Fusion.Execution.Benchmarks/HotChocolate.Fusion.Execution.Benchmarks.csproj" />
</Folder>
<Folder Name="/src/">
Expand Down
47 changes: 47 additions & 0 deletions src/HotChocolate/Fusion/benchmarks/Composition/CorpusPaths.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace HotChocolate.Fusion.Composition.Benchmarks;

/// <summary>
/// Resolves the paths of the large federated graph benchmark corpus (the big-federated-graphs checkout) without
/// hard-coding machine-specific locations. The corpus root is taken from the
/// <c>BIG_FEDERATED_GRAPHS</c> environment variable when set; otherwise the directories above the
/// benchmark binary are searched for a sibling <c>big-federated-graphs</c> checkout.
/// </summary>
internal static class CorpusPaths
{
private const string EnvironmentVariable = "BIG_FEDERATED_GRAPHS";
private const string CorpusDirectoryName = "big-federated-graphs";

private static readonly Lazy<string> s_root = new(ResolveRoot);

public static string SubgraphsPath
=> System.IO.Path.Combine(s_root.Value, "schemas", "edge0-v2", "subgraphs");

private static string ResolveRoot()
{
var configured = Environment.GetEnvironmentVariable(EnvironmentVariable);

if (!string.IsNullOrEmpty(configured))
{
return configured;
}

var current = new DirectoryInfo(AppContext.BaseDirectory);

while (current is not null)
{
var candidate = System.IO.Path.Combine(current.FullName, CorpusDirectoryName);

if (Directory.Exists(candidate))
{
return candidate;
}

current = current.Parent;
}

throw new InvalidOperationException(
$"The large federated graph benchmark corpus was not found. Set the {EnvironmentVariable} "
+ $"environment variable to the {CorpusDirectoryName} checkout, or place the checkout "
+ "next to an ancestor directory of the benchmark binary.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net11.0</TargetFrameworks>
<TargetFramework>net11.0</TargetFramework>
<ServerGarbageCollection>true</ServerGarbageCollection>
<LangVersion>Preview</LangVersion>
<AssemblyName>HotChocolate.Fusion.Composition.Benchmarks</AssemblyName>
<RootNamespace>HotChocolate.Fusion.Composition.Benchmarks</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Fusion.Composition\HotChocolate.Fusion.Composition.csproj" />
</ItemGroup>

</Project>
71 changes: 71 additions & 0 deletions src/HotChocolate/Fusion/benchmarks/Composition/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Diagnostics;
using HotChocolate.Fusion;
using HotChocolate.Fusion.Composition.Benchmarks;
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.Options;

var subgraphsPath = args.Length > 0 ? args[0] : CorpusPaths.SubgraphsPath;
var printErrors = args.Contains("--errors");

var files = Directory.GetFiles(subgraphsPath, "*.graphqls");

if (files.Length == 0)
{
Console.Error.WriteLine($"No .graphqls files found in '{subgraphsPath}'.");
return 1;
}

var sourceSchemas = new List<SourceSchemaText>(files.Length);

foreach (var file in files)
{
var name = Path.GetFileNameWithoutExtension(file);
sourceSchemas.Add(new SourceSchemaText(name, File.ReadAllText(file)));
}

var options = new SchemaComposerOptions();

foreach (var sourceSchema in sourceSchemas)
{
options.SourceSchemas[sourceSchema.Name] = new SourceSchemaOptions
{
Preprocessor = new SourceSchemaPreprocessorOptions { InferKeysFromLookups = false }
};
}

var log = new CompositionLog();
var stopwatch = Stopwatch.StartNew();
var result = new SchemaComposer(sourceSchemas, options, log).Compose();
stopwatch.Stop();

var unsatisfiable = log
.Where(e => e.Code == LogEntryCodes.UnsatisfiableQueryPath)
.Select(e => e.Message)
.ToList();

Console.WriteLine($"subgraphs : {files.Length}");
Console.WriteLine($"outcome : {(result.IsSuccess ? "SUCCESS" : "FAILURE")}");
Console.WriteLine($"wall time : {stopwatch.Elapsed.TotalSeconds:0.00}s");
Console.WriteLine($"UNSATISFIABLE_QUERY_PATH: {unsatisfiable.Count}");
Console.WriteLine($"total error entries : {log.Count(e => e.Severity == LogSeverity.Error)}");
Console.WriteLine("error codes (severity=Error):");

foreach (var group in log
.Where(e => e.Severity == LogSeverity.Error)
.GroupBy(e => e.Code)
.OrderByDescending(g => g.Count()))
{
Console.WriteLine($" {group.Count(),5} {group.Key}");
Console.WriteLine($" e.g. {group.First().Message}");
}

if (printErrors)
{
foreach (var message in unsatisfiable)
{
Console.WriteLine("=====");
Console.WriteLine(message);
}
}

return result.IsSuccess ? 0 : 2;
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@

namespace Fusion.Execution.Benchmarks;

// Parsing benchmarks for the composed Netflix corpus (745 sources).
// Parsing benchmarks for the composed corpus (745 sources).
//
// This isolates the document-parsing layer for the schema SDL and both operations.
// Full plan creation on the same corpus is measured by NetflixPlanningBenchmark
// Full plan creation on the same corpus is measured by CorpusPlanningBenchmark
// (reachable now that PlannerTopologyCache scales to the 745-source graph).
[MemoryDiagnoser]
[Config(typeof(InProcessConfig))]
public class NetflixParsingBenchmark
public class CorpusParsingBenchmark
{
private static readonly string SchemaPath = NetflixCorpusPaths.SchemaPath;
private static readonly string Query1Path = NetflixCorpusPaths.Query1Path;
private static readonly string Query2Path = NetflixCorpusPaths.Query2Path;
private static readonly string SchemaPath = CorpusPaths.SchemaPath;
private static readonly string Query1Path = CorpusPaths.Query1Path;
private static readonly string Query2Path = CorpusPaths.Query2Path;

private byte[] _schemaSdl = null!;
private byte[] _query1 = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
namespace HotChocolate.Fusion.Benchmarks;

/// <summary>
/// Resolves the paths of the Netflix benchmark corpus (the big-federated-graphs checkout) without
/// Resolves the paths of the large federated graph benchmark corpus (the big-federated-graphs checkout) without
/// hard-coding machine-specific locations. The corpus root is taken from the
/// <c>BIG_FEDERATED_GRAPHS</c> environment variable when set; otherwise the directories above the
/// benchmark binary are searched for a sibling <c>big-federated-graphs</c> checkout.
/// </summary>
internal static class NetflixCorpusPaths
internal static class CorpusPaths
{
private const string EnvironmentVariable = "BIG_FEDERATED_GRAPHS";
private const string CorpusDirectoryName = "big-federated-graphs";
Expand Down Expand Up @@ -49,7 +49,7 @@ private static string ResolveRoot()
}

throw new InvalidOperationException(
$"The Netflix benchmark corpus was not found. Set the {EnvironmentVariable} "
$"The large federated graph benchmark corpus was not found. Set the {EnvironmentVariable} "
+ $"environment variable to the {CorpusDirectoryName} checkout, or place the checkout "
+ "next to an ancestor directory of the benchmark binary.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@

namespace Fusion.Execution.Benchmarks;

// Planning benchmarks for the composed Netflix corpus (edge0-v2, 745 sources).
// Planning benchmarks for the composed corpus (edge0-v2, 745 sources).
//
// One iteration is a full plan creation: OperationPlanner.CreatePlan compiles the
// rewritten operation and searches the plan space from scratch (there is no plan
// cache at this layer, so every iteration plans anew). The FusionSchemaDefinition,
// which is expensive to build on this corpus, is built once in GlobalSetup; its
// cost is reported separately on the console rather than folded into the per-plan
// numbers. See NetflixPlanningProbe for the single-shot feasibility profile.
// numbers. See CorpusPlanningProbe for the single-shot feasibility profile.
[MemoryDiagnoser]
[Config(typeof(InProcessConfig))]
public class NetflixPlanningBenchmark
public class CorpusPlanningBenchmark
{
private static readonly string SchemaPath = NetflixCorpusPaths.SchemaPath;
private static readonly string Query1Path = NetflixCorpusPaths.Query1Path;
private static readonly string Query2Path = NetflixCorpusPaths.Query2Path;
private static readonly string SchemaPath = CorpusPaths.SchemaPath;
private static readonly string Query1Path = CorpusPaths.Query1Path;
private static readonly string Query2Path = CorpusPaths.Query2Path;

// A name-safe, at-least-eight-character identifier: it is threaded into the
// names of synthesized lookup operations (Op_{shortHash}_{stepId}) and sliced
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,25 @@

namespace Fusion.Execution.Benchmarks;

// One-shot feasibility probe for the composed Netflix corpus (Phase A).
// One-shot feasibility probe for the composed corpus (Phase A).
// Not a BenchmarkDotNet benchmark: it measures cold-start schema build and a
// single plan per query with wall time + allocation deltas, so it can report a
// "wall" (non-termination / crash) on any layer as a first-class finding.
internal static class NetflixPlanningProbe
internal static class CorpusPlanningProbe
{
public static void Run(string[] args)
{
var schemaPath = args.Length > 1
? args[1]
: NetflixCorpusPaths.SchemaPath;
: CorpusPaths.SchemaPath;
var query1Path = args.Length > 2
? args[2]
: NetflixCorpusPaths.Query1Path;
: CorpusPaths.Query1Path;
var query2Path = args.Length > 3
? args[3]
: NetflixCorpusPaths.Query2Path;
: CorpusPaths.Query2Path;

Console.WriteLine("=== Netflix planning feasibility probe ===");
Console.WriteLine("=== corpus planning feasibility probe ===");
Console.WriteLine($"runtime : {Environment.Version}, ServerGC={System.Runtime.GCSettings.IsServerGC}");
Console.WriteLine($"processors : {Environment.ProcessorCount}");
Console.WriteLine($"schema : {schemaPath}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

if (args.Length > 0 && args[0] == "probe")
{
NetflixPlanningProbe.Run(args);
CorpusPlanningProbe.Run(args);
}
else if (args.Length == 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,23 @@ internal static class GenerateLookupFields
/// </param>
public static void Apply(MutableSchemaDefinition schema)
{
// A source schema can declare resolvable @key entities without a Query type (e.g. a
// mutation-only subgraph). Those entities still need lookups to be routable, so synthesize a
// Query type to host the generated @internal @lookup fields. It is removed again below if no
// lookup ends up being generated so an empty Query is never introduced.
var synthesizedQuery = false;

if (schema.QueryType is null)
{
return;
if (!HasResolvableKeyEntity(schema))
{
return;
}

var syntheticQuery = new MutableObjectTypeDefinition("Query");
schema.Types.Add(syntheticQuery);
schema.QueryType = syntheticQuery;
synthesizedQuery = true;
}

var internalDef = new MutableDirectiveDefinition("internal");
Expand Down Expand Up @@ -139,6 +153,44 @@ public static void Apply(MutableSchemaDefinition schema)
}
}
}

// If a Query type was synthesized but no lookup field was ultimately generated, drop it so
// the schema does not carry an empty Query type.
if (synthesizedQuery && schema.QueryType is { } synthetic && synthetic.Fields.Count == 0)
{
schema.Types.Remove(synthetic);
schema.QueryType = null;
}
}

private static bool HasResolvableKeyEntity(MutableSchemaDefinition schema)
{
foreach (var type in schema.Types)
{
if (type is not MutableComplexTypeDefinition complexType)
{
continue;
}

foreach (var keyDirective in complexType.Directives["key"])
{
if (!keyDirective.Arguments.TryGetValue("fields", out var fieldsValue)
|| fieldsValue is not StringValueNode)
{
continue;
}

if (keyDirective.Arguments.TryGetValue("resolvable", out var resolvableValue)
&& resolvableValue is BooleanValueNode { Value: false })
{
continue;
}

return true;
}
}

return false;
}

private static GenerateLookupFieldResult? GenerateLookupField(
Expand Down
Loading
Loading