Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/BenchmarkDotNet/Extensions/XDocumentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#if NETSTANDARD2_0
using System.Xml.Linq;

namespace BenchmarkDotNet.Extensions;

internal static class XDocumentExtensions
{
/// <summary>
/// Helper extension method for netstandard2.0.
/// It provides an API `SaveAsync`, but the actual processing is performed synchronously.
/// </summary>
public static async ValueTask SaveAsync(this XDocument doc, Stream stream, SaveOptions options, CancellationToken cancellationToken)
{
doc.Save(stream, options);
}
}
#endif
9 changes: 1 addition & 8 deletions src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,7 @@ private static async ValueTask<Summary[]> RunCore(BenchmarkRunInfo[] benchmarkRu
var buildPartitions = BenchmarkPartitioner.CreateForBuild(supportedBenchmarks, resolver);
eventProcessor.OnStartBuildStage(buildPartitions);

var sequentialBuildPartitions = buildPartitions.Where(partition =>
partition.Benchmarks.Any(x => x.Config.Options.IsSet(ConfigOptions.DisableParallelBuild))
// .Net SDK 8+ supports ArtifactsPath for proper parallel builds.
// Older SDKs may produce builds with incorrect bindings if more than 1 partition is built in parallel.
|| (partition.RepresentativeBenchmarkCase.GetToolchain().Generator is DotNetCliGenerator
&& partition.RepresentativeBenchmarkCase.GetRuntime().RuntimeMoniker.GetRuntimeVersion().Major < 8)
)
.ToArray();
var sequentialBuildPartitions = buildPartitions.Where(partition => partition.Benchmarks.Any(x => x.Config.Options.IsSet(ConfigOptions.DisableParallelBuild))).ToArray();
var parallelBuildPartitions = buildPartitions.Except(sequentialBuildPartitions).ToArray();

Dictionary<BuildPartition, BuildResult> buildResults = parallelBuildPartitions.Length > 0
Expand Down
11 changes: 11 additions & 0 deletions src/BenchmarkDotNet/Templates/BenchmarkDotNet.Build.props.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project>
<!-- Define properties for DllGatherer.csproj. -->
<PropertyGroup Condition="'$(MSBuildProjectFile)' == '$DLLGATHERER_PROJ$'">
</PropertyGroup>

<!-- Define properties for BenchmarkDotNet.Autogenerated.csproj. -->
<PropertyGroup Condition="'$(MSBuildProjectFile)' == '$AUTOGENERATED_PROJ$'">
<OutputPath>$OUTPUT_PATH$</OutputPath>
<PublishDir>$PUBLISH_DIR$</PublishDir>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>
<!-- Custom MSBuild target for DllGatherer. -->
<Target Name="BdnWriteOutputPath" Condition="'$(MSBuildProjectFile)' == '$DLLGATHERER_PROJ$'" AfterTargets="Build">
<WriteLinesToFile
File="$(MSBuildThisFileDirectory)DllGatherer.txt"
Lines="$(OutputPath)"
Overwrite="true" />
</Target>
</Project>
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet/Templates/CsProj.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<Project Sdk="$SDKNAME$">
<Import Project="$(MSBuildThisFileDirectory)BenchmarkDotNet.Build.props" />

<PropertyGroup>
<AssemblyTitle>$PROGRAMNAME$</AssemblyTitle>
Expand Down Expand Up @@ -36,4 +37,5 @@

$RUNTIMESETTINGS$

<Import Project="$(MSBuildThisFileDirectory)BenchmarkDotNet.Build.targets" />
</Project>
7 changes: 4 additions & 3 deletions src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" DefaultTarget="Publish">
<Project Sdk="Microsoft.NET.Sdk" DefaultTarget="Publish">
<Import Project="$(MSBuildThisFileDirectory)BenchmarkDotNet.Build.props" />

<PropertyGroup>
<OriginalCSProjPath>$CSPROJPATH$</OriginalCSProjPath>
<MonoPropsPath>$([System.IO.Path]::ChangeExtension('$(OriginalCSProjPath)', '.Mono.props'))</MonoPropsPath>
Expand All @@ -19,7 +21,6 @@
<PublishTrimmed>false</PublishTrimmed>
<AssemblyName>$PROGRAMNAME$</AssemblyName>
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<StartupObject>BenchmarkDotNet.Autogenerated.UniqueProgramName</StartupObject>
<SelfContained>true</SelfContained>
Expand Down Expand Up @@ -73,7 +74,6 @@
</AotInputAssemblies>
</ItemGroup>


<MonoAOTCompiler
CompilerBinaryPath="$COMPILERBINARYPATH$"
Mode="Normal"
Expand All @@ -93,4 +93,5 @@
</Target>

<Import Project="$(MonoTargetsPath)" Condition="Exists($(MonoTargetsPath))" />
<Import Project="$(MSBuildThisFileDirectory)BenchmarkDotNet.Build.targets" />
</Project>
5 changes: 3 additions & 2 deletions src/BenchmarkDotNet/Templates/R2RCsProj.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk" DefaultTarget="Publish">
<Import Project="$(MSBuildThisFileDirectory)BenchmarkDotNet.Build.props" />

<!-- Properties same as standard CsProj.txt template -->
<PropertyGroup>
Expand All @@ -16,8 +17,6 @@
<!-- fix for NETSDK1150: https://docs.microsoft.com/en-us/dotnet/core/compatibility/sdk/5.0/referencing-executable-generates-error -->
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
<StartupObject>BenchmarkDotNet.Autogenerated.UniqueProgramName</StartupObject>
<!-- workaround for 'Found multiple publish output files with the same relative path.' error -->
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>

<PropertyGroup>
Expand Down Expand Up @@ -61,4 +60,6 @@
</Crossgen2Pack>
</ItemGroup>
</Target>

<Import Project="$(MSBuildThisFileDirectory)BenchmarkDotNet.Build.targets" />
</Project>
14 changes: 12 additions & 2 deletions src/BenchmarkDotNet/Templates/WasmCsProj.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<Project Sdk="$SDKNAME$" DefaultTargets="publish">
<Project Sdk="$SDKNAME$" DefaultTargets="publish">
<Import Project="$(MSBuildThisFileDirectory)BenchmarkDotNet.Build.props" />

<PropertyGroup>
<OriginalCSProjPath>$CSPROJPATH$</OriginalCSProjPath>
<WasmPropsPath>$([System.IO.Path]::ChangeExtension('$(OriginalCSProjPath)', '.Wasm.props'))</WasmPropsPath>
Expand All @@ -23,10 +25,17 @@ $CORECLR_OVERRIDES$
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
<RunAOTCompilation>$RUN_AOT$</RunAOTCompilation>
<PublishTrimmed>$(RunAOTCompilation)</PublishTrimmed>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
<EnableDefaultWasmAssembliesToBundle>false</EnableDefaultWasmAssembliesToBundle>
<StartupObject>BenchmarkDotNet.Autogenerated.UniqueProgramName</StartupObject>

<!-- Suppress error NETSDK1152: Found multiple publish output files with the same relative path. -->
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>

<!-- Suppress error NETSDK1085: The 'NoBuild' property was set to true but the 'Build' target was invoked. -->
<!-- It's occurred when running `dotnet publish` command on Wasm project. -->
<!-- These errors are occurred on Analyzers/CodeFixers projects. So it's safe to ignore. -->
<BuildProjectReferences Condition="'$(NoBuild)' == 'true'">false</BuildProjectReferences>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -60,4 +69,5 @@ $CORECLR_OVERRIDES$
</Target>

<Import Project="$(WasmTargetsPath)" Condition="Exists($(WasmTargetsPath))" />
<Import Project="$(MSBuildThisFileDirectory)BenchmarkDotNet.Build.targets" />
</Project>
100 changes: 64 additions & 36 deletions src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ namespace BenchmarkDotNet.Toolchains.CsProj
public class CsProjGenerator : DotNetCliGenerator, IEquatable<CsProjGenerator>
{
private const string DefaultSdkName = "Microsoft.NET.Sdk";
internal const string DllGathererProjectName = "DllGatherer.csproj";
internal const string AutogeneratedProjectName = "BenchmarkDotNet.Autogenerated.csproj";

private static readonly ImmutableArray<string> SettingsWeWantToCopy = new[]
{
Expand Down Expand Up @@ -68,11 +70,40 @@ protected override string GetBuildArtifactsDirectoryPath(BuildPartition buildPar
}

protected override string GetProjectFilePath(string buildArtifactsDirectoryPath)
=> Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.csproj");
=> Path.Combine(buildArtifactsDirectoryPath, AutogeneratedProjectName);

protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration)
=> Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker);

protected override async ValueTask GenerateCustomBuildHooksAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken)
{
var benchmark = buildPartition.RepresentativeBenchmarkCase;
var benchmarkProjectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger);

var buildArtifactsDirectoryPath = artifactsPaths.BuildArtifactsDirectoryPath;

// Trim tfm part from binariesDirectoryPath on IntegrationTests. (When ArtifactsPath is not used, it's appended automatically)
var binariesDirectoryPath = buildPartition.ForcedNoDependenciesForIntegrationTests
? artifactsPaths.BinariesDirectoryPath.Substring(0, artifactsPaths.BinariesDirectoryPath.Length - TargetFrameworkMoniker.Length - 1)
: artifactsPaths.BinariesDirectoryPath;

var propsPath = Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Build.props");
var props = GetTemplateContent("BenchmarkDotNet.Build.props.txt");
await File.WriteAllTextAsync(propsPath, props.ToString(), cancellationToken).ConfigureAwait(false);

var targetsPath = Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Build.targets");
var targets = GetTemplateContent("BenchmarkDotNet.Build.targets.txt");
await File.WriteAllTextAsync(targetsPath, targets.ToString(), cancellationToken).ConfigureAwait(false);

string GetTemplateContent(string templateName)
=> new StringBuilder(ResourceHelper.LoadTemplate(templateName))
.Replace("$DLLGATHERER_PROJ$", DllGathererProjectName)
.Replace("$AUTOGENERATED_PROJ$", AutogeneratedProjectName)
.Replace("$OUTPUT_PATH$", binariesDirectoryPath)
.Replace("$PUBLISH_DIR$", artifactsPaths.PublishDirectoryPath)
.ToString();
}

protected override ValueTask GenerateBuildScriptAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, CancellationToken cancellationToken)
{
string projectFilePath = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, NullLogger.Instance).FullName;
Expand All @@ -89,6 +120,7 @@ protected override ValueTask GenerateBuildScriptAsync(BuildPartition buildPartit

protected override async ValueTask GenerateProjectAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken)
{
// Generate Autogenerated project file.
await File.WriteAllTextAsync(
artifactsPaths.ProjectFilePath,
await GenerateBuildProject(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false),
Expand All @@ -97,35 +129,15 @@ await GenerateBuildProject(buildPartition, artifactsPaths, logger, cancellationT
.ConfigureAwait(false);

// Integration tests are built without dependencies, so we skip gathering dlls.
if (!buildPartition.ForcedNoDependenciesForIntegrationTests)
{
await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false);
}
}

private async ValueTask<string> GenerateBuildProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken)
{
var benchmark = buildPartition.RepresentativeBenchmarkCase;
var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger);

var xmlDoc = new XmlDocument();
xmlDoc.Load(projectFile.FullName);
var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile);
if (buildPartition.ForcedNoDependenciesForIntegrationTests)
return;

return new StringBuilder(await ResourceHelper.LoadTemplateAsync("CsProj.txt", cancellationToken).ConfigureAwait(false))
.Replace("$PLATFORM$", buildPartition.Platform.ToConfig())
.Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath))
.Replace("$CSPROJPATH$", projectFile.FullName)
.Replace("$TFM$", TargetFrameworkMoniker)
.Replace("$PROGRAMNAME$", artifactsPaths.ProgramName)
.Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(benchmark.Job.Environment.Gc, buildPartition.Resolver))
.Replace("$COPIEDSETTINGS$", customProperties)
.Replace("$SDKNAME$", sdkName)
.ToString();
// Gather DLL references and modify Autogenerated project file
await GatherReferencesAsync(buildPartition, artifactsPaths, logger, cancellationToken).ConfigureAwait(false);
}

private static string GetDllGathererPath(string filePath)
=> Path.Combine(Path.GetDirectoryName(filePath)!, $"DllGatherer{Path.GetExtension(filePath)}");
=> Path.Combine(Path.GetDirectoryName(filePath)!, $"{Path.GetFileNameWithoutExtension(DllGathererProjectName)}{Path.GetExtension(filePath)}");

protected async ValueTask GatherReferencesAsync(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken)
Comment thread
timcassell marked this conversation as resolved.
{
Expand All @@ -141,11 +153,7 @@ protected async ValueTask GatherReferencesAsync(BuildPartition buildPartition, A

using (var gathererStream = File.Create(gathererProject))
{
#if NETSTANDARD2_0
doc.Save(gathererStream, SaveOptions.None);
#else
await doc.SaveAsync(gathererStream, SaveOptions.None, cancellationToken).ConfigureAwait(false);
#endif
}

await File.WriteAllTextAsync(emptyMainFile, """
Expand Down Expand Up @@ -186,13 +194,16 @@ public static int Main(string[] args)
return;
}

// Gets bin directory of DllGather project from output file that generated by custom MSBuild target.
var binDirectory = File.ReadAllText(Path.Combine(artifactsPaths.BuildArtifactsDirectoryPath, "DllGatherer.txt")).Trim();

// Delete the dll from the gatherer project to prevent duplicate references.
File.Delete(Path.Combine(artifactsPaths.BinariesDirectoryPath, $"{artifactsPaths.ProgramName}.dll"));
File.Delete(Path.Combine(binDirectory, $"{artifactsPaths.ProgramName}.dll"));

doc = XDocument.Load(artifactsPaths.ProjectFilePath);
var itemGroup = new XElement("ItemGroup");
doc.Root!.Add(itemGroup);
foreach (var assemblyFile in Directory.GetFiles(artifactsPaths.BinariesDirectoryPath, "*.dll"))
foreach (var assemblyFile in Directory.GetFiles(binDirectory, "*.dll"))
{
itemGroup.Add(new XElement("Reference",
new XAttribute("Include", Path.GetFileNameWithoutExtension(assemblyFile)),
Expand All @@ -202,11 +213,28 @@ public static int Main(string[] args)
}

using var projectStream = File.Create(artifactsPaths.ProjectFilePath);
#if NETSTANDARD2_0
doc.Save(projectStream, SaveOptions.None);
#else
await doc.SaveAsync(projectStream, SaveOptions.None, cancellationToken).ConfigureAwait(false);
#endif
}

private async ValueTask<string> GenerateBuildProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger, CancellationToken cancellationToken)
{
var benchmark = buildPartition.RepresentativeBenchmarkCase;
var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger);

var xmlDoc = new XmlDocument();
xmlDoc.Load(projectFile.FullName);
var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile);

return new StringBuilder(await ResourceHelper.LoadTemplateAsync("CsProj.txt", cancellationToken).ConfigureAwait(false))
.Replace("$PLATFORM$", buildPartition.Platform.ToConfig())
.Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath))
.Replace("$CSPROJPATH$", projectFile.FullName)
.Replace("$TFM$", TargetFrameworkMoniker)
.Replace("$PROGRAMNAME$", artifactsPaths.ProgramName)
.Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(benchmark.Job.Environment.Gc, buildPartition.Resolver))
.Replace("$COPIEDSETTINGS$", customProperties)
.Replace("$SDKNAME$", sdkName)
.ToString();
}

/// <summary>
Expand Down
Loading