From 38f6dcb90d4907bddda958c6c0b2380cd84edf44 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sun, 29 Jun 2025 01:06:55 -0400 Subject: [PATCH 1/6] Fix `IsNetCore` check for single-file apps without AOT, and `IsNativeAOT` check. --- .../Portability/RuntimeInformation.cs | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs index fb5150db2d..c803af1a14 100644 --- a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs +++ b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs @@ -2,22 +2,16 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Management; using System.Reflection; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using BenchmarkDotNet.Detectors; -using BenchmarkDotNet.Detectors.Cpu; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; -using JetBrains.Annotations; -using Microsoft.Win32; -using Perfolizer.Helpers; using static System.Runtime.InteropServices.RuntimeInformation; -using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; namespace BenchmarkDotNet.Portability { @@ -50,10 +44,6 @@ internal static class RuntimeInformation public static readonly bool IsNetNative = FrameworkDescription.StartsWith(".NET Native", StringComparison.OrdinalIgnoreCase); - public static readonly bool IsNetCore = - ((Environment.Version.Major >= 5) || FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase)) - && !string.IsNullOrEmpty(typeof(object).Assembly.Location); - #if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatformGuard("browser")] public static readonly bool IsWasm = OperatingSystem.IsBrowser(); @@ -61,13 +51,8 @@ internal static class RuntimeInformation public static readonly bool IsWasm = IsOSPlatform(OSPlatform.Create("BROWSER")); #endif - public static readonly bool IsNativeAOT = - Environment.Version.Major >= 5 - && string.IsNullOrEmpty(typeof(object).Assembly.Location) // it's merged to a single .exe and .Location returns null - && !IsWasm; // Wasm also returns "" for assembly locations - #if NETSTANDARD2_0 - public static readonly bool IsAot = IsAotMethod(); + public static readonly bool IsAot = IsAotMethod() || IsNetNative; private static bool IsAotMethod() { @@ -88,6 +73,16 @@ private static bool IsAotMethod() public static readonly bool IsAot = !System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeCompiled; #endif + public static bool IsNetCore + => ((Environment.Version.Major >= 5) || FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase)) + && !IsAot; + + public static bool IsNativeAOT + => Environment.Version.Major >= 5 + && IsAot + && !IsWasm && !IsMono; // Wasm and MonoAOTLLVM are also AOT + + public static readonly bool IsTieredJitEnabled = IsNetCore && (Environment.Version.Major < 3 @@ -173,10 +168,21 @@ private static string GetNetCoreVersion() } else { - var coreclrAssemblyInfo = FileVersionInfo.GetVersionInfo(typeof(object).GetTypeInfo().Assembly.Location); - var corefxAssemblyInfo = FileVersionInfo.GetVersionInfo(typeof(Regex).GetTypeInfo().Assembly.Location); + string coreclrLocation = typeof(object).GetTypeInfo().Assembly.Location; + string corefxLocation = typeof(Regex).GetTypeInfo().Assembly.Location; - if (CoreRuntime.TryGetVersion(out var version) && version >= new Version(5, 0)) + // Handle cases where assembly location is empty (e.g. single-file publish, AOT, some test runners) + if (string.IsNullOrEmpty(coreclrLocation) || string.IsNullOrEmpty(corefxLocation)) + { + return CoreRuntime.TryGetVersion(out var ver) && ver.Major >= 5 + ? $".NET {ver} (assembly location unavailable)" + : $".NET Core {ver?.ToString() ?? Unknown} (assembly location unavailable)"; + } + + var coreclrAssemblyInfo = FileVersionInfo.GetVersionInfo(coreclrLocation); + var corefxAssemblyInfo = FileVersionInfo.GetVersionInfo(corefxLocation); + + if (CoreRuntime.TryGetVersion(out var version) && version.Major >= 5) { // after the merge of dotnet/corefx and dotnet/coreclr into dotnet/runtime the version should always be the same Debug.Assert(coreclrAssemblyInfo.FileVersion == corefxAssemblyInfo.FileVersion); @@ -185,9 +191,7 @@ private static string GetNetCoreVersion() } else { - string runtimeVersion = version != default ? version.ToString() : Unknown; - - return $".NET Core {runtimeVersion} (CoreCLR {coreclrAssemblyInfo.FileVersion}, CoreFX {corefxAssemblyInfo.FileVersion})"; + return $".NET Core {version?.ToString() ?? Unknown} (CoreCLR {coreclrAssemblyInfo.FileVersion}, CoreFX {corefxAssemblyInfo.FileVersion})"; } } } From 3aafcc6da5814a751484f2c15f7933688637a8e9 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sun, 29 Jun 2025 03:32:36 -0400 Subject: [PATCH 2/6] Handle `TryGetVersion` for single-file exe in netcoreapp3.x. --- .../Environments/Runtimes/CoreRuntime.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs index 954a8ac7dc..6fd30463aa 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs @@ -99,11 +99,16 @@ internal static bool TryGetVersion(out Version? version) return true; } - var systemPrivateCoreLib = FileVersionInfo.GetVersionInfo(typeof(object).Assembly.Location); - // systemPrivateCoreLib.Product*Part properties return 0 so we have to implement some ugly parsing... - if (TryGetVersionFromProductInfo(systemPrivateCoreLib.ProductVersion, systemPrivateCoreLib.ProductName, out version)) + string coreclrLocation = typeof(object).Assembly.Location; + // Single-file publish has empty assembly location. + if (!string.IsNullOrEmpty(coreclrLocation)) { - return true; + var systemPrivateCoreLib = FileVersionInfo.GetVersionInfo(coreclrLocation); + // systemPrivateCoreLib.Product*Part properties return 0 so we have to implement some ugly parsing... + if (TryGetVersionFromProductInfo(systemPrivateCoreLib.ProductVersion, systemPrivateCoreLib.ProductName, out version)) + { + return true; + } } // it's OK to use this method only after checking the previous ones From 8740e510c392b8f29ca680dd613f1802855995c5 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sun, 29 Jun 2025 23:20:09 -0400 Subject: [PATCH 3/6] Simplify some checks. --- .../Portability/RuntimeInformation.cs | 41 +++++-------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs index c803af1a14..674065384c 100644 --- a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs +++ b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs @@ -42,8 +42,6 @@ internal static class RuntimeInformation FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase); #endif - public static readonly bool IsNetNative = FrameworkDescription.StartsWith(".NET Native", StringComparison.OrdinalIgnoreCase); - #if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatformGuard("browser")] public static readonly bool IsWasm = OperatingSystem.IsBrowser(); @@ -52,7 +50,7 @@ internal static class RuntimeInformation #endif #if NETSTANDARD2_0 - public static readonly bool IsAot = IsAotMethod() || IsNetNative; + public static readonly bool IsAot = IsAotMethod() || FrameworkDescription.StartsWith(".NET Native", StringComparison.OrdinalIgnoreCase); private static bool IsAotMethod() { @@ -166,34 +164,15 @@ private static string GetNetCoreVersion() { return $".NET {Environment.Version}"; } - else - { - string coreclrLocation = typeof(object).GetTypeInfo().Assembly.Location; - string corefxLocation = typeof(Regex).GetTypeInfo().Assembly.Location; - - // Handle cases where assembly location is empty (e.g. single-file publish, AOT, some test runners) - if (string.IsNullOrEmpty(coreclrLocation) || string.IsNullOrEmpty(corefxLocation)) - { - return CoreRuntime.TryGetVersion(out var ver) && ver.Major >= 5 - ? $".NET {ver} (assembly location unavailable)" - : $".NET Core {ver?.ToString() ?? Unknown} (assembly location unavailable)"; - } - - var coreclrAssemblyInfo = FileVersionInfo.GetVersionInfo(coreclrLocation); - var corefxAssemblyInfo = FileVersionInfo.GetVersionInfo(corefxLocation); - if (CoreRuntime.TryGetVersion(out var version) && version.Major >= 5) - { - // after the merge of dotnet/corefx and dotnet/coreclr into dotnet/runtime the version should always be the same - Debug.Assert(coreclrAssemblyInfo.FileVersion == corefxAssemblyInfo.FileVersion); - - return $".NET {version} ({coreclrAssemblyInfo.FileVersion})"; - } - else - { - return $".NET Core {version?.ToString() ?? Unknown} (CoreCLR {coreclrAssemblyInfo.FileVersion}, CoreFX {corefxAssemblyInfo.FileVersion})"; - } - } + string coreclrLocation = typeof(object).GetTypeInfo().Assembly.Location; + // Handle cases where assembly location is empty (e.g. single-file publish, AOT, some test runners) + string fileVersion = string.IsNullOrEmpty(coreclrLocation) + ? "assembly location unavailable" + : FileVersionInfo.GetVersionInfo(coreclrLocation).FileVersion; + return CoreRuntime.TryGetVersion(out var version) && version.Major >= 5 + ? $".NET {version} ({fileVersion})" + : $".NET Core {version?.ToString() ?? Unknown} ({fileVersion})"; } internal static Runtime GetCurrentRuntime() @@ -275,7 +254,7 @@ internal static string GetJitInfo() { if (IsNativeAOT) return "NativeAOT"; - if (IsNetNative || IsAot) + if (IsAot) return "AOT"; if (IsMono || IsWasm) return ""; // There is no helpful information about JIT on Mono From e7b99e88a54007c5ee9f6cd844f5f1f49b4644de Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Mon, 30 Jun 2025 00:20:00 -0400 Subject: [PATCH 4/6] Get version from FrameworkDescription. --- .../Environments/Runtimes/CoreRuntime.cs | 18 ++++++++++++++++++ .../Portability/RuntimeInformation.cs | 10 +++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs index 6fd30463aa..fea9cb94cd 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs @@ -110,6 +110,16 @@ internal static bool TryGetVersion(out Version? version) return true; } } + else + { + // .Net Core 3.X supports single-file publish, .Net Core 2.X does not. + // .Net Core 3.X fixed the version in FrameworkDescription, so we don't need to handle the case of 4.6.x in this branch. + var frameworkDescriptionVersion = GetParsableVersionPart(GetVersionPartFromFrameworkDescription()); + if (Version.TryParse(frameworkDescriptionVersion, out version)) + { + return true; + } + } // it's OK to use this method only after checking the previous ones // because we might have a benchmark app build for .NET Core X but executed using CoreRun Y @@ -130,6 +140,14 @@ internal static bool TryGetVersion(out Version? version) return false; } + internal static string GetVersionPartFromFrameworkDescription() + { + // .NET 10.0.0-preview.5.25277.114 -> 10.0.0-preview.5.25277.114 + // .NET Core 3.1.32 -> 3.1.32 + string frameworkDescription = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; + return new string(frameworkDescription.SkipWhile(c => !char.IsDigit(c)).ToArray()); + } + // sample input: // for dotnet run: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.12\ // for dotnet publish: C:\Users\adsitnik\source\repos\ConsoleApp25\ConsoleApp25\bin\Release\netcoreapp2.0\win-x64\publish\ diff --git a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs index 674065384c..cb2c5b47b6 100644 --- a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs +++ b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs @@ -167,12 +167,12 @@ private static string GetNetCoreVersion() string coreclrLocation = typeof(object).GetTypeInfo().Assembly.Location; // Handle cases where assembly location is empty (e.g. single-file publish, AOT, some test runners) - string fileVersion = string.IsNullOrEmpty(coreclrLocation) - ? "assembly location unavailable" - : FileVersionInfo.GetVersionInfo(coreclrLocation).FileVersion; + string detailedVersion = string.IsNullOrEmpty(coreclrLocation) + ? CoreRuntime.GetVersionPartFromFrameworkDescription() + : $"{CoreRuntime.GetVersionPartFromFrameworkDescription()}, {FileVersionInfo.GetVersionInfo(coreclrLocation).FileVersion}"; return CoreRuntime.TryGetVersion(out var version) && version.Major >= 5 - ? $".NET {version} ({fileVersion})" - : $".NET Core {version?.ToString() ?? Unknown} ({fileVersion})"; + ? $".NET {version} ({detailedVersion})" + : $".NET Core {version?.ToString() ?? Unknown} ({detailedVersion})"; } internal static Runtime GetCurrentRuntime() From eb321b080d6f93cb21596769e44c8c7e7283ee0e Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Mon, 30 Jun 2025 00:43:31 -0400 Subject: [PATCH 5/6] Don't include confusing FrameworkDescription in netcoreapp2.x. --- .../Portability/RuntimeInformation.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs index cb2c5b47b6..2064974a6c 100644 --- a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs +++ b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs @@ -165,14 +165,21 @@ private static string GetNetCoreVersion() return $".NET {Environment.Version}"; } - string coreclrLocation = typeof(object).GetTypeInfo().Assembly.Location; - // Handle cases where assembly location is empty (e.g. single-file publish, AOT, some test runners) - string detailedVersion = string.IsNullOrEmpty(coreclrLocation) - ? CoreRuntime.GetVersionPartFromFrameworkDescription() - : $"{CoreRuntime.GetVersionPartFromFrameworkDescription()}, {FileVersionInfo.GetVersionInfo(coreclrLocation).FileVersion}"; return CoreRuntime.TryGetVersion(out var version) && version.Major >= 5 - ? $".NET {version} ({detailedVersion})" - : $".NET Core {version?.ToString() ?? Unknown} ({detailedVersion})"; + ? $".NET {version} ({GetDetailedVersion()})" + : $".NET Core {version?.ToString() ?? Unknown} ({GetDetailedVersion()})"; + + string GetDetailedVersion() + { + string coreclrLocation = typeof(object).GetTypeInfo().Assembly.Location; + // Single-file publish has empty assembly location. + if (string.IsNullOrEmpty(coreclrLocation)) + return CoreRuntime.GetVersionPartFromFrameworkDescription(); + // .Net Core 2.X has confusing FrameworkDescription like 4.6.X. + if (version?.Major >= 3) + return $"{CoreRuntime.GetVersionPartFromFrameworkDescription()}, {FileVersionInfo.GetVersionInfo(coreclrLocation).FileVersion}"; + return FileVersionInfo.GetVersionInfo(coreclrLocation).FileVersion; + } } internal static Runtime GetCurrentRuntime() From ec038ab5af15aa7355e69e16be0991a02a0c6198 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Mon, 30 Jun 2025 04:55:02 -0400 Subject: [PATCH 6/6] Rename GetVersionFromFrameworkDescription. --- src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs | 4 ++-- src/BenchmarkDotNet/Portability/RuntimeInformation.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs index fea9cb94cd..472b629405 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs @@ -114,7 +114,7 @@ internal static bool TryGetVersion(out Version? version) { // .Net Core 3.X supports single-file publish, .Net Core 2.X does not. // .Net Core 3.X fixed the version in FrameworkDescription, so we don't need to handle the case of 4.6.x in this branch. - var frameworkDescriptionVersion = GetParsableVersionPart(GetVersionPartFromFrameworkDescription()); + var frameworkDescriptionVersion = GetParsableVersionPart(GetVersionFromFrameworkDescription()); if (Version.TryParse(frameworkDescriptionVersion, out version)) { return true; @@ -140,7 +140,7 @@ internal static bool TryGetVersion(out Version? version) return false; } - internal static string GetVersionPartFromFrameworkDescription() + internal static string GetVersionFromFrameworkDescription() { // .NET 10.0.0-preview.5.25277.114 -> 10.0.0-preview.5.25277.114 // .NET Core 3.1.32 -> 3.1.32 diff --git a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs index 2064974a6c..9b8348a125 100644 --- a/src/BenchmarkDotNet/Portability/RuntimeInformation.cs +++ b/src/BenchmarkDotNet/Portability/RuntimeInformation.cs @@ -174,10 +174,10 @@ string GetDetailedVersion() string coreclrLocation = typeof(object).GetTypeInfo().Assembly.Location; // Single-file publish has empty assembly location. if (string.IsNullOrEmpty(coreclrLocation)) - return CoreRuntime.GetVersionPartFromFrameworkDescription(); + return CoreRuntime.GetVersionFromFrameworkDescription(); // .Net Core 2.X has confusing FrameworkDescription like 4.6.X. if (version?.Major >= 3) - return $"{CoreRuntime.GetVersionPartFromFrameworkDescription()}, {FileVersionInfo.GetVersionInfo(coreclrLocation).FileVersion}"; + return $"{CoreRuntime.GetVersionFromFrameworkDescription()}, {FileVersionInfo.GetVersionInfo(coreclrLocation).FileVersion}"; return FileVersionInfo.GetVersionInfo(coreclrLocation).FileVersion; } }