|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using BenchmarkDotNet.Attributes;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace MSBuild.Benchmarks;
[MemoryDiagnoser]
public class DefiningProjectModifiersBenchmark
{
/// <summary>
/// Number of items per project file.
/// </summary>
private const int ItemsPerProject = 100;
/// <summary>
/// Number of times each modifier is read per item, simulating repeated metadata access
/// during evaluation, task execution, etc.
/// </summary>
private const int RepeatedReads = 10;
private string _tempDir = null!;
private ProjectInstance _singleProjectInstance = null!;
private ProjectInstance _multiProjectInstance = null!;
private TaskItem[] _taskItemsWithDefiningProject = null!;
[GlobalSetup]
public void GlobalSetup()
{
_tempDir = Path.Combine(Path.GetTempPath(), "MSBuildBenchmarks", Guid.NewGuid().ToString("N"));
string srcDir = Path.Combine(_tempDir, "src");
Directory.CreateDirectory(srcDir);
// Create dummy files.
for (int i = 0; i < ItemsPerProject; i++)
{
File.WriteAllText(Path.Combine(srcDir, $"File{i}.cs"), string.Empty);
}
// --- Single-project scenario ---
// All items defined in one project file. DefiningProjectFullPath is the same for all items,
// so a cache keyed by defining project path would hit on every item after the first.
using (var pc = new ProjectCollection())
{
var root = Microsoft.Build.Construction.ProjectRootElement.Create(pc);
root.FullPath = Path.Combine(_tempDir, "SingleProject.csproj");
var itemGroup = root.AddItemGroup();
for (int i = 0; i < ItemsPerProject; i++)
{
itemGroup.AddItem("Compile", Path.Combine(srcDir, $"File{i}.cs"));
}
var project = new Project(root, null, null, pc);
_singleProjectInstance = project.CreateProjectInstance();
}
// --- Multi-project scenario ---
// Items imported from a second project file. The main project and the imported project
// each define items, so there are two distinct DefiningProjectFullPath values.
using (var pc = new ProjectCollection())
{
// Imported project defines half the items.
var importRoot = Microsoft.Build.Construction.ProjectRootElement.Create(pc);
importRoot.FullPath = Path.Combine(_tempDir, "Imported.props");
var importItemGroup = importRoot.AddItemGroup();
for (int i = 0; i < ItemsPerProject / 2; i++)
{
importItemGroup.AddItem("Compile", Path.Combine(srcDir, $"File{i}.cs"));
}
importRoot.Save();
// Main project imports the props file and defines the other half.
var mainRoot = Microsoft.Build.Construction.ProjectRootElement.Create(pc);
mainRoot.FullPath = Path.Combine(_tempDir, "MainProject.csproj");
mainRoot.AddImport("Imported.props");
var mainItemGroup = mainRoot.AddItemGroup();
for (int i = ItemsPerProject / 2; i < ItemsPerProject; i++)
{
mainItemGroup.AddItem("Compile", Path.Combine(srcDir, $"File{i}.cs"));
}
var project = new Project(mainRoot, null, null, pc);
_multiProjectInstance = project.CreateProjectInstance();
}
// --- TaskItem instances with defining project set ---
// Copy from ProjectItemInstance so that _definingProject is populated.
var sourceItems = _singleProjectInstance.GetItems("Compile").ToArray();
_taskItemsWithDefiningProject = new TaskItem[sourceItems.Length];
for (int i = 0; i < sourceItems.Length; i++)
{
_taskItemsWithDefiningProject[i] = new TaskItem(sourceItems[i]);
}
}
[GlobalCleanup]
public void GlobalCleanup()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
// -----------------------------------------------------------------------
// ProjectItemInstance: Read all DefiningProject* modifiers on one item.
// Cold-cache baseline for all four DefiningProject modifiers.
// -----------------------------------------------------------------------
[Benchmark]
public string ProjectItemInstance_AllDefiningProjectModifiers_Once()
{
ProjectItemInstance item = _singleProjectInstance.GetItems("Compile").First();
string last = null!;
last = item.GetMetadataValue(ItemSpecModifiers.DefiningProjectFullPath);
last = item.GetMetadataValue(ItemSpecModifiers.DefiningProjectDirectory);
last = item.GetMetadataValue(ItemSpecModifiers.DefiningProjectName);
last = item.GetMetadataValue(ItemSpecModifiers.DefiningProjectExtension);
return last;
}
// -----------------------------------------------------------------------
// ProjectItemInstance: Read DefiningProjectDirectory repeatedly.
// This is the most expensive DefiningProject modifier — it resolves
// FullPath, RootDir, and Directory internally. Repeated reads on the
// same item should benefit heavily from caching.
// -----------------------------------------------------------------------
[Benchmark]
public string ProjectItemInstance_DefiningProjectDirectory_Repeated()
{
ProjectItemInstance item = _singleProjectInstance.GetItems("Compile").First();
string last = null!;
for (int i = 0; i < RepeatedReads; i++)
{
last = item.GetMetadataValue(ItemSpecModifiers.DefiningProjectDirectory);
}
return last;
}
// -----------------------------------------------------------------------
// ProjectItemInstance: Read DefiningProjectName + DefiningProjectExtension
// on all items from a single project.
// All items share the same defining project, so a per-defining-project
// cache should compute once and return cached results for the rest.
// -----------------------------------------------------------------------
[Benchmark]
public string ProjectItemInstance_DefiningProjectNameExtension_AllItems_SingleProject()
{
string last = null!;
foreach (ProjectItemInstance item in _singleProjectInstance.GetItems("Compile"))
{
last = item.GetMetadataValue(ItemSpecModifiers.DefiningProjectName);
last = item.GetMetadataValue(ItemSpecModifiers.DefiningProjectExtension);
}
return last;
}
// -----------------------------------------------------------------------
// ProjectItemInstance: Read DefiningProjectFullPath on all items from a
// multi-project scenario (main + import).
// Items come from two different defining projects, so a cache keyed by
// defining project path has two entries.
// -----------------------------------------------------------------------
[Benchmark]
public string ProjectItemInstance_DefiningProjectFullPath_AllItems_MultiProject()
{
string last = null!;
foreach (ProjectItemInstance item in _multiProjectInstance.GetItems("Compile"))
{
last = item.GetMetadataValue(ItemSpecModifiers.DefiningProjectFullPath);
}
return last;
}
// -----------------------------------------------------------------------
// ProjectItemInstance: Read DefiningProjectDirectory on all items from a
// multi-project scenario, repeated.
// The most expensive modifier across multiple passes — represents the
// worst case for uncached DefiningProject resolution.
// -----------------------------------------------------------------------
[Benchmark]
public string ProjectItemInstance_DefiningProjectDirectory_AllItems_MultiProject_Repeated()
{
string last = null!;
for (int pass = 0; pass < RepeatedReads; pass++)
{
foreach (ProjectItemInstance item in _multiProjectInstance.GetItems("Compile"))
{
last = item.GetMetadataValue(ItemSpecModifiers.DefiningProjectDirectory);
}
}
return last;
}
// -----------------------------------------------------------------------
// TaskItem: Read all DefiningProject* modifiers on one item.
// Exercises the Utilities.TaskItem → ItemSpecModifiers path with a
// defining project obtained by copying from a ProjectItemInstance.
// -----------------------------------------------------------------------
[Benchmark]
public string TaskItem_AllDefiningProjectModifiers_Once()
{
TaskItem item = _taskItemsWithDefiningProject[0];
string last = null!;
last = item.GetMetadata(ItemSpecModifiers.DefiningProjectFullPath);
last = item.GetMetadata(ItemSpecModifiers.DefiningProjectDirectory);
last = item.GetMetadata(ItemSpecModifiers.DefiningProjectName);
last = item.GetMetadata(ItemSpecModifiers.DefiningProjectExtension);
return last;
}
// -----------------------------------------------------------------------
// TaskItem: Read DefiningProjectName + DefiningProjectExtension across
// all items. All share the same defining project path.
// -----------------------------------------------------------------------
[Benchmark]
public string TaskItem_DefiningProjectNameExtension_AllItems()
{
string last = null!;
for (int i = 0; i < _taskItemsWithDefiningProject.Length; i++)
{
last = _taskItemsWithDefiningProject[i].GetMetadata(ItemSpecModifiers.DefiningProjectName);
last = _taskItemsWithDefiningProject[i].GetMetadata(ItemSpecModifiers.DefiningProjectExtension);
}
return last;
}
// -----------------------------------------------------------------------
// TaskItem: Read DefiningProjectDirectory repeatedly on one item.
// -----------------------------------------------------------------------
[Benchmark]
public string TaskItem_DefiningProjectDirectory_Repeated()
{
TaskItem item = _taskItemsWithDefiningProject[0];
string last = null!;
for (int i = 0; i < RepeatedReads; i++)
{
last = item.GetMetadata(ItemSpecModifiers.DefiningProjectDirectory);
}
return last;
}
}
|