Skip to content

Avoid boxing allocation of Metadata collection#13271

Merged
JanProvaznik merged 2 commits intodotnet:mainfrom
nareshjo:dev/nareshjo/metadata-enumerator-boxing
Feb 20, 2026
Merged

Avoid boxing allocation of Metadata collection#13271
JanProvaznik merged 2 commits intodotnet:mainfrom
nareshjo:dev/nareshjo/metadata-enumerator-boxing

Conversation

@nareshjo
Copy link
Copy Markdown
Contributor

🤖 AI-Generated Pull Request 🤖

This pull request was generated by the VS Perf Rel AI Agent. Please review this AI-generated PR with extra care! For more information, visit our wiki. Please share feedback with TIP Insights

  • Issue: ProcessMetadataElements iterates over itemElement.Metadata, which returns ICollection<ProjectMetadataElement>.

The underlying ProjectElementSiblingSubTypeCollection<T> exposes a zero-alloc public struct Enumerator, but the ICollection<T> return type forces foreach through the explicit IEnumerable<T>.GetEnumerator() interface implementation, boxing the struct enumerator on every call.
This fires once per item element with metadata during evaluation — a hot path in large project builds.

This matches the allocation stack flow showing EvaluateEvaluateItemGroupElementEvaluateItemElementProcessItemElementBuildIncludeOperationProcessMetadataElementsIEnumerable<T>.GetEnumeratorJIT_NewSVR::GCHeap::AllocTypeAllocated!Enumerator[ProjectMetadataElement]

microsoft.build.dll!LazyItemEvaluator.ProcessItemElement
microsoft.build.dll!LazyItemEvaluator.BuildIncludeOperation
microsoft.build.dll!LazyItemEvaluator.ProcessMetadataElements
microsoft.build.dll!ProjectElementSiblingSubTypeCollection`1.System.Collections.Generic.IEnumerable<T>.GetEnumerator
clr.dll!JIT_New
clr.dll!SVR::GCHeap::Alloc
clr.dll!SVR::gc_heap::try_allocate_more_space
TypeAllocated!Enumerator[Microsoft.Build.Construction.ProjectMetadataElement]
  • Issue type: Eliminate unnecessary struct enumerator boxing allocation in hot evaluation path

  • Proposed fix: Follow the established Children / ChildrenEnumerable pattern already in ProjectElementContainer — add an internal MetadataEnumerable property on ProjectItemElement that returns the concrete ProjectElementSiblingSubTypeCollection<ProjectMetadataElement> type, placed directly next to the public Metadata property.
    Both delegate to the same GetChildrenOfType<ProjectMetadataElement>() call. The consumer in LazyItemEvaluator.ProcessMetadataElements is updated to use MetadataEnumerable, allowing foreach to resolve the public struct GetEnumerator() directly and eliminating the boxing allocation.

Best practices wiki
See related failure in PRISM
ADO work item

Copilot AI review requested due to automatic review settings February 19, 2026 23:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes performance by eliminating boxing allocations when iterating over metadata elements in MSBuild's evaluation path. The optimization addresses a hot path where foreach over itemElement.Metadata causes the struct enumerator to be boxed due to the ICollection<T> interface. By introducing an internal MetadataEnumerable property that returns the concrete collection type, the compiler can use the public struct GetEnumerator() directly, avoiding the allocation.

Changes:

  • Added internal MetadataEnumerable property to ProjectItemElement that returns the concrete ProjectElementSiblingSubTypeCollection<ProjectMetadataElement> type
  • Updated LazyItemEvaluator.ProcessMetadataElements to use MetadataEnumerable instead of Metadata to avoid boxing

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/Build/Construction/ProjectItemElement.cs Adds internal MetadataEnumerable property following the established Children/ChildrenEnumerable pattern to expose concrete collection type
src/Build/Evaluation/LazyItemEvaluator.cs Updates hot path in ProcessMetadataElements to use MetadataEnumerable instead of Metadata to eliminate struct enumerator boxing

@JanProvaznik JanProvaznik enabled auto-merge (squash) February 20, 2026 12:42
@JanProvaznik JanProvaznik enabled auto-merge (squash) February 20, 2026 12:42
@JanProvaznik JanProvaznik merged commit 000f2a7 into dotnet:main Feb 20, 2026
10 checks passed
@nareshjo nareshjo deleted the dev/nareshjo/metadata-enumerator-boxing branch February 20, 2026 19:07
JanProvaznik pushed a commit to JanProvaznik/msbuild that referenced this pull request Feb 25, 2026
**&#129302; AI-Generated Pull Request &#129302;**

This pull request was generated by the VS Perf Rel AI Agent. Please
review this AI-generated PR with extra care! For more information, visit
our
[wiki](https://devdiv.visualstudio.com/DevDiv/_wiki/wikis/DevDiv.wiki/49206/PerfRel-Agent).
Please share feedback with [TIP
Insights](mailto:tipinsights@microsoft.com)

- Issue: `ProcessMetadataElements` iterates over `itemElement.Metadata`,
which returns `ICollection<ProjectMetadataElement>`.

The underlying `ProjectElementSiblingSubTypeCollection<T>` exposes a
zero-alloc public struct `Enumerator`, but the `ICollection<T>` return
type forces `foreach` through the explicit
`IEnumerable<T>.GetEnumerator()` interface implementation, boxing the
struct enumerator on every call.
This fires once per item element with metadata during evaluation — a hot
path in large project builds.

This matches the allocation stack flow showing `Evaluate` →
`EvaluateItemGroupElement` → `EvaluateItemElement` →
`ProcessItemElement` → `BuildIncludeOperation` →
`ProcessMetadataElements` → `IEnumerable<T>.GetEnumerator` → `JIT_New` →
`SVR::GCHeap::Alloc` →
`TypeAllocated!Enumerator[ProjectMetadataElement]`

```
microsoft.build.dll!LazyItemEvaluator.ProcessItemElement
microsoft.build.dll!LazyItemEvaluator.BuildIncludeOperation
microsoft.build.dll!LazyItemEvaluator.ProcessMetadataElements
microsoft.build.dll!ProjectElementSiblingSubTypeCollection`1.System.Collections.Generic.IEnumerable<T>.GetEnumerator
clr.dll!JIT_New
clr.dll!SVR::GCHeap::Alloc
clr.dll!SVR::gc_heap::try_allocate_more_space
TypeAllocated!Enumerator[Microsoft.Build.Construction.ProjectMetadataElement]
```

- Issue type: Eliminate unnecessary struct enumerator boxing allocation
in hot evaluation path

- Proposed fix: Follow the established `Children` / `ChildrenEnumerable`
pattern already in `ProjectElementContainer` — add an `internal
MetadataEnumerable` property on `ProjectItemElement` that returns the
concrete
`ProjectElementSiblingSubTypeCollection<ProjectMetadataElement>` type,
placed directly next to the public `Metadata` property.
Both delegate to the same `GetChildrenOfType<ProjectMetadataElement>()`
call. The consumer in `LazyItemEvaluator.ProcessMetadataElements` is
updated to use `MetadataEnumerable`, allowing `foreach` to resolve the
public struct `GetEnumerator()` directly and eliminating the boxing
allocation.

[Best practices
wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/24181/Garbage-collection-(GC))
[See related failure in
PRISM](https://prism.vsdata.io/failure/?eventType=allocation&failureType=dualdirection&failureHash=505520c2-9b99-3dc0-9f15-5b69762959c5)
[ADO work
item](https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2742617)

Co-authored-by: Naresh Joshi <Naresh.Joshi@microsoft.com>
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.

3 participants