Skip to content

Optimize: Reduce allocations in source generator hot paths - #28

Merged
DJGosnell merged 2 commits into
masterfrom
fix/25-reduce-generator-allocations
Mar 20, 2026
Merged

Optimize: Reduce allocations in source generator hot paths#28
DJGosnell merged 2 commits into
masterfrom
fix/25-reduce-generator-allocations

Conversation

@DJGosnell

@DJGosnell DJGosnell commented Mar 20, 2026

Copy link
Copy Markdown
Member

Summary

Reduces allocations and redundant work across 14 files in the source generator's hot paths. With ~3,000 interceptors in the test project, these optimizations target per-site and per-interceptor overhead: single-pass collection partitioning, a BuilderKind enum replacing scattered string.Contains() checks, pre-grouped field lookups, StringBuilder for in-loop string mutations, mutable translation context, ConditionalWeakTable-backed ToDisplayString caching, and deferred dictionary updates.

Reason for Change

The generator produces correct output but has accumulated allocation-heavy patterns that compound across ~3,000 interceptor sites — triple-filtering collections, duplicating partitions, allocating intermediate strings in loops, copying dictionaries on every join, and performing repeated string.Contains() type classification across 5 files.

Impact

  • Finding 1 (HIGH): Single-pass loop replaces 3 sequential .Where().ToList() calls for site classification
  • Finding 2 (HIGH): Single-pass scalar/collection parameter split with pre-allocated lists
  • Finding 3 (HIGH): StringBuilder.Replace for SQL placeholder and raw SQL parameter substitution loops
  • Finding 4 (HIGH): Combined GetConstantValue + GetTypeInfo into single flow, eliminating redundant semantic model query per non-constant captured expression
  • Finding 5 (MEDIUM): Pre-grouped Dictionary<string, List<CachedExtractorField>> eliminates per-interceptor O(N) linear scans
  • Finding 6 (MEDIUM): BuilderKind enum replaces ~15 scattered string.Contains() checks across 5 files with single classification at site creation
  • Finding 7 (MEDIUM): WithJoinedEntity() mutates dictionary in place — callers reassign immediately so immutability was unnecessary
  • Finding 8 (MEDIUM): SymbolDisplayCache using ConditionalWeakTable<ITypeSymbol, string> caches display strings across ~3,000 invocations for ~20 unique entity types
  • Finding 9 (LOW): ToArray() instead of ToList() for join entity type collections that are only iterated
  • Finding 10 (LOW): Deferred update pattern — iterate dictionary directly, collect pending updates, apply after loop
  • Finding 11 (LOW): Single-pass StringBuilder escaping; shared EscapeForCSharpString helper

Migration Steps

None — internal generator changes only.

Performance Considerations

All changes reduce allocations and CPU work. The ConditionalWeakTable cache replaces far more string allocations than it introduces. The BuilderKind enum adds one byte per UsageSiteInfo but eliminates multiple string scans downstream.

Security Considerations

None.

Breaking Changes

  • Consumer-facing: None
  • Internal:
    • BuilderKind enum and property added to UsageSiteInfo
    • staticFields parameter changed from List to pre-filtered List? in GenerateWhereInterceptor, GenerateJoinedWhereInterceptor, GenerateModificationWhereInterceptor
    • ExpressionTranslationContext.WithJoinedEntity() now mutates in place instead of returning a copy
    • TryInlineConstant removed (inlined into TranslateCapturedValue with TryInlineConstantFromCompilation fallback)
    • SymbolDisplayCache utility added; Analyzers project now links Utilities/ folder

🤖 Generated with Claude Code

DJGosnell and others added 2 commits March 19, 2026 23:50
…ot 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>
…roaches

- 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>
@DJGosnell
DJGosnell force-pushed the fix/25-reduce-generator-allocations branch from 4ceefad to 7f2c6ed Compare March 20, 2026 03:50
@DJGosnell
DJGosnell merged commit 1040136 into master Mar 20, 2026
1 check passed
@DJGosnell
DJGosnell deleted the fix/25-reduce-generator-allocations branch March 20, 2026 03:54
DJGosnell added a commit that referenced this pull request Apr 29, 2026
(A) findings — addressed in code:
- Doc comments on `LikeExpr.NeedsEscape` and `EscapeLikeMetaChars` updated
  to flag dialect-aware render-time escaping (#1)
- New `MaybeDoubleBackslashes` helper: prefix/suffix in non-folded LIKE
  concat path now defensively backslash-doubled for MySQL+default (#7)
- Inline comments at both literal-pattern emission sites explain the
  single-quote-first / backslash-double-second order (#9)
- `SqlDialectConfig.ParseAttribute` returns `(Config, Schema)` tuple in
  a single pass; `ContextParser` consumes the tuple (#28)
- Code comment in `ParseAttribute` documents the SQLite default for
  missing `Dialect=` as intentional pre-existing behavior (#11)
- All 11 method bodies in `SqlAssembler.cs` now uniformly use the
  `var dialect = config.Dialect;` shadow at entry (#25)
- Extracted `IsDockerUnavailable`, `TableExistsAsync`, `ExecAsync` from
  the two MySQL test containers into shared `TestContainerHelpers.cs` (#26)
- Added `<remarks>` footgun warning to the back-compat
  `Render(SqlDialect)` overload — silently defaults the carrier flag (#33)
- Updated `MySqlBackslashEscapes` XML doc to make non-MySQL no-op
  behavior explicit; future QRY rule noted as follow-up (#37)

(B) findings — added gap-filling tests:
- `Contains_ParameterBound_BackslashEscapesTrue_NoDoubling` proves the
  parameter-bound LIKE path bypasses doubling (#20)
- `Where_Contains_LiteralBackslash_DefaultMode_ReturnsMatch` integration
  test exercises the seeded-but-unqueried `"a\b"` row through the full
  pipeline against live default-mode MySQL (#21)
- `Where_Contains_AnsiForm_NoBackslashEscapesSession_ReturnsMatch`
  integration test exercises the opt-out roundtrip with new
  `MyAnsiSessionDb` context against default-mode container with
  session-level `SET sql_mode = ...,NO_BACKSLASH_ESCAPES` (#2)

All 3,384 tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance: Reduce allocations and redundant work in source generator hot paths

1 participant