Fix cross-AppDomain TaskItem modifier cache regression#13493
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a .NET Framework Visual Studio allocation regression caused by copying an embedded modifier-cache struct across AppDomains by moving the cache onto the item objects themselves via an internal interface.
Changes:
- Replace
ItemSpecModifiers.Cachestruct with anIItemSpecModifierCacheinterface and update modifier computation to use it. - Implement per-item modifier caching directly on
TaskItem,ProjectItem,ProjectItemInstance.TaskItem, andTaskParameterTaskItem. - Refactor
ItemSpecModifiers.GetItemSpecModifierinto caching vs non-caching overloads and extract defining-project modifier resolution.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Utilities/TaskItem.cs | Moves derivable modifier caching onto TaskItem via IItemSpecModifierCache and clears cache on ItemSpec changes. |
| src/Shared/TaskParameter.cs | Updates TaskParameterTaskItem to use IItemSpecModifierCache for modifier caching. |
| src/Framework/ItemSpecModifiers.cs | Reworks modifier computation to support caching through IItemSpecModifierCache and adds a non-caching overload. |
| src/Framework/IItemSpecModifierCache.cs | Introduces internal interface for per-item derivable modifier caching across AppDomain boundaries. |
| src/Framework.UnitTests/FileUtilities_Tests.cs | Updates tests to use the new cache interface instead of the removed struct cache. |
| src/Build/Instance/ProjectItemInstance.cs | Implements cache interface on ProjectItemInstance.TaskItem, clears cache on spec changes, and copies cache on clone. |
| src/Build/Definition/ProjectItem.cs | Implements cache interface on ProjectItem and routes built-in metadata through the new cache API. |
| src/Build/Definition/BuiltInMetadata.cs | Switches built-in metadata APIs from ref Cache to IItemSpecModifierCache. |
|
experimentally inserting to run speedometer at exp/dev/dustinca/fix-cross-appdomain-taskitem-cache branch (ETA results in 8h) |
Recently, ItemSpecModifiers.GetItemSpecModifier was changed to take a Cache struct rather than a single "string? fullPath" parameter. Unfortunately, this increases memory allocations to an unexpected degree -- especially in cross-AppDomain scenarios. This change reduces the size of the Cache struct to just a single FullPath field, which should remove the allocation regression.
3057496 to
3941d2b
Compare
|
@JanProvaznik: It looks like this PR definitely fixes the regression. Here's the experimental insertion: https://devdiv.visualstudio.com/DevDiv/_git/VS/pullrequest/726186
There seems to be some sort of regression around methods jitted, but I can't see how this PR would cause that.
|
|
Methods jitted is a limitation of the experimental insertions, these numbers definitely look good! |
|
/review |
|
✅ Expert Code Review (command) completed successfully! Expert reviewer subagent launched for PR #13493. It is running in background (agent_id: expert-review-pr-13493) and will post its own review comments, inline annotations, and final verdict directly to the PR. No separate action needed from this orchestrator. |
There was a problem hiding this comment.
24-Dimension Review Summary
Clean, well-scoped fix for a real ~200 MB allocation regression confirmed by VS experimental insertion.
What changed: The Cache struct is reduced from 6 string fields to just FullPath. RootDir, Filename, Extension, RelativeDir, and Directory are now recomputed on each access instead of cached.
Why it's correct: TaskItem inherits MarshalByRefObject on .NET Framework. Marshaling a 6-field struct cross-AppDomain required copying all reference fields, causing the regression. The recomputed modifiers are cheap (string slicing / Path helpers) relative to the marshaling cost. FullPath — the only expensive one (involves I/O-capable GetFullPath) — remains cached.
| # | Dimension | Verdict |
|---|---|---|
| 1 | Backwards Compatibility | ✅ LGTM — internal optimization only; external behavior unchanged |
| 2 | ChangeWave Discipline | ✅ LGTM — no behavioral change, no ChangeWave needed |
| 3 | Performance & Allocation | ✅ LGTM — trades cheap recomputation for expensive cross-AppDomain marshaling; validated by VS perf data |
| 4 | Test Coverage | ✅ LGTM — existing tests cover correctness; cross-AppDomain regression is validated by VS insertion |
| 5 | Error Message Quality | ✅ LGTM — N/A |
| 6 | Logging & Diagnostics | ✅ LGTM — N/A |
| 7 | String Comparison | ✅ LGTM — no changes |
| 8 | API Surface | ✅ LGTM — Cache is internal |
| 9 | Target Authoring | ✅ LGTM — N/A |
| 10 | Design | ✅ LGTM — appropriate fix for the root cause |
| 11 | Cross-Platform | ✅ LGTM — no platform-specific concerns |
| 12 | Code Simplification | ✅ LGTM — net -25 lines, simpler struct |
| 13 | Concurrency | ✅ LGTM — single-field struct; reference writes are atomic; computed value is deterministic |
| 14 | Naming | ✅ LGTM — no changes |
| 15 | SDK Integration | ✅ LGTM — N/A |
| 16 | Idiomatic C# | ✅ LGTM |
| 17 | File I/O & Paths | ✅ LGTM — no changes to path handling |
| 18 | Documentation | ✅ NIT — see inline comment suggesting doc comment on Cache to prevent re-introducing regression |
| 19 | Build Infrastructure | ✅ LGTM — N/A |
| 20 | Scope & PR Discipline | ✅ NIT — trailing whitespace on line 1 |
| 21 | Evaluation Model | ✅ LGTM — no evaluation changes |
| 22 | Correctness & Edge Cases | ✅ LGTM — compute methods are deterministic; same results as cached version |
| 23 | Dependency Management | ✅ LGTM — N/A |
| 24 | Security | ✅ LGTM — N/A |
All 24 dimensions pass — two NITs (documentation + trailing whitespace), no blocking or major issues. Approving.
Generated by Expert Code Review (command) for issue #13493 · ● 2.9M
|
/backport to vs18.6 |
|
Started backporting to |
### Description https://dev.azure.com/devdiv/DevDiv/_workitems/edit/2860443/ Recently, `ItemSpecModifiers.GetItemSpecModifier` was changed to take a `Cache` struct rather than a single "string? fullPath" parameter. Unfortunately, this increases memory allocations to an unexpected degree -- especially in cross-AppDomain scenarios. This change reduces the size of the Cache struct to just a single FullPath field, which removes the allocation regression. ### Customer Impact caught by automated VS perf tests to cause 100s of MB extra allocations ### Regression? (was it working in a previous release or preview?) Yes ### Risk (see [taxonomy](https://devdiv.visualstudio.com/DevDiv/_wiki/wikis/DevDiv.wiki/545/NET-Servicing#risk-taxonomy)) Low ### Link the PR to the original issue and to the PR to main. Backport of #13493 to vs18.6 --------- Co-authored-by: Dustin Campbell <[email protected]> Co-authored-by: Jan Provazník <[email protected]>
Updated [Microsoft.Build](https://github.com/dotnet/msbuild) from 18.6.3 to 18.7.1. <details> <summary>Release notes</summary> _Sourced from [Microsoft.Build's releases](https://github.com/dotnet/msbuild/releases)._ ## 18.7.1 ## What's Changed * Fix TraceEngine file contention deadlock in multithreaded mode by @JanProvaznik in dotnet/msbuild#13446 * Remove duplicate test cases in MultithreadableTaskAnalyzer by @Youssef1313 in dotnet/msbuild#13483 * Ensure ThreadSafeTaskAnalyzer.Tests is considered as a unit test project by @Youssef1313 in dotnet/msbuild#13481 * Fix MSBuildTask0002 analyzer warnings in already-migrated tasks by @JanProvaznik in dotnet/msbuild#13466 * Fix race conditions in task host path resolution by @AR-May in dotnet/msbuild#13485 * Migrate ToolTask and Al task to TaskEnvironment API by @OvesN in dotnet/msbuild#13423 * Bump main to 18.7, add vs18.6 to merge flow by @MichalPavlik in dotnet/msbuild#13472 * Avoid allocations in GetHashCode implementations by @DustinCampbell in dotnet/msbuild#13475 * Add PATs rotation to agentic workflow(s) by @JanKrivanek in dotnet/msbuild#13496 * Fix ASP.NET WebSite projects to copy netstandard.dll facade when required by @JanProvaznik in dotnet/msbuild#13058 * Migrate AspNetCompiler to TaskEnvironment API by @OvesN in dotnet/msbuild#13424 * Add review workflow by @JanKrivanek in dotnet/msbuild#13503 * Strengthen reviewer skill: add step-back analysis dimensions by @JanProvaznik in dotnet/msbuild#13504 * Add 'Request Speedometer Perf Run' to VS experimental insertion build policies by @Copilot in dotnet/msbuild#13505 * Remove duplicate @ prefix from issueAuthor in GitOps by @akoeplinger in dotnet/msbuild#13492 * Improve review aw by @JanKrivanek in dotnet/msbuild#13510 * Migrates unit tests to use RoslynCodeTaskFactory to enable running tests under .NET Core by @jankratochvilcz in dotnet/msbuild#13500 * Fix cross-AppDomain TaskItem modifier cache regression by @DustinCampbell in dotnet/msbuild#13493 * Discourage review agent from approving PRs by @JanKrivanek in dotnet/msbuild#13512 * Stop trying to deploy ValueTuple by @rainersigwald in dotnet/msbuild#13507 * Ad-hoc re-sign bootstrap dotnet on macOS to prevent SIGKILL by @jankratochvilcz in dotnet/msbuild#13513 * RoslynCodeTaskFactory: Log MSB3753 when task class does not implement ITask by @jankratochvilcz in dotnet/msbuild#13517 * Update gh-aw (upon mcp policy changes) by @JanKrivanek in dotnet/msbuild#13526 * Eliminate XmlChildNodes allocations in GetXmlNodeInnerContents by @nareshjo in dotnet/msbuild#13509 * Fix telemetry allocation regression: per-engine collector ownership by @JanProvaznik in dotnet/msbuild#13516 * Migrate to xunit.v3 by @Youssef1313 in dotnet/msbuild#13482 * Fix stray brace in HandleBuildCancel trace string causing MSB1025 by @Copilot in dotnet/msbuild#13535 * Bumping to 10.0.4 runtime packages by @MichalPavlik in dotnet/msbuild#13533 * Remove early return in GetCanonicalForm, always call System.IO.Path by @OvesN in dotnet/msbuild#13532 * Do not overwrite GetCopyToOutputDirectoryItemsDependsOn, just add new… by @snechaev in dotnet/msbuild#13474 * Migrate GetReferenceAssemblyPaths task to TaskEnvironment API by @OvesN in dotnet/msbuild#13495 * Stabilize ToolTaskThatTimeoutAndRetry test by @rainersigwald in dotnet/msbuild#13489 * [automated] Merge branch 'vs18.6' => 'main' by @github-actions[bot] in dotnet/msbuild#13506 * Add extra test assertions around tests by @Youssef1313 in dotnet/msbuild#13536 * Add static eval for repo skills/agents via skill-validator by @JanKrivanek in dotnet/msbuild#13537 * Migrate SGen task to Task environment API by @OvesN in dotnet/msbuild#13457 * Fix TerminalLogger assert failure for metaproj files and cached project eval ID by @OvesN in dotnet/msbuild#13480 * Filter out approving review from pr-reviewer agent by @JanKrivanek in dotnet/msbuild#13553 * Use a unique task name per invocation to tabilize RoslynCodeTaskFactory_ReuseCompilation test by @huulinhnguyen-dev in dotnet/msbuild#13551 * Brief doc on feedback/logging/data systems by @rainersigwald in dotnet/msbuild#13554 * Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID 13881982 by @dotnet-bot in dotnet/msbuild#13437 * Stage 3: Forward BuildProjectFile* callbacks from OOP TaskHost to worker node by @JanProvaznik in dotnet/msbuild#13350 * Enable TaskHost Callbacks by default by @JanProvaznik in dotnet/msbuild#13579 * Remove unactionable info from reviewer agent by @JanKrivanek in dotnet/msbuild#13578 * Enlighten RequiresFramework35SP1Assembly task for multithreaded mode by @jankratochvilcz in dotnet/msbuild#13575 * Make SdkResolver-provided environment variables take precedence over ambient environment by @Copilot in dotnet/msbuild#12655 * Add dotnet/skills marketplace and enable plugins by @Evangelink in dotnet/msbuild#13582 * The skills/agents check filters-in only touched files by @JanKrivanek in dotnet/msbuild#13586 * Fix skill-validation workflow failing when agents directory is deleted by @JeremyKuhne in dotnet/msbuild#13592 ... (truncated) Commits viewable in [compare view](dotnet/msbuild@v18.6.3...v18.7.1). </details> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>


The ItemSpecModifiers.Cache struct I introduced with #13386 caused a surprising ~200 MB allocation regression in Visual Studio scenarios on .NET Framework. Essentially, when TaskItem (a MarshalByRefObject) cached modifiers in an embedded struct, there's a huge cost to copy that struct cross-AppDomain.
To fix the problem, reduce the Cache struct to just a single field to store the full path. This should effectively bring memory allocations back in line.