Skip to content

perf: halve the generator's time and allocations per pass - #335

Merged
glennawatson merged 1 commit into
mainfrom
perf/generator-allocations
Sep 26, 2026
Merged

glennawatson merged 1 commit into
mainfrom
perf/generator-allocations

Conversation

@glennawatson

@glennawatson glennawatson commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The generator does about half the work per build and IDE edit, and the code it writes compiles for every thread safety mode.

  • Faster generation. A pass over 128 registrations drops from 21.0 ms / 3,980 KB to 9.6 ms / 1,824 KB, and the rerun after an unrelated edit from 18.5 ms / 3,128 KB to 8.2 ms / 1,476 KB. Calls that cannot be markers, such as Splat's own resolver.Register<T>(() => ...), are now rejected from syntax before any binding.
  • Cache-friendly pipeline. One syntax provider serves both marker methods. Pipeline models are value-only records with no symbols or locations. The generated file is built from registrations alone, so an edit that only moves a registration regenerates nothing.
  • Generated code writes through SourceWriter. Registrations are laid out one dependency per line, and a missing dependency throws through one helper per file. Lazy singletons share the method's single closure instead of adding one each. With no registrations, no registrations file is emitted.
  • RegisterLazySingleton<TInterface, TConcrete>(LazyThreadSafetyMode.X) compiles. The mode used to be written as a bare member name. Modes and contracts held in fields are now fully qualified.
  • Analyzers. StyleSharp, PerformanceSharp and SecuritySharp replace the previous style and code-quality analyzers, with the shared .editorconfig. Findings are fixed in code, not suppressed.
  • Benchmarks. A new benchmark project measures generation (cold pass and incremental edits) and the generated runtime code, with EventPipe GcVerbose traces. Results and how to read the traces are in src/benchmarks/README.md.
  • Snapshot tests without Verify. Generated files and generator diagnostics are compared with snapshots by an in-repo helper, as in ReactiveUI.Binding; run the tests with ACCEPT_SNAPSHOTS=1 to accept changes. The fixed marker source is stored once instead of by every test, and a test keeps every snapshot path within the Windows path limit on CI.
  • LF line endings. A .gitattributes checks every text file out with LF on every platform, as the analyzers require.
  • Dependencies. NuGet packages are updated; Microsoft.CodeAnalysis.CSharp stays at 4.14 as the oldest supported compiler. Third-party actions and the shared actions-common workflows are pinned to commit SHAs.

Why

A CreateSyntaxProvider transform re-runs for every matching call whenever the compilation changes, so every keystroke re-bound every call named Register in the project, lambdas included. The trace put 86% of an unrelated edit's cost there. Separately, snapshot tests never checked that the generated code compiles, which hid the thread safety mode bug.

Breaking changes

None. Registration order, resolution behaviour and exception messages are unchanged. The generated registrations file has a new layout.

How this was verified

  • Snapshot tests now also require the generated code to compile.
  • Unit tests cover the syntax predicate, extraction edge cases (aliases, static imports, referenced types, contract and mode forms), the graph diagnostics, the emitter layout, and what an edit re-runs, via tracked pipeline steps.
  • The test app runs against the new generator.
  • The benchmarks were run before and after on the same sources.

Notes for the reviewer

  • Where to start: start with Generator.cs, RoslynHelpers.IsRegistrationInvocation and MetadataExtractor.cs, then CodeGeneration/CodeGenerator.cs.
  • Mechanical, safe to skim:
    • the .verified.cs/.verified.txt snapshots (existing registration snapshots are unchanged apart from the new layout);
    • file-header and doc-comment changes;
    • most of the analyzer and test-helper edits made for the new analyzers.
  • Diagnostic snapshots: diagnostics now carry a location built from stored values, so these snapshots show the span without the source excerpt. Line and column are unchanged.
  • Interceptors are deliberately not used. The marker calls are empty methods that registration never runs through, and SetupIOC is already a direct call. Intercepting the markers would change when registration happens and on which resolver.
  • actions-common is pinned to a main commit. It has no release tags, so each of those pins carries a # main comment.

Checklist

  • I have read the Contribute guide
  • The PR title follows Conventional Commits
  • Tests cover this change, or the summary says why they do not
  • New or changed public API has XML documentation

@glennawatson
glennawatson force-pushed the perf/generator-allocations branch from 0db9d48 to aa44c88 Compare September 26, 2026 09:07
@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (943f924) to head (aa44c88).

Additional details and impacted files
@@             Coverage Diff              @@
##             main      #335       +/-   ##
============================================
+ Coverage   55.47%   100.00%   +44.52%     
============================================
  Files           7        20       +13     
  Lines         530       976      +446     
  Branches       60       171      +111     
============================================
+ Hits          294       976      +682     
+ Misses        224         0      -224     
+ Partials       12         0       -12     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@glennawatson
glennawatson merged commit 9679373 into main Sep 26, 2026
12 checks passed
@glennawatson
glennawatson deleted the perf/generator-allocations branch September 26, 2026 09:18
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.

1 participant