perf(source-gen): stop InfrastructureGenerator pinning an old Compilation - #6926
Conversation
…tion InfrastructureGenerator used CompilationProvider.WithComparer(...) to skip the reference walk on syntax-only edits. When a comparer reports "equal", Roslyn's InputNode keeps the previous entry in its state table, so the driver held the first Compilation (syntax trees, bound state) alive until references changed, which in an IDE can be the whole session. Drop the comparer and memoize the reference walk in a ConditionalWeakTable keyed on the backing array of Compilation.ExternalReferences. Syntax-only edits keep that array, so the memo hits and AssemblyInfoModel equality keeps the generated source cached. Adding, removing, rebuilding or editing a reference produces a new array, so extraction still reruns (keeps #6912). The cache holds only strings, never a Compilation or MetadataReference. Co-Authored-By: Claude <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 1 remain after this review. 📝 WalkthroughWalkthroughThe generator now memoizes assembly-info extraction per driver when reference identity, compilation options, and assembly name permit reuse. It tracks selected metadata names and source shadowing. Tests cover reuse, invalidation, script compilations, generated outputs, and compilation collection. ChangesAssembly Info Memoization
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant CompilationProvider
participant AssemblyInfoMemo
participant ExtractAssemblyInfo
CompilationProvider->>AssemblyInfoMemo: Provide compilation
AssemblyInfoMemo->>AssemblyInfoMemo: Check reference identity and compilation settings
AssemblyInfoMemo->>ExtractAssemblyInfo: Extract when reuse is not permitted
ExtractAssemblyInfo-->>AssemblyInfoMemo: Return model and selected metadata names
AssemblyInfoMemo-->>CompilationProvider: Return assembly information
Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The new cache does not appear to expand which assemblies can be initialized. It checks compilation context and source-type shadowing before reuse, and reference changes trigger fresh extraction. Some unusual binding and concurrency cases remain untested. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit checks the cache at dawn Comment |
ReviewGood fix. The diagnosis is right: a I did not run the tests myself. I judged this from the diff and the PR's own measurements. (The Notes and suggestions, none blocking:
Overall this is a solid, well-tested fix and I'm happy to approve. The only substantive suggestion is documenting the |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
…e shadows a selected type GetTypeByMetadataName prefers a source type over a referenced one, so the selected typeof() targets can depend on source. The reference-keyed memo is now only stored when no candidate was shadowed, and on reuse each selected type is checked against the current source assembly; if any is shadowed, extraction reruns. Returned models now always match a fresh extraction, including across independent drivers sharing a reference array.
ReviewThis fixes a real leak. Concerns, most important first:
Nothing blocking beyond the |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 242761574f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…g options Two compilations sharing one reference array can still bind references differently when AssemblyIdentityComparer, MetadataReferenceResolver or MetadataImportOptions differ, so the memo now only reuses a model extracted under the same options. Also verify the ImmutableArray<T> single-field layout once at startup and disable the memo (never correctness, only speed) if it does not hold, and document that ExtractAssemblyInfo must stay reference-derived.
|
Follow-up to the github-actions review points, in f5d8c55:
|
|
Review of #6926 The diagnosis is right. A comparer that reports "equal" makes 1.
2. Complexity and correctness surface
3. Per-edit cost
I found no functional bugs. I'd like #1 reconsidered before merging, or at least the reasoning for keeping it documented. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f5d8c5521c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…enerator The scripting host binds submissions with the internal CompilationOptions.ReferencesSupersedeLowerVersions option, which the memo cannot compare. Skip the memo for any script compilation, and document why the key is the ImmutableArray backing array and why the holder race is benign.
|
Replies to the latest github-actions review. Changes are in e93802c. 1. The
The startup layout check disables the memo if the assumption ever breaks. That costs speed only and never gives a wrong result. 2. Per-instance cache vs static. A per-generator-instance cache would not remove the shadow check. Within one driver a source edit can still shadow a selected type, and the in-driver test Race on 3. Shadow-check cost. On reuse, Also (codex follow-up): |
|
Review of PR 6926 The diagnosis is right. A Concerns
Positives
The leak fix is correct and worth merging. I would simplify the memoization along the lines of point 2, or justify why a process-wide cache is needed. If this shape stays, please add a test that fails when |
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.71.0 to 1.72.4. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.72.4 <!-- Release notes generated using configuration in .github/release.yml at v1.72.4 --> ## What's Changed ### Other Changes * fix(source-gen): stop parameter resolver keeping every non-public test-class method (IL2111) by @thomhurst in thomhurst/TUnit#6937 ### Dependencies * chore(deps): update tunit to 1.72.0 by @thomhurst in thomhurst/TUnit#6934 **Full Changelog**: thomhurst/TUnit@v1.72.0...v1.72.4 ## 1.72.0 <!-- Release notes generated using configuration in .github/release.yml at v1.72.0 --> ## What's Changed ### Other Changes * perf(source-gen): resolve parameter reflection info through a shared runtime helper by @thomhurst in thomhurst/TUnit#6923 * perf(analyzers): trim remaining analyzer hot-path symbol lookups and binds by @thomhurst in thomhurst/TUnit#6928 * perf(mocks): move shared MockCall wrapper plumbing into runtime base classes by @thomhurst in thomhurst/TUnit#6929 * perf(source-gen): close incremental caching gaps in static property and property injection generators by @thomhurst in thomhurst/TUnit#6925 * perf(source-gen): stop InfrastructureGenerator pinning an old Compilation by @thomhurst in thomhurst/TUnit#6926 * perf(assertions-analyzers): cache assertion symbols and cut per-call work by @thomhurst in thomhurst/TUnit#6927 * perf(source-gen): emit hooks per class with direct, non-async bodies by @thomhurst in thomhurst/TUnit#6924 * test: fix flaky ObjectInitializer continuation-thread test by @thomhurst in thomhurst/TUnit#6932 * fix: CI flakes from leaked hook contexts, ActivityCollector race and Repro5700 rendezvous by @thomhurst in thomhurst/TUnit#6933 * fix(aspnetcore): honor WebApplicationFactoryClientOptions in CreateClient by @thomhurst in thomhurst/TUnit#6931 ### Dependencies * chore(deps): update tunit to 1.71.0 by @thomhurst in thomhurst/TUnit#6920 **Full Changelog**: thomhurst/TUnit@v1.71.0...v1.72.0 Commits viewable in [compare view](thomhurst/TUnit@v1.71.0...v1.72.4). </details> Updated [TUnit.AspNetCore](https://github.com/thomhurst/TUnit) from 1.71.0 to 1.72.4. <details> <summary>Release notes</summary> _Sourced from [TUnit.AspNetCore's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.72.4 <!-- Release notes generated using configuration in .github/release.yml at v1.72.4 --> ## What's Changed ### Other Changes * fix(source-gen): stop parameter resolver keeping every non-public test-class method (IL2111) by @thomhurst in thomhurst/TUnit#6937 ### Dependencies * chore(deps): update tunit to 1.72.0 by @thomhurst in thomhurst/TUnit#6934 **Full Changelog**: thomhurst/TUnit@v1.72.0...v1.72.4 ## 1.72.0 <!-- Release notes generated using configuration in .github/release.yml at v1.72.0 --> ## What's Changed ### Other Changes * perf(source-gen): resolve parameter reflection info through a shared runtime helper by @thomhurst in thomhurst/TUnit#6923 * perf(analyzers): trim remaining analyzer hot-path symbol lookups and binds by @thomhurst in thomhurst/TUnit#6928 * perf(mocks): move shared MockCall wrapper plumbing into runtime base classes by @thomhurst in thomhurst/TUnit#6929 * perf(source-gen): close incremental caching gaps in static property and property injection generators by @thomhurst in thomhurst/TUnit#6925 * perf(source-gen): stop InfrastructureGenerator pinning an old Compilation by @thomhurst in thomhurst/TUnit#6926 * perf(assertions-analyzers): cache assertion symbols and cut per-call work by @thomhurst in thomhurst/TUnit#6927 * perf(source-gen): emit hooks per class with direct, non-async bodies by @thomhurst in thomhurst/TUnit#6924 * test: fix flaky ObjectInitializer continuation-thread test by @thomhurst in thomhurst/TUnit#6932 * fix: CI flakes from leaked hook contexts, ActivityCollector race and Repro5700 rendezvous by @thomhurst in thomhurst/TUnit#6933 * fix(aspnetcore): honor WebApplicationFactoryClientOptions in CreateClient by @thomhurst in thomhurst/TUnit#6931 ### Dependencies * chore(deps): update tunit to 1.71.0 by @thomhurst in thomhurst/TUnit#6920 **Full Changelog**: thomhurst/TUnit@v1.71.0...v1.72.0 Commits viewable in [compare view](thomhurst/TUnit@v1.71.0...v1.72.4). </details> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
InfrastructureGeneratorusedCompilationProvider.WithComparer(new PreventCompilationTriggerOnEveryKeystrokeComparer())so syntax-only edits would skip the reference walk. The side effect: when a comparer says two inputs are equal, Roslyn'sInputNode.UpdateStateTablecallsTryUseCachedEntriesand keeps the old item in the node's state table. The generator driver therefore kept the firstCompilation(all its syntax trees and bound state) alive until a reference changed. In an IDE that can be the whole session.What changed
PreventCompilationTriggerOnEveryKeystrokeComparer, which nothing else used.CompilationProvider.Selectnow goes through a small per-driver memo (AssemblyInfoMemo), created inInitializeand captured by the transform. It holds one entry: the last reference list, options, assembly name and the extracted model.ImmutableArray<T> ==, which compares the backing array by reference. Syntax-only edits reuse that array, so the memo hits. Adding, removing, rebuilding or editing a reference (including an IDE project reference, per fix(source-gen): model equality covers every emitted field; infrastructure refreshes on reference changes #6912) produces a new array, so extraction reruns.#rdirectives, previous submissions) always extract fresh.GetTypeByMetadataNameprefers source). Results that depend on shadowing are never stored, and on each hit the selected types are re-checked against the current source.Compilationor a syntax tree.Why memoize at all
On TUnit.TestProject (211 references, 568 source files, Roslyn 4.14), the reference walk on a new compilation after a syntax edit takes about 6.5 ms median (9 ms p90). In the IDE that would run on every keystroke. On a memo hit, the shadow re-check takes about 0.7 ms, most of which is building the source namespace members, a cost the compiler pays anyway.
Retention measurements
The harness drove a
GeneratorDriverover 20 syntax-only edits, then ranGC.Collect()and checked aWeakReferenceto the first compilation.CompilationProvider.Selectwithout a comparerWithComparer(always-equal)Results were the same with step tracking on and off.
The memo depends on the host keeping the same
ExternalReferencesarray across edits. I checked that with anAdhocWorkspaceon Roslyn 4.14:Command-line builds are unaffected, because they only run the generator once.
Testing done
tests/TUnit.SourceGenerator.IncrementalTests(run withdotnet vstest; this project is not run in CI): 57/57 pass.EditSource_DoesNotRetainPreviousCompilationfails onmainand passes on this branch.EditSource_ShouldNotRegeneratenow expects the step to beUnchangedrather thanCached, since the cheapSelectreruns. It also asserts that the source outputs stay cached.Memo_*tests callAssemblyInfoMemodirectly (viaInternalsVisibleTo). They cover reuse across syntax edits, and re-extraction on a reference change, an options change, an assembly-name change, and for scripts. Instance identity has to be checked on the memo itself, because the driver keeps the previous output instance whenever a rerun produces an equal model. With the memo disabled, the reuse tests fail.EditSource_ShadowingSelectedType_ShouldRegenerateandEditSource_SourceSensitiveSelectionIsNotReused.FreshDriver_SameReferences_DoesNotShareMemoconfirms that separate drivers do not share the memo.AddReference_ShouldRegenerateandEditProjectReference_ShouldRegenerate(from fix(source-gen): model equality covers every emitted field; infrastructure refreshes on reference changes #6912) still pass.tests/TUnit.Core.SourceGenerator.Testson net10.0: 172 passed, 1 skipped. Generated output is unchanged.Summary by CodeRabbit