Skip to content

Commit 1040136

Browse files
DJGosnellclaude
andauthored
Optimize: Reduce allocations in source generator hot paths (#28)
* Optimize: Reduce allocations and redundant work in source generator hot paths - Single-pass partitioning for site classification and scalar/collection param split - BuilderKind enum replaces scattered string.Contains() checks across 5 files - Pre-grouped staticFields lookup eliminates per-interceptor linear scans - StringBuilder for SQL placeholder replacements in loops - Mutable ExpressionTranslationContext.WithJoinedEntity() eliminates dictionary copies - Array allocations instead of List for join entity type collections - Single-pass string escaping in SyntacticClauseTranslator and shared helper in ExpressionSyntaxTranslator Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Optimize: Address remaining performance findings with alternative approaches - Finding #4: Combine GetConstantValue + GetTypeInfo into single flow in TranslateCapturedValue, eliminating redundant semantic model query on non-constant captured expressions - Finding #8: Add SymbolDisplayCache using ConditionalWeakTable to cache ToDisplayString results across invocations for the same ITypeSymbol, avoiding ~3,000 redundant string allocations for ~20 unique entity types - Finding #10: Deferred update pattern for dictionary iteration in TokenizeCollectionParameters, avoiding .ToList() key copy by collecting pending updates and applying after iteration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3b2b0cb commit 1040136

14 files changed

Lines changed: 242 additions & 143 deletions

src/Quarry.Analyzers/Quarry.Analyzers.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<Compile Include="..\Quarry.Generator\Projection\ProjectionAnalyzer.cs" Link="Shared\Projection\ProjectionAnalyzer.cs" />
2929
<Compile Include="..\Quarry.Generator\DiagnosticDescriptors.cs" Link="Shared\DiagnosticDescriptors.cs" />
3030
<Compile Include="..\Quarry.Generator\Sql\*.cs" Link="Shared\Sql\%(Filename)%(Extension)" />
31+
<Compile Include="..\Quarry.Generator\Utilities\*.cs" Link="Shared\Utilities\%(Filename)%(Extension)" />
3132
</ItemGroup>
3233

3334
<!-- Shared project (compiled into this assembly) -->

src/Quarry.Generator/Generation/InterceptorCodeGenerator.Carrier.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Text;
33
using Quarry.Generators.Models;
44
using Quarry.Generators.Sql;
5+
using Quarry.Generators.Translation;
56

67
namespace Quarry.Generators.Generation;
78

@@ -232,7 +233,7 @@ private static void GenerateAllTransitionInterceptor(
232233
{
233234
var entityType = GetShortTypeName(site.EntityTypeName);
234235
// Determine receiver and return types based on builder kind
235-
var isDelete = site.BuilderTypeName != null && site.BuilderTypeName.Contains("Delete");
236+
var isDelete = site.BuilderKind is BuilderKind.Delete or BuilderKind.ExecutableDelete;
236237
var receiverType = isDelete
237238
? $"IDeleteBuilder<{entityType}>"
238239
: $"IUpdateBuilder<{entityType}>";
@@ -429,9 +430,16 @@ private static void EmitCarrierClauseBody(
429430
var clauseInfo = site.ClauseInfo;
430431
if (clauseInfo != null && clauseInfo.Parameters.Count > 0)
431432
{
432-
// Separate collection params (Contains-pattern) from scalar params
433-
var scalarParams = clauseInfo.Parameters.Where(p => p.ExpressionPath != "__CONTAINS_COLLECTION__").ToList();
434-
var collectionParams = clauseInfo.Parameters.Where(p => p.ExpressionPath == "__CONTAINS_COLLECTION__").ToList();
433+
// Single-pass partition: scalar vs collection params
434+
var scalarParams = new List<ParameterInfo>(clauseInfo.Parameters.Count);
435+
var collectionParams = new List<ParameterInfo>();
436+
foreach (var p in clauseInfo.Parameters)
437+
{
438+
if (p.ExpressionPath == "__CONTAINS_COLLECTION__")
439+
collectionParams.Add(p);
440+
else
441+
scalarParams.Add(p);
442+
}
435443

436444
if (hasResolvableCapturedParams && scalarParams.Any(p => p.IsCaptured && p.CanGenerateDirectPath))
437445
{

src/Quarry.Generator/Generation/InterceptorCodeGenerator.Execution.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -910,12 +910,12 @@ private static void GenerateRuntimeToDiagnosticsInterceptor(
910910
thisParamType = $"{thisType}<{entityType}>";
911911
concreteParamType = $"{concreteType}<{entityType}>";
912912
}
913-
else if (thisType.Contains("DeleteBuilder"))
913+
else if (site.BuilderKind is BuilderKind.Delete or BuilderKind.ExecutableDelete)
914914
{
915915
thisParamType = $"IExecutableDeleteBuilder<{entityType}>";
916916
concreteParamType = $"ExecutableDeleteBuilder<{entityType}>";
917917
}
918-
else if (thisType.Contains("UpdateBuilder"))
918+
else if (site.BuilderKind is BuilderKind.Update or BuilderKind.ExecutableUpdate)
919919
{
920920
thisParamType = $"IExecutableUpdateBuilder<{entityType}>";
921921
concreteParamType = $"ExecutableUpdateBuilder<{entityType}>";

src/Quarry.Generator/Generation/InterceptorCodeGenerator.Joins.cs

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site
4343
if (isChainedJoin && site.JoinedEntityTypeName != null)
4444
{
4545
var joinedType = GetShortTypeName(site.JoinedEntityTypeName);
46-
var priorTypes = joinedEntityTypeNames!.Select(GetShortTypeName).ToList();
47-
var allTypes = priorTypes.Concat(new[] { joinedType }).ToList();
46+
var priorTypes = joinedEntityTypeNames!.Select(GetShortTypeName).ToArray();
47+
var allTypes = priorTypes.Concat(new[] { joinedType }).ToArray();
4848

4949
// Determine receiver and return builder type names
50-
var receiverBuilderName = GetJoinedBuilderTypeName(priorTypes.Count);
50+
var receiverBuilderName = GetJoinedBuilderTypeName(priorTypes.Length);
5151
var concreteReceiverBuilderName = ToConcreteTypeName(receiverBuilderName);
52-
var returnBuilderName = GetJoinedBuilderTypeName(allTypes.Count);
52+
var returnBuilderName = GetJoinedBuilderTypeName(allTypes.Length);
5353
var receiverTypeArgs = string.Join(", ", priorTypes);
5454
var returnTypeArgs = string.Join(", ", allTypes);
5555
var funcTypeArgs = string.Join(", ", allTypes) + ", bool";
@@ -84,7 +84,7 @@ private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site
8484
if (isFirstInChain)
8585
{
8686
// For chained join first-in-chain, the incoming builder is the pre-join type
87-
var preJoinBuilderType = GetJoinedConcreteBuilderTypeName(priorTypes.Count, priorTypes.ToArray());
87+
var preJoinBuilderType = GetJoinedConcreteBuilderTypeName(priorTypes.Length, priorTypes);
8888
EmitCarrierChainEntry(sb, carrier, prebuiltChain, site, preJoinBuilderType, joinReturnType, null, siteParams, globalParamOffset);
8989
}
9090
else
@@ -279,11 +279,11 @@ private static string GetJoinedBuilderTypeName(int entityCount)
279279
/// <summary>
280280
/// Generates a Where() interceptor for joined query builders.
281281
/// </summary>
282-
private static void GenerateJoinedWhereInterceptor(StringBuilder sb, UsageSiteInfo site, string methodName, List<CachedExtractorField> staticFields, int? clauseBit = null,
282+
private static void GenerateJoinedWhereInterceptor(StringBuilder sb, UsageSiteInfo site, string methodName, List<CachedExtractorField>? methodFields, int? clauseBit = null,
283283
PrebuiltChainInfo? prebuiltChain = null, bool isFirstInChain = false, CarrierClassInfo? carrier = null)
284284
{
285-
var entityTypes = site.JoinedEntityTypeNames!.Select(GetShortTypeName).ToList();
286-
var builderName = GetJoinedBuilderTypeName(entityTypes.Count);
285+
var entityTypes = site.JoinedEntityTypeNames!.Select(GetShortTypeName).ToArray();
286+
var builderName = GetJoinedBuilderTypeName(entityTypes.Length);
287287
var concreteBuilderName = ToConcreteTypeName(builderName);
288288
var thisType = site.BuilderTypeName;
289289
var returnType = ToReturnTypeName(thisType);
@@ -295,7 +295,7 @@ private static void GenerateJoinedWhereInterceptor(StringBuilder sb, UsageSiteIn
295295
var exprParamName = hasResolvableCapturedParams ? "expr" : "_";
296296
var bitSuffix = ClauseBitSuffix(clauseBit);
297297

298-
var methodFields = staticFields.Where(f => f.MethodName == methodName).ToList();
298+
methodFields ??= new List<CachedExtractorField>();
299299
if (methodFields.Count > 0)
300300
{
301301
sb.AppendLine($" [UnconditionalSuppressMessage(\"Trimming\", \"IL2075\",");
@@ -312,8 +312,8 @@ private static void GenerateJoinedWhereInterceptor(StringBuilder sb, UsageSiteIn
312312
{
313313
// Tuple element types could not be resolved by the semantic model (generated entity types).
314314
// Use arity-matching generic parameters so the compiler infers the concrete TResult.
315-
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Count).Select(i => $"T{i}"));
316-
var constraints = string.Join(" ", Enumerable.Range(1, entityTypes.Count).Select(i => $"where T{i} : class"));
315+
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Length).Select(i => $"T{i}"));
316+
var constraints = string.Join(" ", Enumerable.Range(1, entityTypes.Length).Select(i => $"where T{i} : class"));
317317
sb.AppendLine($" public static {builderName}<{allTypeParams}, TResult> {methodName}<{allTypeParams}, TResult>(");
318318
sb.AppendLine($" this {thisBuilderName}<{allTypeParams}, TResult> builder,");
319319
sb.AppendLine($" Expression<Func<{allTypeParams}, bool>> {exprParamName}) {constraints}");
@@ -353,7 +353,7 @@ private static void GenerateJoinedWhereInterceptor(StringBuilder sb, UsageSiteIn
353353
globalParamOffset += clause.Site.ClauseInfo.Parameters.Count;
354354
}
355355

356-
var joinedBuilderTypeName = GetJoinedConcreteBuilderTypeName(entityTypes.Count, entityTypes.ToArray());
356+
var joinedBuilderTypeName = GetJoinedConcreteBuilderTypeName(entityTypes.Length, entityTypes);
357357
var returnInterface = site.ResultTypeName != null
358358
? $"{builderName}<{typeArgs}, {SanitizeTupleResultType(GetShortTypeName(site.ResultTypeName))}>"
359359
: $"{builderName}<{typeArgs}>";
@@ -381,7 +381,7 @@ private static void GenerateJoinedWhereInterceptor(StringBuilder sb, UsageSiteIn
381381
var isBrokenTuple = resultType.Contains("object") && resultType.StartsWith("(");
382382
if (isBrokenTuple)
383383
{
384-
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Count).Select(i => $"T{i}"));
384+
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Length).Select(i => $"T{i}"));
385385
sb.AppendLine($" var __b = Unsafe.As<{concreteBuilderName}<{allTypeParams}, TResult>>(builder);");
386386
}
387387
else
@@ -481,8 +481,8 @@ private static void GenerateJoinedWhereInterceptor(StringBuilder sb, UsageSiteIn
481481
private static void GenerateJoinedOrderByInterceptor(StringBuilder sb, UsageSiteInfo site, string methodName, int? clauseBit = null,
482482
PrebuiltChainInfo? prebuiltChain = null, bool isFirstInChain = false, CarrierClassInfo? carrier = null)
483483
{
484-
var entityTypes = site.JoinedEntityTypeNames!.Select(GetShortTypeName).ToList();
485-
var builderName = GetJoinedBuilderTypeName(entityTypes.Count);
484+
var entityTypes = site.JoinedEntityTypeNames!.Select(GetShortTypeName).ToArray();
485+
var builderName = GetJoinedBuilderTypeName(entityTypes.Length);
486486
var concreteBuilderName = ToConcreteTypeName(builderName);
487487
var thisType = site.BuilderTypeName;
488488
var returnType = ToReturnTypeName(thisType);
@@ -516,8 +516,8 @@ private static void GenerateJoinedOrderByInterceptor(StringBuilder sb, UsageSite
516516
{
517517
// Arity-matching: include all type params with class constraints.
518518
// Also used when tuple result type has unresolved element types (broken tuple).
519-
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Count).Select(i => $"T{i}"));
520-
var constraints = string.Join(" ", Enumerable.Range(1, entityTypes.Count).Select(i => $"where T{i} : class"));
519+
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Length).Select(i => $"T{i}"));
520+
var constraints = string.Join(" ", Enumerable.Range(1, entityTypes.Length).Select(i => $"where T{i} : class"));
521521
sb.AppendLine($" public static {builderName}<{allTypeParams}, TResult> {methodName}<{allTypeParams}, TResult, TKey>(");
522522
sb.AppendLine($" this {thisBuilderName}<{allTypeParams}, TResult> builder,");
523523
sb.AppendLine($" Expression<Func<{allTypeParams}, TKey>> _,");
@@ -535,8 +535,8 @@ private static void GenerateJoinedOrderByInterceptor(StringBuilder sb, UsageSite
535535
}
536536
else
537537
{
538-
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Count).Select(i => $"T{i}"));
539-
var constraints = string.Join(" ", Enumerable.Range(1, entityTypes.Count).Select(i => $"where T{i} : class"));
538+
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Length).Select(i => $"T{i}"));
539+
var constraints = string.Join(" ", Enumerable.Range(1, entityTypes.Length).Select(i => $"where T{i} : class"));
540540
sb.AppendLine($" public static {builderName}<{allTypeParams}> {methodName}<{allTypeParams}, TKey>(");
541541
sb.AppendLine($" this {thisBuilderName}<{allTypeParams}> builder,");
542542
sb.AppendLine($" Expression<Func<{allTypeParams}, TKey>> _,");
@@ -566,7 +566,7 @@ private static void GenerateJoinedOrderByInterceptor(StringBuilder sb, UsageSite
566566
globalParamOffset += clause.Site.ClauseInfo.Parameters.Count;
567567
}
568568

569-
var joinedBuilderTypeName = GetJoinedConcreteBuilderTypeName(entityTypes.Count, entityTypes.ToArray());
569+
var joinedBuilderTypeName = GetJoinedConcreteBuilderTypeName(entityTypes.Length, entityTypes);
570570
var returnInterface = site.ResultTypeName != null
571571
? $"{builderName}<{typeArgs}, {SanitizeTupleResultType(GetShortTypeName(site.ResultTypeName))}>"
572572
: $"{builderName}<{typeArgs}>";
@@ -595,7 +595,7 @@ private static void GenerateJoinedOrderByInterceptor(StringBuilder sb, UsageSite
595595
}
596596
else if (site.ResultTypeName != null)
597597
{
598-
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Count).Select(i => $"T{i}"));
598+
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Length).Select(i => $"T{i}"));
599599
sb.AppendLine($" var __b = Unsafe.As<{concreteBuilderName}<{allTypeParams}, TResult>>(builder);");
600600
}
601601
else if (keyType != null)
@@ -604,7 +604,7 @@ private static void GenerateJoinedOrderByInterceptor(StringBuilder sb, UsageSite
604604
}
605605
else
606606
{
607-
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Count).Select(i => $"T{i}"));
607+
var allTypeParams = string.Join(", ", Enumerable.Range(1, entityTypes.Length).Select(i => $"T{i}"));
608608
sb.AppendLine($" var __b = Unsafe.As<{concreteBuilderName}<{allTypeParams}>>(builder);");
609609
}
610610
var builderVar = "__b";
@@ -654,8 +654,8 @@ private static void GenerateJoinedOrderByInterceptor(StringBuilder sb, UsageSite
654654
private static void GenerateJoinedSelectInterceptor(StringBuilder sb, UsageSiteInfo site, string methodName,
655655
PrebuiltChainInfo? prebuiltChain = null, bool isFirstInChain = false, CarrierClassInfo? carrier = null)
656656
{
657-
var entityTypes = site.JoinedEntityTypeNames!.Select(GetShortTypeName).ToList();
658-
var builderName = GetJoinedBuilderTypeName(entityTypes.Count);
657+
var entityTypes = site.JoinedEntityTypeNames!.Select(GetShortTypeName).ToArray();
658+
var builderName = GetJoinedBuilderTypeName(entityTypes.Length);
659659
var concreteBuilderName = ToConcreteTypeName(builderName);
660660
var thisType = site.BuilderTypeName;
661661
var returnType = ToReturnTypeName(thisType);
@@ -692,7 +692,7 @@ private static void GenerateJoinedSelectInterceptor(StringBuilder sb, UsageSiteI
692692
if (clause.Site.ClauseInfo != null)
693693
globalParamOffset += clause.Site.ClauseInfo.Parameters.Count;
694694
}
695-
var joinedBuilderType = GetJoinedConcreteBuilderTypeName(entityTypes.Count, entityTypes.ToArray());
695+
var joinedBuilderType = GetJoinedConcreteBuilderTypeName(entityTypes.Length, entityTypes);
696696
int? clauseBit = null;
697697
EmitCarrierChainEntry(sb, carrier, prebuiltChain, site, joinedBuilderType, targetInterface, clauseBit, siteParams, globalParamOffset);
698698
}

src/Quarry.Generator/Generation/InterceptorCodeGenerator.Modifications.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal static partial class InterceptorCodeGenerator
2020
/// </summary>
2121
private static void GenerateModificationWhereInterceptor(
2222
StringBuilder sb, UsageSiteInfo site, string methodName,
23-
List<CachedExtractorField> staticFields, bool isDelete,
23+
List<CachedExtractorField>? methodFields, bool isDelete,
2424
int? clauseBit = null, PrebuiltChainInfo? prebuiltChain = null,
2525
bool isFirstInChain = false, CarrierClassInfo? carrier = null)
2626
{
@@ -34,7 +34,7 @@ private static void GenerateModificationWhereInterceptor(
3434
var exprParamName = hasResolvableCapturedParams ? "expr" : "_";
3535

3636
// Emit trim suppression if we'll use FieldInfo.GetValue inline
37-
var methodFields = staticFields.Where(f => f.MethodName == methodName).ToList();
37+
methodFields ??= new List<CachedExtractorField>();
3838
if (methodFields.Count > 0)
3939
{
4040
sb.AppendLine($" [UnconditionalSuppressMessage(\"Trimming\", \"IL2075\",");
@@ -43,7 +43,7 @@ private static void GenerateModificationWhereInterceptor(
4343

4444
var thisType = site.BuilderTypeName;
4545
var returnType = ToReturnTypeName(thisType);
46-
var isExecutable = thisType.Contains($"Executable{modKind}Builder");
46+
var isExecutable = site.BuilderKind is BuilderKind.ExecutableDelete or BuilderKind.ExecutableUpdate;
4747
var concreteType = ToConcreteTypeName(returnType);
4848
var receiverType = $"{thisType}<{entityType}>";
4949

@@ -238,7 +238,7 @@ private static void GenerateUpdateSetInterceptor(StringBuilder sb, UsageSiteInfo
238238
// Determine the receiver type: UpdateBuilder<T> or ExecutableUpdateBuilder<T> (or interface variants)
239239
var thisType = site.BuilderTypeName;
240240
var returnType = ToReturnTypeName(thisType);
241-
var isExecutable = thisType.Contains("ExecutableUpdateBuilder");
241+
var isExecutable = site.BuilderKind is BuilderKind.ExecutableUpdate;
242242
var concreteBaseName = isExecutable ? "ExecutableUpdateBuilder" : "UpdateBuilder";
243243
var returnInterfaceBaseName = "I" + concreteBaseName;
244244
// Interceptors for generic methods on generic types need arity = type params + method params.
@@ -312,7 +312,7 @@ private static void GenerateUpdateSetPocoInterceptor(StringBuilder sb, UsageSite
312312

313313
// Determine the receiver type: UpdateBuilder<T> or ExecutableUpdateBuilder<T> (or interface variants)
314314
var thisType = site.BuilderTypeName;
315-
var isExecutable = thisType.Contains("ExecutableUpdateBuilder");
315+
var isExecutable = site.BuilderKind is BuilderKind.ExecutableUpdate;
316316
var concreteBaseName = isExecutable ? "ExecutableUpdateBuilder" : "UpdateBuilder";
317317
var returnInterfaceBaseName = "I" + concreteBaseName;
318318
// Set(T entity) has no method-level type params, only class-level T.

0 commit comments

Comments
 (0)