Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
bab81cb
feat: Add RAM Frequency and Type to Environment Summary (#3041)
cloudsealed Oct 2, 2026
207e88f
feat: Add SMBIOS GetSystemFirmwareTable P/Invoke for Windows memory d…
cloudsealed Oct 2, 2026
05eaa81
refactor: Use CsWin32 generated PInvoke GetSystemFirmwareTable
cloudsealed Oct 2, 2026
0b9b9a5
fix: Correct SMBIOS memory type enumeration mapping (DDR3=0x18, DDR4=…
cloudsealed Oct 2, 2026
cb20cab
fix: Pass 0 for FirmwareTableID in GetSystemFirmwareTable to parse fu…
cloudsealed Oct 2, 2026
ab023a6
fix: Update SMBIOS LPDDR, MRDIMM and LPDDR6 memory type mappings per …
cloudsealed Oct 2, 2026
f80c348
fix: Expand SMBIOS memory type mappings per DMTF DSP0134 3.10.0 spec
cloudsealed Oct 2, 2026
79ded60
feat: Add RAM frequency and memory type detection for macOS
cloudsealed Oct 10, 2026
1eb5367
refactor: Separate macOS memory specs extraction into GetMacMemorySpe…
cloudsealed Oct 10, 2026
e3e7451
refactor: Use ProcessHelper to avoid deadlock on large stdout buffers
cloudsealed Oct 10, 2026
b49d075
refactor: Change frequency unit from MHz to MT/s per SMBIOS 2.3+
cloudsealed Oct 10, 2026
891ef4f
fix: Improve memory type regex to capture full type string
cloudsealed Oct 10, 2026
20a7f2f
feat: Add memory timing (CAS latency) detection from SMBIOS
cloudsealed Oct 10, 2026
d1a11d7
fix: Use system_profiler JSON output for robust macOS memory detection
cloudsealed Oct 11, 2026
bb66b8c
fix: Add 'Total' label to memory output format
cloudsealed Oct 11, 2026
8988024
fix: Use MT/s directly from SMBIOS, remove unnecessary DDR conversion
cloudsealed Oct 12, 2026
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
272 changes: 230 additions & 42 deletions src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using Windows.Win32;
using Windows.Win32.System.SystemInformation;
Expand All @@ -12,24 +17,52 @@ public class PhysicalMemoryInfo
{
public long TotalPhysicalBytes { get; }
public long? AvailablePhysicalBytes { get; }
public long? FrequencyMts { get; }
public string? MemoryType { get; }
public byte? CasLatency { get; }

public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null)
public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null, long? frequencyMts = null, string? memoryType = null, byte? casLatency = null)
{
TotalPhysicalBytes = totalPhysicalBytes;
AvailablePhysicalBytes = availablePhysicalBytes;
FrequencyMts = frequencyMts;
MemoryType = memoryType;
CasLatency = casLatency;
}

public string ToFormattedString()
{
double totalGb = TotalPhysicalBytes / (1024.0 * 1024.0 * 1024.0);
var sb = new StringBuilder();
sb.Append($"{Math.Round(totalGb, 2)} GB");
Comment thread
timcassell marked this conversation as resolved.
Outdated

if (MemoryType.IsNotBlank() || FrequencyMts.HasValue || CasLatency.HasValue)
{
sb.Append(" (");
if (MemoryType.IsNotBlank())
sb.Append(MemoryType);
if (FrequencyMts.HasValue)
{
if (MemoryType.IsNotBlank())
sb.Append('-');
sb.Append($"{FrequencyMts}MT/s");
}
if (CasLatency.HasValue)
{
if (MemoryType.IsNotBlank() || FrequencyMts.HasValue)
sb.Append('-');
sb.Append($"CAS{CasLatency}");
}
sb.Append(')');
}

if (AvailablePhysicalBytes.HasValue)
{
double availableGb = AvailablePhysicalBytes.Value / (1024.0 * 1024.0 * 1024.0);
return $"{Math.Round(totalGb, 2)} GB Total, {Math.Round(availableGb, 2)} GB Available";
sb.Append($", {Math.Round(availableGb, 2)} GB Available");
Comment thread
timcassell marked this conversation as resolved.
}

return $"{Math.Round(totalGb, 2)} GB";
return sb.ToString();
}
}

Expand All @@ -56,12 +89,15 @@ public static class SystemMemory
return null;
}

[SupportedOSPlatform("windows5.1.2600")]
[SupportedOSPlatform("windows6.0.6000")]
private static PhysicalMemoryInfo? GetWindowsMemory()
{
var memStatus = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>() };
if (PInvoke.GlobalMemoryStatusEx(ref memStatus))
return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys);
{
var (freq, type, cas) = GetLinuxOrWindowsMemoryDetails();
return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys, freq, type, cas);
}

return null;
}
Expand Down Expand Up @@ -91,67 +127,219 @@ public static class SystemMemory
}

if (total > 0)
return new PhysicalMemoryInfo(total, available);
{
var (freq, type, cas) = GetLinuxOrWindowsMemoryDetails();
return new PhysicalMemoryInfo(total, available, freq, type, cas);
}
}
return null;
}

private static PhysicalMemoryInfo? GetMacMemory()
private static (long? FrequencyMts, string? MemoryType, byte? CasLatency) GetLinuxOrWindowsMemoryDetails()
{
long total = 0;
long? available = null;

// 1. Get Total Memory
var sysctlInfo = new ProcessStartInfo("sysctl", "-n hw.memsize")
try
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
if (OsDetector.IsWindows7OrLater())
{
return GetWindowsSmbiosMemoryDetails();
}

using (var process = Process.Start(sysctlInfo))
{
if (process != null)
// Try reading /sys/devices/system/edac/mc/ on Linux
if (Directory.Exists("/sys/devices/system/edac/mc"))
{
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
long.TryParse(output.Trim(), out total);
foreach (var mcDir in Directory.GetDirectories("/sys/devices/system/edac/mc", "mc*"))
{
foreach (var dimmDir in Directory.GetDirectories(mcDir, "dimm*"))
{
string typePath = Path.Combine(dimmDir, "dimm_mem_type");
if (File.Exists(typePath))
{
string memType = File.ReadAllText(typePath).Trim();
if (!string.IsNullOrEmpty(memType))
return (null, memType, null);
}
}
}
}
}
catch
{
// Ignore fallback failures
}

if (total == 0) return null;
return (null, null, null);
}

// 2. Get Free Memory using vm_stat
var vmStatInfo = new ProcessStartInfo("vm_stat")
[SupportedOSPlatform("windows6.0.6000")]
private static unsafe (long? FrequencyMts, string? MemoryType, byte? CasLatency) GetWindowsSmbiosMemoryDetails()
{
try
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
const uint rsMb = 0x52534D42; // 'RSMB'
var provider = (FIRMWARE_TABLE_PROVIDER)rsMb;
uint bufferSize = PInvoke.GetSystemFirmwareTable(provider, 0, null, 0);
if (bufferSize == 0)
return (null, null, null);

using (var process = Process.Start(vmStatInfo))
{
if (process != null)
byte[] buffer = new byte[bufferSize];
fixed (byte* pBuffer = buffer)
{
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
if (PInvoke.GetSystemFirmwareTable(provider, 0, pBuffer, bufferSize) == 0)
return (null, null, null);
}

// SMBIOS table header: Data at offset 8
int offset = 8;
long? maxSpeed = null;
string? detectedType = null;
byte? casLatency = null;

while (offset + 4 <= buffer.Length)
{
byte type = buffer[offset];
byte length = buffer[offset + 1];

if (length < 4 || offset + length > buffer.Length)
break;

long pageSize = 4096;
var pageSizeMatch = Regex.Match(output, @"page size of (\d+) bytes");
if (pageSizeMatch.Success && long.TryParse(pageSizeMatch.Groups[1].Value, out long parsedPageSize))
// Type 17: Memory Device
if (type == 17 && length >= 0x15)
{
pageSize = parsedPageSize;
// Memory Type at offset 0x12
byte rawMemoryType = buffer[offset + 0x12];
string? memTypeStr = MapSmbiosMemoryType(rawMemoryType);
if (memTypeStr != null && detectedType == null)
{
detectedType = memTypeStr;
}

// Speed in MHz at offset 0x15 (WORD), convert to MT/s
if (length >= 0x17)
{
ushort speedMhz = (ushort)(buffer[offset + 0x15] | (buffer[offset + 0x16] << 8));
if (speedMhz > 0 && speedMhz < 0xFFFF)
{
long speedMts = speedMhz * 2; // DDR: MT/s = MHz × 2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SMBIOS specification define this speed unit as MT/s
So this logic for DDR memory should be removed.

if (maxSpeed == null || speedMts > maxSpeed.Value)
{
maxSpeed = speedMts;
}
}
}

// CAS Latency at offset 0x1C (BYTE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I've confirmed SMBIOS Specification.
https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.10.0.pdf

0x1C is defined as Extended Size field (That is used for represent size over 32GB)
And it looks like there is no field to get CAS Latency

if (length >= 0x1D && casLatency == null)
{
byte cas = buffer[offset + 0x1C];
if (cas > 0 && cas < 100) // Reasonable CAS latency range
{
casLatency = cas;

@timcassell timcassell Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least in CI, this value is not included (I haven't tested locally). Unsure if the cas < 100 guard is really necessary. And cas > 0 is redundant (byte is [0, 255]).

}
}
}

var match = Regex.Match(output, @"Pages free:\s+(\d+)");
if (match.Success && long.TryParse(match.Groups[1].Value, out long pagesFree))
// Move past formatted area
offset += length;

// Skip unformatted string section (double null-terminated)
while (offset < buffer.Length - 1 && !(buffer[offset] == 0 && buffer[offset + 1] == 0))
{
available = pagesFree * pageSize;
offset++;
}
offset += 2;
}

return (maxSpeed, detectedType, casLatency);
}
catch
{
return (null, null, null);
}
}

private static string? MapSmbiosMemoryType(byte type) => type switch
{
0x12 => "DDR",
0x13 => "DDR2",
0x18 => "DDR3",
0x1A => "DDR4",
0x1B => "LPDDR",
0x1C => "LPDDR2",
0x1D => "LPDDR3",
0x1E => "LPDDR4",
0x20 => "HBM",
0x21 => "HBM2",
0x22 => "DDR5",
0x23 => "LPDDR5",
0x24 => "HBM3",
0x25 => "MRDIMM",
0x26 => "LPDDR6",
_ => null
};

private static PhysicalMemoryInfo? GetMacMemory()
{
long total = 0;
long? available = null;

// 1. Get Total Memory
string? sysctlOutput = ProcessHelper.RunAndReadOutput("sysctl", "-n hw.memsize");
if (string.IsNullOrEmpty(sysctlOutput) || !long.TryParse(sysctlOutput!.Trim(), out total) || total == 0)
return null;

// 2. Get Free Memory using vm_stat
string? vmStatOutput = ProcessHelper.RunAndReadOutput("vm_stat");
if (!string.IsNullOrEmpty(vmStatOutput))
{
long pageSize = 4096;
var pageSizeMatch = Regex.Match(vmStatOutput, @"page size of (\d+) bytes");
if (pageSizeMatch.Success && long.TryParse(pageSizeMatch.Groups[1].Value, out long parsedPageSize))
{
pageSize = parsedPageSize;
}

var match = Regex.Match(vmStatOutput, @"Pages free:\s+(\d+)");
if (match.Success && long.TryParse(match.Groups[1].Value, out long pagesFree))
{
available = pagesFree * pageSize;
}
}

var (freq, type) = GetMacMemorySpecs();
return new PhysicalMemoryInfo(total, available, freq, type, null);
}

private static (long? Frequency, string? Type) GetMacMemorySpecs()
{
try
{
string? output = ProcessHelper.RunAndReadOutput("system_profiler", "SPMemoryDataType");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not have macos environment thought.
It looks like system_profiler SPMemoryDataType command results can be localized.
So it might be better to use -json option. and parse results with System.Text.Json

if (string.IsNullOrEmpty(output))
return (null, null);

long? freq = null;
string? type = null;

var speedMatch = Regex.Match(output, @"Speed:\s+(\d+)\s*MHz");
if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speedMhz))
{
freq = speedMhz * 2; // Convert MHz to MT/s
}

var typeMatch = Regex.Match(output, @"Type:\s+(\S+)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On macos(arm64) CI environment.
It looks like not able to gets memory details information.

Memory:

      Memory: 7 GB
      Type: Unknown
      Manufacturer: Unknown

And following information is printed.

Memory: 7 GB (Unknown), 0.56 GB Available

I though it's better to remove (Unknown) because it's confusing what's Unknow means.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On macos(x64).

It looks like memory type information can't get as expected.
And printed as following.

Memory: 14 GB (RAM-3200MT/s), 0.6 GB Available

if (typeMatch.Success)
{
type = typeMatch.Groups[1].Value;
}

return (freq, type);
}
catch
{
// Fallback
}

return new PhysicalMemoryInfo(total, available);
return (null, null);
}
}
}
1 change: 1 addition & 0 deletions src/BenchmarkDotNet/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ PowerGetActiveScheme
LocalFree
GlobalMemoryStatusEx
GetLogicalProcessorInformationEx
GetSystemFirmwareTable
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using BenchmarkDotNet.Environments;
using Xunit;

namespace BenchmarkDotNet.Tests.Environments
{
public class PhysicalMemoryInfoTests
{
[Fact]
public void PhysicalMemoryInfo_ParsesAndConstructsCorrectly()
{
var memInfo = new PhysicalMemoryInfo(16000000000, null, 6400, "DDR4", 16);
Assert.Equal(16000000000L, memInfo.TotalPhysicalBytes);
Assert.Equal(6400L, memInfo.FrequencyMts);
Assert.Equal("DDR4", memInfo.MemoryType);
Assert.Equal((byte?)16, memInfo.CasLatency);
}
}
}
Loading