Skip to content

Commit 4025481

Browse files
committed
Split builds into 2 steps to gather assembly references.
1 parent 8fbe3a9 commit 4025481

File tree

15 files changed

+211
-60
lines changed

15 files changed

+211
-60
lines changed

src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ private void EnsureSymbolsForNativeRuntime(DiagnoserActionParameters parameters)
229229
// We install the tool in a dedicated directory in order to always use latest version and avoid issues with broken existing configs.
230230
string toolPath = Path.Combine(Path.GetTempPath(), "BenchmarkDotNet", "symbols");
231231
DotNetCliCommand cliCommand = new(
232+
projPath: string.Empty,
232233
cliPath: cliPath,
233234
arguments: $"tool install dotnet-symbol --tool-path \"{toolPath}\"",
234235
generateResult: null,

src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace BenchmarkDotNet.Toolchains
44
{
55
public class ArtifactsPaths
66
{
7-
public static readonly ArtifactsPaths Empty = new ArtifactsPaths("", "", "", "", "", "", "", "", "", "", "", "");
7+
public static readonly ArtifactsPaths Empty = new("", "", "", "", "", "", "", "", "", "", "", "", "");
88

99
[PublicAPI] public string RootArtifactsFolderPath { get; }
1010
[PublicAPI] public string BuildArtifactsDirectoryPath { get; }
@@ -13,6 +13,7 @@ public class ArtifactsPaths
1313
[PublicAPI] public string ProgramCodePath { get; }
1414
[PublicAPI] public string AppConfigPath { get; }
1515
[PublicAPI] public string NuGetConfigPath { get; }
16+
[PublicAPI] public string BuildForReferencesProjectFilePath { get; }
1617
[PublicAPI] public string ProjectFilePath { get; }
1718
[PublicAPI] public string BuildScriptFilePath { get; }
1819
[PublicAPI] public string ExecutablePath { get; }
@@ -27,6 +28,7 @@ public ArtifactsPaths(
2728
string programCodePath,
2829
string appConfigPath,
2930
string nuGetConfigPath,
31+
string buildForReferencesProjectFilePath,
3032
string projectFilePath,
3133
string buildScriptFilePath,
3234
string executablePath,
@@ -40,6 +42,7 @@ public ArtifactsPaths(
4042
ProgramCodePath = programCodePath;
4143
AppConfigPath = appConfigPath;
4244
NuGetConfigPath = nuGetConfigPath;
45+
BuildForReferencesProjectFilePath = buildForReferencesProjectFilePath;
4346
ProjectFilePath = projectFilePath;
4447
BuildScriptFilePath = buildScriptFilePath;
4548
ExecutablePath = executablePath;

src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,33 +61,60 @@ protected override string GetBuildArtifactsDirectoryPath(BuildPartition buildPar
6161
protected override string GetProjectFilePath(string buildArtifactsDirectoryPath)
6262
=> Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.csproj");
6363

64+
protected override string GetProjectFilePathForReferences(string buildArtifactsDirectoryPath)
65+
=> Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.ForReferences.csproj");
66+
6467
protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration)
6568
=> Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker);
6669

6770
[SuppressMessage("ReSharper", "StringLiteralTypo")] // R# complains about $variables$
68-
protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger)
69-
{
70-
var benchmark = buildPartition.RepresentativeBenchmarkCase;
71-
var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger);
72-
73-
var xmlDoc = new XmlDocument();
74-
xmlDoc.Load(projectFile.FullName);
75-
var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile);
76-
77-
var content = new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt"))
71+
private string LoadCsProj(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, string projectFile, string customProperties, string sdkName)
72+
=> new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt"))
7873
.Replace("$PLATFORM$", buildPartition.Platform.ToConfig())
7974
.Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath))
80-
.Replace("$CSPROJPATH$", projectFile.FullName)
75+
.Replace("$CSPROJPATH$", projectFile)
8176
.Replace("$TFM$", TargetFrameworkMoniker)
8277
.Replace("$PROGRAMNAME$", artifactsPaths.ProgramName)
83-
.Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(benchmark.Job.Environment.Gc, buildPartition.Resolver))
78+
.Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(buildPartition.RepresentativeBenchmarkCase.Job.Environment.Gc, buildPartition.Resolver))
8479
.Replace("$COPIEDSETTINGS$", customProperties)
8580
.Replace("$SDKNAME$", sdkName)
8681
.ToString();
8782

83+
protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger)
84+
{
85+
var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger);
86+
87+
var xmlDoc = new XmlDocument();
88+
xmlDoc.Load(projectFile.FullName);
89+
var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile);
90+
91+
GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName);
92+
93+
var content = LoadCsProj(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName);
94+
8895
File.WriteAllText(artifactsPaths.ProjectFilePath, content);
8996
}
9097

98+
protected void GenerateBuildForReferencesProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, string projectFile, string customProperties, string sdkName)
99+
{
100+
var content = LoadCsProj(buildPartition, artifactsPaths, projectFile, customProperties, sdkName);
101+
102+
// We don't include the generated .notcs file when building the reference dlls, only in the final build.
103+
var xmlDoc = new XmlDocument();
104+
xmlDoc.Load(new StringReader(content));
105+
XmlElement projectElement = xmlDoc.DocumentElement;
106+
projectElement.RemoveChild(projectElement.SelectSingleNode("ItemGroup/Compile").ParentNode);
107+
108+
var startupObjectElement = projectElement.SelectSingleNode("PropertyGroup/StartupObject");
109+
startupObjectElement.ParentNode.RemoveChild(startupObjectElement);
110+
111+
// We need to change the output type to library since we're only compiling for dlls.
112+
var outputTypeElement = projectElement.SelectSingleNode("PropertyGroup/OutputType");
113+
outputTypeElement.InnerText = "Library";
114+
115+
xmlDoc.Save(artifactsPaths.BuildForReferencesProjectFilePath);
116+
}
117+
91118
/// <summary>
92119
/// returns an MSBuild string that defines Runtime settings
93120
/// </summary>

src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.IO;
3+
using System.Xml;
24
using BenchmarkDotNet.Jobs;
35
using BenchmarkDotNet.Loggers;
46
using BenchmarkDotNet.Running;
@@ -25,22 +27,69 @@ public DotNetCliBuilder(string targetFrameworkMoniker, string? customDotNetCliPa
2527

2628
public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger)
2729
{
28-
BuildResult buildResult = new DotNetCliCommand(
29-
CustomDotNetCliPath,
30-
string.Empty,
31-
generateResult,
32-
logger,
33-
buildPartition,
34-
Array.Empty<EnvironmentVariable>(),
35-
buildPartition.Timeout,
36-
logOutput: LogOutput)
30+
var cliCommand = new DotNetCliCommand(
31+
generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath,
32+
CustomDotNetCliPath,
33+
string.Empty,
34+
generateResult,
35+
logger,
36+
buildPartition,
37+
Array.Empty<EnvironmentVariable>(),
38+
buildPartition.Timeout,
39+
logOutput: LogOutput);
40+
41+
BuildResult buildResult;
42+
// Integration tests are built without dependencies, so we skip the first step.
43+
if (!buildPartition.ForcedNoDependenciesForIntegrationTests)
44+
{
45+
// We build the original project first to obtain all dlls.
46+
buildResult = cliCommand.RestoreThenBuild();
47+
48+
if (!buildResult.IsBuildSuccess)
49+
return buildResult;
50+
51+
// After the dlls are built, we gather the assembly references, then build the benchmark project.
52+
GatherReferences(generateResult.ArtifactsPaths);
53+
}
54+
55+
buildResult = cliCommand.WithProjPath(generateResult.ArtifactsPaths.ProjectFilePath)
3756
.RestoreThenBuild();
57+
3858
if (buildResult.IsBuildSuccess &&
3959
buildPartition.RepresentativeBenchmarkCase.Job.Environment.LargeAddressAware)
4060
{
4161
LargeAddressAware.SetLargeAddressAware(generateResult.ArtifactsPaths.ExecutablePath);
4262
}
4363
return buildResult;
4464
}
65+
66+
internal static void GatherReferences(ArtifactsPaths artifactsPaths)
67+
{
68+
var xmlDoc = new XmlDocument();
69+
xmlDoc.Load(artifactsPaths.ProjectFilePath);
70+
XmlElement projectElement = xmlDoc.DocumentElement;
71+
72+
// Add reference to every dll.
73+
var itemGroup = xmlDoc.CreateElement("ItemGroup");
74+
projectElement.AppendChild(itemGroup);
75+
foreach (var assemblyFile in Directory.GetFiles(artifactsPaths.BinariesDirectoryPath, "*.dll"))
76+
{
77+
var assemblyName = Path.GetFileNameWithoutExtension(assemblyFile);
78+
// The dummy csproj was used to build the original project, but it also outputs a dll for itself which we need to ignore because it's not valid.
79+
if (assemblyName == artifactsPaths.ProgramName)
80+
{
81+
continue;
82+
}
83+
var referenceElement = xmlDoc.CreateElement("Reference");
84+
itemGroup.AppendChild(referenceElement);
85+
referenceElement.SetAttribute("Include", assemblyName);
86+
var hintPath = xmlDoc.CreateElement("HintPath");
87+
referenceElement.AppendChild(hintPath);
88+
var locationNode = xmlDoc.CreateTextNode(assemblyFile);
89+
hintPath.AppendChild(locationNode);
90+
}
91+
92+
xmlDoc.Save(artifactsPaths.ProjectFilePath);
93+
}
4594
}
4695
}

src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.ComponentModel;
43
using System.IO;
54
using System.Linq;
65
using System.Text;
76
using BenchmarkDotNet.Characteristics;
8-
using BenchmarkDotNet.Environments;
97
using BenchmarkDotNet.Extensions;
108
using BenchmarkDotNet.Jobs;
119
using BenchmarkDotNet.Loggers;
@@ -20,6 +18,10 @@ public class DotNetCliCommand
2018
{
2119
[PublicAPI] public string CliPath { get; }
2220

21+
[PublicAPI] public string FilePath { get; }
22+
23+
[PublicAPI] public string TargetFrameworkMoniker { get; }
24+
2325
[PublicAPI] public string Arguments { get; }
2426

2527
[PublicAPI] public GenerateResult GenerateResult { get; }
@@ -34,11 +36,13 @@ public class DotNetCliCommand
3436

3537
[PublicAPI] public bool LogOutput { get; }
3638

37-
public DotNetCliCommand(string cliPath, string arguments, GenerateResult generateResult, ILogger logger,
39+
public DotNetCliCommand(string cliPath, string filePath, string tfm, string arguments, GenerateResult generateResult, ILogger logger,
3840
BuildPartition buildPartition, IReadOnlyList<EnvironmentVariable> environmentVariables, TimeSpan timeout, bool logOutput = false)
3941
{
4042
CliPath = cliPath ?? DotNetCliCommandExecutor.DefaultDotNetCliPath.Value;
4143
Arguments = arguments;
44+
FilePath = filePath;
45+
TargetFrameworkMoniker = tfm;
4246
GenerateResult = generateResult;
4347
Logger = logger;
4448
BuildPartition = buildPartition;
@@ -48,10 +52,13 @@ public DotNetCliCommand(string cliPath, string arguments, GenerateResult generat
4852
}
4953

5054
public DotNetCliCommand WithArguments(string arguments)
51-
=> new(CliPath, arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput);
55+
=> new(CliPath, arguments, FilePath, TargetFrameworkMoniker, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput);
56+
57+
public DotNetCliCommand WithFilePath(string filePath)
58+
=> new(CliPath, Arguments, filePath, TargetFrameworkMoniker, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput);
5259

5360
public DotNetCliCommand WithCliPath(string cliPath)
54-
=> new(cliPath, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput);
61+
=> new(cliPath, Arguments, FilePath, TargetFrameworkMoniker, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, LogOutput);
5562

5663
[PublicAPI]
5764
public BuildResult RestoreThenBuild()
@@ -69,12 +76,12 @@ public BuildResult RestoreThenBuild()
6976
if (BuildPartition.ForcedNoDependenciesForIntegrationTests)
7077
{
7178
var restoreResult = DotNetCliCommandExecutor.Execute(WithArguments(
72-
GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-dependencies", "restore-no-deps", excludeOutput: true)));
79+
GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-dependencies", "restore-no-deps", excludeOutput: true)));
7380
if (!restoreResult.IsSuccess)
7481
return BuildResult.Failure(GenerateResult, restoreResult.AllInformation);
7582

7683
return DotNetCliCommandExecutor.Execute(WithArguments(
77-
GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps", excludeOutput: true)))
84+
GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps", excludeOutput: true)))
7885
.ToBuildResult(GenerateResult);
7986
}
8087
else
@@ -110,28 +117,30 @@ public BuildResult RestoreThenBuildThenPublish()
110117

111118
public DotNetCliCommandResult Restore()
112119
=> DotNetCliCommandExecutor.Execute(WithArguments(
113-
GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, Arguments, "restore")));
120+
GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, Arguments, "restore")));
114121

115122
public DotNetCliCommandResult Build()
116123
=> DotNetCliCommandExecutor.Execute(WithArguments(
117-
GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, Arguments, "build")));
124+
GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, Arguments, "build")));
118125

119126
public DotNetCliCommandResult BuildNoRestore()
120127
=> DotNetCliCommandExecutor.Execute(WithArguments(
121-
GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-restore", "build-no-restore")));
128+
GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-restore", "build-no-restore")));
122129

123130
public DotNetCliCommandResult Publish()
124131
=> DotNetCliCommandExecutor.Execute(WithArguments(
125-
GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, Arguments, "publish")));
132+
GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, Arguments, "publish")));
126133

127134
// PublishNoBuildAndNoRestore was removed because we set --output in the build step. We use the implicit build included in the publish command.
128135
public DotNetCliCommandResult PublishNoRestore()
129136
=> DotNetCliCommandExecutor.Execute(WithArguments(
130-
GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-restore", "publish-no-restore")));
137+
GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, FilePath, $"{Arguments} --no-restore", "publish-no-restore")));
131138

132-
internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false)
139+
internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false)
133140
=> new StringBuilder()
134141
.AppendArgument("restore")
142+
.AppendArgument($"-f {tfm}")
143+
.AppendArgument($"\"{filePath}\"")
135144
.AppendArgument(string.IsNullOrEmpty(artifactsPaths.PackagesDirectoryName) ? string.Empty : $"--packages \"{artifactsPaths.PackagesDirectoryName}\"")
136145
.AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver))
137146
.AppendArgument(extraArguments)
@@ -140,9 +149,12 @@ internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPar
140149
.MaybeAppendOutputPaths(artifactsPaths, true, excludeOutput)
141150
.ToString();
142151

143-
internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false)
152+
internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false)
144153
=> new StringBuilder()
145-
.AppendArgument($"build -c {buildPartition.BuildConfiguration}") // we don't need to specify TFM, our auto-generated project contains always single one
154+
.AppendArgument("build")
155+
.AppendArgument($"\"{filePath}\"")
156+
.AppendArgument($"-f {tfm}")
157+
.AppendArgument($"-c {buildPartition.BuildConfiguration}")
146158
.AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver))
147159
.AppendArgument(extraArguments)
148160
.AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration))
@@ -151,9 +163,12 @@ internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildParti
151163
.MaybeAppendOutputPaths(artifactsPaths, excludeOutput: excludeOutput)
152164
.ToString();
153165

154-
internal static string GetPublishCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string? extraArguments = null, string? binLogSuffix = null)
166+
internal static string GetPublishCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string filePath, string tfm, string? extraArguments = null, string? binLogSuffix = null)
155167
=> new StringBuilder()
156-
.AppendArgument($"publish -c {buildPartition.BuildConfiguration}") // we don't need to specify TFM, our auto-generated project contains always single one
168+
.AppendArgument("publish")
169+
.AppendArgument($"\"{filePath}\"")
170+
.AppendArgument($"-f {tfm}")
171+
.AppendArgument($"-c {buildPartition.BuildConfiguration}")
157172
.AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver))
158173
.AppendArgument(extraArguments)
159174
.AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration))

0 commit comments

Comments
 (0)