Skip to content

Commit 5c17476

Browse files
authored
Use builder pattern for ImmutableDictionary construction in ProjectItemInstance (dotnet#13673)
## Use builder pattern for ImmutableDictionary construction in ProjectItemInstance Two places in `ProjectItemInstance` build `ImmutableDictionary` instances by feeding data into `SetItems`, which creates intermediate immutable trees that are immediately discarded. Switch both to use `ImmutableDictionaryExtensions.EmptyMetadata.ToBuilder()` (or the existing dictionary's `.ToBuilder()`) to build mutably, then produce one final immutable dictionary via `ToImmutable()`. ### Change 1: `TranslateWithInterning` deserialization `TranslateWithInterning` used a lazy LINQ enumerable (`Enumerable.Range(0, count).Select(...)`) fed into `EmptyMetadata.SetItems()`, building the immutable dictionary one entry at a time. Each entry creates a new intermediate dictionary tree, and the previous tree is discarded. Allocation traces show this path at **286 MB sampled** (2.9% of total allocations during Visual Studio solution load). Switch to `EmptyMetadata.ToBuilder()` with an eager for-loop, then `ToImmutable()`. This also eliminates the LINQ/closure allocations from `Enumerable.Range` + `Select`. ### Change 2: `MetadataCollection` multi-definition merge `MetadataCollection` merged inherited item definitions by calling `SetItems` in a loop, creating N-1 intermediate immutable trees. Switch to `.ToBuilder()` on the base dictionary, merge all definitions mutably, then `ToImmutable()`. ### Before - Lazy enumerable or loop feeds N entries/definitions into `.SetItems()` -- N intermediate `ImmutableDictionary` trees - Each intermediate tree is discarded on the next iteration ### After - Eager for-loop or merge into a `Builder` (mutable, no intermediate trees) - One final dictionary from `Builder.ToImmutable()` *Cowritten with Copilot*
2 parents 9be000e + ba7c5df commit 5c17476

1 file changed

Lines changed: 24 additions & 11 deletions

File tree

src/Build/Instance/ProjectItemInstance.cs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,18 +1255,28 @@ internal ImmutableDictionary<string, string> MetadataCollection
12551255

12561256
// Otherwise, merge remaining inherited item definitions. Front of the list is highest priority,
12571257
// so walk backwards. Skip the last entry since we've already used it as the base.
1258+
// Use a builder to merge all definitions at once instead of creating N-1 intermediate
1259+
// immutable trees from chained SetItems calls.
1260+
var builder = lastItemDefinition.ToBuilder();
1261+
12581262
for (int i = _itemDefinitions.Count - 2; i >= 0; i--)
12591263
{
1260-
lastItemDefinition = lastItemDefinition.SetItems(_itemDefinitions[i].BackingMetadata);
1264+
foreach (var kvp in _itemDefinitions[i].BackingMetadata)
1265+
{
1266+
builder[kvp.Key] = kvp.Value;
1267+
}
12611268
}
12621269

12631270
// Finally any direct metadata win.
12641271
if (_directMetadata != null)
12651272
{
1266-
lastItemDefinition = lastItemDefinition.SetItems(_directMetadata);
1273+
foreach (var kvp in _directMetadata)
1274+
{
1275+
builder[kvp.Key] = kvp.Value;
1276+
}
12671277
}
12681278

1269-
return lastItemDefinition;
1279+
return builder.ToImmutable();
12701280
}
12711281
}
12721282

@@ -1917,14 +1927,17 @@ internal void TranslateWithInterning(ITranslator translator, LookasideStringInte
19171927
int count = translator.Reader.ReadInt32();
19181928
if (count > 0)
19191929
{
1920-
IEnumerable<KeyValuePair<string, string>> metaData =
1921-
Enumerable.Range(0, count).Select(_ =>
1922-
{
1923-
int key = translator.Reader.ReadInt32();
1924-
int value = translator.Reader.ReadInt32();
1925-
return new KeyValuePair<string, string>(interner.GetString(key), interner.GetString(value));
1926-
});
1927-
_directMetadata = ImmutableDictionaryExtensions.EmptyMetadata.SetItems(metaData);
1930+
// Use a builder to avoid intermediate immutable dictionary allocations
1931+
// from feeding a lazy enumerable into SetItems.
1932+
var builder = ImmutableDictionaryExtensions.EmptyMetadata.ToBuilder();
1933+
for (int i = 0; i < count; i++)
1934+
{
1935+
int key = translator.Reader.ReadInt32();
1936+
int value = translator.Reader.ReadInt32();
1937+
builder[interner.GetString(key)] = interner.GetString(value);
1938+
}
1939+
1940+
_directMetadata = builder.ToImmutable();
19281941
}
19291942
else
19301943
{

0 commit comments

Comments
 (0)