diff --git a/src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs index 21bb2821df..a8e22eb4f5 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs @@ -1,8 +1,8 @@ using System; +using System.Reflection; using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Portability; namespace BenchmarkDotNet.Environments { @@ -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}"), + }; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs index 88093a6a52..7c87f0c561 100644 --- a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs +++ b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs @@ -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 @@ -10,7 +12,7 @@ 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"), @@ -18,7 +20,33 @@ private static readonly (int minReleaseNumber, string version)[] FrameworkVersio (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()) + { + 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() { @@ -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 diff --git a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs index 9b8348a125..530c4dd8a4 100644 --- a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs +++ b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs @@ -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 diff --git a/src/BenchmarkDotNet/Running/BenchmarkCase.cs b/src/BenchmarkDotNet/Running/BenchmarkCase.cs index b517ab5676..2a8f749a62 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkCase.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkCase.cs @@ -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(); diff --git a/src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs b/src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs index 4892de7f0c..20afe08cd4 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs @@ -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 @@ -22,18 +20,16 @@ internal class BenchmarkRuntimePropertiesComparer : IEqualityComparer 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; @@ -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 x, IReadOnlyList y) { diff --git a/src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs b/src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs index 2beab5018c..89918679a9 100644 --- a/src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs +++ b/src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs @@ -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; @@ -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) diff --git a/tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs b/tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs index b9cf360c8f..9de46632d0 100644 --- a/tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs @@ -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(config); }