Skip to content
Merged
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
46 changes: 30 additions & 16 deletions src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using System.Reflection;
using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Portability;

namespace BenchmarkDotNet.Environments
{
Expand Down Expand Up @@ -47,27 +47,41 @@ internal static ClrRuntime GetCurrentVersion()
{
if (!OsDetector.IsWindows())
{
throw new NotSupportedException(".NET Framework supports Windows OS only.");
throw new PlatformNotSupportedException(".NET Framework supports Windows OS only.");
}

// this logic is put to a separate method to avoid any assembly loading issues on non Windows systems
string sdkVersion = FrameworkVersionHelper.GetLatestNetDeveloperPackVersion();

string version = sdkVersion
string version = FrameworkVersionHelper.GetLatestNetDeveloperPackVersion()
?? FrameworkVersionHelper.GetFrameworkReleaseVersion(); // .NET Developer Pack is not installed
return GetRuntimeFromVersion(version);
}

switch (version)
internal static ClrRuntime GetTargetOrCurrentVersion(Assembly? assembly)
{
if (!OsDetector.IsWindows())
{
case "4.6.1": return Net461;
case "4.6.2": return Net462;
case "4.7": return Net47;
case "4.7.1": return Net471;
case "4.7.2": return Net472;
case "4.8": return Net48;
case "4.8.1": return Net481;
default: // unlikely to happen but theoretically possible
return new ClrRuntime(RuntimeMoniker.NotRecognized, $"net{version.Replace(".", null)}", $".NET Framework {version}");
throw new PlatformNotSupportedException(".NET Framework supports Windows OS only.");
}

// Try to determine the Framework version that the assembly was compiled for.
string? version = FrameworkVersionHelper.GetTargetFrameworkVersion(assembly);
return version != null
? GetRuntimeFromVersion(version)
// Fallback to the current running Framework version.
: GetCurrentVersion();
}

private static ClrRuntime GetRuntimeFromVersion(string version)
=> version switch
{
"4.6.1" => Net461,
"4.6.2" => Net462,
"4.7" => Net47,
"4.7.1" => Net471,
"4.7.2" => Net472,
"4.8" => Net48,
"4.8.1" => Net481,
// unlikely to happen but theoretically possible
_ => new ClrRuntime(RuntimeMoniker.NotRecognized, $"net{version.Replace(".", null)}", $".NET Framework {version}"),
};
}
}
56 changes: 41 additions & 15 deletions src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;
using Microsoft.Win32;

namespace BenchmarkDotNet.Helpers
Expand All @@ -10,15 +12,41 @@ internal static class FrameworkVersionHelper
// magic numbers come from https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed
// should be ordered by release number
private static readonly (int minReleaseNumber, string version)[] FrameworkVersions =
{
[
(533320, "4.8.1"), // value taken from Windows 11 arm64 insider build
(528040, "4.8"),
(461808, "4.7.2"),
(461308, "4.7.1"),
(460798, "4.7"),
(394802, "4.6.2"),
(394254, "4.6.1")
};
];

internal static string? GetTargetFrameworkVersion(Assembly? assembly)
{
if (assembly is null)
{
return null;
}

// Look for a TargetFrameworkAttribute with a supported Framework version.
foreach (var attribute in assembly.GetCustomAttributes<TargetFrameworkAttribute>())
{
switch (attribute.FrameworkName)
{
case ".NETFramework,Version=v4.6.1": return "4.6.1";
case ".NETFramework,Version=v4.6.2": return "4.6.2";
case ".NETFramework,Version=v4.7": return "4.7";
case ".NETFramework,Version=v4.7.1": return "4.7.1";
case ".NETFramework,Version=v4.7.2": return "4.7.2";
case ".NETFramework,Version=v4.8": return "4.8";
case ".NETFramework,Version=v4.8.1": return "4.8.1";
}
}

// TargetFrameworkAttribute not found, or the assembly targeted a version older than we support.
return null;
}

internal static string GetFrameworkDescription()
{
Expand Down Expand Up @@ -57,30 +85,28 @@ internal static string MapToReleaseVersion(string servicingVersion)


#if NET6_0_OR_GREATER
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
[SupportedOSPlatform("windows")]
#endif
private static int? GetReleaseNumberFromWindowsRegistry()
{
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
using (var ndpKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"))
{
if (ndpKey == null)
return null;
return Convert.ToInt32(ndpKey.GetValue("Release"));
}
using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
using var ndpKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\");
if (ndpKey == null)
return null;
return Convert.ToInt32(ndpKey.GetValue("Release"));
}

#if NET6_0_OR_GREATER
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
[SupportedOSPlatform("windows")]
#endif
internal static string GetLatestNetDeveloperPackVersion()
internal static string? GetLatestNetDeveloperPackVersion()
{
if (!(GetReleaseNumberFromWindowsRegistry() is int releaseNumber))
if (GetReleaseNumberFromWindowsRegistry() is not int releaseNumber)
return null;

return FrameworkVersions
.FirstOrDefault(v => releaseNumber >= v.minReleaseNumber && IsDeveloperPackInstalled(v.version))
.version;
.FirstOrDefault(v => releaseNumber >= v.minReleaseNumber && IsDeveloperPackInstalled(v.version))
.version;
}

// Reference Assemblies exists when Developer Pack is installed
Expand Down
5 changes: 5 additions & 0 deletions src/BenchmarkDotNet/Portability/RuntimeInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ string GetDetailedVersion()
}
}

internal static Runtime GetTargetOrCurrentRuntime(Assembly? assembly)
=> !IsMono && !IsWasm && IsFullFramework // Match order of checks in GetCurrentRuntime().
? ClrRuntime.GetTargetOrCurrentVersion(assembly)
: GetCurrentRuntime();

internal static Runtime GetCurrentRuntime()
{
//do not change the order of conditions because it may cause incorrect determination of runtime
Expand Down
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Running/BenchmarkCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal BenchmarkCase(Descriptor descriptor, Job job, ParameterInstances parame

public Runtime GetRuntime() => Job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic)
? Job.Environment.Runtime
: RuntimeInformation.GetCurrentRuntime();
: RuntimeInformation.GetTargetOrCurrentRuntime(Descriptor.WorkloadMethod.DeclaringType.Assembly);

public void Dispose() => Parameters.Dispose();

Expand Down
22 changes: 3 additions & 19 deletions src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Characteristics;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Portability;
using BenchmarkDotNet.Toolchains;

namespace BenchmarkDotNet.Running
Expand All @@ -22,18 +20,16 @@ internal class BenchmarkRuntimePropertiesComparer : IEqualityComparer<BenchmarkC
{
internal static readonly IEqualityComparer<BenchmarkCase> Instance = new BenchmarkRuntimePropertiesComparer();

private static readonly Runtime Current = RuntimeInformation.GetCurrentRuntime();

public bool Equals(BenchmarkCase x, BenchmarkCase y)
{
if (x == null && y == null)
if (x == y)
return true;
if (x == null || y == null)
return false;
var jobX = x.Job;
var jobY = y.Job;

if (AreDifferent(GetRuntime(jobX), GetRuntime(jobY))) // Mono vs .NET vs Core
if (AreDifferent(x.GetRuntime(), y.GetRuntime())) // Mono vs .NET vs Core
return false;
if (AreDifferent(x.GetToolchain(), y.GetToolchain())) // Mono vs .NET vs Core vs InProcess
return false;
Expand Down Expand Up @@ -90,20 +86,8 @@ public int GetHashCode(BenchmarkCase obj)
return hashCode.ToHashCode();
}

private static Runtime GetRuntime(Job job)
=> job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic)
? job.Environment.Runtime
: Current;

private static bool AreDifferent(object x, object y)
{
if (x == null && y == null)
return false;
if (x == null || y == null)
return true;

return !x.Equals(y);
}
=> !Equals(x, y);

private static bool AreDifferent(IReadOnlyList<Argument> x, IReadOnlyList<Argument> y)
{
Expand Down
32 changes: 20 additions & 12 deletions src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Portability;
using BenchmarkDotNet.Running;
Expand All @@ -19,28 +18,37 @@ namespace BenchmarkDotNet.Toolchains
{
internal static class ToolchainExtensions
{
internal static IToolchain GetToolchain(this BenchmarkCase benchmarkCase) => GetToolchain(benchmarkCase.Job, benchmarkCase.Descriptor);

internal static IToolchain GetToolchain(this Job job) => GetToolchain(job, null);
internal static IToolchain GetToolchain(this BenchmarkCase benchmarkCase)
=> benchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain)
? toolchain
: GetToolchain(
benchmarkCase.GetRuntime(),
benchmarkCase.Descriptor,
benchmarkCase.Job.HasDynamicBuildCharacteristic(),
benchmarkCase.Job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic)
);

private static IToolchain GetToolchain(Job job, Descriptor descriptor)
internal static IToolchain GetToolchain(this Job job)
=> job.Infrastructure.TryGetToolchain(out var toolchain)
? toolchain
: GetToolchain(
job.ResolveValue(EnvironmentMode.RuntimeCharacteristic, EnvironmentResolver.Instance),
descriptor,
job.HasDynamicBuildCharacteristic());
null,
job.HasDynamicBuildCharacteristic(),
job.Environment.HasValue(EnvironmentMode.RuntimeCharacteristic)
);

internal static IToolchain GetToolchain(this Runtime runtime, Descriptor? descriptor = null, bool preferMsBuildToolchains = false)
internal static IToolchain GetToolchain(this Runtime runtime, Descriptor? descriptor = null, bool preferMsBuildToolchains = false, bool isRuntimeExplicit = false)
{
switch (runtime)
{
case ClrRuntime clrRuntime:
if (!preferMsBuildToolchains && RuntimeInformation.IsFullFramework
&& RuntimeInformation.GetCurrentRuntime().MsBuildMoniker == runtime.MsBuildMoniker)
{
bool UseRoslyn()
=> !isRuntimeExplicit
|| runtime.MsBuildMoniker == ClrRuntime.GetTargetOrCurrentVersion(descriptor?.WorkloadMethod.DeclaringType.Assembly).MsBuildMoniker;

if (!preferMsBuildToolchains && RuntimeInformation.IsFullFramework && UseRoslyn())
return RoslynToolchain.Instance;
}

return clrRuntime.RuntimeMoniker != RuntimeMoniker.NotRecognized
? GetToolchain(clrRuntime.RuntimeMoniker)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,6 @@ public class MultipleFrameworksTest : BenchmarkTestExecutor
[InlineData(RuntimeMoniker.Net80)]
public void EachFrameworkIsRebuilt(RuntimeMoniker runtime)
{
#if NET461
// We cannot detect what target framework version the host was compiled for on full Framework,
// which causes the RoslynToolchain to be used instead of CsProjClassicNetToolchain when the host is full Framework
// (because full Framework always uses the version that's installed on the machine, unlike Core),
// which means if the machine has net48 installed (not net481), the net461 host with net48 runtime moniker
// will not be recompiled, causing the test to fail.

// If we ever change the default toolchain to CsProjClassicNetToolchain instead of RoslynToolchain, we can remove this check.
if (runtime == RuntimeMoniker.Net48)
{
// XUnit doesn't provide Assert.Skip API yet.
return;
}
#endif
var config = ManualConfig.CreateEmpty().AddJob(Job.Dry.WithRuntime(runtime.GetRuntime()).WithEnvironmentVariable(TfmEnvVarName, runtime.ToString()));
CanExecute<ValuePerTfm>(config);
}
Expand Down