From 3941d2b946a0742bed6b04d58116f666706ade88 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 7 Apr 2026 14:52:45 -0700 Subject: [PATCH 1/2] Reduce ItemSpecModifiers.Cache size 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. --- src/Framework/ItemSpecModifiers.cs | 39 ++++++------------------------ 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/src/Framework/ItemSpecModifiers.cs b/src/Framework/ItemSpecModifiers.cs index aded6fb420a..4b31f8a77e2 100644 --- a/src/Framework/ItemSpecModifiers.cs +++ b/src/Framework/ItemSpecModifiers.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -52,35 +52,10 @@ internal static class ItemSpecModifiers DefiningProjectExtension ]; - /// - /// - /// Caches derivable item-spec modifier results for a single item spec. - /// Stored on item instances (e.g., TaskItem, ProjectItemInstance.TaskItem) - /// alongside the item spec, replacing the former string _fullPath field. - /// - /// - /// Time-based modifiers (ModifiedTime, CreatedTime, AccessedTime) and RecursiveDir - /// are intentionally excluded — time-based modifiers hit the file system and should - /// not be cached, and RecursiveDir requires wildcard context that only the caller has. - /// - /// - /// DefiningProject* modifiers are cached separately in a static shared cache - /// () keyed by the defining project path, - /// since many items share the same defining project. - /// - /// internal struct Cache { public string? FullPath; - public string? RootDir; - public string? Filename; - public string? Extension; - public string? RelativeDir; - public string? Directory; - - /// - /// Clears all cached values. Called when the item spec changes. - /// + public void Clear() => this = default; } @@ -420,19 +395,19 @@ public static string GetItemSpecModifier( return cache.FullPath ??= ComputeFullPath(currentDirectory, itemSpec); case ItemSpecModifierKind.RootDir: - return cache.RootDir ??= ComputeRootDir(cache.FullPath ??= ComputeFullPath(currentDirectory, itemSpec)); + return ComputeRootDir(cache.FullPath ??= ComputeFullPath(currentDirectory, itemSpec)); case ItemSpecModifierKind.Filename: - return cache.Filename ??= ComputeFilename(itemSpec); + return ComputeFilename(itemSpec); case ItemSpecModifierKind.Extension: - return cache.Extension ??= ComputeExtension(itemSpec); + return ComputeExtension(itemSpec); case ItemSpecModifierKind.RelativeDir: - return cache.RelativeDir ??= ComputeRelativeDir(itemSpec); + return ComputeRelativeDir(itemSpec); case ItemSpecModifierKind.Directory: - return cache.Directory ??= ComputeDirectory(cache.FullPath ??= ComputeFullPath(currentDirectory, itemSpec)); + return ComputeDirectory(cache.FullPath ??= ComputeFullPath(currentDirectory, itemSpec)); case ItemSpecModifierKind.RecursiveDir: return string.Empty; From 077a2fb184d4020e99f9dabe4e2d95f32e5dde9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Thu, 9 Apr 2026 11:34:58 +0200 Subject: [PATCH 2/2] Apply suggestion from @JanProvaznik --- src/Framework/ItemSpecModifiers.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Framework/ItemSpecModifiers.cs b/src/Framework/ItemSpecModifiers.cs index 4b31f8a77e2..f44d3c7d500 100644 --- a/src/Framework/ItemSpecModifiers.cs +++ b/src/Framework/ItemSpecModifiers.cs @@ -52,6 +52,12 @@ internal static class ItemSpecModifiers DefiningProjectExtension ]; + /// + /// Per-item cache for the FullPath modifier. Other derivable modifiers (RootDir, + /// Filename, Extension, RelativeDir, Directory) are intentionally NOT cached here + /// because TaskItem is a MarshalByRefObject on .NET Framework, and copying a + /// multi-field struct cross-AppDomain causes allocation regression in VS. + /// internal struct Cache { public string? FullPath;