From 7ddf7122d193b201461f71680a03bccc21c8d7bb Mon Sep 17 00:00:00 2001 From: Ankit Sharma Date: Fri, 10 Apr 2026 12:19:46 +0530 Subject: [PATCH 1/8] Added raw/bare disk I/O support via RawDiskTarget parameter --- .../DiskSpd/DiskSpdExecutorTests.cs | 160 ++++++++++ .../DiskSpd/DiskSpdExecutor.cs | 46 ++- .../DiskExtensionsTests.cs | 47 +++ .../DiskFiltersTests.cs | 64 ++++ .../VirtualClient.Contracts/DiskFilters.cs | 19 +- .../profiles/PERF-IO-DISKSPD-RAWDISK.json | 285 ++++++++++++++++++ 6 files changed, 618 insertions(+), 3 deletions(-) create mode 100644 src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs index dffe0d439a..fc59f1ea66 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs @@ -366,6 +366,166 @@ public async Task DiskSpdExecutorDeletesTestFilesByDefault() } } + [Test] + public void DiskSpdExecutorWithRawDiskTargetUsesPhysicalDeviceNumberSyntax_SingleProcessModel() + { + // Bare disks use VC's internal \\.\.PHYSICALDISK{N} identifier. + // The executor uses DiskSpd's native #N syntax (e.g. #1, #2) derived from disk.Index. + IEnumerable bareDisks = new List + { + this.CreateDisk(1, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK1"), + this.CreateDisk(2, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK2") + }; + + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.ProcessModel)] = WorkloadProcessModel.SingleProcess; + + List capturedArguments = new List(); + this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + capturedArguments.Add(arguments); + return new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments } + }; + }; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + executor.CreateWorkloadProcesses("diskspd.exe", "-b4K -r4K -t1 -o1 -w100", bareDisks, WorkloadProcessModel.SingleProcess); + } + + Assert.AreEqual(1, capturedArguments.Count); + // DiskSpd #N syntax -- derived from disk.Index, not disk.DevicePath. + // DiskSpd uses IOCTL_DISK_GET_DRIVE_GEOMETRY_EX internally; no -c needed. + Assert.IsTrue(capturedArguments[0].Contains(" #1")); + Assert.IsTrue(capturedArguments[0].Contains(" #2")); + // No file path style references should appear. + Assert.IsFalse(capturedArguments[0].Contains("diskspd-test.dat")); + } + + [Test] + public void DiskSpdExecutorWithRawDiskTargetUsesPhysicalDeviceNumberSyntax_SingleProcessPerDiskModel() + { + IEnumerable bareDisks = new List + { + this.CreateDisk(1, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK1"), + this.CreateDisk(2, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK2") + }; + + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + + List capturedArguments = new List(); + int processCount = 0; + this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + capturedArguments.Add(arguments); + processCount++; + return new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments } + }; + }; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + executor.CreateWorkloadProcesses("diskspd.exe", "-b4K -r4K -t1 -o1 -w100", bareDisks, WorkloadProcessModel.SingleProcessPerDisk); + } + + Assert.AreEqual(2, processCount); + // Each process targets exactly one drive via DiskSpd's #N syntax (derived from disk.Index). + Assert.IsTrue(capturedArguments[0].Contains(" #1")); + Assert.IsFalse(capturedArguments[0].Contains(" #2")); + Assert.IsTrue(capturedArguments[1].Contains(" #2")); + Assert.IsFalse(capturedArguments[1].Contains(" #1")); + } + + [Test] + public void DiskSpdExecutorWithRawDiskTargetDoesNotAppendFilenamesToCommandLine() + { + Disk bareDisk = this.CreateDisk(1, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK1"); + + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + + string capturedArguments = null; + this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + capturedArguments = arguments; + return new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments } + }; + }; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + executor.CreateWorkloadProcesses( + "diskspd.exe", + "-b4K -r4K -t1 -o1 -w100", + new[] { bareDisk }, + WorkloadProcessModel.SingleProcess); + } + + Assert.IsNotNull(capturedArguments); + // DiskSpd's #N syntax is used -- derived from disk.Index=1. + // DiskSpd queries disk capacity via IOCTL; -c and \\.\PhysicalDriveN both cause errors. + Assert.IsTrue(capturedArguments.Contains(" #1")); + // No test-file extension should be present. + Assert.IsFalse(capturedArguments.Contains(".dat")); + } + + [Test] + public void DiskSpdExecutorWithRawDiskTargetStoresDeviceNumberPathsInTestFiles() + { + // TestFiles is iterated by DeleteTestFilesAsync. For raw disk targets the paths must be + // the #N device number strings -- not file paths and not \\.\.PhysicalDriveN. + // File.Exists("#1") returns false, so DeleteTestFilesAsync becomes a correct no-op. + IEnumerable bareDisks = new List + { + this.CreateDisk(1, PlatformID.Win32NT, os: false, @"\\.\.PHYSICALDISK1"), + this.CreateDisk(2, PlatformID.Win32NT, os: false, @"\\.\.PHYSICALDISK2") + }; + + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + + this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + new InMemoryProcess { StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments } }; + + IEnumerable processes; + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + processes = executor.CreateWorkloadProcesses( + "diskspd.exe", "-b4K -r4K -t1 -o1 -w100", bareDisks, WorkloadProcessModel.SingleProcessPerDisk).ToList(); + } + + Assert.AreEqual(2, processes.Count()); + CollectionAssert.AreEqual(new[] { "#1" }, processes.ElementAt(0).TestFiles); + CollectionAssert.AreEqual(new[] { "#2" }, processes.ElementAt(1).TestFiles); + } + + [Test] + public void DiskSpdExecutorRawDiskTargetDefaultsToFalse() + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + Assert.IsFalse(executor.RawDiskTarget); + } + } + + [Test] + public void DiskSpdExecutorThrowsWhenBothRawDiskTargetAndDiskFillAreEnabled() + { + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.DiskFill)] = true; + this.profileParameters[nameof(DiskSpdExecutor.DiskFillSize)] = "500G"; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + WorkloadException exc = Assert.Throws(() => executor.Validate()); + Assert.AreEqual(ErrorReason.InvalidProfileDefinition, exc.Reason); + } + } + private IEnumerable SetupWorkloadScenario( bool testRemoteDisks = false, bool testOSDisk = false, string processModel = WorkloadProcessModel.SingleProcess) { diff --git a/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs b/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs index 0ee10a2e34..1bd5167619 100644 --- a/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs @@ -181,6 +181,25 @@ public string ProcessModel } } + /// + /// True/false whether to target the raw physical device path directly (e.g. \\.\PhysicalDrive1) + /// instead of a test file on a mounted volume. Use this for bare disk (unformatted) scenarios. + /// When enabled the step is skipped and no test file path is appended to + /// the DiskSpd command line — the device path is passed instead. + /// + public bool RawDiskTarget + { + get + { + return this.Parameters.GetValue(nameof(this.RawDiskTarget), false); + } + + set + { + this.Parameters[nameof(this.RawDiskTarget)] = value; + } + } + /// /// The disk I/O queue depth to use for running disk I/O operations. /// Default = 16. @@ -311,8 +330,22 @@ protected override async Task CleanupAsync(EventContext telemetryContext, Cancel /// The disks under test. protected DiskWorkloadProcess CreateWorkloadProcess(string executable, string commandArguments, string testedInstance, params Disk[] disksToTest) { - string[] testFiles = disksToTest.Select(disk => this.GetTestFiles(disk.GetPreferredAccessPath(this.Platform))).ToArray(); - string diskSpdArguments = $"{commandArguments} {string.Join(" ", testFiles)}"; + string diskSpdArguments; + string[] testFiles; + + if (this.RawDiskTarget) + { + // DiskSpd has a native syntax for targeting a physical drive by its index: #. + // This is the correct format for raw physical disk access; DiskSpd uses + // IOCTL_DISK_GET_DRIVE_GEOMETRY_EX internally to determine the drive capacity. + testFiles = disksToTest.Select(disk => $"#{disk.Index}").ToArray(); + diskSpdArguments = $"{commandArguments} {string.Join(" ", testFiles)}"; + } + else + { + testFiles = disksToTest.Select(disk => this.GetTestFiles(disk.GetPreferredAccessPath(this.Platform))).ToArray(); + diskSpdArguments = $"{commandArguments} {string.Join(" ", testFiles)}"; + } IProcessProxy process = this.SystemManagement.ProcessManager.CreateProcess(executable, diskSpdArguments); @@ -667,6 +700,15 @@ protected override void Validate() $"to be defined (e.g. 496G).", ErrorReason.InvalidProfileDefinition); } + + if (this.RawDiskTarget && this.DiskFill) + { + throw new WorkloadException( + $"Invalid profile definition. The '{nameof(DiskSpdExecutor.DiskFill)}' option cannot be used together with " + + $"'{nameof(DiskSpdExecutor.RawDiskTarget)}'. Disk fill operations create test files on a mounted volume and are " + + $"not applicable to raw physical device access.", + ErrorReason.InvalidProfileDefinition); + } } private void CaptureMetrics(DiskWorkloadProcess workload, EventContext telemetryContext) diff --git a/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskExtensionsTests.cs b/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskExtensionsTests.cs index 8c32998dd6..8b5245ec20 100644 --- a/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskExtensionsTests.cs +++ b/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskExtensionsTests.cs @@ -189,5 +189,52 @@ public void GetDefaultMountPointNameExtensionUsesASpecificPrefixWhenProvided() Assert.AreEqual(expectedMountPointName, actualMountPointName); } } + + [Test] + public void GetPreferredAccessPathThrowsForBareDiskWithNoVolumes_Windows() + { + // GetPreferredAccessPath is for file-based workloads only. A disk with no volumes + // cannot be used for file I/O, so the original throw behavior is correct. + // Raw disk access bypasses this method entirely via RawDiskTarget in DiskSpdExecutor. + Disk bareDisk = this.fixture.CreateDisk(1, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK1"); + + Assert.Throws(() => bareDisk.GetPreferredAccessPath(PlatformID.Win32NT)); + } + + [Test] + public void GetPreferredAccessPathThrowsForBareDiskWithNoVolumes_Unix() + { + // Same as Windows: a bare Linux disk with no volumes cannot be used for file I/O. + Disk bareDisk = this.fixture.CreateDisk(1, PlatformID.Unix, os: false, @"/dev/sdb"); + + Assert.Throws(() => bareDisk.GetPreferredAccessPath(PlatformID.Unix)); + } + + [Test] + public void GetPreferredAccessPathReturnsVolumeMountPointForFormattedNonOsDisk_Windows() + { + // A formatted non-OS Windows disk with a volume should return the volume access path. + this.disks = this.fixture.CreateDisks(PlatformID.Win32NT, true); + Disk dataDisk = this.disks.First(d => !d.IsOperatingSystem()); + + string path = dataDisk.GetPreferredAccessPath(PlatformID.Win32NT); + string expectedPath = dataDisk.Volumes.First().AccessPaths.First(); + + Assert.AreEqual(expectedPath, path); + } + + [Test] + public void GetPreferredAccessPathReturnsVolumeMountPointForFormattedNonOsDisk_Unix() + { + this.disks = this.fixture.CreateDisks(PlatformID.Unix, true); + Disk dataDisk = this.disks.First(d => !d.IsOperatingSystem()); + + string path = dataDisk.GetPreferredAccessPath(PlatformID.Unix); + string expectedPath = dataDisk.Volumes + .OrderByDescending(v => v.SizeInBytes(PlatformID.Unix)) + .First().AccessPaths.First(); + + Assert.AreEqual(expectedPath, path); + } } } diff --git a/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskFiltersTests.cs b/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskFiltersTests.cs index a6a9179616..d322b63c37 100644 --- a/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskFiltersTests.cs +++ b/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskFiltersTests.cs @@ -524,5 +524,69 @@ public void DiskFiltersHandlesAnomaliesEncounters_1() Assert.AreEqual("/dev/sdi", filteredDisks.ElementAt(30).DevicePath); Assert.AreEqual("/dev/sdj", filteredDisks.ElementAt(31).DevicePath); } + + [Test] + public void DiskFiltersIncludeOfflineFilterKeepsOfflineDisksOnWindows() + { + // Arrange: create 4 disks; mark one as offline. + this.disks = this.mockFixture.CreateDisks(PlatformID.Win32NT, true); + this.disks.ElementAt(0).Properties["Status"] = "Online"; + this.disks.ElementAt(1).Properties["Status"] = "Online"; + this.disks.ElementAt(2).Properties["Status"] = "Offline (Policy)"; + this.disks.ElementAt(3).Properties["Status"] = "Online"; + + // "none" alone would remove the offline disk; "none&IncludeOffline" should retain it. + string filterString = "none&IncludeOffline"; + IEnumerable result = DiskFilters.FilterDisks(this.disks, filterString, PlatformID.Win32NT); + + Assert.AreEqual(4, result.Count()); + Assert.IsTrue(result.Any(d => d.Properties["Status"].ToString().Contains("Offline"))); + } + + [Test] + public void DiskFiltersIncludeOfflineFilterIsCaseInsensitive() + { + this.disks = this.mockFixture.CreateDisks(PlatformID.Win32NT, true); + this.disks.ElementAt(1).Properties["Status"] = "Offline"; + + // All casing variants should be accepted. + foreach (string variant in new[] { "none&IncludeOffline", "none&includeoffline", "none&INCLUDEOFFLINE" }) + { + IEnumerable result = DiskFilters.FilterDisks(this.disks, variant, PlatformID.Win32NT); + Assert.AreEqual(4, result.Count(), $"Expected offline disk retained for filter '{variant}'"); + } + } + + [Test] + public void DiskFiltersWithoutIncludeOfflineDoesNotRetainOfflineDisksOnWindows() + { + this.disks = this.mockFixture.CreateDisks(PlatformID.Win32NT, true); + this.disks.ElementAt(2).Properties["Status"] = "Offline (Policy)"; + + // Default behaviour: the offline disk is excluded. + IEnumerable result = DiskFilters.FilterDisks(this.disks, "none", PlatformID.Win32NT); + + Assert.AreEqual(3, result.Count()); + Assert.IsFalse(result.Any(d => d.Properties.ContainsKey("Status") && + d.Properties["Status"].ToString().Contains("Offline"))); + } + + [Test] + public void DiskFiltersIncludeOfflineCanBeCombinedWithBiggestSizeFilter() + { + this.disks = this.mockFixture.CreateDisks(PlatformID.Win32NT, true); + // Make the offline disk the biggest. + this.disks.ElementAt(0).Properties["Size"] = "100 GB"; + this.disks.ElementAt(1).Properties["Size"] = "100 GB"; + this.disks.ElementAt(2).Properties["Size"] = "2000 GB"; // offline + biggest + this.disks.ElementAt(2).Properties["Status"] = "Offline"; + this.disks.ElementAt(3).Properties["Size"] = "100 GB"; + + string filterString = "BiggestSize&IncludeOffline"; + IEnumerable result = DiskFilters.FilterDisks(this.disks, filterString, PlatformID.Win32NT); + + Assert.AreEqual(1, result.Count()); + Assert.IsTrue(object.ReferenceEquals(this.disks.ElementAt(2), result.First())); + } } } diff --git a/src/VirtualClient/VirtualClient.Contracts/DiskFilters.cs b/src/VirtualClient/VirtualClient.Contracts/DiskFilters.cs index 9fa9682e54..45ba8a08fd 100644 --- a/src/VirtualClient/VirtualClient.Contracts/DiskFilters.cs +++ b/src/VirtualClient/VirtualClient.Contracts/DiskFilters.cs @@ -31,8 +31,15 @@ public static IEnumerable FilterDisks(IEnumerable disks, string filt // filterName1:value1&filterName2:value2&filterDoesNotRequireValue&filter4:value4 List filters = filterString.Split("&", StringSplitOptions.RemoveEmptyEntries).ToList(); + // Allow callers to opt into keeping offline disks (e.g. bare/unformatted disks for raw disk I/O). + bool includeOffline = filters.Any(f => f.Trim().Equals(Filters.IncludeOffline, StringComparison.OrdinalIgnoreCase)); + disks = DiskFilters.FilterStoragePathByPrefix(disks, platform); - disks = DiskFilters.FilterOfflineDisksOnWindows(disks, platform); + if (!includeOffline) + { + disks = DiskFilters.FilterOfflineDisksOnWindows(disks, platform); + } + disks = DiskFilters.FilterReadOnlyDisksOnWindows(disks, platform); foreach (string filter in filters) @@ -86,6 +93,10 @@ public static IEnumerable FilterDisks(IEnumerable disks, string filt disks = DiskFilters.DiskPathFilter(disks, filterValue); break; + case Filters.IncludeOffline: + // Already handled before the filter loop; treated as a no-op here. + break; + default: throw new EnvironmentSetupException($"Disk filter '{filter}' is not supported.", ErrorReason.DiskInformationNotAvailable); } @@ -248,6 +259,12 @@ private static class Filters /// Disk path filter. /// public const string DiskPath = "diskpath"; + + /// + /// Include offline disks filter. By default offline disks are excluded on Windows. + /// Use this filter to include them (e.g. bare/unformatted disks for raw disk I/O). + /// + public const string IncludeOffline = "includeoffline"; } } } diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json new file mode 100644 index 0000000000..37c6db7b5a --- /dev/null +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json @@ -0,0 +1,285 @@ +{ + "Description": "DiskSpd I/O Stress Performance Workload (Raw Disk / Bare Metal)", + "Metadata": { + "RecommendedMinimumExecutionTime": "00:30:00", + "SupportedPlatforms": "win-x64,win-arm64", + "SupportedOperatingSystems": "Windows" + }, + "Parameters": { + "DiskFilter": "OsDisk:false&IncludeOffline", + "Duration": "00:05:00", + "ThreadCount": "{calculate({LogicalCoreCount}/2)}", + "QueueDepth": "{calculate(512/{ThreadCount})}", + "ProcessModel": "SingleProcess" + }, + "Actions": [ + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "RandomWrite_4k_BlockSize", + "MetricScenario": "diskspd_rawdisk_randwrite_4k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b4K -r4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,randwrite,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "RandomWrite_8k_BlockSize", + "MetricScenario": "diskspd_rawdisk_randwrite_8k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b8K -r4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,randwrite,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "RandomWrite_16k_BlockSize", + "MetricScenario": "diskspd_rawdisk_randwrite_16k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b16K -r4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,randwrite,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "RandomWrite_1024k_BlockSize", + "MetricScenario": "diskspd_rawdisk_randwrite_1024k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b1024k -r4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,randwrite,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "SequentialWrite_4k_BlockSize", + "MetricScenario": "diskspd_rawdisk_write_4k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b4K -si4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,write,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "SequentialWrite_8k_BlockSize", + "MetricScenario": "diskspd_rawdisk_write_8k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b8K -si4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,write,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "SequentialWrite_16k_BlockSize", + "MetricScenario": "diskspd_rawdisk_write_16k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b16K -si4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,write,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "SequentialWrite_1024k_BlockSize", + "MetricScenario": "diskspd_rawdisk_write_1024k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b1024k -si4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,write,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "RandomRead_4k_BlockSize", + "MetricScenario": "diskspd_rawdisk_randread_4k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b4K -r4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,randread,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "RandomRead_8k_BlockSize", + "MetricScenario": "diskspd_rawdisk_randread_8k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b8K -r4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,randread,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "RandomRead_16k_BlockSize", + "MetricScenario": "diskspd_rawdisk_randread_16k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b16K -r4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,randread,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "RandomRead_1024k_BlockSize", + "MetricScenario": "diskspd_rawdisk_randread_1024k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b1024k -r4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,randread,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "SequentialRead_4k_BlockSize", + "MetricScenario": "diskspd_rawdisk_read_4k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b4K -si4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,read,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "SequentialRead_8k_BlockSize", + "MetricScenario": "diskspd_rawdisk_read_8k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b8K -si4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,read,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "SequentialRead_16k_BlockSize", + "MetricScenario": "diskspd_rawdisk_read_16k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b16K -si4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,read,rawdisk" + } + }, + { + "Type": "DiskSpdExecutor", + "Parameters": { + "Scenario": "SequentialRead_1024k_BlockSize", + "MetricScenario": "diskspd_rawdisk_read_1024k_d{QueueDepth}_th{ThreadCount}", + "PackageName": "diskspd", + "DiskFilter": "$.Parameters.DiskFilter", + "CommandLine": "-b1024k -si4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "QueueDepth": "$.Parameters.QueueDepth", + "ProcessModel": "$.Parameters.ProcessModel", + "RawDiskTarget": true, + "Tags": "IO,DiskSpd,read,rawdisk" + } + } + ], + "Dependencies": [ + { + "Type": "DependencyPackageInstallation", + "Parameters": { + "Scenario": "InstallDiskSpdPackage", + "BlobContainer": "packages", + "BlobName": "diskspd.2.0.21.zip", + "PackageName": "diskspd", + "Extract": true + } + } + ] +} From 1c61f6cf317043d766ea7307290d9e4ded170f0a Mon Sep 17 00:00:00 2001 From: Ankit Sharma Date: Fri, 10 Apr 2026 12:26:10 +0530 Subject: [PATCH 2/8] added SetDiskSanPolicy dependency to the profile to mark disks readable --- .../profiles/PERF-IO-DISKSPD-RAWDISK.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json index 37c6db7b5a..2a664f5d84 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json @@ -271,6 +271,12 @@ } ], "Dependencies": [ + { + "Type": "SetDiskSanPolicy", + "Parameters": { + "Scenario": "SetSanPolicyOnlineAll" + } + }, { "Type": "DependencyPackageInstallation", "Parameters": { From 88c496bd737dfb65da1dc130ab32eec2dcf00849 Mon Sep 17 00:00:00 2001 From: Ankit Sharma Date: Tue, 14 Apr 2026 01:09:47 +0530 Subject: [PATCH 3/8] Added raw HDD disk support with auto-discovery of disks. --- .../DiskSpd/DiskSpdExecutorTests.cs | 487 ++++++++++++++++++ .../DiskSpd/DiskSpdExecutor.cs | 158 +++++- .../profiles/PERF-IO-DISKSPD-RAWDISK.json | 264 +--------- 3 files changed, 640 insertions(+), 269 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs index fc59f1ea66..20bb7f2a86 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs @@ -512,6 +512,262 @@ public void DiskSpdExecutorRawDiskTargetDefaultsToFalse() } } + // ----------------------------------------------------------------------- + // GetRawDiskIndexRange tests + // ----------------------------------------------------------------------- + + [Test] + public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_CorrectCount() + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange("6-10"); + + // Indices 6, 7, 8, 9, 10 → 5 disks + Assert.AreEqual(5, disks.Count()); + } + } + + [Test] + public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_IndicesAreCorrect() + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange("6-10").ToList(); + + CollectionAssert.AreEqual( + new[] { 6, 7, 8, 9, 10 }, + disks.Select(d => d.Index)); + } + } + + [Test] + public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_DevicePathsAreCorrect() + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange("6-8").ToList(); + + // Device paths use the same convention as WindowsDiskManager: \\.\PHYSICALDISK{N} + CollectionAssert.AreEqual( + new[] { @"\\.\PHYSICALDISK6", @"\\.\PHYSICALDISK7", @"\\.\PHYSICALDISK8" }, + disks.Select(d => d.DevicePath)); + } + } + + [Test] + public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_SingleDisk() + { + // Range where start == end should yield exactly one disk. + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange("42-42").ToList(); + + Assert.AreEqual(1, disks.Count()); + Assert.AreEqual(42, disks.First().Index); + } + } + + [Test] + public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_IgnoresWhitespace() + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange(" 6 - 8 ").ToList(); + + Assert.AreEqual(3, disks.Count()); + CollectionAssert.AreEqual(new[] { 6, 7, 8 }, disks.Select(d => d.Index)); + } + } + + [Test] + public void DiskSpdExecutorGetRawDiskIndexRange_ParsesCommaSeparatedList_CorrectCount() + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange("6,10,15"); + + Assert.AreEqual(3, disks.Count()); + } + } + + [Test] + public void DiskSpdExecutorGetRawDiskIndexRange_ParsesCommaSeparatedList_IndicesAreCorrect() + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange("6,10,15").ToList(); + + CollectionAssert.AreEqual(new[] { 6, 10, 15 }, disks.Select(d => d.Index)); + } + } + + [Test] + public void DiskSpdExecutorGetRawDiskIndexRange_ParsesCommaSeparatedList_IgnoresWhitespace() + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange(" 6 , 7 , 8 ").ToList(); + + CollectionAssert.AreEqual(new[] { 6, 7, 8 }, disks.Select(d => d.Index)); + } + } + + [Test] + public void DiskSpdExecutorGetRawDiskIndexRange_ParsesLargeJBODRange() + { + // Mirrors the SEscript's range(6, 181) → indices 6..180 + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange("6-180").ToList(); + + Assert.AreEqual(175, disks.Count()); + Assert.AreEqual(6, disks.First().Index); + Assert.AreEqual(180, disks.Last().Index); + } + } + + [Test] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void DiskSpdExecutorGetRawDiskIndexRange_ThrowsOnNullOrWhitespace(string invalidRange) + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + Assert.Throws(() => executor.GetRawDiskIndexRange(invalidRange)); + } + } + + // ----------------------------------------------------------------------- + // RawDiskIndexRange in ExecuteAsync tests + // ----------------------------------------------------------------------- + + [Test] + public void DiskSpdExecutorWithRawDiskIndexRange_BypassesDiskManagerEnumeration() + { + // When RawDiskIndexRange is set, the disks come from GetRawDiskIndexRange, not DiskManager. + // Verify this by: (a) GetRawDiskIndexRange returns disks without any DiskManager call, + // and (b) those disks can be fed directly into CreateWorkloadProcesses without error. + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.RawDiskIndexRange)] = "6-8"; + + // Record whether DiskManager was called. + bool diskManagerCalled = false; + this.DiskManager.OnGetDisks().Returns((CancellationToken token) => + { + diskManagerCalled = true; + return Task.FromResult(this.disks); + }); + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + // GetRawDiskIndexRange must NOT call DiskManager. + IEnumerable disksFromRange = executor.GetRawDiskIndexRange("6-8"); + Assert.AreEqual(3, disksFromRange.Count()); + } + + Assert.IsFalse(diskManagerCalled, + "DiskManager must NOT be called when disks are obtained via GetRawDiskIndexRange."); + } + + [Test] + public void DiskSpdExecutorWithRawDiskIndexRange_CreatesOneProcessPerDisk() + { + // Range "6-8" → disks 6, 7, 8 → 3 processes (SingleProcessPerDisk model). + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + + List capturedArguments = new List(); + this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + capturedArguments.Add(arguments); + return new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments } + }; + }; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disksFromRange = executor.GetRawDiskIndexRange("6-8"); + executor.CreateWorkloadProcesses( + "diskspd.exe", + "-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rxml", + disksFromRange, + WorkloadProcessModel.SingleProcessPerDisk); + } + + Assert.AreEqual(3, capturedArguments.Count, "Expected one process per disk in the range."); + Assert.IsTrue(capturedArguments[0].TrimEnd().EndsWith("#6")); + Assert.IsTrue(capturedArguments[1].TrimEnd().EndsWith("#7")); + Assert.IsTrue(capturedArguments[2].TrimEnd().EndsWith("#8")); + } + + [Test] + public void DiskSpdExecutorWithRawDiskIndexRange_UsesRawDiskIndexSyntaxInCommandLine() + { + // Confirm #N notation (not a file path) is written to each spawned process. + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + + List capturedArguments = new List(); + this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + capturedArguments.Add(arguments); + return new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments } + }; + }; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disksFromRange = executor.GetRawDiskIndexRange("10-11"); + executor.CreateWorkloadProcesses( + "diskspd.exe", + "-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rxml", + disksFromRange, + WorkloadProcessModel.SingleProcessPerDisk); + } + + Assert.AreEqual(2, capturedArguments.Count); + foreach (string args in capturedArguments) + { + // #N notation must be present; no test file extension should appear. + Assert.IsTrue(args.Contains(" #10") || args.Contains(" #11"), + $"Expected '#N' syntax but got: {args}"); + Assert.IsFalse(args.Contains(".dat"), + $"Unexpected .dat file path: {args}"); + } + } + + [Test] + public void DiskSpdExecutorWithoutRawDiskIndexRange_GetDisksToTestUsesFilteredDiskSet() + { + // When RawDiskIndexRange is NOT set, GetDisksToTest must use DiskFilters and return + // disks from the DiskManager-sourced collection (not a hardcoded range). + // This is the normal (non-raw-range) code path. + IEnumerable nonOsDisks = this.disks.Where(disk => !disk.IsOperatingSystem()); + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disksToTest = executor.GetDisksToTest(this.disks); + + // The default filter excludes the OS disk, so only non-OS disks should be returned. + CollectionAssert.AreEquivalent( + nonOsDisks.Select(d => d.DevicePath), + disksToTest.Select(d => d.DevicePath)); + } + } + + [Test] + public void DiskSpdExecutorRawDiskIndexRangeDefaultsToNull() + { + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + Assert.IsNull(executor.RawDiskIndexRange); + } + } + [Test] public void DiskSpdExecutorThrowsWhenBothRawDiskTargetAndDiskFillAreEnabled() { @@ -526,6 +782,227 @@ public void DiskSpdExecutorThrowsWhenBothRawDiskTargetAndDiskFillAreEnabled() } } + // ----------------------------------------------------------------------- + // Log file naming tests (ExecuteWorkloadAsync) + // ----------------------------------------------------------------------- + + [Test] + public async Task DiskSpdExecutorWithRawDiskTarget_UsesScenarioAndDiskIndexAsLogFileName() + { + // When RawDiskTarget=true the log filename must encode both the scenario + // and the disk ordinal: "{Scenario}_disk{N}.log" + this.profileParameters[nameof(DiskSpdExecutor.Scenario)] = "RandomRead_128k_BlockSize"; + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.LogToFile)] = true; + + string capturedLogFilePath = null; + this.FileSystem + .Setup(fs => fs.File.WriteAllTextAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((path, content, ct) => + { + if (path.EndsWith(".log", StringComparison.OrdinalIgnoreCase)) + { + capturedLogFilePath = path; + } + }) + .Returns(Task.CompletedTask); + + InMemoryProcess process = new InMemoryProcess + { + OnStart = () => true, + OnHasExited = () => true + }; + process.StandardOutput.Append(this.output); + + DiskWorkloadProcess workload = new DiskWorkloadProcess(process, "SingleProcessPerDisk,OsDisk:false,1", "#6"); + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + await executor.ExecuteWorkloadsAsync(new[] { workload }, CancellationToken.None).ConfigureAwait(false); + } + + Assert.IsNotNull(capturedLogFilePath, "Expected a log file to be written."); + Assert.IsTrue( + Path.GetFileName(capturedLogFilePath).Contains("_disk6"), + $"Expected log filename to contain '_disk6' but was: {capturedLogFilePath}"); + } + + [Test] + public async Task DiskSpdExecutorWithoutRawDiskTarget_DoesNotAddDiskIndexToLogFileName() + { + // When RawDiskTarget is false the log filename must NOT contain a '_disk' suffix; + // it should fall back to the default (scenario name only). + this.profileParameters[nameof(DiskSpdExecutor.Scenario)] = "RandomRead_128k_BlockSize"; + // RawDiskTarget intentionally not set — defaults to false. + this.profileParameters[nameof(DiskSpdExecutor.LogToFile)] = true; + + string capturedLogFilePath = null; + this.FileSystem + .Setup(fs => fs.File.WriteAllTextAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((path, content, ct) => + { + if (path.EndsWith(".log", StringComparison.OrdinalIgnoreCase)) + { + capturedLogFilePath = path; + } + }) + .Returns(Task.CompletedTask); + + InMemoryProcess process = new InMemoryProcess + { + OnStart = () => true, + OnHasExited = () => true + }; + process.StandardOutput.Append(this.output); + + DiskWorkloadProcess workload = new DiskWorkloadProcess(process, "SingleProcessPerDisk,OsDisk:false,1", "D:\\any\\file.dat"); + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + await executor.ExecuteWorkloadsAsync(new[] { workload }, CancellationToken.None).ConfigureAwait(false); + } + + Assert.IsNotNull(capturedLogFilePath, "Expected a log file to be written."); + Assert.IsFalse( + Path.GetFileName(capturedLogFilePath).Contains("_disk"), + $"Expected no '_disk' suffix in log filename when RawDiskTarget=false, but was: {capturedLogFilePath}"); + } + + [Test] + public async Task DiskSpdExecutorWithRawDiskTarget_LogFilenameContainsCorrectDiskOrdinalForEachProcess() + { + // Multiple workloads (#42 and #180) must each produce a log file whose + // name encodes their own disk index, not a shared or wrong value. + this.profileParameters[nameof(DiskSpdExecutor.Scenario)] = "SequentialRead_1024k_BlockSize"; + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.LogToFile)] = true; + + List capturedPaths = new List(); + this.FileSystem + .Setup(fs => fs.File.WriteAllTextAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((path, content, ct) => + { + if (path.EndsWith(".log", StringComparison.OrdinalIgnoreCase)) + { + capturedPaths.Add(path); + } + }) + .Returns(Task.CompletedTask); + + InMemoryProcess p42 = new InMemoryProcess { OnStart = () => true, OnHasExited = () => true }; + InMemoryProcess p180 = new InMemoryProcess { OnStart = () => true, OnHasExited = () => true }; + p42.StandardOutput.Append(this.output); + p180.StandardOutput.Append(this.output); + + List workloads = new List + { + new DiskWorkloadProcess(p42, "instance", "#42"), + new DiskWorkloadProcess(p180, "instance", "#180") + }; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + await executor.ExecuteWorkloadsAsync(workloads, CancellationToken.None).ConfigureAwait(false); + } + + Assert.AreEqual(2, capturedPaths.Count, "Expected one log file per workload."); + Assert.IsTrue( + capturedPaths.Any(p => Path.GetFileName(p).Contains("_disk42")), + $"Expected a log file with '_disk42'. Paths: {string.Join(", ", capturedPaths)}"); + Assert.IsTrue( + capturedPaths.Any(p => Path.GetFileName(p).Contains("_disk180")), + $"Expected a log file with '_disk180'. Paths: {string.Join(", ", capturedPaths)}"); + } + + [Test] + public async Task DiskSpdExecutorRawDiskTarget_ParsesGetPhysicalDiskOutput() + { + // Simulate Get-PhysicalDisk output: one integer DeviceId per line. + string psOutput = "6\r\n7\r\n8\r\n180"; + + this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + if (exe.Equals("powershell.exe", StringComparison.OrdinalIgnoreCase)) + { + InMemoryProcess p = new InMemoryProcess { OnStart = () => true, OnHasExited = () => true }; + p.StandardOutput.Append(psOutput); + return p; + } + + return new InMemoryProcess { OnStart = () => true, OnHasExited = () => true }; + }; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = await executor.DiscoverRawDisksAsync(CancellationToken.None); + + Assert.AreEqual(4, disks.Count()); + Assert.AreEqual(6, disks.ElementAt(0).Index); + Assert.AreEqual(7, disks.ElementAt(1).Index); + Assert.AreEqual(8, disks.ElementAt(2).Index); + Assert.AreEqual(180, disks.ElementAt(3).Index); + } + } + + [Test] + public async Task DiskSpdExecutorRawDiskTarget_InvokesGetPhysicalDiskViaPowerShell() + { + // The discovery method must invoke powershell.exe with Get-PhysicalDisk. + string capturedExe = null; + string capturedArgs = null; + + this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + capturedExe = exe; + capturedArgs = arguments; + InMemoryProcess p = new InMemoryProcess { OnStart = () => true, OnHasExited = () => true }; + p.StandardOutput.Append("6\r\n7"); + return p; + }; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + await executor.DiscoverRawDisksAsync(CancellationToken.None); + } + + Assert.AreEqual("powershell.exe", capturedExe, "Expected powershell.exe to be invoked."); + Assert.IsTrue( + capturedArgs.Contains("Get-PhysicalDisk"), + $"Expected 'Get-PhysicalDisk' in the PowerShell command but got: {capturedArgs}"); + Assert.IsTrue( + capturedArgs.Contains("MediaType") && capturedArgs.Contains("HDD"), + $"Expected HDD MediaType filter in the PowerShell command but got: {capturedArgs}"); + } + + [Test] + public void DiskSpdExecutorRawDiskTarget_ExplicitRangeSkipsPowerShellDiscovery() + { + // When RawDiskTarget=true and RawDiskIndexRange is supplied, the explicit range wins: + // powershell.exe must NOT be invoked and the resulting disks come from the range. + this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.RawDiskIndexRange)] = "6-8"; + + bool powershellInvoked = false; + this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + if (exe.Equals("powershell.exe", StringComparison.OrdinalIgnoreCase)) + { + powershellInvoked = true; + } + + return new InMemoryProcess { OnStart = () => true, OnHasExited = () => true }; + }; + + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) + { + IEnumerable disks = executor.GetRawDiskIndexRange("6-8"); + + Assert.IsFalse(powershellInvoked, "powershell.exe must not be invoked when RawDiskIndexRange is set."); + Assert.AreEqual(3, disks.Count(), "Expected exactly 3 disks for range 6-8."); + Assert.IsTrue(disks.All(d => d.Index >= 6 && d.Index <= 8), "All disk indices must be in [6, 8]."); + } + } + private IEnumerable SetupWorkloadScenario( bool testRemoteDisks = false, bool testOSDisk = false, string processModel = WorkloadProcessModel.SingleProcess) { @@ -576,6 +1053,16 @@ public TestDiskSpdExecutor(IServiceCollection dependencies, IDictionary GetRawDiskIndexRange(string range) + { + return base.GetRawDiskIndexRange(range); + } + + public new Task> DiscoverRawDisksAsync(CancellationToken cancellationToken) + { + return base.DiscoverRawDisksAsync(cancellationToken); + } + public new void Validate() { base.Validate(); diff --git a/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs b/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs index 1bd5167619..f68982e371 100644 --- a/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs @@ -200,6 +200,27 @@ public bool RawDiskTarget } } + /// + /// When is true, specifies the inclusive range of physical disk + /// indices to test directly (e.g. "6-180" or "6,7,8"). Bypasses DiskManager/DiskPart + /// enumeration entirely. + /// When not set and is true, disk indices are discovered + /// automatically at runtime via Get-PhysicalDisk (HDD media type only). + /// + public string RawDiskIndexRange + { + get + { + this.Parameters.TryGetValue(nameof(this.RawDiskIndexRange), out IConvertible value); + return value?.ToString(); + } + + set + { + this.Parameters[nameof(this.RawDiskIndexRange)] = value; + } + } + /// /// The disk I/O queue depth to use for running disk I/O operations. /// Default = 16. @@ -446,16 +467,48 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel // Apply parameters to the DiskSpd command line options. await this.EvaluateParametersAsync(telemetryContext); - IEnumerable disks = await this.SystemManagement.DiskManager.GetDisksAsync(cancellationToken); + IEnumerable disksToTest; - if (disks?.Any() != true) + if (this.RawDiskTarget && !string.IsNullOrWhiteSpace(this.RawDiskIndexRange)) { - throw new WorkloadException( - "Unexpected scenario. The disks defined for the system could not be properly enumerated.", - ErrorReason.WorkloadUnexpectedAnomaly); + // Explicit index range supplied — build disk list directly without any + // OS enumeration. Useful when the exact range is known (e.g. "6-180"). + disksToTest = this.GetRawDiskIndexRange(this.RawDiskIndexRange); + + this.Logger.LogMessage($"{nameof(DiskSpdExecutor)}.SelectDisks", telemetryContext.Clone() + .AddContext("disks", disksToTest) + .AddContext("rawDiskIndexRange", this.RawDiskIndexRange)); + } + else if (this.RawDiskTarget) + { + // No explicit range — discover HDD indices at runtime via Get-PhysicalDisk. + // This is the default raw-disk path: it sees offline JBOD drives that + // DiskPart/DiskManager cannot enumerate, and filters to MediaType=HDD + // to exclude OS SSDs/NVMe devices. + disksToTest = await this.DiscoverRawDisksAsync(cancellationToken); + + this.Logger.LogMessage($"{nameof(DiskSpdExecutor)}.SelectDisks", telemetryContext.Clone() + .AddContext("disks", disksToTest) + .AddContext("discoveryMethod", "GetPhysicalDisk")); } + else + { + IEnumerable disks = await this.SystemManagement.DiskManager.GetDisksAsync(cancellationToken); - IEnumerable disksToTest = this.GetDisksToTest(disks); + if (disks?.Any() != true) + { + throw new WorkloadException( + "Unexpected scenario. The disks defined for the system could not be properly enumerated.", + ErrorReason.WorkloadUnexpectedAnomaly); + } + + disksToTest = this.GetDisksToTest(disks); + + this.Logger.LogMessage($"{nameof(DiskSpdExecutor)}.SelectDisks", telemetryContext.Clone() + .AddContext("disks", disksToTest)); + + telemetryContext.AddContext(nameof(disks), disks); + } if (disksToTest?.Any() != true) { @@ -466,13 +519,9 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel ErrorReason.DependencyNotFound); } - this.Logger.LogMessage($"{nameof(DiskSpdExecutor)}.SelectDisks", telemetryContext.Clone() - .AddContext("disks", disksToTest)); - disksToTest.ToList().ForEach(disk => this.Logger.LogTraceMessage($"Disk Target: '{disk}'")); telemetryContext.AddContext("executable", this.ExecutablePath); - telemetryContext.AddContext(nameof(disks), disks); telemetryContext.AddContext(nameof(disksToTest), disksToTest); this.workloadProcesses.AddRange(this.CreateWorkloadProcesses(this.ExecutablePath, this.CommandLine, disksToTest, this.ProcessModel)); @@ -521,6 +570,86 @@ protected Task ExecuteWorkloadsAsync(IEnumerable workloads, } } + /// + /// Discovers physical disk indices at runtime by running Get-PhysicalDisk via PowerShell. + /// Get-PhysicalDisk enumerates offline drives (e.g. JBOD) that DiskPart/DiskManager does not. + /// Used when is true and no is specified. + /// + protected virtual async Task> DiscoverRawDisksAsync(CancellationToken cancellationToken) + { + // Filter to HDD media type only + // This excludes OS SSDs (disk0-5) and other non-HDD devices that Get-PhysicalDisk + // would otherwise return, ensuring only JBOD HDDs are targeted. + // Output: one integer DeviceId per line (sorted numerically), e.g. "6\r\n7\r\n8\r\n...\r\n180" + const string psArguments = "-NonInteractive -NoProfile -Command " + + "\"Get-PhysicalDisk | Where-Object { $_.MediaType -eq 'HDD' } | Select-Object -ExpandProperty DeviceId | Sort-Object { [int]$_ }\""; + + List disks = new List(); + + using (IProcessProxy process = this.SystemManagement.ProcessManager.CreateProcess("powershell.exe", psArguments)) + { + await process.StartAndWaitAsync(cancellationToken); + + if (process.ExitCode != 0) + { + throw new WorkloadException( + $"Failed to discover raw disks using 'Get-PhysicalDisk'. " + + $"Exit code: {process.ExitCode}. {process.StandardError}", + ErrorReason.WorkloadUnexpectedAnomaly); + } + + foreach (string line in process.StandardOutput.ToString() + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (int.TryParse(line.Trim(), out int index)) + { + disks.Add(new Disk(index, $@"\\.\PHYSICALDISK{index}")); + } + } + } + + return disks; + } + + /// + /// Constructs a list of objects directly from a physical disk index + /// range string, bypassing DiskManager/DiskPart enumeration entirely. This is used when + /// is true and is set. + /// + /// + /// A range string in the form "6-180" (inclusive) or a comma-separated list "6,7,8". + /// + protected IEnumerable GetRawDiskIndexRange(string range) + { + range.ThrowIfNullOrWhiteSpace(nameof(range)); + + List disks = new List(); + + if (range.Contains('-')) + { + string[] parts = range.Split('-', 2); + int start = int.Parse(parts[0].Trim()); + int end = int.Parse(parts[1].Trim()); + + for (int i = start; i <= end; i++) + { + disks.Add(new Disk(i, $@"\\.\PHYSICALDISK{i}")); + } + } + else + { + foreach (string token in range.Split(',')) + { + if (int.TryParse(token.Trim(), out int idx)) + { + disks.Add(new Disk(idx, $@"\\.\PHYSICALDISK{idx}")); + } + } + } + + return disks; + } + /// /// Returns the disks to test from the set of all disks. /// @@ -759,7 +888,14 @@ await this.Logger.LogMessageAsync($"{nameof(DiskSpdExecutor)}.ExecuteProcess", t if (!cancellationToken.IsCancellationRequested) { - await this.LogProcessDetailsAsync(workload.Process, telemetryContext, "DiskSpd", logToFile: true); + string logFileName = null; + if (this.RawDiskTarget && workload.TestFiles?.Any() == true) + { + string diskIndex = workload.TestFiles.First().TrimStart('#'); + logFileName = $"{this.Scenario}_disk{diskIndex}"; + } + + await this.LogProcessDetailsAsync(workload.Process, telemetryContext, "DiskSpd", logToFile: true, logFileName: logFileName); if (this.DiskFill) { diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json index 2a664f5d84..db5d84b61d 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json @@ -6,277 +6,25 @@ "SupportedOperatingSystems": "Windows" }, "Parameters": { - "DiskFilter": "OsDisk:false&IncludeOffline", - "Duration": "00:05:00", - "ThreadCount": "{calculate({LogicalCoreCount}/2)}", - "QueueDepth": "{calculate(512/{ThreadCount})}", - "ProcessModel": "SingleProcess" + "Duration": "00:01:00", + "ProcessModel": "SingleProcessPerDisk" }, "Actions": [ { "Type": "DiskSpdExecutor", "Parameters": { - "Scenario": "RandomWrite_4k_BlockSize", - "MetricScenario": "diskspd_rawdisk_randwrite_4k_d{QueueDepth}_th{ThreadCount}", + "Scenario": "RandomRead_128k_BlockSize", + "MetricScenario": "diskspd_rawdisk_randread_128k_d32_th1", "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b4K -r4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", + "CommandLine": "-b128K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext", "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,randwrite,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "RandomWrite_8k_BlockSize", - "MetricScenario": "diskspd_rawdisk_randwrite_8k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b8K -r4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,randwrite,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "RandomWrite_16k_BlockSize", - "MetricScenario": "diskspd_rawdisk_randwrite_16k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b16K -r4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,randwrite,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "RandomWrite_1024k_BlockSize", - "MetricScenario": "diskspd_rawdisk_randwrite_1024k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b1024k -r4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,randwrite,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "SequentialWrite_4k_BlockSize", - "MetricScenario": "diskspd_rawdisk_write_4k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b4K -si4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,write,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "SequentialWrite_8k_BlockSize", - "MetricScenario": "diskspd_rawdisk_write_8k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b8K -si4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,write,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "SequentialWrite_16k_BlockSize", - "MetricScenario": "diskspd_rawdisk_write_16k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b16K -si4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,write,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "SequentialWrite_1024k_BlockSize", - "MetricScenario": "diskspd_rawdisk_write_1024k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b1024k -si4K -t{ThreadCount} -o{QueueDepth} -w100 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,write,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "RandomRead_4k_BlockSize", - "MetricScenario": "diskspd_rawdisk_randread_4k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b4K -r4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,randread,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "RandomRead_8k_BlockSize", - "MetricScenario": "diskspd_rawdisk_randread_8k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b8K -r4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,randread,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "RandomRead_16k_BlockSize", - "MetricScenario": "diskspd_rawdisk_randread_16k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b16K -r4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,randread,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "RandomRead_1024k_BlockSize", - "MetricScenario": "diskspd_rawdisk_randread_1024k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b1024k -r4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", "ProcessModel": "$.Parameters.ProcessModel", "RawDiskTarget": true, "Tags": "IO,DiskSpd,randread,rawdisk" } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "SequentialRead_4k_BlockSize", - "MetricScenario": "diskspd_rawdisk_read_4k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b4K -si4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,read,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "SequentialRead_8k_BlockSize", - "MetricScenario": "diskspd_rawdisk_read_8k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b8K -si4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,read,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "SequentialRead_16k_BlockSize", - "MetricScenario": "diskspd_rawdisk_read_16k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b16K -si4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,read,rawdisk" - } - }, - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "SequentialRead_1024k_BlockSize", - "MetricScenario": "diskspd_rawdisk_read_1024k_d{QueueDepth}_th{ThreadCount}", - "PackageName": "diskspd", - "DiskFilter": "$.Parameters.DiskFilter", - "CommandLine": "-b1024k -si4K -t{ThreadCount} -o{QueueDepth} -w0 -d{Duration.TotalSeconds} -Suw -W15 -D -L -Rtext", - "Duration": "$.Parameters.Duration", - "ThreadCount": "$.Parameters.ThreadCount", - "QueueDepth": "$.Parameters.QueueDepth", - "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "Tags": "IO,DiskSpd,read,rawdisk" - } } ], "Dependencies": [ - { - "Type": "SetDiskSanPolicy", - "Parameters": { - "Scenario": "SetSanPolicyOnlineAll" - } - }, { "Type": "DependencyPackageInstallation", "Parameters": { @@ -288,4 +36,4 @@ } } ] -} +} \ No newline at end of file From e848be60add17df11b1c39f1ab43e9c030de08ef Mon Sep 17 00:00:00 2001 From: Ankit Sharma Date: Tue, 14 Apr 2026 19:52:42 +0530 Subject: [PATCH 4/8] added functional tests, updated profile and added documentation for diskspd on raw disk scenario. --- .../DiskSpdProfileTests.cs | 96 +++++++++++++++++++ .../profiles/PERF-IO-DISKSPD-RAWDISK.json | 7 +- .../workloads/diskspd/diskspd-profiles.md | 69 +++++++++++++ 3 files changed, 170 insertions(+), 2 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs index 8ed6b43992..279fd42f4c 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs @@ -117,6 +117,102 @@ public void DiskSpdWorkloadProfileActionsWillNotBeExecutedIfTheWorkloadPackageDo } } + [Test] + [TestCase("PERF-IO-DISKSPD-RAWDISK.json")] + public void DiskSpdRawDiskWorkloadProfileParametersAreInlinedCorrectly(string profile) + { + this.mockFixture.Setup(PlatformID.Win32NT); + using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.mockFixture.Dependencies)) + { + WorkloadAssert.ParameterReferencesInlined(executor.Profile); + } + } + + [Test] + [TestCase("PERF-IO-DISKSPD-RAWDISK.json")] + public async Task DiskSpdRawDiskWorkloadProfileInstallsTheExpectedDependenciesOnWindowsPlatform(string profile) + { + // Raw disk profiles do not require disk formatting — disks are accessed directly at the raw block level. + this.mockFixture.Setup(PlatformID.Win32NT); + + using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.mockFixture.Dependencies, dependenciesOnly: true)) + { + await executor.ExecuteAsync(ProfileTiming.OneIteration(), CancellationToken.None).ConfigureAwait(false); + + // Workload dependency package expectations + // The workload dependency package should have been installed at this point. + WorkloadAssert.WorkloadPackageInstalled(this.mockFixture, "diskspd"); + } + } + + [Test] + [TestCase("PERF-IO-DISKSPD-RAWDISK.json")] + public async Task DiskSpdRawDiskWorkloadProfileExecutesTheExpectedWorkloadsOnWindowsPlatform(string profile) + { + IEnumerable expectedCommands = DiskSpdProfileTests.GetDiskSpdRawDiskProfileExpectedCommands(); + + // Setup the expectations for the workload + // - Workload package is installed and exists. + // - Workload binaries/executables exist on the file system. + // - Raw disk discovery returns 2 HDD disks (indices 6 and 7). + // - The workload generates valid results. + this.mockFixture.Setup(PlatformID.Win32NT); + this.mockFixture.SetupPackage("diskspd", expectedFiles: $@"win-x64\diskspd.exe"); + + this.mockFixture.ProcessManager.OnCreateProcess = (command, arguments, workingDir) => + { + IProcessProxy process = this.mockFixture.CreateProcess(command, arguments, workingDir); + if (arguments.Contains("Get-PhysicalDisk", StringComparison.OrdinalIgnoreCase)) + { + // Simulate discovery of 2 HDD raw disks (indices 6 and 7) + process.StandardOutput.Append("6\r\n7"); + } + else if (arguments.Contains("diskspd", StringComparison.OrdinalIgnoreCase)) + { + process.StandardOutput.Append(TestDependencies.GetResourceFileContents("Results_DiskSpd.txt")); + } + + return process; + }; + + using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.mockFixture.Dependencies)) + { + await executor.ExecuteAsync(ProfileTiming.OneIteration(), CancellationToken.None).ConfigureAwait(false); + + WorkloadAssert.CommandsExecuted(this.mockFixture, expectedCommands.ToArray()); + } + } + + [Test] + [TestCase("PERF-IO-DISKSPD-RAWDISK.json")] + public void DiskSpdRawDiskWorkloadProfileActionsWillNotBeExecutedIfWorkloadPackageDoesNotExist(string profile) + { + this.mockFixture.Setup(PlatformID.Win32NT); + + // We ensure the workload package does not exist. + this.mockFixture.PackageManager.Clear(); + + using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.mockFixture.Dependencies)) + { + executor.ExecuteDependencies = false; + + DependencyException error = Assert.ThrowsAsync(() => executor.ExecuteAsync(ProfileTiming.OneIteration(), CancellationToken.None)); + Assert.AreEqual(ErrorReason.WorkloadDependencyMissing, error.Reason); + Assert.IsFalse(this.mockFixture.ProcessManager.Commands.Contains("diskspd.exe")); + } + } + + private static IEnumerable GetDiskSpdRawDiskProfileExpectedCommands() + { + return new List + { + // ProcessModel=SingleProcessPerDisk: one diskspd process per discovered raw disk. + // Duration=00:01:00 -> 60 seconds; disks #6 and #7 discovered via Get-PhysicalDisk. + @"-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rtext #6", + @"-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rtext #7" + }; + } + private static IEnumerable GetDiskSpdStressProfileExpectedCommands() { return new List diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json index db5d84b61d..3c85f76467 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json @@ -7,7 +7,9 @@ }, "Parameters": { "Duration": "00:01:00", - "ProcessModel": "SingleProcessPerDisk" + "ProcessModel": "SingleProcessPerDisk", + "CommandLine": "-b128K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext", + "RawDiskIndexRange": "" }, "Actions": [ { @@ -16,10 +18,11 @@ "Scenario": "RandomRead_128k_BlockSize", "MetricScenario": "diskspd_rawdisk_randread_128k_d32_th1", "PackageName": "diskspd", - "CommandLine": "-b128K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext", + "CommandLine": "$.Parameters.CommandLine", "Duration": "$.Parameters.Duration", "ProcessModel": "$.Parameters.ProcessModel", "RawDiskTarget": true, + "RawDiskIndexRange": "$.Parameters.RawDiskIndexRange", "Tags": "IO,DiskSpd,randread,rawdisk" } } diff --git a/website/docs/workloads/diskspd/diskspd-profiles.md b/website/docs/workloads/diskspd/diskspd-profiles.md index 8a84a0371e..e1d76eec6e 100644 --- a/website/docs/workloads/diskspd/diskspd-profiles.md +++ b/website/docs/workloads/diskspd/diskspd-profiles.md @@ -143,4 +143,73 @@ aspects of the workload execution. # Run specific scenarios only. Each action in a profile as a 'Scenario' name. VirtualClient.exe --profile=PERF-IO-DISKSPD.json --system=Demo --timeout=1440 --scenarios=RandomWrite_4k_BlockSize,RandomWrite_8k_BlockSize,RandomRead_8k_BlockSize,RandomRead_4k_BlockSize + ``` + +## PERF-IO-DISKSPD-RAWDISK.json +Runs a read I/O workload using the DiskSpd toolset targeting raw physical HDD disks directly (no filesystem). This profile is a Windows-only profile +designed for bare-metal or JBOD scenarios where disks are not formatted or mounted. It targets disks at the raw block level using DiskSpd's native `#N` +physical disk index syntax, bypassing the Windows volume manager entirely. + +The profile auto-discovers HDD disks at runtime using `Get-PhysicalDisk | Where-Object { $_.MediaType -eq 'HDD' }`, which correctly enumerates offline +JBOD drives that DiskPart/DiskManager cannot see. An explicit `RawDiskIndexRange` parameter can be supplied to override auto-discovery. One DiskSpd +process is launched per discovered disk (`ProcessModel=SingleProcessPerDisk`). + +* [Workload Profile](https://github.com/microsoft/VirtualClient/blob/main/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json) + +* **Supported Platform/Architectures** + * win-x64 + * win-arm64 + +* **Supported Operating Systems** + * Windows 10 / Windows 11 + * Windows Server 2016 / 2019 / 2022 + +* **Supports Disconnected Scenarios** + * Yes. When the DiskSpd package is included in the 'packages' directory. + +* **Dependencies** + * Internet connection (for downloading the DiskSpd package on first run). + * Physical HDD disks present on the system (auto-discovered via `Get-PhysicalDisk`). + +* **Scenarios** + * Random Read Operations + * 128k block size, queue depth 32, 1 thread per disk (`RandomRead_128k_BlockSize`) + +* **Profile Parameters** + The following parameters can be optionally supplied on the command line to modify the behaviors of the workload. + + | Parameter | Purpose | Default Value | + |--------------------|---------|---------------| + | CommandLine | Optional. The DiskSpd command line arguments template. Supports `{Duration.TotalSeconds}` substitution. | `-b128K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext` | + | Duration | Optional. The duration of each DiskSpd scenario/action. | 1 minute | + | ProcessModel | Optional. Defines how DiskSpd processes are distributed across disks. `SingleProcessPerDisk` runs one process per raw disk. | SingleProcessPerDisk | + | RawDiskIndexRange | Optional. Overrides auto-discovery when provided. Accepted forms: a hyphen range (e.g. `6-180`), a single index (e.g. `36`), or a comma-separated list (e.g. `37,38,39,40` — see note below). When empty or omitted, HDD disks are auto-discovered via `Get-PhysicalDisk`. | (auto-discovered HDD disks) | + + > **Note on comma-separated lists:** The VC CLI uses `",,,"` as the delimiter between multiple `--parameters` values. A comma-separated `RawDiskIndexRange` (e.g. `35,38`) must therefore be followed by `,,,` so the parser treats it as a single value rather than splitting on the commas. The simplest way is to append a trailing `",,,"` (e.g. `"RawDiskIndexRange=35,38,,,"`) or include another parameter (e.g. `"RawDiskIndexRange=37,38,39,40,,,Duration=00:00:30"`). Contiguous ranges via hyphen syntax (e.g. `30-40`) have no such requirement. + +* **Usage Examples** + The following section provides a few basic examples of how to use the workload profile. + + ``` bash + # Run the workload — HDD disks are auto-discovered via Get-PhysicalDisk (MediaType=HDD) + VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 + + # Auto-discover disks with a custom duration (30 seconds per scenario) + VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="Duration=00:00:30" + + # Target a single disk by index + VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=36,,,Duration=00:00:30" + + # Target a contiguous range of disks using hyphen syntax (disks 30 through 31) + VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=30-31,,,Duration=00:00:30" + + # Target a non-contiguous set of disks using a comma-separated list + # Append a trailing ",,," so the parser treats the value as a single token + VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=35,38,,," + + # Comma-separated list combined with another parameter (trailing ",,," not needed in this case) + VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=37,38,39,40,,,Duration=00:00:30" + + # Override the command line (e.g. change block size to 64K) + VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="CommandLine=-b64K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext,,,Duration=00:00:30" ``` \ No newline at end of file From 14fdcb99210562047ef747203fdef381460d1ee7 Mon Sep 17 00:00:00 2001 From: Ankit Sharma Date: Wed, 15 Apr 2026 00:52:34 +0530 Subject: [PATCH 5/8] updated the profile name --- .../DiskSpdProfileTests.cs | 8 ++++---- ...son => PERF-IO-DISKSPD-PHYSICAL-DISK.json} | 0 .../workloads/diskspd/diskspd-profiles.md | 20 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) rename src/VirtualClient/VirtualClient.Main/profiles/{PERF-IO-DISKSPD-RAWDISK.json => PERF-IO-DISKSPD-PHYSICAL-DISK.json} (100%) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs index 279fd42f4c..2d30858a5d 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs @@ -118,7 +118,7 @@ public void DiskSpdWorkloadProfileActionsWillNotBeExecutedIfTheWorkloadPackageDo } [Test] - [TestCase("PERF-IO-DISKSPD-RAWDISK.json")] + [TestCase("PERF-IO-DISKSPD-PHYSICAL-DISK.json")] public void DiskSpdRawDiskWorkloadProfileParametersAreInlinedCorrectly(string profile) { this.mockFixture.Setup(PlatformID.Win32NT); @@ -129,7 +129,7 @@ public void DiskSpdRawDiskWorkloadProfileParametersAreInlinedCorrectly(string pr } [Test] - [TestCase("PERF-IO-DISKSPD-RAWDISK.json")] + [TestCase("PERF-IO-DISKSPD-PHYSICAL-DISK.json")] public async Task DiskSpdRawDiskWorkloadProfileInstallsTheExpectedDependenciesOnWindowsPlatform(string profile) { // Raw disk profiles do not require disk formatting — disks are accessed directly at the raw block level. @@ -146,7 +146,7 @@ public async Task DiskSpdRawDiskWorkloadProfileInstallsTheExpectedDependenciesOn } [Test] - [TestCase("PERF-IO-DISKSPD-RAWDISK.json")] + [TestCase("PERF-IO-DISKSPD-PHYSICAL-DISK.json")] public async Task DiskSpdRawDiskWorkloadProfileExecutesTheExpectedWorkloadsOnWindowsPlatform(string profile) { IEnumerable expectedCommands = DiskSpdProfileTests.GetDiskSpdRawDiskProfileExpectedCommands(); @@ -184,7 +184,7 @@ public async Task DiskSpdRawDiskWorkloadProfileExecutesTheExpectedWorkloadsOnWin } [Test] - [TestCase("PERF-IO-DISKSPD-RAWDISK.json")] + [TestCase("PERF-IO-DISKSPD-PHYSICAL-DISK.json")] public void DiskSpdRawDiskWorkloadProfileActionsWillNotBeExecutedIfWorkloadPackageDoesNotExist(string profile) { this.mockFixture.Setup(PlatformID.Win32NT); diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json similarity index 100% rename from src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json rename to src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json diff --git a/website/docs/workloads/diskspd/diskspd-profiles.md b/website/docs/workloads/diskspd/diskspd-profiles.md index e1d76eec6e..2bbde9d261 100644 --- a/website/docs/workloads/diskspd/diskspd-profiles.md +++ b/website/docs/workloads/diskspd/diskspd-profiles.md @@ -145,7 +145,7 @@ aspects of the workload execution. VirtualClient.exe --profile=PERF-IO-DISKSPD.json --system=Demo --timeout=1440 --scenarios=RandomWrite_4k_BlockSize,RandomWrite_8k_BlockSize,RandomRead_8k_BlockSize,RandomRead_4k_BlockSize ``` -## PERF-IO-DISKSPD-RAWDISK.json +## PERF-IO-DISKSPD-PHYSICAL-DISK.json Runs a read I/O workload using the DiskSpd toolset targeting raw physical HDD disks directly (no filesystem). This profile is a Windows-only profile designed for bare-metal or JBOD scenarios where disks are not formatted or mounted. It targets disks at the raw block level using DiskSpd's native `#N` physical disk index syntax, bypassing the Windows volume manager entirely. @@ -154,7 +154,7 @@ The profile auto-discovers HDD disks at runtime using `Get-PhysicalDisk | Where- JBOD drives that DiskPart/DiskManager cannot see. An explicit `RawDiskIndexRange` parameter can be supplied to override auto-discovery. One DiskSpd process is launched per discovered disk (`ProcessModel=SingleProcessPerDisk`). -* [Workload Profile](https://github.com/microsoft/VirtualClient/blob/main/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-RAWDISK.json) +* [Workload Profile](https://github.com/microsoft/VirtualClient/blob/main/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json) * **Supported Platform/Architectures** * win-x64 @@ -192,24 +192,24 @@ process is launched per discovered disk (`ProcessModel=SingleProcessPerDisk`). ``` bash # Run the workload — HDD disks are auto-discovered via Get-PhysicalDisk (MediaType=HDD) - VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 # Auto-discover disks with a custom duration (30 seconds per scenario) - VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="Duration=00:00:30" + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="Duration=00:00:30" # Target a single disk by index - VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=36,,,Duration=00:00:30" + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=36,,,Duration=00:00:30" # Target a contiguous range of disks using hyphen syntax (disks 30 through 31) - VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=30-31,,,Duration=00:00:30" + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=30-31,,,Duration=00:00:30" # Target a non-contiguous set of disks using a comma-separated list # Append a trailing ",,," so the parser treats the value as a single token - VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=35,38,,," + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=35,38,,," # Comma-separated list combined with another parameter (trailing ",,," not needed in this case) - VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=37,38,39,40,,,Duration=00:00:30" + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=37,38,39,40,,,Duration=00:00:30" # Override the command line (e.g. change block size to 64K) - VirtualClient.exe --profile=PERF-IO-DISKSPD-RAWDISK.json --system=Demo --timeout=1440 --parameters="CommandLine=-b64K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext,,,Duration=00:00:30" - ``` \ No newline at end of file + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="CommandLine=-b64K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext,,,Duration=00:00:30" + ``` From 6e0783d31ac79c894f868e1221fad5fc9ccc29af Mon Sep 17 00:00:00 2001 From: Ankit Sharma Date: Wed, 15 Apr 2026 13:38:41 +0530 Subject: [PATCH 6/8] removed rawindex parameter and added diskindex parameter (supported through diskfilter) --- .../DiskSpdProfileTests.cs | 6 +- .../TestDependencies.cs | 34 ++- .../DiskSpd/DiskSpdExecutorTests.cs | 266 ++++-------------- .../DiskSpd/DiskSpdExecutor.cs | 108 +------ .../DiskFiltersTests.cs | 113 ++++++++ .../VirtualClient.Contracts/DiskFilters.cs | 76 +++++ .../PERF-IO-DISKSPD-PHYSICAL-DISK.json | 11 +- .../workloads/diskspd/diskspd-profiles.md | 32 +-- 8 files changed, 311 insertions(+), 335 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs index 2d30858a5d..3d5325d715 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs @@ -154,7 +154,7 @@ public async Task DiskSpdRawDiskWorkloadProfileExecutesTheExpectedWorkloadsOnWin // Setup the expectations for the workload // - Workload package is installed and exists. // - Workload binaries/executables exist on the file system. - // - Raw disk discovery returns 2 HDD disks (indices 6 and 7). + // - DiskFilter=DiskIndex:hdd triggers Get-PhysicalDisk auto-discovery; mock returns disks 6 and 7. // - The workload generates valid results. this.mockFixture.Setup(PlatformID.Win32NT); this.mockFixture.SetupPackage("diskspd", expectedFiles: $@"win-x64\diskspd.exe"); @@ -164,7 +164,7 @@ public async Task DiskSpdRawDiskWorkloadProfileExecutesTheExpectedWorkloadsOnWin IProcessProxy process = this.mockFixture.CreateProcess(command, arguments, workingDir); if (arguments.Contains("Get-PhysicalDisk", StringComparison.OrdinalIgnoreCase)) { - // Simulate discovery of 2 HDD raw disks (indices 6 and 7) + // Simulate auto-discovery of 2 HDD raw disks (indices 6 and 7) process.StandardOutput.Append("6\r\n7"); } else if (arguments.Contains("diskspd", StringComparison.OrdinalIgnoreCase)) @@ -206,7 +206,7 @@ private static IEnumerable GetDiskSpdRawDiskProfileExpectedCommands() { return new List { - // ProcessModel=SingleProcessPerDisk: one diskspd process per discovered raw disk. + // ProcessModel=SingleProcessPerDisk: one diskspd process per auto-discovered raw disk. // Duration=00:01:00 -> 60 seconds; disks #6 and #7 discovered via Get-PhysicalDisk. @"-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rtext #6", @"-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rtext #7" diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/TestDependencies.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/TestDependencies.cs index 7ffa41c715..1b0a6ef3e1 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/TestDependencies.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/TestDependencies.cs @@ -4,6 +4,7 @@ namespace VirtualClient.Actions { using System; + using System.Collections.Generic; using System.IO; using System.Reflection; using Microsoft.Extensions.DependencyInjection; @@ -39,13 +40,44 @@ static TestDependencies() /// /// Creates a for the workload profile provided (e.g. PERF-IO-FIO-STRESS.json). /// - public static ProfileExecutor CreateProfileExecutor(string profile, IServiceCollection dependencies, bool dependenciesOnly = false) + /// The name of the workload profile file (e.g. PERF-IO-DISKSPD.json). + /// Service dependencies required by the profile executor. + /// True to execute only profile dependencies, not actions. + /// + /// Optional parameter overrides applied to every non-disk-fill action in the profile after inlining + /// (e.g. new Dictionary<string, IConvertible> {{ "DiskFilter", "DiskIndex:6,7" }}). + /// Useful for testing scenarios that would normally be driven by CLI --parameters. + /// + public static ProfileExecutor CreateProfileExecutor( + string profile, + IServiceCollection dependencies, + bool dependenciesOnly = false, + IDictionary parameterOverrides = null) { ExecutionProfile workloadProfile = ExecutionProfile.ReadProfileAsync(Path.Combine(TestDependencies.ProfileDirectory, profile)) .GetAwaiter().GetResult(); workloadProfile.Inline(); + if (parameterOverrides?.Count > 0) + { + foreach (ExecutionProfileElement action in workloadProfile.Actions) + { + // Do not apply overrides to disk-fill actions — DiskFill is incompatible with + // DiskIndex: targeting and would fail validation if both are set. + bool isDiskFill = action.Parameters.TryGetValue("DiskFill", out IConvertible df) + && bool.TryParse(df?.ToString(), out bool dfValue) && dfValue; + + if (!isDiskFill) + { + foreach (KeyValuePair kvp in parameterOverrides) + { + action.Parameters[kvp.Key] = kvp.Value; + } + } + } + } + ComponentSettings settings = new ComponentSettings { ExitWait = TimeSpan.Zero diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs index 20bb7f2a86..4477d03220 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/DiskSpd/DiskSpdExecutorTests.cs @@ -367,7 +367,7 @@ public async Task DiskSpdExecutorDeletesTestFilesByDefault() } [Test] - public void DiskSpdExecutorWithRawDiskTargetUsesPhysicalDeviceNumberSyntax_SingleProcessModel() + public void DiskSpdExecutorWithDiskIndexFilterUsesPhysicalDeviceNumberSyntax_SingleProcessModel() { // Bare disks use VC's internal \\.\.PHYSICALDISK{N} identifier. // The executor uses DiskSpd's native #N syntax (e.g. #1, #2) derived from disk.Index. @@ -377,7 +377,7 @@ public void DiskSpdExecutorWithRawDiskTargetUsesPhysicalDeviceNumberSyntax_Singl this.CreateDisk(2, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK2") }; - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.DiskFilter)] = "DiskIndex:1,2"; this.profileParameters[nameof(DiskSpdExecutor.ProcessModel)] = WorkloadProcessModel.SingleProcess; List capturedArguments = new List(); @@ -405,7 +405,7 @@ public void DiskSpdExecutorWithRawDiskTargetUsesPhysicalDeviceNumberSyntax_Singl } [Test] - public void DiskSpdExecutorWithRawDiskTargetUsesPhysicalDeviceNumberSyntax_SingleProcessPerDiskModel() + public void DiskSpdExecutorWithDiskIndexFilterUsesPhysicalDeviceNumberSyntax_SingleProcessPerDiskModel() { IEnumerable bareDisks = new List { @@ -413,7 +413,7 @@ public void DiskSpdExecutorWithRawDiskTargetUsesPhysicalDeviceNumberSyntax_Singl this.CreateDisk(2, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK2") }; - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.DiskFilter)] = "DiskIndex:1,2"; List capturedArguments = new List(); int processCount = 0; @@ -441,11 +441,11 @@ public void DiskSpdExecutorWithRawDiskTargetUsesPhysicalDeviceNumberSyntax_Singl } [Test] - public void DiskSpdExecutorWithRawDiskTargetDoesNotAppendFilenamesToCommandLine() + public void DiskSpdExecutorWithDiskIndexFilterDoesNotAppendFilenamesToCommandLine() { Disk bareDisk = this.CreateDisk(1, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK1"); - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.DiskFilter)] = "DiskIndex:1"; string capturedArguments = null; this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => @@ -468,25 +468,24 @@ public void DiskSpdExecutorWithRawDiskTargetDoesNotAppendFilenamesToCommandLine( Assert.IsNotNull(capturedArguments); // DiskSpd's #N syntax is used -- derived from disk.Index=1. - // DiskSpd queries disk capacity via IOCTL; -c and \\.\PhysicalDriveN both cause errors. Assert.IsTrue(capturedArguments.Contains(" #1")); // No test-file extension should be present. Assert.IsFalse(capturedArguments.Contains(".dat")); } [Test] - public void DiskSpdExecutorWithRawDiskTargetStoresDeviceNumberPathsInTestFiles() + public void DiskSpdExecutorWithDiskIndexFilterStoresDeviceNumberPathsInTestFiles() { // TestFiles is iterated by DeleteTestFilesAsync. For raw disk targets the paths must be - // the #N device number strings -- not file paths and not \\.\.PhysicalDriveN. - // File.Exists("#1") returns false, so DeleteTestFilesAsync becomes a correct no-op. + // the #N device number strings -- not file paths. File.Exists("#1") returns false, + // so DeleteTestFilesAsync becomes a correct no-op. IEnumerable bareDisks = new List { - this.CreateDisk(1, PlatformID.Win32NT, os: false, @"\\.\.PHYSICALDISK1"), - this.CreateDisk(2, PlatformID.Win32NT, os: false, @"\\.\.PHYSICALDISK2") + this.CreateDisk(1, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK1"), + this.CreateDisk(2, PlatformID.Win32NT, os: false, @"\\.\PHYSICALDISK2") }; - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.DiskFilter)] = "DiskIndex:1,2"; this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => new InMemoryProcess { StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments } }; @@ -503,156 +502,14 @@ public void DiskSpdExecutorWithRawDiskTargetStoresDeviceNumberPathsInTestFiles() CollectionAssert.AreEqual(new[] { "#2" }, processes.ElementAt(1).TestFiles); } - [Test] - public void DiskSpdExecutorRawDiskTargetDefaultsToFalse() - { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - Assert.IsFalse(executor.RawDiskTarget); - } - } - - // ----------------------------------------------------------------------- - // GetRawDiskIndexRange tests - // ----------------------------------------------------------------------- - - [Test] - public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_CorrectCount() - { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange("6-10"); - - // Indices 6, 7, 8, 9, 10 → 5 disks - Assert.AreEqual(5, disks.Count()); - } - } - - [Test] - public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_IndicesAreCorrect() - { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange("6-10").ToList(); - - CollectionAssert.AreEqual( - new[] { 6, 7, 8, 9, 10 }, - disks.Select(d => d.Index)); - } - } - - [Test] - public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_DevicePathsAreCorrect() - { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange("6-8").ToList(); - - // Device paths use the same convention as WindowsDiskManager: \\.\PHYSICALDISK{N} - CollectionAssert.AreEqual( - new[] { @"\\.\PHYSICALDISK6", @"\\.\PHYSICALDISK7", @"\\.\PHYSICALDISK8" }, - disks.Select(d => d.DevicePath)); - } - } - - [Test] - public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_SingleDisk() - { - // Range where start == end should yield exactly one disk. - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange("42-42").ToList(); - - Assert.AreEqual(1, disks.Count()); - Assert.AreEqual(42, disks.First().Index); - } - } - - [Test] - public void DiskSpdExecutorGetRawDiskIndexRange_ParsesDashRange_IgnoresWhitespace() - { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange(" 6 - 8 ").ToList(); - - Assert.AreEqual(3, disks.Count()); - CollectionAssert.AreEqual(new[] { 6, 7, 8 }, disks.Select(d => d.Index)); - } - } - - [Test] - public void DiskSpdExecutorGetRawDiskIndexRange_ParsesCommaSeparatedList_CorrectCount() - { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange("6,10,15"); - - Assert.AreEqual(3, disks.Count()); - } - } - - [Test] - public void DiskSpdExecutorGetRawDiskIndexRange_ParsesCommaSeparatedList_IndicesAreCorrect() - { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange("6,10,15").ToList(); - - CollectionAssert.AreEqual(new[] { 6, 10, 15 }, disks.Select(d => d.Index)); - } - } - - [Test] - public void DiskSpdExecutorGetRawDiskIndexRange_ParsesCommaSeparatedList_IgnoresWhitespace() - { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange(" 6 , 7 , 8 ").ToList(); - - CollectionAssert.AreEqual(new[] { 6, 7, 8 }, disks.Select(d => d.Index)); - } - } - - [Test] - public void DiskSpdExecutorGetRawDiskIndexRange_ParsesLargeJBODRange() - { - // Mirrors the SEscript's range(6, 181) → indices 6..180 - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange("6-180").ToList(); - - Assert.AreEqual(175, disks.Count()); - Assert.AreEqual(6, disks.First().Index); - Assert.AreEqual(180, disks.Last().Index); - } - } - - [Test] - [TestCase(null)] - [TestCase("")] - [TestCase(" ")] - public void DiskSpdExecutorGetRawDiskIndexRange_ThrowsOnNullOrWhitespace(string invalidRange) - { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - Assert.Throws(() => executor.GetRawDiskIndexRange(invalidRange)); - } - } - // ----------------------------------------------------------------------- - // RawDiskIndexRange in ExecuteAsync tests + // DiskIndex filter in ExecuteAsync tests // ----------------------------------------------------------------------- [Test] - public void DiskSpdExecutorWithRawDiskIndexRange_BypassesDiskManagerEnumeration() + public void DiskSpdExecutorWithDiskIndexFilter_BypassesDiskManagerEnumeration() { - // When RawDiskIndexRange is set, the disks come from GetRawDiskIndexRange, not DiskManager. - // Verify this by: (a) GetRawDiskIndexRange returns disks without any DiskManager call, - // and (b) those disks can be fed directly into CreateWorkloadProcesses without error. - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; - this.profileParameters[nameof(DiskSpdExecutor.RawDiskIndexRange)] = "6-8"; - - // Record whether DiskManager was called. + // When DiskFilter=DiskIndex:6-8, TryGetDiskIndexes resolves disks without any DiskManager call. bool diskManagerCalled = false; this.DiskManager.OnGetDisks().Returns((CancellationToken token) => { @@ -660,22 +517,19 @@ public void DiskSpdExecutorWithRawDiskIndexRange_BypassesDiskManagerEnumeration( return Task.FromResult(this.disks); }); - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - // GetRawDiskIndexRange must NOT call DiskManager. - IEnumerable disksFromRange = executor.GetRawDiskIndexRange("6-8"); - Assert.AreEqual(3, disksFromRange.Count()); - } + bool result = DiskFilters.TryGetDiskIndexes("DiskIndex:6-8", out IEnumerable indexes); - Assert.IsFalse(diskManagerCalled, - "DiskManager must NOT be called when disks are obtained via GetRawDiskIndexRange."); + Assert.IsTrue(result); + Assert.IsNotNull(indexes); + Assert.AreEqual(3, indexes.Count()); + Assert.IsFalse(diskManagerCalled, "DiskManager must NOT be called by TryGetDiskIndexes."); } [Test] - public void DiskSpdExecutorWithRawDiskIndexRange_CreatesOneProcessPerDisk() + public void DiskSpdExecutorWithDiskIndexFilter_CreatesOneProcessPerDisk() { - // Range "6-8" → disks 6, 7, 8 → 3 processes (SingleProcessPerDisk model). - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + // DiskFilter=DiskIndex:6-8 → disks 6, 7, 8 → 3 processes (SingleProcessPerDisk model). + this.profileParameters[nameof(DiskSpdExecutor.DiskFilter)] = "DiskIndex:6-8"; List capturedArguments = new List(); this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => @@ -687,9 +541,11 @@ public void DiskSpdExecutorWithRawDiskIndexRange_CreatesOneProcessPerDisk() }; }; + DiskFilters.TryGetDiskIndexes("DiskIndex:6-8", out IEnumerable indexes); + IEnumerable disksFromRange = indexes.Select(i => new Disk(i, $@"\\.\PHYSICALDISK{i}")).ToList(); + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) { - IEnumerable disksFromRange = executor.GetRawDiskIndexRange("6-8"); executor.CreateWorkloadProcesses( "diskspd.exe", "-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rxml", @@ -704,10 +560,10 @@ public void DiskSpdExecutorWithRawDiskIndexRange_CreatesOneProcessPerDisk() } [Test] - public void DiskSpdExecutorWithRawDiskIndexRange_UsesRawDiskIndexSyntaxInCommandLine() + public void DiskSpdExecutorWithDiskIndexFilter_UsesPhysicalDiskIndexSyntaxInCommandLine() { // Confirm #N notation (not a file path) is written to each spawned process. - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.DiskFilter)] = "DiskIndex:10-11"; List capturedArguments = new List(); this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => @@ -719,9 +575,11 @@ public void DiskSpdExecutorWithRawDiskIndexRange_UsesRawDiskIndexSyntaxInCommand }; }; + DiskFilters.TryGetDiskIndexes("DiskIndex:10-11", out IEnumerable indexes); + IEnumerable disksFromRange = indexes.Select(i => new Disk(i, $@"\\.\PHYSICALDISK{i}")).ToList(); + using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) { - IEnumerable disksFromRange = executor.GetRawDiskIndexRange("10-11"); executor.CreateWorkloadProcesses( "diskspd.exe", "-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rxml", @@ -732,7 +590,6 @@ public void DiskSpdExecutorWithRawDiskIndexRange_UsesRawDiskIndexSyntaxInCommand Assert.AreEqual(2, capturedArguments.Count); foreach (string args in capturedArguments) { - // #N notation must be present; no test file extension should appear. Assert.IsTrue(args.Contains(" #10") || args.Contains(" #11"), $"Expected '#N' syntax but got: {args}"); Assert.IsFalse(args.Contains(".dat"), @@ -741,7 +598,7 @@ public void DiskSpdExecutorWithRawDiskIndexRange_UsesRawDiskIndexSyntaxInCommand } [Test] - public void DiskSpdExecutorWithoutRawDiskIndexRange_GetDisksToTestUsesFilteredDiskSet() + public void DiskSpdExecutorWithoutDiskIndexFilter_GetDisksToTestUsesFilteredDiskSet() { // When RawDiskIndexRange is NOT set, GetDisksToTest must use DiskFilters and return // disks from the DiskManager-sourced collection (not a hardcoded range). @@ -760,18 +617,9 @@ public void DiskSpdExecutorWithoutRawDiskIndexRange_GetDisksToTestUsesFilteredDi } [Test] - public void DiskSpdExecutorRawDiskIndexRangeDefaultsToNull() + public void DiskSpdExecutorThrowsWhenBothDiskIndexFilterAndDiskFillAreEnabled() { - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - Assert.IsNull(executor.RawDiskIndexRange); - } - } - - [Test] - public void DiskSpdExecutorThrowsWhenBothRawDiskTargetAndDiskFillAreEnabled() - { - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; + this.profileParameters[nameof(DiskSpdExecutor.DiskFilter)] = "DiskIndex:6-8"; this.profileParameters[nameof(DiskSpdExecutor.DiskFill)] = true; this.profileParameters[nameof(DiskSpdExecutor.DiskFillSize)] = "500G"; @@ -787,12 +635,11 @@ public void DiskSpdExecutorThrowsWhenBothRawDiskTargetAndDiskFillAreEnabled() // ----------------------------------------------------------------------- [Test] - public async Task DiskSpdExecutorWithRawDiskTarget_UsesScenarioAndDiskIndexAsLogFileName() + public async Task DiskSpdExecutorWithDiskIndexFilter_UsesScenarioAndDiskIndexAsLogFileName() { - // When RawDiskTarget=true the log filename must encode both the scenario - // and the disk ordinal: "{Scenario}_disk{N}.log" + // When the test file is a #N physical disk target the log filename must encode both + // the scenario and the disk ordinal: "{Scenario}_disk{N}.log" this.profileParameters[nameof(DiskSpdExecutor.Scenario)] = "RandomRead_128k_BlockSize"; - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; this.profileParameters[nameof(DiskSpdExecutor.LogToFile)] = true; string capturedLogFilePath = null; @@ -828,12 +675,11 @@ public async Task DiskSpdExecutorWithRawDiskTarget_UsesScenarioAndDiskIndexAsLog } [Test] - public async Task DiskSpdExecutorWithoutRawDiskTarget_DoesNotAddDiskIndexToLogFileName() + public async Task DiskSpdExecutorWithoutDiskIndexFilter_DoesNotAddDiskIndexToLogFileName() { - // When RawDiskTarget is false the log filename must NOT contain a '_disk' suffix; - // it should fall back to the default (scenario name only). + // When the test file is a regular file path the log filename must NOT contain a '_disk' + // suffix; it should fall back to the default (scenario name only). this.profileParameters[nameof(DiskSpdExecutor.Scenario)] = "RandomRead_128k_BlockSize"; - // RawDiskTarget intentionally not set — defaults to false. this.profileParameters[nameof(DiskSpdExecutor.LogToFile)] = true; string capturedLogFilePath = null; @@ -865,16 +711,15 @@ public async Task DiskSpdExecutorWithoutRawDiskTarget_DoesNotAddDiskIndexToLogFi Assert.IsNotNull(capturedLogFilePath, "Expected a log file to be written."); Assert.IsFalse( Path.GetFileName(capturedLogFilePath).Contains("_disk"), - $"Expected no '_disk' suffix in log filename when RawDiskTarget=false, but was: {capturedLogFilePath}"); + $"Expected no '_disk' suffix in log filename when test file is not a '#N' target, but was: {capturedLogFilePath}"); } [Test] - public async Task DiskSpdExecutorWithRawDiskTarget_LogFilenameContainsCorrectDiskOrdinalForEachProcess() + public async Task DiskSpdExecutorWithDiskIndexFilter_LogFilenameContainsCorrectDiskOrdinalForEachProcess() { // Multiple workloads (#42 and #180) must each produce a log file whose // name encodes their own disk index, not a shared or wrong value. this.profileParameters[nameof(DiskSpdExecutor.Scenario)] = "SequentialRead_1024k_BlockSize"; - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; this.profileParameters[nameof(DiskSpdExecutor.LogToFile)] = true; List capturedPaths = new List(); @@ -975,13 +820,10 @@ public async Task DiskSpdExecutorRawDiskTarget_InvokesGetPhysicalDiskViaPowerShe } [Test] - public void DiskSpdExecutorRawDiskTarget_ExplicitRangeSkipsPowerShellDiscovery() + public void DiskSpdExecutorWithExplicitDiskIndexRange_DoesNotInvokePowerShellDiscovery() { - // When RawDiskTarget=true and RawDiskIndexRange is supplied, the explicit range wins: - // powershell.exe must NOT be invoked and the resulting disks come from the range. - this.profileParameters[nameof(DiskSpdExecutor.RawDiskTarget)] = true; - this.profileParameters[nameof(DiskSpdExecutor.RawDiskIndexRange)] = "6-8"; - + // When DiskFilter=DiskIndex: (not "hdd"), TryGetDiskIndexes returns + // explicit indexes without any OS call. PowerShell must NOT be invoked. bool powershellInvoked = false; this.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => { @@ -993,14 +835,13 @@ public void DiskSpdExecutorRawDiskTarget_ExplicitRangeSkipsPowerShellDiscovery() return new InMemoryProcess { OnStart = () => true, OnHasExited = () => true }; }; - using (TestDiskSpdExecutor executor = new TestDiskSpdExecutor(this.Dependencies, this.profileParameters)) - { - IEnumerable disks = executor.GetRawDiskIndexRange("6-8"); + bool result = DiskFilters.TryGetDiskIndexes("DiskIndex:6-8", out IEnumerable indexes); - Assert.IsFalse(powershellInvoked, "powershell.exe must not be invoked when RawDiskIndexRange is set."); - Assert.AreEqual(3, disks.Count(), "Expected exactly 3 disks for range 6-8."); - Assert.IsTrue(disks.All(d => d.Index >= 6 && d.Index <= 8), "All disk indices must be in [6, 8]."); - } + Assert.IsTrue(result); + Assert.IsNotNull(indexes); + Assert.IsFalse(powershellInvoked, "PowerShell must not be invoked for an explicit DiskIndex range."); + Assert.AreEqual(3, indexes.Count()); + Assert.IsTrue(indexes.All(i => i >= 6 && i <= 8)); } private IEnumerable SetupWorkloadScenario( @@ -1053,11 +894,6 @@ public TestDiskSpdExecutor(IServiceCollection dependencies, IDictionary GetRawDiskIndexRange(string range) - { - return base.GetRawDiskIndexRange(range); - } - public new Task> DiscoverRawDisksAsync(CancellationToken cancellationToken) { return base.DiscoverRawDisksAsync(cancellationToken); diff --git a/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs b/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs index f68982e371..59c84e6c00 100644 --- a/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/DiskSpd/DiskSpdExecutor.cs @@ -181,46 +181,6 @@ public string ProcessModel } } - /// - /// True/false whether to target the raw physical device path directly (e.g. \\.\PhysicalDrive1) - /// instead of a test file on a mounted volume. Use this for bare disk (unformatted) scenarios. - /// When enabled the step is skipped and no test file path is appended to - /// the DiskSpd command line — the device path is passed instead. - /// - public bool RawDiskTarget - { - get - { - return this.Parameters.GetValue(nameof(this.RawDiskTarget), false); - } - - set - { - this.Parameters[nameof(this.RawDiskTarget)] = value; - } - } - - /// - /// When is true, specifies the inclusive range of physical disk - /// indices to test directly (e.g. "6-180" or "6,7,8"). Bypasses DiskManager/DiskPart - /// enumeration entirely. - /// When not set and is true, disk indices are discovered - /// automatically at runtime via Get-PhysicalDisk (HDD media type only). - /// - public string RawDiskIndexRange - { - get - { - this.Parameters.TryGetValue(nameof(this.RawDiskIndexRange), out IConvertible value); - return value?.ToString(); - } - - set - { - this.Parameters[nameof(this.RawDiskIndexRange)] = value; - } - } - /// /// The disk I/O queue depth to use for running disk I/O operations. /// Default = 16. @@ -354,7 +314,7 @@ protected DiskWorkloadProcess CreateWorkloadProcess(string executable, string co string diskSpdArguments; string[] testFiles; - if (this.RawDiskTarget) + if (DiskFilters.TryGetDiskIndexes(this.DiskFilter, out _)) { // DiskSpd has a native syntax for targeting a physical drive by its index: #. // This is the correct format for raw physical disk access; DiskSpd uses @@ -469,22 +429,21 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel IEnumerable disksToTest; - if (this.RawDiskTarget && !string.IsNullOrWhiteSpace(this.RawDiskIndexRange)) + if (DiskFilters.TryGetDiskIndexes(this.DiskFilter, out IEnumerable diskIndexes) && diskIndexes != null) { - // Explicit index range supplied — build disk list directly without any - // OS enumeration. Useful when the exact range is known (e.g. "6-180"). - disksToTest = this.GetRawDiskIndexRange(this.RawDiskIndexRange); + // Explicit index range supplied (e.g. DiskFilter=DiskIndex:6-180 or DiskIndex:6,7,8). + // Build disk list directly without any OS enumeration. + disksToTest = diskIndexes.Select(i => new Disk(i, $@"\\.\PHYSICALDISK{i}")).ToList(); this.Logger.LogMessage($"{nameof(DiskSpdExecutor)}.SelectDisks", telemetryContext.Clone() .AddContext("disks", disksToTest) - .AddContext("rawDiskIndexRange", this.RawDiskIndexRange)); + .AddContext("diskFilter", this.DiskFilter)); } - else if (this.RawDiskTarget) + else if (DiskFilters.TryGetDiskIndexes(this.DiskFilter, out _)) { - // No explicit range — discover HDD indices at runtime via Get-PhysicalDisk. - // This is the default raw-disk path: it sees offline JBOD drives that - // DiskPart/DiskManager cannot enumerate, and filters to MediaType=HDD - // to exclude OS SSDs/NVMe devices. + // DiskIndex:hdd sentinel — discover HDD indices at runtime via Get-PhysicalDisk. + // This sees offline JBOD drives that DiskPart/DiskManager cannot enumerate, + // and filters to MediaType=HDD to exclude OS SSDs/NVMe devices. disksToTest = await this.DiscoverRawDisksAsync(cancellationToken); this.Logger.LogMessage($"{nameof(DiskSpdExecutor)}.SelectDisks", telemetryContext.Clone() @@ -573,7 +532,7 @@ protected Task ExecuteWorkloadsAsync(IEnumerable workloads, /// /// Discovers physical disk indices at runtime by running Get-PhysicalDisk via PowerShell. /// Get-PhysicalDisk enumerates offline drives (e.g. JBOD) that DiskPart/DiskManager does not. - /// Used when is true and no is specified. + /// Used when DiskFilter=DiskIndex:hdd is specified. /// protected virtual async Task> DiscoverRawDisksAsync(CancellationToken cancellationToken) { @@ -611,45 +570,6 @@ protected virtual async Task> DiscoverRawDisksAsync(Cancellati return disks; } - /// - /// Constructs a list of objects directly from a physical disk index - /// range string, bypassing DiskManager/DiskPart enumeration entirely. This is used when - /// is true and is set. - /// - /// - /// A range string in the form "6-180" (inclusive) or a comma-separated list "6,7,8". - /// - protected IEnumerable GetRawDiskIndexRange(string range) - { - range.ThrowIfNullOrWhiteSpace(nameof(range)); - - List disks = new List(); - - if (range.Contains('-')) - { - string[] parts = range.Split('-', 2); - int start = int.Parse(parts[0].Trim()); - int end = int.Parse(parts[1].Trim()); - - for (int i = start; i <= end; i++) - { - disks.Add(new Disk(i, $@"\\.\PHYSICALDISK{i}")); - } - } - else - { - foreach (string token in range.Split(',')) - { - if (int.TryParse(token.Trim(), out int idx)) - { - disks.Add(new Disk(idx, $@"\\.\PHYSICALDISK{idx}")); - } - } - } - - return disks; - } - /// /// Returns the disks to test from the set of all disks. /// @@ -830,11 +750,11 @@ protected override void Validate() ErrorReason.InvalidProfileDefinition); } - if (this.RawDiskTarget && this.DiskFill) + if (DiskFilters.TryGetDiskIndexes(this.DiskFilter, out _) && this.DiskFill) { throw new WorkloadException( $"Invalid profile definition. The '{nameof(DiskSpdExecutor.DiskFill)}' option cannot be used together with " + - $"'{nameof(DiskSpdExecutor.RawDiskTarget)}'. Disk fill operations create test files on a mounted volume and are " + + $"a 'DiskIndex:' disk filter. Disk fill operations create test files on a mounted volume and are " + $"not applicable to raw physical device access.", ErrorReason.InvalidProfileDefinition); } @@ -889,7 +809,7 @@ await this.Logger.LogMessageAsync($"{nameof(DiskSpdExecutor)}.ExecuteProcess", t if (!cancellationToken.IsCancellationRequested) { string logFileName = null; - if (this.RawDiskTarget && workload.TestFiles?.Any() == true) + if (workload.TestFiles?.Any() == true && workload.TestFiles.First().StartsWith("#", StringComparison.Ordinal)) { string diskIndex = workload.TestFiles.First().TrimStart('#'); logFileName = $"{this.Scenario}_disk{diskIndex}"; diff --git a/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskFiltersTests.cs b/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskFiltersTests.cs index d322b63c37..aae4bd2533 100644 --- a/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskFiltersTests.cs +++ b/src/VirtualClient/VirtualClient.Contracts.UnitTests/DiskFiltersTests.cs @@ -588,5 +588,118 @@ public void DiskFiltersIncludeOfflineCanBeCombinedWithBiggestSizeFilter() Assert.AreEqual(1, result.Count()); Assert.IsTrue(object.ReferenceEquals(this.disks.ElementAt(2), result.First())); } + + // ----------------------------------------------------------------------- + // TryGetDiskIndexes tests + // ----------------------------------------------------------------------- + + [Test] + public void DiskFiltersTryGetDiskIndexes_ReturnsFalseForNonDiskIndexFilter() + { + Assert.IsFalse(DiskFilters.TryGetDiskIndexes("BiggestSize", out _)); + Assert.IsFalse(DiskFilters.TryGetDiskIndexes("none", out _)); + Assert.IsFalse(DiskFilters.TryGetDiskIndexes("osdisk:false", out _)); + Assert.IsFalse(DiskFilters.TryGetDiskIndexes(null, out _)); + Assert.IsFalse(DiskFilters.TryGetDiskIndexes(string.Empty, out _)); + Assert.IsFalse(DiskFilters.TryGetDiskIndexes(" ", out _)); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_ParsesDashRange_CorrectCount() + { + bool result = DiskFilters.TryGetDiskIndexes("DiskIndex:6-10", out IEnumerable indexes); + + Assert.IsTrue(result); + Assert.IsNotNull(indexes); + // Indices 6, 7, 8, 9, 10 → 5 entries + Assert.AreEqual(5, indexes.Count()); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_ParsesDashRange_IndicesAreCorrect() + { + DiskFilters.TryGetDiskIndexes("DiskIndex:6-10", out IEnumerable indexes); + + CollectionAssert.AreEqual(new[] { 6, 7, 8, 9, 10 }, indexes.ToList()); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_ParsesDashRange_SingleDisk() + { + DiskFilters.TryGetDiskIndexes("DiskIndex:42-42", out IEnumerable indexes); + + Assert.AreEqual(1, indexes.Count()); + Assert.AreEqual(42, indexes.First()); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_ParsesDashRange_IgnoresWhitespace() + { + DiskFilters.TryGetDiskIndexes("DiskIndex: 6 - 8 ", out IEnumerable indexes); + + Assert.AreEqual(3, indexes.Count()); + CollectionAssert.AreEqual(new[] { 6, 7, 8 }, indexes.ToList()); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_ParsesDashRange_LargeJBODRange() + { + bool result = DiskFilters.TryGetDiskIndexes("DiskIndex:6-180", out IEnumerable indexes); + + Assert.IsTrue(result); + Assert.AreEqual(175, indexes.Count()); + Assert.AreEqual(6, indexes.First()); + Assert.AreEqual(180, indexes.Last()); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_ParsesCommaSeparatedList_CorrectCount() + { + DiskFilters.TryGetDiskIndexes("DiskIndex:6,10,15", out IEnumerable indexes); + + Assert.AreEqual(3, indexes.Count()); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_ParsesCommaSeparatedList_IndicesAreCorrect() + { + DiskFilters.TryGetDiskIndexes("DiskIndex:6,10,15", out IEnumerable indexes); + + CollectionAssert.AreEqual(new[] { 6, 10, 15 }, indexes.ToList()); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_ParsesCommaSeparatedList_IgnoresWhitespace() + { + DiskFilters.TryGetDiskIndexes("DiskIndex: 6 , 7 , 8 ", out IEnumerable indexes); + + CollectionAssert.AreEqual(new[] { 6, 7, 8 }, indexes.ToList()); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_IsCaseInsensitive() + { + Assert.IsTrue(DiskFilters.TryGetDiskIndexes("diskindex:6-8", out _)); + Assert.IsTrue(DiskFilters.TryGetDiskIndexes("DISKINDEX:6-8", out _)); + Assert.IsTrue(DiskFilters.TryGetDiskIndexes("DiskIndex:6-8", out _)); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_HddSentinel_ReturnsTrueWithNullIndexes() + { + bool result = DiskFilters.TryGetDiskIndexes("DiskIndex:hdd", out IEnumerable indexes); + + Assert.IsTrue(result, "Expected true for the 'hdd' auto-discover sentinel."); + Assert.IsNull(indexes, "Expected null indexes for the 'hdd' sentinel — caller should use OS discovery."); + } + + [Test] + public void DiskFiltersTryGetDiskIndexes_HddSentinel_IsCaseInsensitive() + { + Assert.IsTrue(DiskFilters.TryGetDiskIndexes("DiskIndex:HDD", out IEnumerable i1)); + Assert.IsNull(i1); + Assert.IsTrue(DiskFilters.TryGetDiskIndexes("DiskIndex:hdd", out IEnumerable i2)); + Assert.IsNull(i2); + } } } diff --git a/src/VirtualClient/VirtualClient.Contracts/DiskFilters.cs b/src/VirtualClient/VirtualClient.Contracts/DiskFilters.cs index 45ba8a08fd..a86ff663bf 100644 --- a/src/VirtualClient/VirtualClient.Contracts/DiskFilters.cs +++ b/src/VirtualClient/VirtualClient.Contracts/DiskFilters.cs @@ -18,6 +18,82 @@ public static class DiskFilters /// public const string DefaultDiskFilter = "BiggestSize"; + /// + /// Attempts to parse a "DiskIndex:" filter string and extract explicit physical disk indexes. + /// + /// + /// Supported forms: + /// + /// DiskIndex:6-180 — inclusive range, populates with 6..180. + /// DiskIndex:6,10,15 — comma-separated list, populates with {6, 10, 15}. + /// + /// DiskIndex:hdd — auto-discover sentinel; returns true with set to + /// null, signalling the caller to perform OS-based HDD discovery (e.g. Get-PhysicalDisk on Windows). + /// + /// + /// + /// The disk filter string (e.g. the value of the DiskFilter parameter). + /// + /// The parsed disk indexes when an explicit range or list is provided; null when the value is "hdd" + /// (indicating OS-based auto-discovery should be used instead). + /// + /// + /// true if begins with "DiskIndex:" (whether explicit or the "hdd" sentinel); + /// false if the filter is not a DiskIndex filter at all. + /// + public static bool TryGetDiskIndexes(string diskFilter, out IEnumerable indexes) + { + indexes = null; + + if (string.IsNullOrWhiteSpace(diskFilter)) + { + return false; + } + + const string prefix = "DiskIndex:"; + + if (!diskFilter.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + string value = diskFilter.Substring(prefix.Length).Trim(); + + // "DiskIndex:hdd" is a sentinel meaning "discover HDD disks at runtime via Get-PhysicalDisk". + if (value.Equals("hdd", StringComparison.OrdinalIgnoreCase)) + { + // indexes remains null — caller should perform OS-based auto-discovery. + return true; + } + + List parsed = new List(); + + if (value.Contains('-')) + { + string[] parts = value.Split('-', 2); + if (int.TryParse(parts[0].Trim(), out int start) && int.TryParse(parts[1].Trim(), out int end)) + { + for (int i = start; i <= end; i++) + { + parsed.Add(i); + } + } + } + else + { + foreach (string token in value.Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + if (int.TryParse(token.Trim(), out int idx)) + { + parsed.Add(idx); + } + } + } + + indexes = parsed; + return true; + } + /// /// Filters the disks based on filter strings /// diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json index 3c85f76467..49fe96c79d 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json @@ -9,21 +9,20 @@ "Duration": "00:01:00", "ProcessModel": "SingleProcessPerDisk", "CommandLine": "-b128K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext", - "RawDiskIndexRange": "" + "DiskFilter": "DiskIndex:hdd" }, "Actions": [ { "Type": "DiskSpdExecutor", "Parameters": { "Scenario": "RandomRead_128k_BlockSize", - "MetricScenario": "diskspd_rawdisk_randread_128k_d32_th1", + "MetricScenario": "diskspd_physicaldisk_randread_128k_d32_th1", "PackageName": "diskspd", "CommandLine": "$.Parameters.CommandLine", "Duration": "$.Parameters.Duration", "ProcessModel": "$.Parameters.ProcessModel", - "RawDiskTarget": true, - "RawDiskIndexRange": "$.Parameters.RawDiskIndexRange", - "Tags": "IO,DiskSpd,randread,rawdisk" + "DiskFilter": "$.Parameters.DiskFilter", + "Tags": "IO,DiskSpd,randread,physicaldisk" } } ], @@ -39,4 +38,4 @@ } } ] -} \ No newline at end of file +} diff --git a/website/docs/workloads/diskspd/diskspd-profiles.md b/website/docs/workloads/diskspd/diskspd-profiles.md index 2bbde9d261..d0dff42c1f 100644 --- a/website/docs/workloads/diskspd/diskspd-profiles.md +++ b/website/docs/workloads/diskspd/diskspd-profiles.md @@ -150,9 +150,9 @@ Runs a read I/O workload using the DiskSpd toolset targeting raw physical HDD di designed for bare-metal or JBOD scenarios where disks are not formatted or mounted. It targets disks at the raw block level using DiskSpd's native `#N` physical disk index syntax, bypassing the Windows volume manager entirely. -The profile auto-discovers HDD disks at runtime using `Get-PhysicalDisk | Where-Object { $_.MediaType -eq 'HDD' }`, which correctly enumerates offline -JBOD drives that DiskPart/DiskManager cannot see. An explicit `RawDiskIndexRange` parameter can be supplied to override auto-discovery. One DiskSpd -process is launched per discovered disk (`ProcessModel=SingleProcessPerDisk`). +By default the profile auto-discovers HDD disks at runtime using `Get-PhysicalDisk | Where-Object { $_.MediaType -eq 'HDD' }` (via `DiskFilter=DiskIndex:hdd`), +which correctly enumerates offline JBOD drives that DiskPart/DiskManager cannot see. The `DiskFilter` parameter can be overridden on the command line to +target an explicit set of disks by index. One DiskSpd process is launched per discovered disk (`ProcessModel=SingleProcessPerDisk`). * [Workload Profile](https://github.com/microsoft/VirtualClient/blob/main/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json) @@ -178,14 +178,14 @@ process is launched per discovered disk (`ProcessModel=SingleProcessPerDisk`). * **Profile Parameters** The following parameters can be optionally supplied on the command line to modify the behaviors of the workload. - | Parameter | Purpose | Default Value | - |--------------------|---------|---------------| - | CommandLine | Optional. The DiskSpd command line arguments template. Supports `{Duration.TotalSeconds}` substitution. | `-b128K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext` | - | Duration | Optional. The duration of each DiskSpd scenario/action. | 1 minute | - | ProcessModel | Optional. Defines how DiskSpd processes are distributed across disks. `SingleProcessPerDisk` runs one process per raw disk. | SingleProcessPerDisk | - | RawDiskIndexRange | Optional. Overrides auto-discovery when provided. Accepted forms: a hyphen range (e.g. `6-180`), a single index (e.g. `36`), or a comma-separated list (e.g. `37,38,39,40` — see note below). When empty or omitted, HDD disks are auto-discovered via `Get-PhysicalDisk`. | (auto-discovered HDD disks) | + | Parameter | Purpose | Default Value | + |--------------|---------|---------------| + | CommandLine | Optional. The DiskSpd command line arguments template. Supports `{Duration.TotalSeconds}` substitution. | `-b128K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext` | + | Duration | Optional. The duration of each DiskSpd scenario/action. | 1 minute | + | ProcessModel | Optional. Defines how DiskSpd processes are distributed across disks. `SingleProcessPerDisk` runs one process per disk. | SingleProcessPerDisk | + | DiskFilter | Optional. Controls which physical disks are targeted. Use `DiskIndex:hdd` (default) for auto-discovery of all HDD disks via `Get-PhysicalDisk`, `DiskIndex:6-180` for a contiguous range, or `DiskIndex:6,10,15` for a specific set. | `DiskIndex:hdd` | - > **Note on comma-separated lists:** The VC CLI uses `",,,"` as the delimiter between multiple `--parameters` values. A comma-separated `RawDiskIndexRange` (e.g. `35,38`) must therefore be followed by `,,,` so the parser treats it as a single value rather than splitting on the commas. The simplest way is to append a trailing `",,,"` (e.g. `"RawDiskIndexRange=35,38,,,"`) or include another parameter (e.g. `"RawDiskIndexRange=37,38,39,40,,,Duration=00:00:30"`). Contiguous ranges via hyphen syntax (e.g. `30-40`) have no such requirement. + > **Note on `DiskIndex:` syntax:** The VC CLI uses `",,,"` as the delimiter between multiple `--parameters` values. A comma-separated `DiskIndex:` value (e.g. `DiskIndex:35,38`) must therefore be followed by `,,,` so the parser treats it as a single token rather than splitting on the commas (e.g. `"DiskFilter=DiskIndex:35,38,,,"` or `"DiskFilter=DiskIndex:37,38,39,40,,,Duration=00:00:30"`). Contiguous ranges via hyphen syntax (e.g. `DiskIndex:30-40`) have no such requirement. * **Usage Examples** The following section provides a few basic examples of how to use the workload profile. @@ -198,17 +198,17 @@ process is launched per discovered disk (`ProcessModel=SingleProcessPerDisk`). VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="Duration=00:00:30" # Target a single disk by index - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=36,,,Duration=00:00:30" + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="DiskFilter=DiskIndex:36,,,Duration=00:00:30" - # Target a contiguous range of disks using hyphen syntax (disks 30 through 31) - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=30-31,,,Duration=00:00:30" + # Target a contiguous range of disks using hyphen syntax (disks 6 through 180) + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="DiskFilter=DiskIndex:6-180" # Target a non-contiguous set of disks using a comma-separated list # Append a trailing ",,," so the parser treats the value as a single token - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=35,38,,," + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="DiskFilter=DiskIndex:35,38,,," - # Comma-separated list combined with another parameter (trailing ",,," not needed in this case) - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="RawDiskIndexRange=37,38,39,40,,,Duration=00:00:30" + # Comma-separated list combined with another parameter + VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="DiskFilter=DiskIndex:37,38,39,40,,,Duration=00:00:30" # Override the command line (e.g. change block size to 64K) VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="CommandLine=-b64K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext,,,Duration=00:00:30" From d52cb08cf0dcdd0012aa6baf67996107f3bd94c7 Mon Sep 17 00:00:00 2001 From: Ankit Sharma Date: Thu, 16 Apr 2026 11:51:00 +0530 Subject: [PATCH 7/8] remove the new profile from the open source repo and add to internal repo. --- .../DiskSpdProfileTests.cs | 96 ------------------- .../PERF-IO-DISKSPD-PHYSICAL-DISK.json | 41 -------- .../workloads/diskspd/diskspd-profiles.md | 69 ------------- 3 files changed, 206 deletions(-) delete mode 100644 src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs index 3d5325d715..8ed6b43992 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/DiskSpdProfileTests.cs @@ -117,102 +117,6 @@ public void DiskSpdWorkloadProfileActionsWillNotBeExecutedIfTheWorkloadPackageDo } } - [Test] - [TestCase("PERF-IO-DISKSPD-PHYSICAL-DISK.json")] - public void DiskSpdRawDiskWorkloadProfileParametersAreInlinedCorrectly(string profile) - { - this.mockFixture.Setup(PlatformID.Win32NT); - using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.mockFixture.Dependencies)) - { - WorkloadAssert.ParameterReferencesInlined(executor.Profile); - } - } - - [Test] - [TestCase("PERF-IO-DISKSPD-PHYSICAL-DISK.json")] - public async Task DiskSpdRawDiskWorkloadProfileInstallsTheExpectedDependenciesOnWindowsPlatform(string profile) - { - // Raw disk profiles do not require disk formatting — disks are accessed directly at the raw block level. - this.mockFixture.Setup(PlatformID.Win32NT); - - using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.mockFixture.Dependencies, dependenciesOnly: true)) - { - await executor.ExecuteAsync(ProfileTiming.OneIteration(), CancellationToken.None).ConfigureAwait(false); - - // Workload dependency package expectations - // The workload dependency package should have been installed at this point. - WorkloadAssert.WorkloadPackageInstalled(this.mockFixture, "diskspd"); - } - } - - [Test] - [TestCase("PERF-IO-DISKSPD-PHYSICAL-DISK.json")] - public async Task DiskSpdRawDiskWorkloadProfileExecutesTheExpectedWorkloadsOnWindowsPlatform(string profile) - { - IEnumerable expectedCommands = DiskSpdProfileTests.GetDiskSpdRawDiskProfileExpectedCommands(); - - // Setup the expectations for the workload - // - Workload package is installed and exists. - // - Workload binaries/executables exist on the file system. - // - DiskFilter=DiskIndex:hdd triggers Get-PhysicalDisk auto-discovery; mock returns disks 6 and 7. - // - The workload generates valid results. - this.mockFixture.Setup(PlatformID.Win32NT); - this.mockFixture.SetupPackage("diskspd", expectedFiles: $@"win-x64\diskspd.exe"); - - this.mockFixture.ProcessManager.OnCreateProcess = (command, arguments, workingDir) => - { - IProcessProxy process = this.mockFixture.CreateProcess(command, arguments, workingDir); - if (arguments.Contains("Get-PhysicalDisk", StringComparison.OrdinalIgnoreCase)) - { - // Simulate auto-discovery of 2 HDD raw disks (indices 6 and 7) - process.StandardOutput.Append("6\r\n7"); - } - else if (arguments.Contains("diskspd", StringComparison.OrdinalIgnoreCase)) - { - process.StandardOutput.Append(TestDependencies.GetResourceFileContents("Results_DiskSpd.txt")); - } - - return process; - }; - - using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.mockFixture.Dependencies)) - { - await executor.ExecuteAsync(ProfileTiming.OneIteration(), CancellationToken.None).ConfigureAwait(false); - - WorkloadAssert.CommandsExecuted(this.mockFixture, expectedCommands.ToArray()); - } - } - - [Test] - [TestCase("PERF-IO-DISKSPD-PHYSICAL-DISK.json")] - public void DiskSpdRawDiskWorkloadProfileActionsWillNotBeExecutedIfWorkloadPackageDoesNotExist(string profile) - { - this.mockFixture.Setup(PlatformID.Win32NT); - - // We ensure the workload package does not exist. - this.mockFixture.PackageManager.Clear(); - - using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.mockFixture.Dependencies)) - { - executor.ExecuteDependencies = false; - - DependencyException error = Assert.ThrowsAsync(() => executor.ExecuteAsync(ProfileTiming.OneIteration(), CancellationToken.None)); - Assert.AreEqual(ErrorReason.WorkloadDependencyMissing, error.Reason); - Assert.IsFalse(this.mockFixture.ProcessManager.Commands.Contains("diskspd.exe")); - } - } - - private static IEnumerable GetDiskSpdRawDiskProfileExpectedCommands() - { - return new List - { - // ProcessModel=SingleProcessPerDisk: one diskspd process per auto-discovered raw disk. - // Duration=00:01:00 -> 60 seconds; disks #6 and #7 discovered via Get-PhysicalDisk. - @"-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rtext #6", - @"-b128K -d60 -o32 -t1 -r -w0 -Sh -L -Rtext #7" - }; - } - private static IEnumerable GetDiskSpdStressProfileExpectedCommands() { return new List diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json deleted file mode 100644 index 49fe96c79d..0000000000 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "Description": "DiskSpd I/O Stress Performance Workload (Raw Disk / Bare Metal)", - "Metadata": { - "RecommendedMinimumExecutionTime": "00:30:00", - "SupportedPlatforms": "win-x64,win-arm64", - "SupportedOperatingSystems": "Windows" - }, - "Parameters": { - "Duration": "00:01:00", - "ProcessModel": "SingleProcessPerDisk", - "CommandLine": "-b128K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext", - "DiskFilter": "DiskIndex:hdd" - }, - "Actions": [ - { - "Type": "DiskSpdExecutor", - "Parameters": { - "Scenario": "RandomRead_128k_BlockSize", - "MetricScenario": "diskspd_physicaldisk_randread_128k_d32_th1", - "PackageName": "diskspd", - "CommandLine": "$.Parameters.CommandLine", - "Duration": "$.Parameters.Duration", - "ProcessModel": "$.Parameters.ProcessModel", - "DiskFilter": "$.Parameters.DiskFilter", - "Tags": "IO,DiskSpd,randread,physicaldisk" - } - } - ], - "Dependencies": [ - { - "Type": "DependencyPackageInstallation", - "Parameters": { - "Scenario": "InstallDiskSpdPackage", - "BlobContainer": "packages", - "BlobName": "diskspd.2.0.21.zip", - "PackageName": "diskspd", - "Extract": true - } - } - ] -} diff --git a/website/docs/workloads/diskspd/diskspd-profiles.md b/website/docs/workloads/diskspd/diskspd-profiles.md index d0dff42c1f..2db96a0250 100644 --- a/website/docs/workloads/diskspd/diskspd-profiles.md +++ b/website/docs/workloads/diskspd/diskspd-profiles.md @@ -144,72 +144,3 @@ aspects of the workload execution. # Run specific scenarios only. Each action in a profile as a 'Scenario' name. VirtualClient.exe --profile=PERF-IO-DISKSPD.json --system=Demo --timeout=1440 --scenarios=RandomWrite_4k_BlockSize,RandomWrite_8k_BlockSize,RandomRead_8k_BlockSize,RandomRead_4k_BlockSize ``` - -## PERF-IO-DISKSPD-PHYSICAL-DISK.json -Runs a read I/O workload using the DiskSpd toolset targeting raw physical HDD disks directly (no filesystem). This profile is a Windows-only profile -designed for bare-metal or JBOD scenarios where disks are not formatted or mounted. It targets disks at the raw block level using DiskSpd's native `#N` -physical disk index syntax, bypassing the Windows volume manager entirely. - -By default the profile auto-discovers HDD disks at runtime using `Get-PhysicalDisk | Where-Object { $_.MediaType -eq 'HDD' }` (via `DiskFilter=DiskIndex:hdd`), -which correctly enumerates offline JBOD drives that DiskPart/DiskManager cannot see. The `DiskFilter` parameter can be overridden on the command line to -target an explicit set of disks by index. One DiskSpd process is launched per discovered disk (`ProcessModel=SingleProcessPerDisk`). - -* [Workload Profile](https://github.com/microsoft/VirtualClient/blob/main/src/VirtualClient/VirtualClient.Main/profiles/PERF-IO-DISKSPD-PHYSICAL-DISK.json) - -* **Supported Platform/Architectures** - * win-x64 - * win-arm64 - -* **Supported Operating Systems** - * Windows 10 / Windows 11 - * Windows Server 2016 / 2019 / 2022 - -* **Supports Disconnected Scenarios** - * Yes. When the DiskSpd package is included in the 'packages' directory. - -* **Dependencies** - * Internet connection (for downloading the DiskSpd package on first run). - * Physical HDD disks present on the system (auto-discovered via `Get-PhysicalDisk`). - -* **Scenarios** - * Random Read Operations - * 128k block size, queue depth 32, 1 thread per disk (`RandomRead_128k_BlockSize`) - -* **Profile Parameters** - The following parameters can be optionally supplied on the command line to modify the behaviors of the workload. - - | Parameter | Purpose | Default Value | - |--------------|---------|---------------| - | CommandLine | Optional. The DiskSpd command line arguments template. Supports `{Duration.TotalSeconds}` substitution. | `-b128K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext` | - | Duration | Optional. The duration of each DiskSpd scenario/action. | 1 minute | - | ProcessModel | Optional. Defines how DiskSpd processes are distributed across disks. `SingleProcessPerDisk` runs one process per disk. | SingleProcessPerDisk | - | DiskFilter | Optional. Controls which physical disks are targeted. Use `DiskIndex:hdd` (default) for auto-discovery of all HDD disks via `Get-PhysicalDisk`, `DiskIndex:6-180` for a contiguous range, or `DiskIndex:6,10,15` for a specific set. | `DiskIndex:hdd` | - - > **Note on `DiskIndex:` syntax:** The VC CLI uses `",,,"` as the delimiter between multiple `--parameters` values. A comma-separated `DiskIndex:` value (e.g. `DiskIndex:35,38`) must therefore be followed by `,,,` so the parser treats it as a single token rather than splitting on the commas (e.g. `"DiskFilter=DiskIndex:35,38,,,"` or `"DiskFilter=DiskIndex:37,38,39,40,,,Duration=00:00:30"`). Contiguous ranges via hyphen syntax (e.g. `DiskIndex:30-40`) have no such requirement. - -* **Usage Examples** - The following section provides a few basic examples of how to use the workload profile. - - ``` bash - # Run the workload — HDD disks are auto-discovered via Get-PhysicalDisk (MediaType=HDD) - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 - - # Auto-discover disks with a custom duration (30 seconds per scenario) - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="Duration=00:00:30" - - # Target a single disk by index - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="DiskFilter=DiskIndex:36,,,Duration=00:00:30" - - # Target a contiguous range of disks using hyphen syntax (disks 6 through 180) - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="DiskFilter=DiskIndex:6-180" - - # Target a non-contiguous set of disks using a comma-separated list - # Append a trailing ",,," so the parser treats the value as a single token - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="DiskFilter=DiskIndex:35,38,,," - - # Comma-separated list combined with another parameter - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="DiskFilter=DiskIndex:37,38,39,40,,,Duration=00:00:30" - - # Override the command line (e.g. change block size to 64K) - VirtualClient.exe --profile=PERF-IO-DISKSPD-PHYSICAL-DISK.json --system=Demo --timeout=1440 --parameters="CommandLine=-b64K -d{Duration.TotalSeconds} -o32 -t1 -r -w0 -Sh -L -Rtext,,,Duration=00:00:30" - ``` From 09442116f68480d78caf070f16c8cf59c2e9645c Mon Sep 17 00:00:00 2001 From: Ankit Sharma Date: Thu, 16 Apr 2026 11:55:50 +0530 Subject: [PATCH 8/8] spacing issues --- website/docs/workloads/diskspd/diskspd-profiles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/workloads/diskspd/diskspd-profiles.md b/website/docs/workloads/diskspd/diskspd-profiles.md index 2db96a0250..8a84a0371e 100644 --- a/website/docs/workloads/diskspd/diskspd-profiles.md +++ b/website/docs/workloads/diskspd/diskspd-profiles.md @@ -143,4 +143,4 @@ aspects of the workload execution. # Run specific scenarios only. Each action in a profile as a 'Scenario' name. VirtualClient.exe --profile=PERF-IO-DISKSPD.json --system=Demo --timeout=1440 --scenarios=RandomWrite_4k_BlockSize,RandomWrite_8k_BlockSize,RandomRead_8k_BlockSize,RandomRead_4k_BlockSize - ``` + ``` \ No newline at end of file