From 38bbe3e41ec57c5745b968f4f4c27b1b65891814 Mon Sep 17 00:00:00 2001 From: Ciaran Liedeman Date: Sat, 27 Jun 2026 21:58:19 +0200 Subject: [PATCH] perf(strawberryshake): index data-type components by runtime name DataTypeDescriptorMapper scaled super-quadratically with operation count because, for every component of every data type, it resolved the matching descriptor with `context.Types.Single(t => t.RuntimeType.Name.Equals(name))`. `context.Types` grows with the number of operations (tens of thousands of entries for large clients), and `Single` performs a full linear scan that never short-circuits. Total cost was O(sum-of-components x |Types|), and since component count, data-type count, and the type list all grow together, the wall time exploded as ops increased. Fix: build a `Dictionary` keyed on `RuntimeType.Name` once, then resolve each component via an O(1) lookup. `RuntimeTypeMatch` keeps the first match plus a count so the original `Single(...)` semantics are preserved exactly: an absent name and a duplicated name both still throw. The overall pass is now near-linear in the number of types. Generated output is byte-for-byte unchanged: across the CSharp generator test suite (114 tests), the produced sources are identical with and without this change (verified by diffing snapshot output captured with the fix stashed vs. applied; the suite's pre-existing failures in this environment are formatter drift unrelated to this mapper and have identical counts either way). Measured on the real 672-op ILOPS client (schema reused, warm), isolating DataTypeDescriptorMapper.Map: N=200 (types=33,215): 2,804 ms -> 40 ms (~70x) N=275 (types=48,189): 11,376 ms -> 34 ms (~335x) The mapper is now flat across N instead of super-quadratic. Relates to #17 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Mappers/DataTypeDescriptorMapper.cs | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Mappers/DataTypeDescriptorMapper.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Mappers/DataTypeDescriptorMapper.cs index 02bd3c9ef53..c70d8ff0953 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Mappers/DataTypeDescriptorMapper.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Mappers/DataTypeDescriptorMapper.cs @@ -27,6 +27,26 @@ private static IEnumerable CollectDataTypes( .OfType() .ToList(); + // Index every type by its runtime type name so that component lookups below run in + // O(1) instead of scanning the whole (potentially very large) type list per component. + // A name can map to more than one type; the Single(...) semantics of the original + // lookup are preserved at resolution time (see GetTypeByRuntimeName). + var typesByRuntimeName = + new Dictionary(StringComparer.Ordinal); + + foreach (var type in context.Types) + { + var runtimeName = type.RuntimeType.Name; + if (typesByRuntimeName.TryGetValue(runtimeName, out var match)) + { + typesByRuntimeName[runtimeName] = match.WithAdditional(); + } + else + { + typesByRuntimeName[runtimeName] = new RuntimeTypeMatch(type); + } + } + var dataTypeInfos = new Dictionary(StringComparer.Ordinal); foreach (var dataType in dataTypes) @@ -74,8 +94,7 @@ private static IEnumerable CollectDataTypes( dataTypeInfo.Name, NamingConventions.CreateStateNamespace(context.Namespace), dataTypeInfo.Components - .Select(name => context.Types.Single( - t => t.RuntimeType.Name.Equals(name))) + .Select(name => GetTypeByRuntimeName(typesByRuntimeName, name)) .OfType() .ToList(), implements, @@ -83,6 +102,49 @@ private static IEnumerable CollectDataTypes( } } + private static INamedTypeDescriptor GetTypeByRuntimeName( + Dictionary typesByRuntimeName, + string runtimeName) + { + // Mirror the previous Single(...) semantics: zero matches and more than one match + // both throw, exactly as Enumerable.Single did over the full type list. + if (!typesByRuntimeName.TryGetValue(runtimeName, out var match)) + { + throw new InvalidOperationException( + $"Sequence contains no matching element for runtime type '{runtimeName}'."); + } + + if (match.Count > 1) + { + throw new InvalidOperationException( + $"Sequence contains more than one matching element for runtime type " + + $"'{runtimeName}'."); + } + + return match.First; + } + + private readonly struct RuntimeTypeMatch + { + public RuntimeTypeMatch(INamedTypeDescriptor first) + { + First = first; + Count = 1; + } + + private RuntimeTypeMatch(INamedTypeDescriptor first, int count) + { + First = first; + Count = count; + } + + public INamedTypeDescriptor First { get; } + + public int Count { get; } + + public RuntimeTypeMatch WithAdditional() => new(First, Count + 1); + } + private sealed class DataTypeInfo { public DataTypeInfo(string name, string? description)