Last active
June 28, 2025 19:59
-
-
Save nanoDBA/fbee092058bd7e8956c8d9b261c5b231 to your computer and use it in GitHub Desktop.
Download / Extract and Run DiskSpd - Automated EC2 volume and DiskSpd benchmarking for Windows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ------------------------------------------------------------------------------ | |
| # File: Test-DiskSpd.ps1 | |
| # Description: 🚀 Automated EC2 volume and DiskSpd benchmarking for Windows | |
| # Purpose: Retrieves detailed EC2 volume info and runs robust DiskSpd I/O | |
| # benchmarks on specified drives. Handles AWS, local, and hybrid | |
| # scenarios with safety checks, resumable downloads, and cleanup. | |
| # Built for DBAs and sysadmins who want to know their storage is | |
| # actually as fast as the cloud bill says! 😎 | |
| # Created: 2025-06-26 | |
| # Modified: 2025-06-26 | |
| # Adapted | |
| # from: https://sqlperformance.com/2015/08/io-subsystem/diskspd-test-storage | |
| # ------------------------------------------------------------------------------ | |
| # AI-Updated: 2025-06-26 12:00:00 | Marker: AIUPD-20250626120000-546573742d4469736b5370642e707331 #| | |
| # Summary: | |
| # 📊 This script combines AWS EC2 volume discovery with DiskSpd performance | |
| # testing. It auto-detects drive details, benchmarks with best-practice | |
| # parameters, and logs results with context for later analysis. Handles | |
| # download, extraction, and cleanup of DiskSpd, and works even if BITS is | |
| # missing. All output is designed for clarity and confidence. | |
| # | |
| # Features: | |
| # - 🔍 EC2 volume info (type, IOPS, throughput, etc.) | |
| # - ⚡ DiskSpd download with BITS or iwr fallback | |
| # - 🏗️ Automated extraction and cleanup | |
| # - 📝 Timestamped, descriptive results files | |
| # - 🛡️ Safety checks for free space and test file cleanup | |
| # - 🤖 Wrapper for Expand-Archive for user-friendly params | |
| # - 💬 Verbose and error output for troubleshooting | |
| # | |
| # Recommendations: | |
| # - 🔧 Test in a non-prod environment first (I/O tests can be disruptive) | |
| # - ⏰ Run during maintenance windows for best results | |
| # - 📈 Monitor disk and network usage during tests | |
| # - 🛑 Clean up large test files to avoid storage surprises | |
| # | |
| # DBA Pro Tip: Trust, but verify your cloud storage claims. This script makes | |
| # it easy (and fun) to catch underperforming volumes before your users do! 😜 | |
| #region Configuration 🛠️ | |
| # Let's get this party started by making sure all our ducks (and drives) are in a row. | |
| # If you don't have AWS Tools or DiskSpd, this script will try to help you out. | |
| # If it can't, well... you probably didn't want to benchmark anything anyway. 😏 | |
| #endregion | |
| #region Helper Functions 🤖 | |
| # Because PowerShell is better with friends (and subroutines). | |
| function Expand-ArchiveCompat { | |
| [CmdletBinding()] | |
| param() | |
| Expand-Archive @PSBoundParameters | |
| } | |
| # Helper: Parse DiskSpd XML and export summary to CSV | |
| function Get-DiskSpdSummary { | |
| param ( | |
| [Parameter(Mandatory = $true)] | |
| [string]$XmlFile, | |
| [Parameter(Mandatory = $true)] | |
| [string]$CsvFile, | |
| [Parameter(Mandatory = $true)] | |
| [string]$DriveLetter | |
| ) | |
| $x = [xml](Get-Content $XmlFile) | |
| $testTime = [double]$x.Results.TimeSpan.TestTimeSeconds | |
| $blockSize = ($x.Results.Profile.TimeSpans.TimeSpan.Targets.Target.BlockSize | Select-Object -First 1) | |
| $threadCount = $x.Results.TimeSpan.ThreadCount | |
| $rb = ($x.Results.TimeSpan.Thread.Target | Measure-Object -Sum -Property ReadBytes).Sum | |
| $wb = ($x.Results.TimeSpan.Thread.Target | Measure-Object -Sum -Property WriteBytes).Sum | |
| $rc = ($x.Results.TimeSpan.Thread.Target | Measure-Object -Sum -Property ReadCount).Sum | |
| $wc = ($x.Results.TimeSpan.Thread.Target | Measure-Object -Sum -Property WriteCount).Sum | |
| $readMBps = [math]::Round($rb / $testTime / 1MB, 2) | |
| $writeMBps = [math]::Round($wb / $testTime / 1MB, 2) | |
| $totalMBps = [math]::Round(($rb + $wb) / $testTime / 1MB, 2) | |
| $readIOPS = [math]::Round($rc / $testTime, 2) | |
| $writeIOPS = [math]::Round($wc / $testTime, 2) | |
| $totalIOPS = [math]::Round(($rc + $wc) / $testTime, 2) | |
| $avgReadLat = [math]::Round($x.Results.TimeSpan.AvgReadLatencyMilliseconds, 2) | |
| $avgWriteLat = [math]::Round($x.Results.TimeSpan.AvgWriteLatencyMilliseconds, 2) | |
| $o = [PSCustomObject]@{ | |
| ComputerName = $x.Results.System.ComputerName | |
| Drive = $DriveLetter | |
| TestTime = Get-Date | |
| TestTimeSeconds = $testTime | |
| BlockSize = $blockSize | |
| ThreadCount = $threadCount | |
| ReadThroughputMBps = $readMBps | |
| WriteThroughputMBps = $writeMBps | |
| TotalThroughputMBps = $totalMBps | |
| ReadIOPS = $readIOPS | |
| WriteIOPS = $writeIOPS | |
| TotalIOPS = $totalIOPS | |
| AvgReadLatencyMs = $avgReadLat | |
| AvgWriteLatencyMs = $avgWriteLat | |
| } | |
| # Export all details to CSV (including bytes/counts for later analysis) | |
| $csvObj = [PSCustomObject]@{ | |
| ComputerName = $x.Results.System.ComputerName | |
| Drive = $DriveLetter | |
| TestTime = Get-Date | |
| TestTimeSeconds = $testTime | |
| BlockSize = $blockSize | |
| ThreadCount = $threadCount | |
| ReadThroughputMBps = $readMBps | |
| WriteThroughputMBps = $writeMBps | |
| TotalThroughputMBps = $totalMBps | |
| ReadIOPS = $readIOPS | |
| WriteIOPS = $writeIOPS | |
| TotalIOPS = $totalIOPS | |
| AvgReadLatencyMs = $avgReadLat | |
| AvgWriteLatencyMs = $avgWriteLat | |
| ReadCount = $rc | |
| WriteCount = $wc | |
| ReadBytes = $rb | |
| WriteBytes = $wb | |
| } | |
| $csvObj | Export-Csv -Path $CsvFile -NoTypeInformation -Append | |
| return $o | |
| } | |
| #endregion | |
| #region EC2 Volume Details Retrieval 🔍 | |
| # This function tries every trick in the book to figure out what disks you have. | |
| # If your disk layout is weird, it will get weird right back at you. 🙃 | |
| # It even talks to AWS, so make sure your credentials are less flaky than your storage. | |
| function Get-EC2DriveVolumeDetails { | |
| <#! | |
| .SYNOPSIS | |
| Retrieves details about EC2 instance drive volumes, including size, type, IOPS, and more. 🔍 | |
| .DESCRIPTION | |
| This function tries every trick in the book to figure out what disks you have on your EC2 instance. If your disk layout is weird, it will get weird right back at you. 🙃 It even talks to AWS, so make sure your credentials are less flaky than your storage. Returns a custom object with drive, size, and EBS details. | |
| .PARAMETER DriveLetter | |
| One or more characters representing the Windows drive letters (e.g., 'C', 'D'). If omitted, retrieves details for all lettered drives (C, D, E, etc.). | |
| .EXAMPLE | |
| Get-EC2DriveVolumeDetails | |
| # Retrieves details for all local drives on the EC2 instance. | |
| .EXAMPLE | |
| Get-EC2DriveVolumeDetails -DriveLetter C | |
| # Retrieves details for the C: drive only. | |
| .EXAMPLE | |
| Get-EC2DriveVolumeDetails -DriveLetter C, D | |
| # Retrieves details for both C: and D:. | |
| .NOTES | |
| Requires AWS Tools for PowerShell: Install-Module AWS.Tools.Common; Install-Module AWS.Tools.EC2 | |
| Ensure valid AWS credentials/permissions (ec2:DescribeVolumes) are in place. | |
| Verbose output can help debug fallback paths (dynamic disks, WMI). | |
| .LINK | |
| https://docs.aws.amazon.com/powershell/latest/reference/items/Get-EC2Volume.html | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory = $false)] | |
| [char[]]$DriveLetter | |
| ) | |
| #-------------------------------------------- | |
| # Helper: WMI fallback for dynamic disks | |
| #-------------------------------------------- | |
| function Get-WmiDiskSize ([char]$letter) { | |
| try { | |
| $cimVolume = Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter='$($letter):'" | |
| if (-not $cimVolume) { return $null } | |
| $partitions = Get-CimAssociatedInstance -InputObject $cimVolume -Association Win32_VolumeToDiskPartition | |
| if (-not $partitions) { return $null } | |
| foreach ($p in $partitions) { | |
| $drives = Get-CimAssociatedInstance -InputObject $p -Association Win32_DiskDriveToDiskPartition | |
| foreach ($d in $drives) { | |
| if ($d.Size -and [long]$d.Size -gt 0) { | |
| return [long]$d.Size | |
| } | |
| } | |
| } | |
| return $null | |
| } | |
| catch { | |
| Write-Verbose "WMI fallback error: $($_.Exception.Message)" | |
| return $null | |
| } | |
| } | |
| #-------------------------------------------- | |
| # 1) Retrieve IMDSv2 token or fallback to IMDSv1 | |
| #-------------------------------------------- | |
| $token = $null | |
| try { | |
| $token = Invoke-RestMethod -Uri "http://169.254.169.254/latest/api/token" ` | |
| -Method PUT ` | |
| -Headers @{ "X-aws-ec2-metadata-token-ttl-seconds" = "21600" } ` | |
| -ErrorAction Stop | |
| } | |
| catch { | |
| Write-Verbose "IMDSv2 token retrieval failed: $($_.Exception.Message). Falling back to IMDSv1." | |
| } | |
| #-------------------------------------------- | |
| # 2) Get instance identity (InstanceId, Region) | |
| #-------------------------------------------- | |
| try { | |
| if ($token) { | |
| $instanceIdentityDoc = Invoke-RestMethod -Uri "http://169.254.169.254/latest/dynamic/instance-identity/document" ` | |
| -Headers @{ "X-aws-ec2-metadata-token" = $token } ` | |
| -ErrorAction Stop | |
| } | |
| else { | |
| $instanceIdentityDoc = Invoke-RestMethod -Uri "http://169.254.169.254/latest/dynamic/instance-identity/document" -ErrorAction Stop | |
| } | |
| } | |
| catch { | |
| Write-Error "Unable to retrieve instance metadata: $($_.Exception.Message)" | |
| return | |
| } | |
| $instanceId = $instanceIdentityDoc.instanceId | |
| $region = $instanceIdentityDoc.region | |
| #-------------------------------------------- | |
| # 3) Determine which drive letters to process | |
| #-------------------------------------------- | |
| if (-not $PSBoundParameters.ContainsKey('DriveLetter')) { | |
| # If DriveLetter not specified, gather all lettered volumes | |
| $allVolumes = Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | |
| $driveLetters = $allVolumes.DriveLetter | |
| Write-Verbose "No DriveLetter param given; auto-detected letters: $driveLetters" | |
| } | |
| else { | |
| $driveLetters = $DriveLetter | |
| } | |
| $results = @() | |
| foreach ($drv in $driveLetters) { | |
| #-------------------------------------------- | |
| # 4) Get disk size | |
| #-------------------------------------------- | |
| $diskSizeBytes = $null | |
| # Attempt partition->disk | |
| $partition = Get-Partition -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter -eq $drv } | Select-Object -First 1 | |
| if ($partition) { | |
| $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction SilentlyContinue | |
| if ($disk) { | |
| $diskSizeBytes = $disk.Size | |
| } | |
| } | |
| else { | |
| # Attempt volume->disk | |
| $vol = Get-Volume -DriveLetter $drv -ErrorAction SilentlyContinue | |
| if ($vol) { | |
| $disk = $vol | Get-Disk -ErrorAction SilentlyContinue | |
| if ($disk) { | |
| $diskSizeBytes = $disk.Size | |
| } | |
| else { | |
| # Possibly dynamic | |
| if ($vol.Size) { | |
| $diskSizeBytes = [long]$vol.Size | |
| Write-Verbose "Using Get-Volume.Size = $($vol.Size) for drive $drv (likely dynamic disk)." | |
| } | |
| } | |
| } | |
| } | |
| # If still no size, use WMI fallback | |
| if (-not $diskSizeBytes) { | |
| Write-Verbose "No size found for drive $drv. Trying WMI fallback..." | |
| $diskSizeBytes = Get-WmiDiskSize $drv | |
| if ($diskSizeBytes) { | |
| Write-Verbose "WMI fallback returned $diskSizeBytes bytes for drive $drv." | |
| } | |
| } | |
| # If still no size, skip | |
| if (-not $diskSizeBytes) { | |
| Write-Error "Failed to determine physical disk size for drive $drv. Skipping." | |
| continue | |
| } | |
| $diskSizeGB = [math]::Round($diskSizeBytes / 1GB) | |
| Write-Verbose "Drive $drv is approx. $diskSizeGB GB." | |
| #-------------------------------------------- | |
| # 5) Match to EBS volume | |
| #-------------------------------------------- | |
| if (-not (Get-Command Get-EC2Volume -ErrorAction SilentlyContinue)) { | |
| Write-Warning "AWS.Tools.EC2 module not installed. Install with: Install-Module AWS.Tools.EC2" | |
| $results += [PSCustomObject]@{ | |
| DriveLetter = $drv | |
| DiskSizeGB = $diskSizeGB | |
| Note = "Install AWS.Tools.EC2 for IOPS/Throughput/VolumeType details." | |
| } | |
| continue | |
| } | |
| try { | |
| $volumes = Get-EC2Volume -Region $region -ErrorAction Stop | | |
| Where-Object { $_.Attachments.InstanceId -contains $instanceId } | |
| $matchedVolume = $volumes | Where-Object { $_.Size -eq $diskSizeGB } | Select-Object -First 1 | |
| if (-not $matchedVolume) { | |
| Write-Warning "No EBS volume matched drive $drv by size = $diskSizeGB GB." | |
| continue | |
| } | |
| $attachment = $matchedVolume.Attachments | Where-Object { $_.InstanceId -eq $instanceId } | |
| $results += [PSCustomObject]@{ | |
| DriveLetter = $drv | |
| DiskSizeGB = $diskSizeGB | |
| VolumeId = $matchedVolume.VolumeId | |
| VolumeType = $matchedVolume.VolumeType | |
| IOPS = $matchedVolume.Iops | |
| Throughput = $matchedVolume.Throughput | |
| State = $matchedVolume.State | |
| Device = $attachment.Device | |
| DeleteOnTermination = $attachment.DeleteOnTermination | |
| } | |
| } | |
| catch { | |
| Write-Error "Error calling Get-EC2Volume: $($_.Exception.Message)" | |
| } | |
| } | |
| return $results | |
| } | |
| #endregion | |
| #region DiskSpd Test Execution & Download ⚡🚚 | |
| # This is where the magic happens. We'll hammer your disk with I/O and see if it | |
| # cries uncle. If you run this on production, don't blame us if your users revolt. 😜 | |
| # Handles DiskSpd download (with BITS/iwr), extraction, test execution, and cleanup. | |
| function Test-DiskSpd { | |
| <#! | |
| .SYNOPSIS | |
| Runs a DiskSpd test on a specified drive letter or all local drives (with -AllDrives -Force). ⚡ | |
| .DESCRIPTION | |
| The Test-DiskSpd function executes a storage performance test using Microsoft's DiskSpd tool. It dynamically retrieves EC2 volume details (if applicable) and ensures sufficient disk space before creating the test file. Results are saved with a timestamped filename. Handles download, extraction, and cleanup of DiskSpd, and works even if BITS is missing. All output is designed for clarity and confidence. If you run this on production, don't blame us if your users revolt. 😜 | |
| If you specify -AllDrives without -Force, the function will throw an error and abort without running any tests. This is a safety feature to prevent accidental I/O tests on all drives. | |
| .PARAMETER DiskSpdDriveLetter | |
| The drive letter on which to run the DiskSpd test. Defaults to 'M'. | |
| .PARAMETER AllDrives | |
| If specified, runs the test on every local volume with a drive letter. Requires -Force. If -AllDrives is used without -Force, the function will throw an error and abort without running any tests. | |
| .PARAMETER Force | |
| Required with -AllDrives. Confirms you really want to run I/O tests on all drives. If not specified with -AllDrives, the function will throw an error and abort. | |
| .PARAMETER WritePercentage | |
| The percentage of the workload that should be writes (remaining percentage will be reads). Defaults to 20%. | |
| .PARAMETER TestTimeSeconds | |
| The duration of the DiskSpd test in seconds. Defaults to 60. | |
| .PARAMETER TestFileSize | |
| The size of the test file to be created. Defaults to '20G'. | |
| .PARAMETER BlockSize | |
| The block size for DiskSpd I/O operations (e.g., '8K', '4K'). Defaults to '8K'. | |
| .PARAMETER KeepTestFile | |
| If specified, the test file will not be deleted after execution. Defaults to $false. | |
| .PARAMETER DownloadDiskSpd | |
| If specified, will download and extract DiskSpd if not found. Defaults to $true. | |
| .PARAMETER CsvResultsFile | |
| The path for the CSV summary output. Defaults to the same base as the TXT results file, but with .csv extension. | |
| .PARAMETER KeepXml | |
| If specified, keeps the intermediate XML file. By default, the XML is deleted after CSV export. | |
| .EXAMPLE | |
| Test-DiskSpd -DiskSpdDriveLetter D -WritePercentage 10 -TestTimeSeconds 120 | |
| # Runs a 120-second test on D: with 10% writes. | |
| .NOTES | |
| Requires DiskSpd.exe from Microsoft. Ensures sufficient free space before creating a large test file. Optionally keeps the test file for later review. DBA Pro Tip: Trust, but verify your cloud storage claims. This script makes it easy (and fun) to catch underperforming volumes before your users do! 😜 | |
| .LINK | |
| https://sqlperformance.com/2015/08/io-subsystem/diskspd-test-storage | |
| https://github.com/microsoft/diskspd | |
| #> | |
| [CmdletBinding(SupportsShouldProcess=$true)] | |
| param ( | |
| [string]$DiskSpdDriveLetter = 'M', | |
| [switch]$AllDrives, | |
| [switch]$Force, | |
| [int]$WritePercentage = 20, | |
| [int]$TestTimeSeconds = 60, | |
| [string]$TestFileSize = "20G", | |
| [string]$BlockSize = "8K", | |
| [switch]$KeepTestFile, | |
| [switch]$DownloadDiskSpd = $true, | |
| [string]$CsvResultsFile, | |
| [switch]$KeepXml | |
| ) | |
| # Handle -AllDrives logic | |
| if ($AllDrives) { | |
| if (-not $Force) { | |
| throw "ERROR: -AllDrives is a dangerous operation and requires -Force. Aborting. No tests will be run." | |
| } | |
| $drives = Get-Volume | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' } | Select-Object -ExpandProperty DriveLetter | |
| $results = @() | |
| foreach ($drv in $drives) { | |
| $params = @{ | |
| DiskSpdDriveLetter = $drv | |
| WritePercentage = $WritePercentage | |
| TestTimeSeconds = $TestTimeSeconds | |
| TestFileSize = $TestFileSize | |
| BlockSize = $BlockSize | |
| KeepTestFile = $KeepTestFile | |
| DownloadDiskSpd = $DownloadDiskSpd | |
| KeepXml = $KeepXml | |
| } | |
| $result = & $MyInvocation.MyCommand @params | |
| $results += $result | |
| } | |
| return $results | |
| } | |
| # Store original working directory | |
| $originalDirectory = Get-Location | |
| try { | |
| # Validate drive letter format and convert if needed | |
| $DiskSpdDriveLetter = $DiskSpdDriveLetter.TrimEnd(':') | |
| # Retrieve EC2 volume details for the selected drive letter | |
| Write-Verbose "Getting EC2 volume details for drive $DiskSpdDriveLetter" | |
| $ec2VolumeDetails = Get-EC2DriveVolumeDetails -DriveLetter $DiskSpdDriveLetter | |
| # Ensure volume details are properly set to prevent issues in the filename | |
| $volumeType = if ($ec2VolumeDetails.VolumeType) { $ec2VolumeDetails.VolumeType } else { 'unknown' } | |
| $diskSizeGB = if ($ec2VolumeDetails.DiskSizeGB) { $ec2VolumeDetails.DiskSizeGB } else { 'unknown' } | |
| $iops = if ($ec2VolumeDetails.IOPS) { $ec2VolumeDetails.IOPS } else { 'unknown' } | |
| $throughput = if ($ec2VolumeDetails.Throughput) { $ec2VolumeDetails.Throughput } else { 'unknown' } | |
| $fileSystem = (Get-Volume -DriveLetter $DiskSpdDriveLetter -ErrorAction SilentlyContinue).FileSystem | |
| if (-not $fileSystem) { $fileSystem = 'unknownFS' } | |
| $allocUnitSize = (Get-Volume -DriveLetter $DiskSpdDriveLetter -ErrorAction SilentlyContinue).AllocationUnitSize | |
| if (-not $allocUnitSize) { $allocUnitSize = 'unknown' } else { $allocUnitSize = "${allocUnitSize}B" } | |
| $computerName = $env:COMPUTERNAME | |
| $fileDescription = "volume_${DiskSpdDriveLetter}_${volumeType}_${diskSizeGB}GB_${iops}_iops_${throughput}_throughput_${BlockSize}_blocksize_${allocUnitSize}_allocunit_${fileSystem}_formatted_${computerName}" | |
| $timestamp = Get-Date -Format "yyyy-MM-dd__HHmmss" | |
| $resultsFile = "$env:USERPROFILE\Documents\${timestamp}_${fileDescription}.txt" | |
| $testFile = "${DiskSpdDriveLetter}:\DiskSpd_testfile.dat" | |
| # Set up paths and variables | |
| $diskSpdUrl = 'https://aka.ms/getdiskspd' | |
| $diskSpdZip = "$env:TEMP\DiskSpd.zip" | |
| $diskSpdPath = 'C:\DiskSpd' | |
| $diskSpdExe = "$diskSpdPath\amd64\diskspd.exe" | |
| # Download DiskSpd ZIP if needed | |
| if (-not (Test-Path $diskSpdExe)) { | |
| Write-Verbose "DiskSpd not found at $diskSpdExe" | |
| if ($DownloadDiskSpd) { | |
| if ($PSCmdlet.ShouldProcess("Download and extract DiskSpd tool", "Download DiskSpd")) { | |
| Write-Output "Downloading DiskSpd from $diskSpdUrl..." | |
| if (-not (Test-Path $diskSpdPath)) { | |
| New-Item -Path $diskSpdPath -ItemType Directory -Force | Out-Null | |
| } | |
| if (Get-Command Start-BitsTransfer -ErrorAction SilentlyContinue) { | |
| Write-Output "Downloading DiskSpd with Start-BitsTransfer..." | |
| Start-BitsTransfer -Destination $diskSpdZip -Source $diskSpdUrl | |
| } else { | |
| Write-Output "Downloading DiskSpd with Invoke-WebRequest..." | |
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | |
| Invoke-WebRequest -Uri $diskSpdUrl -OutFile $diskSpdZip -UseBasicParsing | |
| } | |
| # Extract DiskSpd using .NET (works everywhere) | |
| Add-Type -AssemblyName System.IO.Compression.FileSystem | |
| [System.IO.Compression.ZipFile]::ExtractToDirectory($diskSpdZip, $diskSpdPath) | |
| # Verify extraction | |
| if (Test-Path $diskSpdExe) { | |
| Write-Output "DiskSpd extracted successfully." | |
| } else { | |
| Write-Error "Failed to extract DiskSpd. Please download and extract manually." | |
| return | |
| } | |
| } else { | |
| Write-Output "DiskSpd download skipped." | |
| return | |
| } | |
| } else { | |
| Write-Error "DiskSpd not found at $diskSpdExe. Please install DiskSpd or use -DownloadDiskSpd parameter." | |
| return | |
| } | |
| } | |
| # Check for sufficient free space | |
| $drive = Get-PSDrive -Name $DiskSpdDriveLetter -ErrorAction SilentlyContinue | |
| if (-not $drive) { | |
| Write-Error "Drive $DiskSpdDriveLetter not found or not accessible." | |
| return | |
| } | |
| # Parse test file size | |
| $sizeValue = $TestFileSize -replace '[^0-9]', '' | |
| $sizeUnit = $TestFileSize -replace '[0-9]', '' | |
| $requiredBytes = switch ($sizeUnit.ToUpper()) { | |
| 'G' { [long]$sizeValue * 1GB } | |
| 'M' { [long]$sizeValue * 1MB } | |
| 'K' { [long]$sizeValue * 1KB } | |
| default { [long]$sizeValue } | |
| } | |
| if ($drive.Free -lt $requiredBytes) { | |
| Write-Error "Insufficient free space on drive $DiskSpdDriveLetter. Required: $($requiredBytes / 1GB) GB, Available: $($drive.Free / 1GB) GB" | |
| return | |
| } | |
| # Before launching DiskSpd, use console output for phase status | |
| Write-Output "Preparing to run DiskSpd on drive $DiskSpdDriveLetter..." | |
| # ... any setup, checks, etc. ... | |
| Write-Output "Launching DiskSpd test for $TestTimeSeconds seconds on drive $DiskSpdDriveLetter..." | |
| # Execute DiskSpd test | |
| if ($PSCmdlet.ShouldProcess("Run DiskSpd test on drive $DiskSpdDriveLetter", "Run DiskSpd test")) { | |
| Write-Output "Running DiskSpd test on drive $DiskSpdDriveLetter with $WritePercentage% write workload and $TestFileSize test file size..." | |
| # Use $TestTimeSeconds for DiskSpd -d parameter | |
| $diskSpdCmd = "& '$diskSpdExe' -Rxml -b$BlockSize -d$TestTimeSeconds -o4 -t8 -w$WritePercentage -Z1G -Suw -Ln -si -c$TestFileSize '$testFile'" | |
| # Launch DiskSpd and show test progress | |
| $job = Start-Job -ScriptBlock { param($cmd) Invoke-Expression $cmd } -ArgumentList $diskSpdCmd | |
| $startTime = Get-Date | |
| $progressActivity = "DiskSpd test running on drive $DiskSpdDriveLetter" | |
| while ($true) { | |
| $elapsed = (Get-Date) - $startTime | |
| $percent = [math]::Min([math]::Round(($elapsed.TotalSeconds / $TestTimeSeconds) * 100), 100) | |
| Write-Progress -Activity $progressActivity -Status "$percent% complete" -PercentComplete $percent | |
| if ($job.State -ne 'Running') { break } | |
| Start-Sleep -Seconds 1 | |
| } | |
| # Retrieve output and clean up job | |
| $output = Receive-Job $job -Wait | |
| Remove-Job $job | |
| Write-Progress -Activity $progressActivity -Completed | |
| $output | Out-File -FilePath $resultsFile -Encoding utf8 | |
| # Delete test file if not keeping it | |
| if (-not $KeepTestFile -and (Test-Path $testFile)) { | |
| Remove-Item -Path $testFile -Force | |
| Write-Verbose "Test file deleted: $testFile" | |
| } | |
| # Verify results file | |
| if (Test-Path $resultsFile) { | |
| Write-Output "DiskSpd test completed. Results saved to: $resultsFile" | |
| } else { | |
| Write-Error "Results file $resultsFile not found. Test may have failed." | |
| } | |
| } else { | |
| Write-Output "DiskSpd test simulation complete. Would have saved results to: $resultsFile" | |
| } | |
| # After DiskSpd execution, produce both TXT and XML output, then export summary to CSV | |
| $xmlFile = [System.IO.Path]::ChangeExtension($resultsFile, 'xml') | |
| if (-not $CsvResultsFile) { | |
| $CsvResultsFile = [System.IO.Path]::ChangeExtension($resultsFile, 'csv') | |
| } | |
| # Save XML output | |
| if (Test-Path $xmlFile) { Remove-Item $xmlFile -Force } | |
| $output | Out-File -FilePath $xmlFile -Encoding utf8 | |
| # Export summary to CSV | |
| $summary = Get-DiskSpdSummary -XmlFile $xmlFile -CsvFile $CsvResultsFile -DriveLetter $DiskSpdDriveLetter | |
| # Delete XML unless -KeepXml is set | |
| if (-not $KeepXml) { | |
| Remove-Item $xmlFile -Force -ErrorAction SilentlyContinue | |
| } | |
| return $summary | |
| } | |
| catch { | |
| Write-Error "Error in Test-DiskSpd: $($_.Exception.Message)" | |
| } | |
| finally { | |
| # Restore original working directory | |
| Set-Location $originalDirectory | |
| } | |
| } | |
| #endregion | |
| #region Results & Cleanup ☄️ | |
| # We save the results with a filename so descriptive, even your future self will | |
| # know what you did. Then we clean up after ourselves, because we're not monsters. | |
| # This is handled at the end of Test-DiskSpd. | |
| #endregion | |
| # Example calls | |
| # Get-EC2DriveVolumeDetails -DriveLetter C | |
| # Test-DiskSpd -DiskSpdDriveLetter 'M' -WritePercentage 25 -TestFileSize "20G" | |
| # Test-DiskSpd -DiskSpdDriveLetter 'D' -WritePercentage 50 -TestFileSize "50G" -KeepTestFile |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment