diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 114836c438..b70d1d83f3 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -1,7 +1,11 @@ using BenchmarkDotNet.Detectors; -using System.Diagnostics; +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; +using System.Buffers.Binary; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using Windows.Win32; using Windows.Win32.System.SystemInformation; @@ -12,29 +16,59 @@ public class PhysicalMemoryInfo { public long TotalPhysicalBytes { get; } public long? AvailablePhysicalBytes { get; } + public long? FrequencyMts { get; } + public string? MemoryType { get; } public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null) + : this(totalPhysicalBytes, availablePhysicalBytes, frequencyMts: null, memoryType: null) + { + } + + public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes, long? frequencyMts, string? memoryType) { TotalPhysicalBytes = totalPhysicalBytes; AvailablePhysicalBytes = availablePhysicalBytes; + FrequencyMts = frequencyMts; + MemoryType = memoryType; } public string ToFormattedString() { double totalGb = TotalPhysicalBytes / (1024.0 * 1024.0 * 1024.0); + var sb = new StringBuilder(); + sb.Append($"{Math.Round(totalGb, 2)} GB"); + + if (AvailablePhysicalBytes.HasValue) + sb.Append(" Total"); + + if (MemoryType.IsNotBlank() || FrequencyMts.HasValue) + { + sb.Append(" ("); + if (MemoryType.IsNotBlank()) + sb.Append(MemoryType); + if (FrequencyMts.HasValue) + { + if (MemoryType.IsNotBlank()) + sb.Append('-'); + sb.Append($"{FrequencyMts}MT/s"); + } + 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"); } - return $"{Math.Round(totalGb, 2)} GB"; + return sb.ToString(); } } public static class SystemMemory { + private static readonly TimeSpan SystemProfilerTimeout = TimeSpan.FromSeconds(5); + public static PhysicalMemoryInfo? GetPhysicalMemory() { try @@ -56,12 +90,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() }; if (PInvoke.GlobalMemoryStatusEx(ref memStatus)) - return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys); + { + var (speed, type) = GetWindowsMemoryDetails(); + return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys, speed, type); + } return null; } @@ -91,67 +128,298 @@ public static class SystemMemory } if (total > 0) - return new PhysicalMemoryInfo(total, available); + { + var (speed, type) = GetLinuxMemoryDetails(); + return new PhysicalMemoryInfo(total, available, speed, type); + } } return null; } + private static (long? SpeedMts, string? MemoryType) GetLinuxMemoryDetails() + { + // The raw SMBIOS table is readable only by root on most distros. + try + { + const string dmiTablePath = "/sys/firmware/dmi/tables/DMI"; + if (File.Exists(dmiTablePath)) + { + var details = ParseSmbiosMemoryDevices(File.ReadAllBytes(dmiTablePath)); + if (details.SpeedMts.HasValue || details.MemoryType != null) + return details; + } + } + catch + { + // Fall back to EDAC + } + + // EDAC is world-readable but usually only present on ECC systems, and it does not expose the speed. + try + { + const string edacPath = "/sys/devices/system/edac/mc"; + if (Directory.Exists(edacPath)) + { + foreach (var mcDir in Directory.GetDirectories(edacPath, "mc*")) + { + foreach (var dimmDir in Directory.GetDirectories(mcDir, "dimm*")) + { + string typePath = Path.Combine(dimmDir, "dimm_mem_type"); + if (File.Exists(typePath)) + { + string? memType = NormalizeEdacMemoryType(File.ReadAllText(typePath).Trim()); + if (memType != null) + return (null, memType); + } + } + } + } + } + catch + { + // Ignore fallback failures + } + + return (null, null); + } + + /// + /// Maps a kernel EDAC memory type name (e.g. "Registered-DDR4", "Low-Power-DDR5-RAM") to the plain memory type ("DDR4", "LPDDR5"). + /// + internal static string? NormalizeEdacMemoryType(string edacType) + { + var match = Regex.Match(edacType, @"DDR\d*"); + if (!match.Success) + return null; + + return edacType.StartsWith("Low-Power", StringComparison.Ordinal) ? "LP" + match.Value : match.Value; + } + + [SupportedOSPlatform("windows6.0.6000")] + private static unsafe (long? SpeedMts, string? MemoryType) GetWindowsMemoryDetails() + { + try + { + 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); + + byte[] buffer = new byte[bufferSize]; + fixed (byte* pBuffer = buffer) + { + if (PInvoke.GetSystemFirmwareTable(provider, 0, pBuffer, bufferSize) == 0) + return (null, null); + } + + // RawSMBIOSData header: 4 version bytes, then the DWORD length of the structure table that follows. + const int headerSize = 8; + if (buffer.Length < headerSize) + return (null, null); + int tableLength = (int)Math.Min(BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(4)), (uint)(buffer.Length - headerSize)); + return ParseSmbiosMemoryDevices(buffer.AsSpan(headerSize, tableLength)); + } + catch + { + return (null, null); + } + } + + /// + /// Reads the memory type and speed from the Memory Device (type 17) structures of an SMBIOS structure table (DMTF DSP0134). + /// + internal static (long? SpeedMts, string? MemoryType) ParseSmbiosMemoryDevices(ReadOnlySpan table) + { + const byte memoryDeviceType = 17; + const byte endOfTableType = 127; + + long? speed = null; + string? memoryType = null; + int offset = 0; + + while (offset + 4 <= table.Length) + { + byte structureType = table[offset]; + byte length = table[offset + 1]; + + if (structureType == endOfTableType || length < 4 || offset + length > table.Length) + break; + + if (structureType == memoryDeviceType) + { + var device = table.Slice(offset, length); + if (IsInstalledMemoryDevice(device)) + { + if (memoryType == null && device.Length > 0x12) + memoryType = MapSmbiosMemoryType(device[0x12]); + + // Mixed modules all run at the speed of the slowest one. + long? deviceSpeed = GetMemoryDeviceSpeed(device); + if (deviceSpeed.HasValue && (speed == null || deviceSpeed.Value < speed.Value)) + speed = deviceSpeed; + } + } + + // Skip the formatted area, then the string-set, which ends with a double null. + offset += length; + while (offset + 1 < table.Length && (table[offset] != 0 || table[offset + 1] != 0)) + offset++; + offset += 2; + } + + return (speed, memoryType); + } + + // Size (WORD at 0x0C) is 0 for an empty slot. + private static bool IsInstalledMemoryDevice(ReadOnlySpan device) + => device.Length < 0x0E || BinaryPrimitives.ReadUInt16LittleEndian(device.Slice(0x0C)) != 0; + + // Configured Memory Speed (0x20, SMBIOS 2.7+) is what the module actually runs at. + // Speed (0x15) is the module's maximum rated speed, used only when the configured speed is not reported. + private static long? GetMemoryDeviceSpeed(ReadOnlySpan device) + => ReadSmbiosSpeed(device, wordOffset: 0x20, extendedOffset: 0x58) + ?? ReadSmbiosSpeed(device, wordOffset: 0x15, extendedOffset: 0x54); + + private static long? ReadSmbiosSpeed(ReadOnlySpan device, int wordOffset, int extendedOffset) + { + if (device.Length < wordOffset + 2) + return null; + + ushort speed = BinaryPrimitives.ReadUInt16LittleEndian(device.Slice(wordOffset)); + if (speed == 0) + return null; // unknown + if (speed != 0xFFFF) + return speed; + + // 0xFFFF means the speed is in the DWORD Extended field (SMBIOS 3.3+), whose bit 31 is reserved. + if (device.Length < extendedOffset + 4) + return null; + uint extendedSpeed = BinaryPrimitives.ReadUInt32LittleEndian(device.Slice(extendedOffset)) & 0x7FFFFFFF; + return extendedSpeed == 0 ? null : extendedSpeed; + } + + 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 - var sysctlInfo = new ProcessStartInfo("sysctl", "-n hw.memsize") - { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - }; + string? sysctlOutput = ProcessHelper.RunAndReadOutput("sysctl", "-n hw.memsize"); + if (string.IsNullOrEmpty(sysctlOutput) || !long.TryParse(sysctlOutput!.Trim(), out total) || total == 0) + return null; - using (var process = Process.Start(sysctlInfo)) + // 2. Get Free Memory using vm_stat + string? vmStatOutput = ProcessHelper.RunAndReadOutput("vm_stat"); + if (!string.IsNullOrEmpty(vmStatOutput)) { - if (process != null) + 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)) { - string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - long.TryParse(output.Trim(), out total); + available = pagesFree * pageSize; } } - if (total == 0) return null; + var (speed, type) = GetMacMemoryDetails(); + return new PhysicalMemoryInfo(total, available, speed, type); + } - // 2. Get Free Memory using vm_stat - var vmStatInfo = new ProcessStartInfo("vm_stat") + private static (long? SpeedMts, string? MemoryType) GetMacMemoryDetails() + { + try + { + string? json = ProcessHelper.RunAndReadOutput("system_profiler", "-json SPMemoryDataType", timeout: SystemProfilerTimeout); + if (!string.IsNullOrEmpty(json)) + return ParseMacMemoryJson(json!); + } + catch { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - }; + // Fallback on parsing error + } + + return (null, null); + } + + /// + /// Parses the output of system_profiler -json SPMemoryDataType. + /// Apple silicon reports a single entry with dimm_type and no speed; Intel Macs report one _items entry per slot. + /// + internal static (long? SpeedMts, string? MemoryType) ParseMacMemoryJson(string json) + { + using var doc = JsonDocument.Parse(json); + + if (!doc.RootElement.TryGetProperty("SPMemoryDataType", out var entries) || entries.ValueKind != JsonValueKind.Array) + return (null, null); + + long? speed = null; + string? memoryType = null; - using (var process = Process.Start(vmStatInfo)) + foreach (var entry in entries.EnumerateArray()) { - if (process != null) + ReadMemoryDevice(entry); + + if (entry.TryGetProperty("_items", out var items) && items.ValueKind == JsonValueKind.Array) { - string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); + foreach (var item in items.EnumerateArray()) + ReadMemoryDevice(item); + } + } - 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)) - { - pageSize = parsedPageSize; - } + return (speed, memoryType); + + void ReadMemoryDevice(JsonElement device) + { + if (device.ValueKind != JsonValueKind.Object) + return; - var match = Regex.Match(output, @"Pages free:\s+(\d+)"); - if (match.Success && long.TryParse(match.Groups[1].Value, out long pagesFree)) + // Empty slots report "empty" and virtual machines report placeholders such as "Unknown" or "RAM" (with a made-up speed), + // so only devices with a real memory type are used. + if (!device.TryGetProperty("dimm_type", out var typeElement) || typeElement.ValueKind != JsonValueKind.String + || typeElement.GetString() is not string type || !Regex.IsMatch(type, "DDR|HBM")) + { + return; + } + + memoryType ??= type; + + // Despite the "MHz" label, the value is the transfer rate: DDR4-2667 is reported as "2667 MHz". + if (device.TryGetProperty("dimm_speed", out var speedElement) && speedElement.ValueKind == JsonValueKind.String) + { + var match = Regex.Match(speedElement.GetString() ?? "", @"^\s*(\d+)\s*MHz"); + if (match.Success && long.TryParse(match.Groups[1].Value, out long deviceSpeed) && deviceSpeed > 0 + && (speed == null || deviceSpeed < speed.Value)) { - available = pagesFree * pageSize; + speed = deviceSpeed; } } } - - return new PhysicalMemoryInfo(total, available); } } -} \ No newline at end of file +} diff --git a/src/BenchmarkDotNet/Helpers/ProcessHelper.cs b/src/BenchmarkDotNet/Helpers/ProcessHelper.cs index c93133d293..4a570c8716 100644 --- a/src/BenchmarkDotNet/Helpers/ProcessHelper.cs +++ b/src/BenchmarkDotNet/Helpers/ProcessHelper.cs @@ -14,10 +14,10 @@ internal static class ProcessHelper { /// /// Run external process and return the console output. - /// In the case of any exception, null will be returned. + /// In the case of any exception, or if the process does not finish within , null will be returned. /// internal static string? RunAndReadOutput(string fileName, string arguments = "", ILogger? logger = null, - Dictionary? environmentVariables = null) + Dictionary? environmentVariables = null, TimeSpan? timeout = null) { var processStartInfo = new ProcessStartInfo { @@ -43,9 +43,19 @@ internal static class ProcessHelper { return null; } - string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - return output; + + if (timeout is null) + { + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + return output; + } + + // On timeout, disposing the cleanup helper kills the still-running process. + var readTask = process.StandardOutput.ReadToEndAsync(); + if (!readTask.Wait(timeout.Value) || !process.WaitForExit((int)timeout.Value.TotalMilliseconds)) + return null; + return readTask.Result; } } diff --git a/src/BenchmarkDotNet/NativeMethods.txt b/src/BenchmarkDotNet/NativeMethods.txt index c687e02914..5b0e9f3d13 100644 --- a/src/BenchmarkDotNet/NativeMethods.txt +++ b/src/BenchmarkDotNet/NativeMethods.txt @@ -13,3 +13,4 @@ PowerGetActiveScheme LocalFree GlobalMemoryStatusEx GetLogicalProcessorInformationEx +GetSystemFirmwareTable diff --git a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs new file mode 100644 index 0000000000..26e99e19e5 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs @@ -0,0 +1,238 @@ +using BenchmarkDotNet.Environments; +using System.Buffers.Binary; +using Xunit; + +namespace BenchmarkDotNet.Tests.Environments +{ + public class PhysicalMemoryInfoTests + { + private const long SixteenGb = 16L * 1024 * 1024 * 1024; + private const long EightGb = 8L * 1024 * 1024 * 1024; + + private const byte Ddr4 = 0x1A; + private const byte Ddr5 = 0x22; + private const byte UnknownType = 0x02; + + // Structure lengths per SMBIOS version (DSP0134). + private const byte Smbios23Length = 0x1B; + private const byte Smbios28Length = 0x28; + private const byte Smbios33Length = 0x5C; + + [Theory] + [InlineData(null, null, null, "16 GB")] + [InlineData(EightGb, null, null, "16 GB Total, 8 GB Available")] + [InlineData(null, 6400L, "DDR5", "16 GB (DDR5-6400MT/s)")] + [InlineData(EightGb, 6400L, "DDR5", "16 GB Total (DDR5-6400MT/s), 8 GB Available")] + [InlineData(EightGb, null, "LPDDR5", "16 GB Total (LPDDR5), 8 GB Available")] + [InlineData(EightGb, 3200L, null, "16 GB Total (3200MT/s), 8 GB Available")] + public void ToFormattedString(long? available, long? speed, string? type, string expected) + { + var info = new PhysicalMemoryInfo(SixteenGb, available, speed, type); + Assert.Equal(expected, info.ToFormattedString()); + } + + [Fact] + public void TwoArgumentConstructorLeavesDetailsUnset() + { + var info = new PhysicalMemoryInfo(SixteenGb, EightGb); + Assert.Null(info.FrequencyMts); + Assert.Null(info.MemoryType); + } + + [Fact] + public void SmbiosPrefersConfiguredSpeedOverRatedSpeed() + { + // A DDR4-3600 module running at 2133 (XMP off). + byte[] table = Table(MemoryDevice(Smbios28Length, Ddr4, speed: 3600, configuredSpeed: 2133)); + + Assert.Equal((2133L, "DDR4"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosFallsBackToRatedSpeedBeforeVersion27() + { + byte[] table = Table(MemoryDevice(Smbios23Length, Ddr4, speed: 2400)); + + Assert.Equal((2400L, "DDR4"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosFallsBackToRatedSpeedWhenConfiguredSpeedIsUnknown() + { + byte[] table = Table(MemoryDevice(Smbios28Length, Ddr4, speed: 2400, configuredSpeed: 0)); + + Assert.Equal((2400L, "DDR4"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosReadsExtendedSpeedWhenWordIsSaturated() + { + byte[] table = Table(MemoryDevice(Smbios33Length, Ddr5, speed: 0xFFFF, configuredSpeed: 0xFFFF, extendedSpeed: 88000, extendedConfiguredSpeed: 70000)); + + Assert.Equal((70000L, "DDR5"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosIgnoresSaturatedSpeedWithoutExtendedField() + { + byte[] table = Table(MemoryDevice(Smbios28Length, Ddr5, speed: 0xFFFF, configuredSpeed: 0xFFFF)); + + Assert.Equal(((long?)null, "DDR5"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosSkipsEmptySlotsAndOtherStructures() + { + byte[] table = Table( + OtherStructure(type: 16, "Board"), + MemoryDevice(Smbios28Length, UnknownType, speed: 0, configuredSpeed: 0, size: 0, strings: ["DIMM_A1"]), + MemoryDevice(Smbios28Length, Ddr5, speed: 4800, configuredSpeed: 4800, strings: ["DIMM_A2", "Vendor"])); + + Assert.Equal((4800L, "DDR5"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosReportsSlowestModule() + { + byte[] table = Table( + MemoryDevice(Smbios28Length, Ddr5, speed: 6000, configuredSpeed: 5600), + MemoryDevice(Smbios28Length, Ddr5, speed: 4800, configuredSpeed: 4800)); + + Assert.Equal((4800L, "DDR5"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosStopsAtEndOfTable() + { + byte[] table = Table( + OtherStructure(type: 127), + MemoryDevice(Smbios28Length, Ddr5, speed: 4800, configuredSpeed: 4800)); + + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosToleratesTruncatedTable() + { + byte[] full = Table(MemoryDevice(Smbios28Length, Ddr5, speed: 4800, configuredSpeed: 4800)); + + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseSmbiosMemoryDevices(full.AsSpan(0, 10))); + } + + [Fact] + public void MacIntelReadsPerSlotItems() + { + const string json = """ + { + "SPMemoryDataType" : [ + { + "_items" : [ + { "_name" : "BANK 0/ChannelA-DIMM0", "dimm_size" : "8 GB", "dimm_speed" : "2667 MHz", "dimm_status" : "ok", "dimm_type" : "DDR4" }, + { "_name" : "BANK 1/ChannelB-DIMM0", "dimm_size" : "empty", "dimm_speed" : "empty", "dimm_status" : "empty", "dimm_type" : "empty" } + ], + "_name" : "Memory Slots", + "global_ecc_state" : "ecc_disabled", + "is_memory_upgradeable" : "Yes" + } + ] + } + """; + + Assert.Equal((2667L, "DDR4"), SystemMemory.ParseMacMemoryJson(json)); + } + + [Fact] + public void MacAppleSiliconReadsTypeWithoutSpeed() + { + const string json = """ + { + "SPMemoryDataType" : [ + { "dimm_manufacturer" : "Hynix", "dimm_type" : "LPDDR5", "SPMemoryDataType" : "16 GB" } + ] + } + """; + + Assert.Equal(((long?)null, "LPDDR5"), SystemMemory.ParseMacMemoryJson(json)); + } + + [Fact] + public void MacVirtualMachinePlaceholdersAreIgnored() + { + // Placeholder values seen on the GitHub Actions macOS arm64 and x64 runners. + const string arm64 = """{ "SPMemoryDataType" : [ { "dimm_manufacturer" : "Unknown", "dimm_type" : "Unknown", "SPMemoryDataType" : "7 GB" } ] }"""; + const string x64 = """{ "SPMemoryDataType" : [ { "_items" : [ { "_name" : "DIMM0", "dimm_size" : "14 GB", "dimm_speed" : "1600 MHz", "dimm_type" : "RAM" } ] } ] }"""; + + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseMacMemoryJson(arm64)); + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseMacMemoryJson(x64)); + } + + [Fact] + public void MacWithoutMemoryDataReturnsNothing() + { + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseMacMemoryJson("""{ "SPMemoryDataType" : [] }""")); + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseMacMemoryJson("{}")); + } + + [Theory] + [InlineData("Unbuffered-DDR4", "DDR4")] + [InlineData("Registered-DDR5", "DDR5")] + [InlineData("Load-Reduced-DDR4-RAM", "DDR4")] + [InlineData("Low-Power-DDR5-RAM", "LPDDR5")] + [InlineData("Empty", null)] + [InlineData("Unknown", null)] + public void EdacMemoryTypeIsNormalized(string edacType, string? expected) + { + Assert.Equal(expected, SystemMemory.NormalizeEdacMemoryType(edacType)); + } + + private static byte[] Table(params byte[][] structures) + { + var table = new List(); + foreach (var structure in structures) + table.AddRange(structure); + return table.ToArray(); + } + + private static byte[] OtherStructure(byte type, params string[] strings) + { + var formatted = new byte[4]; + formatted[0] = type; + formatted[1] = (byte)formatted.Length; + return WithStrings(formatted, strings); + } + + private static byte[] MemoryDevice(byte length, byte memoryType, ushort speed, ushort configuredSpeed = 0, uint extendedSpeed = 0, + uint extendedConfiguredSpeed = 0, ushort size = 8192, string[]? strings = null) + { + var formatted = new byte[length]; + formatted[0] = 17; + formatted[1] = length; + BinaryPrimitives.WriteUInt16LittleEndian(formatted.AsSpan(0x0C), size); + formatted[0x12] = memoryType; + BinaryPrimitives.WriteUInt16LittleEndian(formatted.AsSpan(0x15), speed); + if (length >= 0x22) + BinaryPrimitives.WriteUInt16LittleEndian(formatted.AsSpan(0x20), configuredSpeed); + if (length >= 0x5C) + { + BinaryPrimitives.WriteUInt32LittleEndian(formatted.AsSpan(0x54), extendedSpeed); + BinaryPrimitives.WriteUInt32LittleEndian(formatted.AsSpan(0x58), extendedConfiguredSpeed); + } + return WithStrings(formatted, strings ?? []); + } + + // The string-set is a sequence of null-terminated strings followed by one more null; with no strings it is two nulls. + private static byte[] WithStrings(byte[] formatted, string[] strings) + { + var bytes = new List(formatted); + foreach (var s in strings) + { + bytes.AddRange(System.Text.Encoding.ASCII.GetBytes(s)); + bytes.Add(0); + } + if (strings.Length == 0) + bytes.Add(0); + bytes.Add(0); + return bytes.ToArray(); + } + } +}