Skip to content

Commit 7d4210d

Browse files
Fix "Unknown Processor" on Windows if WMIC is not present (#2749)
* allow MosCpuDetector to run on .NET 5+ * Revert "allow MosCpuDetector to run on .NET 5+" This reverts commit 495855a. * add WmiLightCpu Detector * Fix WmicCpuDetector being chosen if not available * enable NativeAOT * simplify IsApplicable * Update spacing of SupportedOsPlatform attribute Co-authored-by: Tim Cassell <[email protected]> * Revert "Fix WmicCpuDetector being chosen if not available" This reverts commit 5972f77. * add WMIC deprecation remarks * remove WmiLight code * update WmiCpuInfoParser to return null if Processor Name isn't detected * remove WmiLightCpuDetector reference from WindowsCpuDetector * Update WmicCpuInfoParser.cs * check if wmicOutput is null or empty instead * add PowershellWmiCpuDetector (parser still not complete) * return null if there's no version of powershell installed * fix Powershell 7+ check Co-authored-by: Tim Cassell <[email protected]> * rework search statement given that regex isn't supported * add parser code * add PowershellWmiCpuDetector to WindowsCpuDetector * rename variable to lower case * improve checking of latest powershell 7+ version * use explicit typing * fix frequency addition issue * revert to how WMIC parser handles processor frequency * add string is null or empty check to WmiCpuDetector * invoke Powershell as "PowerShell" if the file isn't found * add nominal Frequency detection and improve max frequency detection * fix issue with detecting latest Powershell * update comment * Update PowershellWmiCpuDetector.cs * refactor Powershell locating code to PowershellLocator * simplify frequency checks * Create PowershellWmiParserTests.cs * fix null being returned when object is expected. * rename test * simplify max frequency check Co-authored-by: Tim Cassell <[email protected]> * use """ for string in parser test * Update PowershellWmiCpuInfoParserTests.cs * reduce indentation with """ * remove unnecessary test info * move string null check to caller * add nominal frequency support for MosCpuDetector * Update src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuDetector.cs Co-authored-by: Tim Cassell <[email protected]> * remove nullability of parser Co-authored-by: Tim Cassell <[email protected]> * check if tempMaxFrequency > 0 before assignment Co-authored-by: Tim Cassell <[email protected]> * remove nullability of WmicCpuInfoParser * use file scoped namespace * use double instead of int * add null check to LinuxCpuDetector * change nullability of LinuxCpuParser * update LinuxCpuInfoParser nominal and max frequency detection * Update LinuxCpuInfoParser.cs * fix nullability check * simplify nominal frequency comparison Co-authored-by: Tim Cassell <[email protected]> * add null check to macOS * don't accept null string from detector * fix LinuxCpuInfo parser test issues and improve robustness of Linux Cpu checks * fix Powershell Wmi Parser parsing issues * fix .net framework cpu parsing issue * do implicit conversion to double from uint --------- Co-authored-by: Tim Cassell <[email protected]>
1 parent ffce52e commit 7d4210d

14 files changed

+444
-45
lines changed

src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuDetector.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ internal class LinuxCpuDetector : ICpuDetector
2424
["LANGUAGE"] = "C"
2525
};
2626

27-
string cpuInfo = ProcessHelper.RunAndReadOutput("cat", "/proc/cpuinfo") ?? "";
28-
string lscpu = ProcessHelper.RunAndReadOutput("/bin/bash", "-c \"lscpu\"", environmentVariables: languageInvariantEnvironment);
27+
string? cpuInfo = ProcessHelper.RunAndReadOutput("cat", "/proc/cpuinfo") ?? string.Empty;
28+
string? lscpu = ProcessHelper.RunAndReadOutput("/bin/bash", "-c \"lscpu\"", environmentVariables: languageInvariantEnvironment) ?? string.Empty;
29+
30+
if (cpuInfo == string.Empty && lscpu == string.Empty)
31+
return null;
32+
2933
return LinuxCpuInfoParser.Parse(cpuInfo, lscpu);
3034
}
3135
}

src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuInfoParser.cs

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Text.RegularExpressions;
45
using BenchmarkDotNet.Extensions;
@@ -17,6 +18,8 @@ private static class ProcCpu
1718
internal const string CpuCores = "cpu cores";
1819
internal const string ModelName = "model name";
1920
internal const string MaxFrequency = "max freq";
21+
internal const string NominalFrequencyBackup = "nominal freq";
22+
internal const string NominalFrequency = "cpu MHz";
2023
}
2124

2225
private static class Lscpu
@@ -28,12 +31,13 @@ private static class Lscpu
2831

2932
/// <param name="cpuInfo">Output of `cat /proc/cpuinfo`</param>
3033
/// <param name="lscpu">Output of `lscpu`</param>
31-
internal static CpuInfo Parse(string? cpuInfo, string? lscpu)
34+
internal static CpuInfo Parse(string cpuInfo, string lscpu)
3235
{
3336
var processorModelNames = new HashSet<string>();
3437
var processorsToPhysicalCoreCount = new Dictionary<string, int>();
3538
int logicalCoreCount = 0;
36-
Frequency? maxFrequency = null;
39+
double maxFrequency = 0.0;
40+
double nominalFrequency = 0.0;
3741

3842
var logicalCores = SectionsHelper.ParseSections(cpuInfo, ':');
3943
foreach (var logicalCore in logicalCores)
@@ -51,14 +55,43 @@ internal static CpuInfo Parse(string? cpuInfo, string? lscpu)
5155
}
5256

5357
if (logicalCore.TryGetValue(ProcCpu.MaxFrequency, out string maxCpuFreqValue) &&
54-
Frequency.TryParseMHz(maxCpuFreqValue, out var maxCpuFreq))
58+
Frequency.TryParseMHz(maxCpuFreqValue.Replace(',', '.'), out Frequency maxCpuFreq)
59+
&& maxCpuFreq > 0)
5560
{
56-
maxFrequency = maxCpuFreq;
61+
maxFrequency = Math.Max(maxFrequency, maxCpuFreq.ToMHz());
62+
}
63+
64+
bool nominalFrequencyHasValue = logicalCore.TryGetValue(ProcCpu.NominalFrequency, out string nominalFreqValue);
65+
bool nominalFrequencyBackupHasValue = logicalCore.TryGetValue(ProcCpu.NominalFrequencyBackup, out string nominalFreqBackupValue);
66+
67+
double nominalCpuFreq = 0.0;
68+
double nominalCpuBackupFreq = 0.0;
69+
70+
if (nominalFrequencyHasValue &&
71+
double.TryParse(nominalFreqValue, out nominalCpuFreq)
72+
&& nominalCpuFreq > 0)
73+
{
74+
nominalCpuFreq = nominalFrequency == 0 ? nominalCpuFreq : Math.Min(nominalFrequency, nominalCpuFreq);
75+
}
76+
if (nominalFrequencyBackupHasValue &&
77+
double.TryParse(nominalFreqBackupValue, out nominalCpuBackupFreq)
78+
&& nominalCpuBackupFreq > 0)
79+
{
80+
nominalCpuBackupFreq = nominalFrequency == 0 ? nominalCpuBackupFreq : Math.Min(nominalFrequency, nominalCpuBackupFreq);
81+
}
82+
83+
if (nominalFrequencyHasValue && nominalFrequencyBackupHasValue)
84+
{
85+
nominalFrequency = Math.Min(nominalCpuFreq, nominalCpuBackupFreq);
86+
}
87+
else
88+
{
89+
nominalFrequency = nominalCpuFreq == 0.0 ? nominalCpuBackupFreq : nominalCpuFreq;
5790
}
5891
}
5992

6093
int? coresPerSocket = null;
61-
if (lscpu != null)
94+
if (string.IsNullOrEmpty(lscpu) == false)
6295
{
6396
var lscpuParts = lscpu.Split('\n')
6497
.Where(line => line.Contains(':'))
@@ -70,8 +103,8 @@ internal static CpuInfo Parse(string? cpuInfo, string? lscpu)
70103
string value = lscpuParts[i + 1].Trim();
71104

72105
if (name.EqualsWithIgnoreCase(Lscpu.MaxFrequency) &&
73-
Frequency.TryParseMHz(value.Replace(',', '.'), out var maxFrequencyParsed)) // Example: `CPU max MHz: 3200,0000`
74-
maxFrequency = maxFrequencyParsed;
106+
Frequency.TryParseMHz(value.Replace(',', '.'), out Frequency maxFrequencyParsed)) // Example: `CPU max MHz: 3200,0000`
107+
maxFrequency = Math.Max(maxFrequency, maxFrequencyParsed.ToMHz());
75108

76109
if (name.EqualsWithIgnoreCase(Lscpu.ModelName))
77110
processorModelNames.Add(value);
@@ -82,21 +115,33 @@ internal static CpuInfo Parse(string? cpuInfo, string? lscpu)
82115
}
83116
}
84117

85-
var nominalFrequency = processorModelNames
86-
.Select(ParseFrequencyFromBrandString)
87-
.WhereNotNull()
88-
.FirstOrDefault() ?? maxFrequency;
89118
string processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null;
90119
int? physicalProcessorCount = processorsToPhysicalCoreCount.Count > 0 ? processorsToPhysicalCoreCount.Count : null;
91120
int? physicalCoreCount = processorsToPhysicalCoreCount.Count > 0 ? processorsToPhysicalCoreCount.Values.Sum() : coresPerSocket;
121+
122+
Frequency? maxFrequencyActual = maxFrequency > 0 && physicalProcessorCount > 0
123+
? Frequency.FromMHz(maxFrequency) : null;
124+
125+
Frequency? nominalFrequencyActual = nominalFrequency > 0 && physicalProcessorCount > 0
126+
? Frequency.FromMHz(nominalFrequency) : null;
127+
128+
if (nominalFrequencyActual is null)
129+
{
130+
bool nominalFrequencyInBrandString = processorModelNames.Any(x => ParseFrequencyFromBrandString(x) is not null);
131+
132+
if (nominalFrequencyInBrandString)
133+
nominalFrequencyActual = processorModelNames.Select(x => ParseFrequencyFromBrandString(x))
134+
.First(x => x is not null);
135+
}
136+
92137
return new CpuInfo
93138
{
94139
ProcessorName = processorName,
95140
PhysicalProcessorCount = physicalProcessorCount,
96141
PhysicalCoreCount = physicalCoreCount,
97142
LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
98-
NominalFrequencyHz = nominalFrequency?.Hertz.RoundToLong(),
99-
MaxFrequencyHz = maxFrequency?.Hertz.RoundToLong()
143+
NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(),
144+
MaxFrequencyHz = maxFrequencyActual?.Hertz.RoundToLong()
100145
};
101146
}
102147

@@ -107,7 +152,7 @@ internal static CpuInfo Parse(string? cpuInfo, string? lscpu)
107152
if (matches.Count > 0 && matches[0].Groups.Count > 1)
108153
{
109154
string match = Regex.Matches(brandString, pattern, RegexOptions.IgnoreCase)[0].Groups[1].ToString();
110-
return Frequency.TryParseGHz(match, out var result) ? result : null;
155+
return Frequency.TryParseGHz(match, out Frequency result) ? result : null;
111156
}
112157

113158
return null;

src/BenchmarkDotNet/Detectors/Cpu/Windows/MosCpuDetector.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Management;
45
using BenchmarkDotNet.Extensions;
@@ -28,7 +29,8 @@ public bool IsApplicable() => OsDetector.IsWindows() &&
2829
int physicalCoreCount = 0;
2930
int logicalCoreCount = 0;
3031
int processorsCount = 0;
31-
int sumMaxFrequency = 0;
32+
double maxFrequency = 0;
33+
double nominalFrequency = 0;
3234

3335
using (var mosProcessor = new ManagementObjectSearcher("SELECT * FROM Win32_Processor"))
3436
{
@@ -41,14 +43,23 @@ public bool IsApplicable() => OsDetector.IsWindows() &&
4143
processorsCount++;
4244
physicalCoreCount += (int)(uint)moProcessor[WmicCpuInfoKeyNames.NumberOfCores];
4345
logicalCoreCount += (int)(uint)moProcessor[WmicCpuInfoKeyNames.NumberOfLogicalProcessors];
44-
sumMaxFrequency = (int)(uint)moProcessor[WmicCpuInfoKeyNames.MaxClockSpeed];
46+
double tempMaxFrequency = (uint)moProcessor[WmicCpuInfoKeyNames.MaxClockSpeed];
47+
48+
if (tempMaxFrequency > 0)
49+
{
50+
nominalFrequency = nominalFrequency == 0 ? tempMaxFrequency : Math.Min(nominalFrequency, tempMaxFrequency);
51+
}
52+
maxFrequency = Math.Max(maxFrequency, tempMaxFrequency);
4553
}
4654
}
4755
}
4856

4957
string processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null;
50-
Frequency? maxFrequency = sumMaxFrequency > 0 && processorsCount > 0
51-
? Frequency.FromMHz(sumMaxFrequency * 1.0 / processorsCount)
58+
Frequency? maxFrequencyActual = maxFrequency > 0 && processorsCount > 0
59+
? Frequency.FromMHz(maxFrequency)
60+
: null;
61+
Frequency? nominalFrequencyActual = nominalFrequency > 0 && processorsCount > 0
62+
? Frequency.FromMHz(nominalFrequency)
5263
: null;
5364

5465
return new CpuInfo
@@ -57,8 +68,8 @@ public bool IsApplicable() => OsDetector.IsWindows() &&
5768
PhysicalProcessorCount = processorsCount > 0 ? processorsCount : null,
5869
PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null,
5970
LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
60-
NominalFrequencyHz = maxFrequency?.Hertz.RoundToLong(),
61-
MaxFrequencyHz = maxFrequency?.Hertz.RoundToLong()
71+
NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(),
72+
MaxFrequencyHz = maxFrequencyActual?.Hertz.RoundToLong()
6273
};
6374
}
6475
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Runtime.Versioning;
6+
using System.Text.RegularExpressions;
7+
using BenchmarkDotNet.Helpers;
8+
using Perfolizer.Models;
9+
10+
namespace BenchmarkDotNet.Detectors.Cpu.Windows;
11+
12+
/// <summary>
13+
/// CPU information from output of the `wmic cpu get Name, NumberOfCores, NumberOfLogicalProcessors /Format:List` command.
14+
/// Windows only.
15+
/// </summary>
16+
internal class PowershellWmiCpuDetector : ICpuDetector
17+
{
18+
private readonly string windowsPowershellPath =
19+
$"{Environment.SystemDirectory}{Path.DirectorySeparatorChar}WindowsPowerShell{Path.DirectorySeparatorChar}" +
20+
$"v1.0{Path.DirectorySeparatorChar}powershell.exe";
21+
22+
public bool IsApplicable() => OsDetector.IsWindows();
23+
24+
#if NET6_0_OR_GREATER
25+
[SupportedOSPlatform("windows")]
26+
#endif
27+
public CpuInfo? Detect()
28+
{
29+
if (!IsApplicable()) return null;
30+
31+
const string argList = $"{WmicCpuInfoKeyNames.Name}, " +
32+
$"{WmicCpuInfoKeyNames.NumberOfCores}, " +
33+
$"{WmicCpuInfoKeyNames.NumberOfLogicalProcessors}, " +
34+
$"{WmicCpuInfoKeyNames.MaxClockSpeed}";
35+
36+
string output = ProcessHelper.RunAndReadOutput(PowerShellLocator.LocateOnWindows() ?? "PowerShell",
37+
"Get-CimInstance Win32_Processor -Property " + argList);
38+
39+
if (string.IsNullOrEmpty(output))
40+
return null;
41+
42+
return PowershellWmiCpuInfoParser.Parse(output);
43+
}
44+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using BenchmarkDotNet.Extensions;
4+
using BenchmarkDotNet.Helpers;
5+
using Perfolizer.Horology;
6+
using Perfolizer.Models;
7+
8+
namespace BenchmarkDotNet.Detectors.Cpu.Windows;
9+
10+
internal static class PowershellWmiCpuInfoParser
11+
{
12+
internal static CpuInfo Parse(string powershellWmiOutput)
13+
{
14+
HashSet<string> processorModelNames = new HashSet<string>();
15+
16+
int physicalCoreCount = 0;
17+
int logicalCoreCount = 0;
18+
int processorCount = 0;
19+
double maxFrequency = 0.0;
20+
double nominalFrequency = 0.0;
21+
22+
List<Dictionary<string, string>> processors = SectionsHelper.ParseSectionsForPowershellWmi(powershellWmiOutput, ':');
23+
foreach (Dictionary<string, string> processor in processors)
24+
{
25+
if (processor.TryGetValue(WmicCpuInfoKeyNames.NumberOfCores, out string numberOfCoresValue) &&
26+
int.TryParse(numberOfCoresValue, out int numberOfCores) &&
27+
numberOfCores > 0)
28+
physicalCoreCount += numberOfCores;
29+
30+
if (processor.TryGetValue(WmicCpuInfoKeyNames.NumberOfLogicalProcessors, out string numberOfLogicalValue) &&
31+
int.TryParse(numberOfLogicalValue, out int numberOfLogical) &&
32+
numberOfLogical > 0)
33+
logicalCoreCount += numberOfLogical;
34+
35+
if (processor.TryGetValue(WmicCpuInfoKeyNames.Name, out string name))
36+
{
37+
processorModelNames.Add(name);
38+
processorCount++;
39+
}
40+
41+
if (processor.TryGetValue(WmicCpuInfoKeyNames.MaxClockSpeed, out string frequencyValue)
42+
&& double.TryParse(frequencyValue, out double frequency)
43+
&& frequency > 0)
44+
{
45+
nominalFrequency = nominalFrequency == 0 ? frequency : Math.Min(nominalFrequency, frequency);
46+
maxFrequency = Math.Max(maxFrequency, frequency);
47+
}
48+
}
49+
50+
string? processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null;
51+
Frequency? maxFrequencyActual = maxFrequency > 0 && processorCount > 0
52+
? Frequency.FromMHz(maxFrequency) : null;
53+
54+
Frequency? nominalFrequencyActual = nominalFrequency > 0 && processorCount > 0 ?
55+
Frequency.FromMHz(nominalFrequency) : null;
56+
57+
return new CpuInfo
58+
{
59+
ProcessorName = processorName,
60+
PhysicalProcessorCount = processorCount > 0 ? processorCount : null,
61+
PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null,
62+
LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
63+
NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(),
64+
MaxFrequencyHz = maxFrequencyActual?.Hertz.RoundToLong()
65+
};
66+
}
67+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
namespace BenchmarkDotNet.Detectors.Cpu.Windows;
22

3-
internal class WindowsCpuDetector() : CpuDetector(new MosCpuDetector(), new WmicCpuDetector());
3+
internal class WindowsCpuDetector() : CpuDetector(new MosCpuDetector(), new PowershellWmiCpuDetector(),
4+
new WmicCpuDetector());

src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuDetector.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace BenchmarkDotNet.Detectors.Cpu.Windows;
99
/// CPU information from output of the `wmic cpu get Name, NumberOfCores, NumberOfLogicalProcessors /Format:List` command.
1010
/// Windows only.
1111
/// </summary>
12+
/// <remarks>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.
13+
/// <para>WMIC may be removed in a future version of Windows. See <see href="https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmic"/> </para></remarks>
1214
internal class WmicCpuDetector : ICpuDetector
1315
{
1416
private const string DefaultWmicPath = @"C:\Windows\System32\wbem\WMIC.exe";
@@ -24,7 +26,11 @@ internal class WmicCpuDetector : ICpuDetector
2426
$"{WmicCpuInfoKeyNames.NumberOfLogicalProcessors}, " +
2527
$"{WmicCpuInfoKeyNames.MaxClockSpeed}";
2628
string wmicPath = File.Exists(DefaultWmicPath) ? DefaultWmicPath : "wmic";
27-
string wmicOutput = ProcessHelper.RunAndReadOutput(wmicPath, $"cpu get {argList} /Format:List");
29+
string? wmicOutput = ProcessHelper.RunAndReadOutput(wmicPath, $"cpu get {argList} /Format:List");
30+
31+
if (string.IsNullOrEmpty(wmicOutput))
32+
return null;
33+
2834
return WmicCpuInfoParser.Parse(wmicOutput);
2935
}
3036
}

0 commit comments

Comments
 (0)