Contrary to previous iteration, this version returns an object with multiple properties describing statistical analysis of disk performance.
$testPaths='c:'
$expectedBlocksize=4096
$sampleSize=5
$diskSpdZip='https://github.com/microsoft/diskspd/releases/download/v2.0.21a/DiskSpd.zip'
function getDiskSpeed{
param(
[string]$testPath="c:",
[int]$sampleSize=5,
[int64]$blockSize=4096,
[string]$diskSpdZip='https://github.com/microsoft/diskspd/releases/download/v2.0.21a/DiskSpd.zip'
)
function isPathWritable{
param($testPath)
# Create random test file name
$tempFolder=$testPath+"\getDiskSpeed\"
$filename = "diskSpeedTest-"+[guid]::NewGuid()
$tempFilename = (Join-Path $tempFolder $filename)
New-Item -ItemType Directory -Path $tempFolder -Force -EA SilentlyContinue|Out-Null
Try {
# Try to add a new file
# New-Item -ItemType Directory -Path $tempFolder -Force -EA SilentlyContinue
[io.file]::OpenWrite($tempFilename).close()
#Write-Host -ForegroundColor Green "$testPath is writable."
# Delete test file after done
# Remove-Item $tempFilename -Force -ErrorAction SilentlyContinue
# Set return value
$feasible=$true;
}
Catch {
# Return 'false' if there are errors
$feasible=$false;
}
return $feasible;
}
function testUrl($url='https://google.com'){
$HTTP_Request = [System.Net.WebRequest]::Create($url)
$HTTP_Response = $HTTP_Request.GetResponse()
$HTTP_Status = [int]$HTTP_Response.StatusCode
If ($HTTP_Status -eq 200) {
$status=$true
}Else {
$status=$false
}
If (!($null -eq $HTTP_Response)){ $HTTP_Response.Close() }
return $status
}
function initTestPath([string]$path){
if (Test-Path $path -EA SilentlyContinue){
$regexValidDriveLetters="^[A-Za-z]\:{0,1}$"
$validLocalPath=$path.SubString(0,2) -match $regexValidDriveLetters
if ($validLocalPath){
$GLOBAL:localPath=$true;
write-Host "Validating path... Local directory detected."
$volumeName=if($path.Length -le 2){$path+"\"}else{$path.Substring(0,3)}
$blockSize=(Get-WmiObject -Class Win32_Volume | Where-Object {$_.Name -eq $volumeName}).BlockSize
$GLOBAL:blockSize=if($blockSize){$blockSize}else{'unknown'}
write-host "Block size detected as $blockSize."
$driveLettersOnThisComputer=ls function:[A-Z]: -n|?{test-path $_}
if (!($driveLettersOnThisComputer -contains $path.SubString(0,2))){
Write-Host "The provided local path's first 2 characters do not match any volumes in this system.";
return $false;
}
return $(isPathWritable $path)
}else{
$regexUncPath="^\\(?:\\[^<>:`"/\\|?*]+)+$"
if ($path -match $regexUncPath){
$GLOBAL:localPath=$False;
$serverName=[regex]::match($path,'^\\\\(.*)\\').captures.groups[1].value
$blockSize=(get-wmiobject win32_volume -ComputerName $serverName -ea Ignore).Blocksize|select -first 2|select -first 1
$GLOBAL:blockSize=if($blockSize){$blockSize}else{'unknown'}
write-Host "UNC directory detected."
return $(isPathWritable $path)
}else{
Write-Host "The provided path does not match a UNC pattern nor a local drive.";
return $false;
}
}
}else{
Write-Host "The path $path currently is NOT accessible to perform I/O tests.";
Return $false;
}
}
function includeDiskSpd{
# Ensure that diskspd.exe is available in the system
$diskSpeedUtilityAvailable=get-command diskspd.exe -ea SilentlyContinue
if (!($diskSpeedUtilityAvailable)){
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
try{
if(testUrl $diskSpdZip){
$client = new-object System.Net.WebClient
$tempZip="C:\Temp\diskSpd.zip"
$tempFolder='C:\Temp\diskSpd'
$client.DownloadFile($diskSpdZip,$tempZip)
# [System.IO.Compression.ZipFile]::ExtractToDirectory()
# $shell = New-Object -ComObject Shell.Application
# $zip = $shell.NameSpace($tempZip)
# foreach ($item in $zip.items()) {
# $shell.Namespace($tempFolder).CopyHere($item)
# }
Expand-Archive -LiteralPath $tempZip -DestinationPath $tempFolder
copy-item "$tempFolder\amd64\diskspd.exe" C:\Windows\diskspd.exe
}else{
choco install diskspd -y --ignore-checksums
$null=refreshenv
}
$diskSpeedUtilityAvailable=get-command diskspd.exe -ea SilentlyContinue
return [bool]($diskSpeedUtilityAvailable)
}catch{
return $false
}
}else{
return $true
}
}
function getIops{
# Sometimes, the test result throws this error "diskspd Error opening file:" if no switches were used
# The work around is to specify more parameters
# Other variations:
# $testResult=diskspd.exe-d1 -o4 -t4 -b8k -r -L -w50 -c1G $testFile
# $testResult=diskspd.exe -b4K -t1 -r -w50 -o32 -d10 -c8192 $testFile
# Note: remove the -c option to avoid this error when running with unprivileged accounts
# diskspd.exe : WARNING: Error adjusting token privileges for SeManageVolumePrivilege (error code: 1300)
try{
$GLOBAL:blockSize=if($blockSize.gettype().Name -ne 'String'){$blockSize}else{$blockSize}
if ($localPath){
#$expression="diskspd.exe -b8k -d1 -o$processors -t$processors -r -L -w25 -c1G $testfile";
$expression="Diskspd.exe -b$blockSize -d1 -h -L -o$processors -t1 -r -w30 -c1G $testfile 2>&1"
}else{
$expression="Diskspd.exe -b$blockSize -d1 -h -L -o$processors -t1 -r -w30 -c1G $testfile 2>&1";
}
#write-host $expression
$testResult=invoke-expression $expression;
<# diskspd.exe -b8k -d1 -o4 -t4 -r -L -w25 -c1G $testfile
8K block size; 1 second random I/O test;4 threads; 4 outstanding I/O operations;
25% write (implicitly makes read 75% ratio);
#>
}
catch{
$errorMessage = $_.Exception.Message
$failedItem = $_.Exception.ItemName
Write-Host "$errorMessage $failedItem";
continue;
}
$x=$testResult|select-string -Pattern "total*" -CaseSensitive|select-object -First 1|out-String
$iops=$x.split("|")[-3].Trim()
#$mebibytesPerSecond=$x.split("|")[-4].Trim()
return $iops
}
function getIopsSample{
$testArray=@();
for($i=1;$i -le $sampleSize;$i++){
try{
$iops=getIops;
write-host "$i of $sampleSize`: $iops IOPS";
$testArray+=$iops;
}
catch{
$errorMessage = $_.Exception.Message
$failedItem = $_.Exception.ItemName
Write-Host "$errorMessage $failedItem";
break;
}
}
$testArray=$testArray|sort
$min=($testArray|measure -Minimum).Minimum
$max=($testArray|measure -Maximum).Maximum
$median=.{
if ($testArray.count%2) { # case odd count
$median = $testArray[[math]::Floor($testArray.count/2)]}
else {# case even count
$median = ($testArray[$testArray.Count/2],$testArray[$testArray.count/2-1]|measure -Average).average
}
return $median}
$mode=.{$sample=$testArray|%{[math]::round($_,0)}
$dataType = $sample[0].GetType()
try {# Note the use of -NoElement
$results=$testArray|Group-Object -NoElement|Sort-Object Count -Descending|
ForEach-Object -Begin { $topCount = 0 } -Process {
if ($_.Count -lt $topCount) { break }
$topCount = $_.Count
if ($dataType -eq [string]) {$_.Name}
else {$dataType::Parse($_.Name)}
}
}
catch{}
finally{
if($results.count -eq $sample.count){'Sample size too small to obtain Mode'}
else{$results}
}
}
$average=($testArray|measure -Average).Average
return @{
'testDestination'=$testPath
'testOrigin'=$env:computername
'sampleSize'=$sampleSize
'min'=$min
'max'=$max
'mode'=$mode
'median'=$median
'average'=$average
'MBps'=[math]::round($average*16/$blockSize,2)
'blocksize'=if($blockSize.gettype().Name -eq 'String'){"$blockSize - assuming $blockSize"}else{$blockSize}
}
}
function isFileLocked{
param($file=$(New-Object System.IO.FileInfo $testFile))
if (Test-Path $testFile){
try {
$fileHandle = $file.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
if ($fileHandle){
# File handle is open, which means file is not locked
$fileHandle.Close()
}
return $false
}
catch{
# file is locked
return $true
}
}else{return $false}
}
if(!(includeDiskSpd)){
return "Cannot proceed with out Diskspd.exe"
}
if($testPath.Length -eq 1){$testPath+=":";}
if (initTestPath $testPath){
# Set variables
$tempDirectory="$testPath`\getDiskSpeed"
# New-Item -ItemType Directory -Force -Path $tempDirectory|Out-Null
write-host "Obtaining disk speed of $testPath"
$testFile="$tempDirectory`\testfile.dat"
$processors=(Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors
# Trigger several tests and select the highest value
$iops=getIopsSample
# Cleanup
# cmd /c rd $tempDirectory
do {
sleep 1;
isFileLocked|out-null;
}until(!(isFileLocked))
Remove-Item -Recurse -Force $tempDirectory
return $iops;
}else{
write-warning "Path $testPath is not accessible."
}
}
$testPaths|%{getDiskSpeed $_ $sampleSize $expectedBlocksize $diskSpdZip}
function getDiskSpeed{
param(
[string]$driveLetter="c:",
[int]$sampleSize=5
)
if($driveLetter.Length -eq 1){$driveLetter+=":";}
write-host "Obtaining disk speed of $driveLetter"
function isPathWritable{
param($testPath)
# Create random test file name
$tempFolder=$testPath+"\getDiskSpeed\"
$filename = "diskSpeedTest-"+[guid]::NewGuid()
$tempFilename = (Join-Path $tempFolder $filename)
New-Item -ItemType Directory -Path $tempFolder -Force -EA SilentlyContinue|Out-Null
Try {
# Try to add a new file
# New-Item -ItemType Directory -Path $tempFolder -Force -EA SilentlyContinue
[io.file]::OpenWrite($tempFilename).close()
#Write-Host -ForegroundColor Green "$testPath is writable."
# Delete test file after done
# Remove-Item $tempFilename -Force -ErrorAction SilentlyContinue
# Set return value
$feasible=$true;
}
Catch {
# Return 'false' if there are errors
$feasible=$false;
}
return $feasible;
}
# Check if input is a valid drive letter
function validatePath{
param([string]$path=$driveLetter)
if (Test-Path $path -EA SilentlyContinue){
$regexValidDriveLetters="^[A-Za-z]\:{0,1}$"
$validLocalPath=$path.SubString(0,2) -match $regexValidDriveLetters
if ($validLocalPath){
$GLOBAL:localPath=$true;
write-Host "Validating path... Local directory detected."
$volumeName=if($driveLetter.Length -le 2){$driveLetter+"\"}else{$driveLetter.Substring(0,3)}
$GLOBAL:clusterSize=(Get-WmiObject -Class Win32_Volume | Where-Object {$_.Name -eq $volumeName}).BlockSize;
write-host "Cluster size detected as $clusterSize."
$driveLettersOnThisComputer=ls function:[A-Z]: -n|?{test-path $_}
if (!($driveLettersOnThisComputer -contains $path.SubString(0,2))){
Write-Host "The provided local path's first 2 characters do not match any volumes in this system.";
return $false;
}
return $(isPathWritable $path)
}else{
$regexUncPath="^\\(?:\\[^<>:`"/\\|?*]+)+$"
if ($path -match $regexUncPath){
$GLOBAL:localPath=$False;
write-Host "UNC directory detected."
return $(isPathWritable $path)
}else{Write-Host "The provided path does not match a UNC pattern nor a local drive.";return $false;}
}
}else{
Write-Host "The path $path currently does NOT exist";
Return $false;
}
}
if (validatePath){
# Set variables
$tempDirectory="$driveLetter`\getDiskSpeed"
# New-Item -ItemType Directory -Force -Path $tempDirectory|Out-Null
$testFile="$tempDirectory`\testfile.dat"
$processors=(Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors
# Ensure that diskspd.exe is available in the system
$diskSpeedUtilityAvailable=get-command diskspd.exe -ea SilentlyContinue
if (!($diskSpeedUtilityAvailable)){
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
choco install diskspd -y;
refreshenv;
}
function getIops{
# Sometimes, the test result throws this error "diskspd Error opening file:" if no switches were used
# The work around is to specify more parameters
# Other variations:
# $testResult=diskspd.exe-d1 -o4 -t4 -b8k -r -L -w50 -c1G $testFile
# $testResult=diskspd.exe -b4K -t1 -r -w50 -o32 -d10 -c8192 $testFile
# Note: remove the -c option to avoid this error when running with unprivileged accounts
# diskspd.exe : WARNING: Error adjusting token privileges for SeManageVolumePrivilege (error code: 1300)
try{
if ($localPath){
#$expression="diskspd.exe -b8k -d1 -o$processors -t$processors -r -L -w25 -c1G $testfile";
$expression="Diskspd.exe -b$clusterSize -d1 -h -L -o$processors -t1 -r -w30 -c1G $testfile 2>&1";
}else{
$expression="Diskspd.exe -b8K -d1 -h -L -o$processors -t1 -r -w30 -c1G $testfile 2>&1";
}
#write-host $expression
$testResult=invoke-expression $expression;
<# diskspd.exe -b8k -d1 -o4 -t4 -r -L -w25 -c1G $testfile
8K block size; 1 second random I/O test;4 threads; 4 outstanding I/O operations;
25% write (implicitly makes read 75% ratio);
#>
}
catch{
$errorMessage = $_.Exception.Message
$failedItem = $_.Exception.ItemName
Write-Host "$errorMessage $failedItem";
continue;
}
$x=$testResult|select-string -Pattern "total*" -CaseSensitive|select-object -First 1|out-String
$iops=$x.split("|")[-3].Trim()
#$mebibytesPerSecond=$x.split("|")[-4].Trim()
return $iops
}
function selectHighIops{
$testArray=@();
for($i=1;$i -le $sampleSize;$i++){
try{
$iops=getIops;
write-host "$i of $sampleSize`: $iops IOPS";
$testArray+=$iops;
}
catch{
$errorMessage = $_.Exception.Message
$failedItem = $_.Exception.ItemName
Write-Host "$errorMessage $failedItem";
break;
}
}
$highestResult=($testArray|measure -Maximum).Maximum
return $highestResult
}
# Trigger several tests and select the highest value
$selectedIops=selectHighIops
# Cleanup
# cmd /c rd $tempDirectory
function isFileLocked{
param($file=$(New-Object System.IO.FileInfo $testFile))
if (Test-Path $testFile){
try {
$fileHandle = $file.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
if ($fileHandle){
# File handle is open, which means file is not locked
$fileHandle.Close()
}
return $false
}
catch{
# file is locked
return $true
}
}else{return $false}
}
do {
sleep 1;
isFileLocked|out-null;
}until(!(isFileLocked))
Remove-Item -Recurse -Force $tempDirectory
$mebibytesPerSecond=[math]::round($(([int]$selectedIops)/128),2)
return "Highest: $selectedIops IOPS ($mebibytesPerSecond MiB/s)";
}else{return "Cannot get disk speed"}
}
<#
(gwmi -Class win32_volume -Filter "DriveType!=5" -ea stop| ?{$_.DriveLetter -ne $isnull}|`
Select-object @{Name="Letter";Expression={$_.DriveLetter}},`
@{Name="Label";Expression={$_.Label}},`
@{Name="Capacity";Expression={"{0:N2} GiB" -f ($_.Capacity/1073741824)}},`
@{Name = "Available"; Expression = {"{0:N2} GiB" -f ($_.FreeSpace/1073741824)}},`
@{Name = "Utilization"; Expression = {"{0:N2} %" -f ((($_.Capacity-$_.FreeSpace) / $_.Capacity)*100)}},`
@{Name = "diskBrand"; Expression = {getDiskSpeed $_.DriveLetter}},`
@{Name = "diskSpeed"; Expression = {getDiskSpeed $_.DriveLetter}}`
| ft -autosize | Out-String).Trim()
#>
Output
PS H:\> getDiskSpeed
Obtaining disk speed of c:
Validating path... Local directory detected.
Cluster size detected as 4096.
1 of 5: 13255.40 IOPS
2 of 5: 62858.84 IOPS
3 of 5: 64618.56 IOPS
4 of 5: 13274.12 IOPS
5 of 5: 60426.45 IOPS
Highest: 64618.56 IOPS (504.84 MiB/s)
Categories: