diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuDetector.cs index 12c3b188d4..44973b27e9 100644 --- a/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuDetector.cs +++ b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuDetector.cs @@ -24,8 +24,12 @@ internal class LinuxCpuDetector : ICpuDetector ["LANGUAGE"] = "C" }; - string cpuInfo = ProcessHelper.RunAndReadOutput("cat", "/proc/cpuinfo") ?? ""; - string lscpu = ProcessHelper.RunAndReadOutput("/bin/bash", "-c \"lscpu\"", environmentVariables: languageInvariantEnvironment); + string? cpuInfo = ProcessHelper.RunAndReadOutput("cat", "/proc/cpuinfo") ?? string.Empty; + string? lscpu = ProcessHelper.RunAndReadOutput("/bin/bash", "-c \"lscpu\"", environmentVariables: languageInvariantEnvironment) ?? string.Empty; + + if (cpuInfo == string.Empty && lscpu == string.Empty) + return null; + return LinuxCpuInfoParser.Parse(cpuInfo, lscpu); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuInfoParser.cs b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuInfoParser.cs index ed93999653..7d79c3262f 100644 --- a/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuInfoParser.cs +++ b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuInfoParser.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using BenchmarkDotNet.Extensions; @@ -17,6 +18,8 @@ private static class ProcCpu internal const string CpuCores = "cpu cores"; internal const string ModelName = "model name"; internal const string MaxFrequency = "max freq"; + internal const string NominalFrequencyBackup = "nominal freq"; + internal const string NominalFrequency = "cpu MHz"; } private static class Lscpu @@ -28,12 +31,13 @@ private static class Lscpu /// Output of `cat /proc/cpuinfo` /// Output of `lscpu` - internal static CpuInfo Parse(string? cpuInfo, string? lscpu) + internal static CpuInfo Parse(string cpuInfo, string lscpu) { var processorModelNames = new HashSet(); var processorsToPhysicalCoreCount = new Dictionary(); int logicalCoreCount = 0; - Frequency? maxFrequency = null; + double maxFrequency = 0.0; + double nominalFrequency = 0.0; var logicalCores = SectionsHelper.ParseSections(cpuInfo, ':'); foreach (var logicalCore in logicalCores) @@ -51,14 +55,43 @@ internal static CpuInfo Parse(string? cpuInfo, string? lscpu) } if (logicalCore.TryGetValue(ProcCpu.MaxFrequency, out string maxCpuFreqValue) && - Frequency.TryParseMHz(maxCpuFreqValue, out var maxCpuFreq)) + Frequency.TryParseMHz(maxCpuFreqValue.Replace(',', '.'), out Frequency maxCpuFreq) + && maxCpuFreq > 0) { - maxFrequency = maxCpuFreq; + maxFrequency = Math.Max(maxFrequency, maxCpuFreq.ToMHz()); + } + + bool nominalFrequencyHasValue = logicalCore.TryGetValue(ProcCpu.NominalFrequency, out string nominalFreqValue); + bool nominalFrequencyBackupHasValue = logicalCore.TryGetValue(ProcCpu.NominalFrequencyBackup, out string nominalFreqBackupValue); + + double nominalCpuFreq = 0.0; + double nominalCpuBackupFreq = 0.0; + + if (nominalFrequencyHasValue && + double.TryParse(nominalFreqValue, out nominalCpuFreq) + && nominalCpuFreq > 0) + { + nominalCpuFreq = nominalFrequency == 0 ? nominalCpuFreq : Math.Min(nominalFrequency, nominalCpuFreq); + } + if (nominalFrequencyBackupHasValue && + double.TryParse(nominalFreqBackupValue, out nominalCpuBackupFreq) + && nominalCpuBackupFreq > 0) + { + nominalCpuBackupFreq = nominalFrequency == 0 ? nominalCpuBackupFreq : Math.Min(nominalFrequency, nominalCpuBackupFreq); + } + + if (nominalFrequencyHasValue && nominalFrequencyBackupHasValue) + { + nominalFrequency = Math.Min(nominalCpuFreq, nominalCpuBackupFreq); + } + else + { + nominalFrequency = nominalCpuFreq == 0.0 ? nominalCpuBackupFreq : nominalCpuFreq; } } int? coresPerSocket = null; - if (lscpu != null) + if (string.IsNullOrEmpty(lscpu) == false) { var lscpuParts = lscpu.Split('\n') .Where(line => line.Contains(':')) @@ -70,8 +103,8 @@ internal static CpuInfo Parse(string? cpuInfo, string? lscpu) string value = lscpuParts[i + 1].Trim(); if (name.EqualsWithIgnoreCase(Lscpu.MaxFrequency) && - Frequency.TryParseMHz(value.Replace(',', '.'), out var maxFrequencyParsed)) // Example: `CPU max MHz: 3200,0000` - maxFrequency = maxFrequencyParsed; + Frequency.TryParseMHz(value.Replace(',', '.'), out Frequency maxFrequencyParsed)) // Example: `CPU max MHz: 3200,0000` + maxFrequency = Math.Max(maxFrequency, maxFrequencyParsed.ToMHz()); if (name.EqualsWithIgnoreCase(Lscpu.ModelName)) processorModelNames.Add(value); @@ -82,21 +115,33 @@ internal static CpuInfo Parse(string? cpuInfo, string? lscpu) } } - var nominalFrequency = processorModelNames - .Select(ParseFrequencyFromBrandString) - .WhereNotNull() - .FirstOrDefault() ?? maxFrequency; string processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null; int? physicalProcessorCount = processorsToPhysicalCoreCount.Count > 0 ? processorsToPhysicalCoreCount.Count : null; int? physicalCoreCount = processorsToPhysicalCoreCount.Count > 0 ? processorsToPhysicalCoreCount.Values.Sum() : coresPerSocket; + + Frequency? maxFrequencyActual = maxFrequency > 0 && physicalProcessorCount > 0 + ? Frequency.FromMHz(maxFrequency) : null; + + Frequency? nominalFrequencyActual = nominalFrequency > 0 && physicalProcessorCount > 0 + ? Frequency.FromMHz(nominalFrequency) : null; + + if (nominalFrequencyActual is null) + { + bool nominalFrequencyInBrandString = processorModelNames.Any(x => ParseFrequencyFromBrandString(x) is not null); + + if (nominalFrequencyInBrandString) + nominalFrequencyActual = processorModelNames.Select(x => ParseFrequencyFromBrandString(x)) + .First(x => x is not null); + } + return new CpuInfo { ProcessorName = processorName, PhysicalProcessorCount = physicalProcessorCount, PhysicalCoreCount = physicalCoreCount, LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null, - NominalFrequencyHz = nominalFrequency?.Hertz.RoundToLong(), - MaxFrequencyHz = maxFrequency?.Hertz.RoundToLong() + NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(), + MaxFrequencyHz = maxFrequencyActual?.Hertz.RoundToLong() }; } @@ -107,7 +152,7 @@ internal static CpuInfo Parse(string? cpuInfo, string? lscpu) if (matches.Count > 0 && matches[0].Groups.Count > 1) { string match = Regex.Matches(brandString, pattern, RegexOptions.IgnoreCase)[0].Groups[1].ToString(); - return Frequency.TryParseGHz(match, out var result) ? result : null; + return Frequency.TryParseGHz(match, out Frequency result) ? result : null; } return null; diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/MosCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/MosCpuDetector.cs index 8ab16535f6..edc48ba25d 100644 --- a/src/BenchmarkDotNet/Detectors/Cpu/Windows/MosCpuDetector.cs +++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/MosCpuDetector.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Management; using BenchmarkDotNet.Extensions; @@ -28,7 +29,8 @@ public bool IsApplicable() => OsDetector.IsWindows() && int physicalCoreCount = 0; int logicalCoreCount = 0; int processorsCount = 0; - int sumMaxFrequency = 0; + double maxFrequency = 0; + double nominalFrequency = 0; using (var mosProcessor = new ManagementObjectSearcher("SELECT * FROM Win32_Processor")) { @@ -41,14 +43,23 @@ public bool IsApplicable() => OsDetector.IsWindows() && processorsCount++; physicalCoreCount += (int)(uint)moProcessor[WmicCpuInfoKeyNames.NumberOfCores]; logicalCoreCount += (int)(uint)moProcessor[WmicCpuInfoKeyNames.NumberOfLogicalProcessors]; - sumMaxFrequency = (int)(uint)moProcessor[WmicCpuInfoKeyNames.MaxClockSpeed]; + double tempMaxFrequency = (uint)moProcessor[WmicCpuInfoKeyNames.MaxClockSpeed]; + + if (tempMaxFrequency > 0) + { + nominalFrequency = nominalFrequency == 0 ? tempMaxFrequency : Math.Min(nominalFrequency, tempMaxFrequency); + } + maxFrequency = Math.Max(maxFrequency, tempMaxFrequency); } } } string processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null; - Frequency? maxFrequency = sumMaxFrequency > 0 && processorsCount > 0 - ? Frequency.FromMHz(sumMaxFrequency * 1.0 / processorsCount) + Frequency? maxFrequencyActual = maxFrequency > 0 && processorsCount > 0 + ? Frequency.FromMHz(maxFrequency) + : null; + Frequency? nominalFrequencyActual = nominalFrequency > 0 && processorsCount > 0 + ? Frequency.FromMHz(nominalFrequency) : null; return new CpuInfo @@ -57,8 +68,8 @@ public bool IsApplicable() => OsDetector.IsWindows() && PhysicalProcessorCount = processorsCount > 0 ? processorsCount : null, PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null, LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null, - NominalFrequencyHz = maxFrequency?.Hertz.RoundToLong(), - MaxFrequencyHz = maxFrequency?.Hertz.RoundToLong() + NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(), + MaxFrequencyHz = maxFrequencyActual?.Hertz.RoundToLong() }; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuDetector.cs new file mode 100644 index 0000000000..7f61c1b8fc --- /dev/null +++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuDetector.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Text.RegularExpressions; +using BenchmarkDotNet.Helpers; +using Perfolizer.Models; + +namespace BenchmarkDotNet.Detectors.Cpu.Windows; + +/// +/// CPU information from output of the `wmic cpu get Name, NumberOfCores, NumberOfLogicalProcessors /Format:List` command. +/// Windows only. +/// +internal class PowershellWmiCpuDetector : ICpuDetector +{ + private readonly string windowsPowershellPath = + $"{Environment.SystemDirectory}{Path.DirectorySeparatorChar}WindowsPowerShell{Path.DirectorySeparatorChar}" + + $"v1.0{Path.DirectorySeparatorChar}powershell.exe"; + + public bool IsApplicable() => OsDetector.IsWindows(); + + #if NET6_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif + public CpuInfo? Detect() + { + if (!IsApplicable()) return null; + + const string argList = $"{WmicCpuInfoKeyNames.Name}, " + + $"{WmicCpuInfoKeyNames.NumberOfCores}, " + + $"{WmicCpuInfoKeyNames.NumberOfLogicalProcessors}, " + + $"{WmicCpuInfoKeyNames.MaxClockSpeed}"; + + string output = ProcessHelper.RunAndReadOutput(PowerShellLocator.LocateOnWindows() ?? "PowerShell", + "Get-CimInstance Win32_Processor -Property " + argList); + + if (string.IsNullOrEmpty(output)) + return null; + + return PowershellWmiCpuInfoParser.Parse(output); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuInfoParser.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuInfoParser.cs new file mode 100644 index 0000000000..15ad4d6c77 --- /dev/null +++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuInfoParser.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; +using Perfolizer.Horology; +using Perfolizer.Models; + +namespace BenchmarkDotNet.Detectors.Cpu.Windows; + +internal static class PowershellWmiCpuInfoParser +{ + internal static CpuInfo Parse(string powershellWmiOutput) + { + HashSet processorModelNames = new HashSet(); + + int physicalCoreCount = 0; + int logicalCoreCount = 0; + int processorCount = 0; + double maxFrequency = 0.0; + double nominalFrequency = 0.0; + + List> processors = SectionsHelper.ParseSectionsForPowershellWmi(powershellWmiOutput, ':'); + foreach (Dictionary processor in processors) + { + if (processor.TryGetValue(WmicCpuInfoKeyNames.NumberOfCores, out string numberOfCoresValue) && + int.TryParse(numberOfCoresValue, out int numberOfCores) && + numberOfCores > 0) + physicalCoreCount += numberOfCores; + + if (processor.TryGetValue(WmicCpuInfoKeyNames.NumberOfLogicalProcessors, out string numberOfLogicalValue) && + int.TryParse(numberOfLogicalValue, out int numberOfLogical) && + numberOfLogical > 0) + logicalCoreCount += numberOfLogical; + + if (processor.TryGetValue(WmicCpuInfoKeyNames.Name, out string name)) + { + processorModelNames.Add(name); + processorCount++; + } + + if (processor.TryGetValue(WmicCpuInfoKeyNames.MaxClockSpeed, out string frequencyValue) + && double.TryParse(frequencyValue, out double frequency) + && frequency > 0) + { + nominalFrequency = nominalFrequency == 0 ? frequency : Math.Min(nominalFrequency, frequency); + maxFrequency = Math.Max(maxFrequency, frequency); + } + } + + string? processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null; + Frequency? maxFrequencyActual = maxFrequency > 0 && processorCount > 0 + ? Frequency.FromMHz(maxFrequency) : null; + + Frequency? nominalFrequencyActual = nominalFrequency > 0 && processorCount > 0 ? + Frequency.FromMHz(nominalFrequency) : null; + + return new CpuInfo + { + ProcessorName = processorName, + PhysicalProcessorCount = processorCount > 0 ? processorCount : null, + PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null, + LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null, + NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(), + MaxFrequencyHz = maxFrequencyActual?.Hertz.RoundToLong() + }; + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WindowsCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WindowsCpuDetector.cs index 9969a1bca0..1de238b93f 100644 --- a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WindowsCpuDetector.cs +++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WindowsCpuDetector.cs @@ -1,3 +1,4 @@ namespace BenchmarkDotNet.Detectors.Cpu.Windows; -internal class WindowsCpuDetector() : CpuDetector(new MosCpuDetector(), new WmicCpuDetector()); \ No newline at end of file +internal class WindowsCpuDetector() : CpuDetector(new MosCpuDetector(), new PowershellWmiCpuDetector(), + new WmicCpuDetector()); \ No newline at end of file diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuDetector.cs index 3a92d180b8..fe3434ae7f 100644 --- a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuDetector.cs +++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuDetector.cs @@ -9,6 +9,8 @@ namespace BenchmarkDotNet.Detectors.Cpu.Windows; /// CPU information from output of the `wmic cpu get Name, NumberOfCores, NumberOfLogicalProcessors /Format:List` command. /// Windows only. /// +/// WMIC is deprecated by Microsoft starting with Windows 10 21H1 (including Windows Server), and it is not known whether it still ships with Windows by default. +/// WMIC may be removed in a future version of Windows. See internal class WmicCpuDetector : ICpuDetector { private const string DefaultWmicPath = @"C:\Windows\System32\wbem\WMIC.exe"; @@ -24,7 +26,11 @@ internal class WmicCpuDetector : ICpuDetector $"{WmicCpuInfoKeyNames.NumberOfLogicalProcessors}, " + $"{WmicCpuInfoKeyNames.MaxClockSpeed}"; string wmicPath = File.Exists(DefaultWmicPath) ? DefaultWmicPath : "wmic"; - string wmicOutput = ProcessHelper.RunAndReadOutput(wmicPath, $"cpu get {argList} /Format:List"); + string? wmicOutput = ProcessHelper.RunAndReadOutput(wmicPath, $"cpu get {argList} /Format:List"); + + if (string.IsNullOrEmpty(wmicOutput)) + return null; + return WmicCpuInfoParser.Parse(wmicOutput); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoParser.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoParser.cs index a139d39711..295d76262e 100644 --- a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoParser.cs +++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoParser.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; using Perfolizer.Horology; @@ -12,15 +13,16 @@ internal static class WmicCpuInfoParser /// Parses wmic output and returns /// /// Output of `wmic cpu get Name, NumberOfCores, NumberOfLogicalProcessors /Format:List` - internal static CpuInfo Parse(string? wmicOutput) + internal static CpuInfo Parse(string wmicOutput) { - var processorModelNames = new HashSet(); + HashSet processorModelNames = new HashSet(); int physicalCoreCount = 0; int logicalCoreCount = 0; int processorsCount = 0; - var sumMaxFrequency = Frequency.Zero; + double maxFrequency = 0.0; + double nominalFrequency = 0.0; - var processors = SectionsHelper.ParseSections(wmicOutput, '='); + List> processors = SectionsHelper.ParseSections(wmicOutput, '='); foreach (var processor in processors) { if (processor.TryGetValue(WmicCpuInfoKeyNames.NumberOfCores, out string numberOfCoresValue) && @@ -40,26 +42,29 @@ internal static CpuInfo Parse(string? wmicOutput) } if (processor.TryGetValue(WmicCpuInfoKeyNames.MaxClockSpeed, out string frequencyValue) - && int.TryParse(frequencyValue, out int frequency) + && double.TryParse(frequencyValue, out double frequency) && frequency > 0) { - sumMaxFrequency += frequency; + nominalFrequency = nominalFrequency == 0 ? frequency : Math.Min(nominalFrequency, frequency); + maxFrequency = Math.Max(maxFrequency, frequency); } } string? processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null; - Frequency? maxFrequency = sumMaxFrequency > 0 && processorsCount > 0 - ? Frequency.FromMHz(sumMaxFrequency / processorsCount) + Frequency? maxFrequencyActual = maxFrequency > 0 && processorsCount > 0 + ? Frequency.FromMHz(maxFrequency) : null; + Frequency? nominalFrequencyActual = nominalFrequency > 0 && processorsCount > 0 ? Frequency.FromMHz(nominalFrequency) : null; + return new CpuInfo { ProcessorName = processorName, PhysicalProcessorCount = processorsCount > 0 ? processorsCount : null, PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null, LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null, - NominalFrequencyHz = maxFrequency?.Hertz.RoundToLong(), - MaxFrequencyHz = maxFrequency?.Hertz.RoundToLong() + NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(), + MaxFrequencyHz = maxFrequencyActual?.Hertz.RoundToLong() }; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Detectors/Cpu/macOS/MacOsCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/macOS/MacOsCpuDetector.cs index f34de47ac9..2f540e82d3 100644 --- a/src/BenchmarkDotNet/Detectors/Cpu/macOS/MacOsCpuDetector.cs +++ b/src/BenchmarkDotNet/Detectors/Cpu/macOS/MacOsCpuDetector.cs @@ -15,7 +15,11 @@ internal class MacOsCpuDetector : ICpuDetector { if (!IsApplicable()) return null; - string sysctlOutput = ProcessHelper.RunAndReadOutput("sysctl", "-a"); + string? sysctlOutput = ProcessHelper.RunAndReadOutput("sysctl", "-a"); + + if (sysctlOutput is null) + return null; + return SysctlCpuInfoParser.Parse(sysctlOutput); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Detectors/Cpu/macOS/SysctlCpuInfoParser.cs b/src/BenchmarkDotNet/Detectors/Cpu/macOS/SysctlCpuInfoParser.cs index 58d1b53766..5cd4d11c8a 100644 --- a/src/BenchmarkDotNet/Detectors/Cpu/macOS/SysctlCpuInfoParser.cs +++ b/src/BenchmarkDotNet/Detectors/Cpu/macOS/SysctlCpuInfoParser.cs @@ -20,7 +20,7 @@ private static class Sysctl /// Output of `sysctl -a` [SuppressMessage("ReSharper", "StringLiteralTypo")] - internal static CpuInfo Parse(string? sysctlOutput) + internal static CpuInfo Parse(string sysctlOutput) { var sysctl = SectionsHelper.ParseSection(sysctlOutput, ':'); string processorName = sysctl.GetValueOrDefault(Sysctl.ProcessorName); diff --git a/src/BenchmarkDotNet/Helpers/PowerShellLocator.cs b/src/BenchmarkDotNet/Helpers/PowerShellLocator.cs new file mode 100644 index 0000000000..28b8e910f7 --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/PowerShellLocator.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Text.RegularExpressions; +using BenchmarkDotNet.Detectors; + +namespace BenchmarkDotNet.Helpers; + +/// +/// Locates PowerShell on a system, currently only supports on Windows. +/// +internal class PowerShellLocator +{ + private static readonly string WindowsPowershellPath = + $"{Environment.SystemDirectory}{Path.DirectorySeparatorChar}WindowsPowerShell{Path.DirectorySeparatorChar}" + + $"v1.0{Path.DirectorySeparatorChar}powershell.exe"; + +#if NET6_0_OR_GREATER + [SupportedOSPlatform("windows")] +#endif + internal static string? LocateOnWindows() + { + if (OsDetector.IsWindows() == false) + return null; + + string powershellPath; + + try + { + string programFiles = Environment.Is64BitOperatingSystem + ? Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + + string powershell7PlusPath = $"{programFiles}{Path.DirectorySeparatorChar}Powershell{Path.DirectorySeparatorChar}"; + + bool checkForPowershell7Plus = true; + + if (Directory.Exists(powershell7PlusPath)) + { + string[] subDirectories = Directory.EnumerateDirectories(powershell7PlusPath, "*", SearchOption.AllDirectories) + .ToArray(); + + //Use the highest number string directory for PowerShell so that we get the newest major PowerShell version + // Example version directories are 6, 7, and in the future 8. + string? subDirectory = subDirectories.Where(x => Regex.IsMatch(x, "[0-9]")) + .Select(x => x) + .OrderByDescending(x => x) + .FirstOrDefault(); + + if (subDirectory is not null) + { + powershell7PlusPath = $"{subDirectory}{Path.DirectorySeparatorChar}pwsh.exe"; + } + else + { + checkForPowershell7Plus = false; + } + } + + // Optimistically, use Cross-platform new PowerShell when available but fallback to Windows PowerShell if not available. + if (checkForPowershell7Plus) + { + powershellPath = File.Exists(powershell7PlusPath) ? powershell7PlusPath : WindowsPowershellPath; + } + else + { + powershellPath = WindowsPowershellPath; + } + + if (File.Exists(powershellPath) == false) + powershellPath = "PowerShell"; + } + catch + { + powershellPath = "PowerShell"; + } + + return powershellPath; + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Helpers/SectionsHelper.cs b/src/BenchmarkDotNet/Helpers/SectionsHelper.cs index 15c54fd8f9..e16442d0fb 100644 --- a/src/BenchmarkDotNet/Helpers/SectionsHelper.cs +++ b/src/BenchmarkDotNet/Helpers/SectionsHelper.cs @@ -32,5 +32,14 @@ public static List> ParseSections(string? content, ch .Where(s => s.Count > 0) .ToList(); } + + public static List> ParseSectionsForPowershellWmi(string? content, char separator) + { + return + Regex.Split(content ?? "", "(\r*\n)") + .Select(s => ParseSection(s, separator)) + .Where(s => s.Count > 0) + .ToList(); + } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Detectors/Cpu/LinuxCpuInfoParserTests.cs b/tests/BenchmarkDotNet.Tests/Detectors/Cpu/LinuxCpuInfoParserTests.cs index d229a2b06d..0d4de2d9cc 100644 --- a/tests/BenchmarkDotNet.Tests/Detectors/Cpu/LinuxCpuInfoParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/Detectors/Cpu/LinuxCpuInfoParserTests.cs @@ -22,7 +22,7 @@ public void EmptyTest() [Fact] public void MalformedTest() { - var actual = LinuxCpuInfoParser.Parse("malformedkey: malformedvalue\n\nmalformedkey2: malformedvalue2", null); + var actual = LinuxCpuInfoParser.Parse("malformedkey: malformedvalue\n\nmalformedkey2: malformedvalue2", string.Empty); var expected = new CpuInfo(); Output.AssertEqual(expected, actual); } @@ -31,7 +31,7 @@ public void MalformedTest() public void TwoProcessorWithDifferentCoresCountTest() { string cpuInfo = TestHelper.ReadTestFile("ProcCpuInfoProcessorWithDifferentCoresCount.txt"); - var actual = LinuxCpuInfoParser.Parse(cpuInfo, null); + var actual = LinuxCpuInfoParser.Parse(cpuInfo, string.Empty); var expected = new CpuInfo { ProcessorName = "Unknown processor with 2 cores and hyper threading, Unknown processor with 4 cores", @@ -49,7 +49,7 @@ public void TwoProcessorWithDifferentCoresCountTest() public void RealOneProcessorTwoCoresTest() { string cpuInfo = TestHelper.ReadTestFile("ProcCpuInfoRealOneProcessorTwoCores.txt"); - var actual = LinuxCpuInfoParser.Parse(cpuInfo, null); + var actual = LinuxCpuInfoParser.Parse(cpuInfo, string.Empty); var expected = new CpuInfo { ProcessorName = "Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz", @@ -66,14 +66,14 @@ public void RealOneProcessorTwoCoresTest() public void RealOneProcessorFourCoresTest() { string cpuInfo = TestHelper.ReadTestFile("ProcCpuInfoRealOneProcessorFourCores.txt"); - var actual = LinuxCpuInfoParser.Parse(cpuInfo, null); + var actual = LinuxCpuInfoParser.Parse(cpuInfo, string.Empty); var expected = new CpuInfo { ProcessorName = "Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz", PhysicalProcessorCount = 1, PhysicalCoreCount = 4, LogicalCoreCount = 8, - NominalFrequencyHz = 2_500_000_000, + NominalFrequencyHz = 2_494_300_000, MaxFrequencyHz = 2_500_000_000 }; Output.AssertEqual(expected, actual); @@ -154,7 +154,7 @@ r smca fsrm flush_l1d PhysicalProcessorCount = 1, PhysicalCoreCount = 16, LogicalCoreCount = 32, - NominalFrequencyHz = 5_881_000_000, + NominalFrequencyHz = 400_000_000, MaxFrequencyHz = 5_881_000_000 }; Output.AssertEqual(expected, actual); diff --git a/tests/BenchmarkDotNet.Tests/Detectors/Cpu/PowershellWmiCpuInfoParserTests.cs b/tests/BenchmarkDotNet.Tests/Detectors/Cpu/PowershellWmiCpuInfoParserTests.cs new file mode 100644 index 0000000000..a781d70021 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Detectors/Cpu/PowershellWmiCpuInfoParserTests.cs @@ -0,0 +1,122 @@ +using BenchmarkDotNet.Detectors.Cpu.Windows; +using Perfolizer.Models; +using Xunit; +using Xunit.Abstractions; + +namespace BenchmarkDotNet.Tests.Detectors.Cpu; + +public class PowershellWmiCpuInfoParserTests(ITestOutputHelper output) +{ + private ITestOutputHelper Output { get; } = output; + + + [Fact] + public void EmptyTest() + { + CpuInfo? actual = PowershellWmiCpuInfoParser.Parse(string.Empty); + CpuInfo expected = new CpuInfo(); + Output.AssertEqual(expected, actual); + } + + + [Fact] + public void MalformedTest() + { + CpuInfo? actual = PowershellWmiCpuInfoParser + .Parse("malformedkey=malformedvalue\n\nmalformedkey2=malformedvalue2"); + CpuInfo expected = new CpuInfo(); + Output.AssertEqual(expected, actual); + } + + [Fact] + public void RealTwoProcessorEightCoresTest() + { + const string cpuInfo = + """ + MaxClockSpeed:2400 + Name:Intel(R) Xeon(R) CPU E5-2630 v3 + NumberOfCores:8 + NumberOfLogicalProcessors:16 + + + MaxClockSpeed:2400 + Name:Intel(R) Xeon(R) CPU E5-2630 v3 + NumberOfCores:8 + NumberOfLogicalProcessors:16 + + """; + CpuInfo? actual = PowershellWmiCpuInfoParser.Parse(cpuInfo); + + CpuInfo expected = new CpuInfo + { + ProcessorName = "Intel(R) Xeon(R) CPU E5-2630 v3", + PhysicalProcessorCount = 2, + PhysicalCoreCount = 16, + LogicalCoreCount = 32, + NominalFrequencyHz = 2_400_000_000, + MaxFrequencyHz = 2_400_000_000, + }; + + Output.AssertEqual(expected, actual); + } + + [Fact] + public void RealTwoProcessorEightCoresWithWmicBugTest() + { + const string cpuInfo = + "\r\r\n" + + "\r\r\n" + + "MaxClockSpeed:3111\r\r\n" + + "Name:Intel(R) Xeon(R) CPU E5-2687W 0\r\r\n" + + "NumberOfCores:8\r\r\n" + + "NumberOfLogicalProcessors:16\r\r\n" + + "\r\r\n" + + "\r\r\n" + + "MaxClockSpeed:3111\r\r\n" + + "Name:Intel(R) Xeon(R) CPU E5-2687W 0\r\r\n" + + "NumberOfCores:8\r\r\n" + + "NumberOfLogicalProcessors:16\r\r\n" + + "\r\r\n" + + "\r\r\n" + + "\r\r\n"; + + CpuInfo? actual = PowershellWmiCpuInfoParser.Parse(cpuInfo); + + CpuInfo expected = new CpuInfo + { + ProcessorName = "Intel(R) Xeon(R) CPU E5-2687W 0", + PhysicalProcessorCount = 2, + PhysicalCoreCount = 16, + LogicalCoreCount = 32, + NominalFrequencyHz = 3_111_000_000, + MaxFrequencyHz = 3_111_000_000, + }; + + Output.AssertEqual(expected, actual); + } + + [Fact] + public void RealOneProcessorFourCoresTest() + { + const string cpuInfo = """ + + MaxClockSpeed:2500 + Name:Intel(R) Core(TM) i7-4710MQ + NumberOfCores:4 + NumberOfLogicalProcessors:8 + """; + + CpuInfo? actual = PowershellWmiCpuInfoParser.Parse(cpuInfo); + CpuInfo expected = new CpuInfo + { + ProcessorName = "Intel(R) Core(TM) i7-4710MQ", + PhysicalProcessorCount = 1, + PhysicalCoreCount = 4, + LogicalCoreCount = 8, + NominalFrequencyHz = 2_500_000_000, + MaxFrequencyHz = 2_500_000_000, + }; + + Output.AssertEqual(expected, actual); + } +} \ No newline at end of file