Добавлена папка source в CristalDiskMark

This commit is contained in:
2026-05-29 13:04:54 +07:00
commit bdc2295ee4
240 changed files with 94035 additions and 0 deletions
@@ -0,0 +1,329 @@
# From Setup to Start-FleetSweep for Arc Enabled Virtual Machines in HCI clusters
This is the traditional path of setting up VMFleet to deploy Arc enabled VMs on HCI clusters and running it using your desired DiskSpd parameters/flags.
## Prerequisites
Before we begin setting up VMFleet, there are a few prerequisites that you should have ready.
Ensure an HCI cluster is setup.
Ensure that you have 1 CSV per node
Within Storage Spaces Direct, CPU usage is based on the host. Therefore, it is recommended that you split the storage load by creating as many CSVs as there are host nodes. We can go ahead and create a CSV per node in the cluster.
You may run the following:
```
Get-ClusterNode |% {
New-Volume -StoragePoolFriendlyName SU1_Pool* -FriendlyName $_ -FileSystem CSVFS_ReFS -Size <DESIRED SIZE>
}
```
Ensure that you create a “collect” volume.
You may run the following:
```
New-Volume -StoragePoolFriendlyName SU1_Pool* -FriendlyName collect -FileSystem CSVFS_ReFS -Size 200GB
```
If you have ran VMFleet in the past, please ensure that prior VMFleet directories are completely removed from existing volumes.
Retrieve or install a Server Core VHDX file. If you do not have one handy, we can create a new one by following the below instructions.
Download WS2019 Server Core ISO from the public website.
Open Hyper-V Manager.
Click “New”, then “Virtual Machine”.
Navigate through the prompts and pick a location to store your VM.
Once your VM is created, boot up the VM and follow the instructions. This is where you will decide your VM password, or what we will later call, “adminpass”.
This is important as we will use this later, so make sure you write this down.
Log out of the VM, and navigate to where you stored your VM. You should find a “Virtual Hard Disks” folder. Inside, you should find your new Server Core VHDX file.
Rename it to “Base1.vhdx”.
Copy or move the file to the cluster environment that you want to run VMFleet in.
Done!
### Deployment
1. First, we need to install the new PowerShell Module from the PowerShell Gallery and then load it into our terminal. We also need to disable cache as it is not supported currently for ArcVMs. Run the following:
```
Install-Module -Name “VMFleet”
Import-Module VMFleet
Set-ClusterStorageSpacesDirect -CacheState Disabled;
```
2. Sanity Check:
Run "Get-Module VMFleet" to confirm the module exists.
Run "Get-Command -Module VMFleet" to obtain a list of functions included in the module.
We will now set up the directory structure within the “Collect” CSV created earlier. Run "Install-Fleet"
This creates the necessary VMFleet directories which include:
* Collect/control
Contains arc.json which stores Arc configuration and the scripts that the Virtual Machines continuously monitor.
* Control.ps1: the control script the VMs use to implement the control loop (what used to be called “master.ps1”).
* Run.ps1: The VMs continuously look for the most recent version of run.ps1 and runs the newly updated script (parameters).
* Collect/flag
Location where the control script drops the “go”, “pause”, and “done” flag files. Users should not need to look at these files.
* Collect/result
Location of the output files from the VMFleet test run.
* Collect/tools
DiskSpd will be preinstalled in this folder.
3. You need to create a new or use existing Resource group under the same subscription as the Resource bridge VM is under. Set-ArcConfig will take care of creating one if not already present.
4. Setup configuration required for creating Arc enabled Virtual Machines.
```
Set-ArcConfig -ResourceGroup [ENTER_RESOURCE_GROUP] -AzureRegistrationUser [ENTER_AZURE_REGUSER] -AzureRegistrationPassword [ENTER_AZURE_REGPASS]-StoragePathCsv [ENTER_CSV_PATH] -Enabled $true -StoragePathName [ENTER_StoragePath_Name] -ImageName [ENTER_Image_Name] -ResetSalt
```
-"ResourceGroup" is the resource group where ARC VMs will be deployed.
-"AzureRegistrationUser" and "AzureRegistrationPassword" are the azure account credentials.
-"StoragePathCsv" (optional) is the csv path where storage path resource will be created. If not provided, one of the existing csvs which were created earlier will be used by default.
-"StoragePathName" (optional) is the storage path name which will be used to create gallery image. If not provided, default name will be used.
-"ImageName" (optional) is the gallery image name which will be used to create Arc-enabled virtual machines. If not provided, default name will be used. Currently, only windows gallery image is the supported image type for Arc-Enabled VMs.
-"Enabled" (optional) is set to $false by default. Set to $true if Arc-enabled virtual machines are to be created.
-"ResetSalt" (optional) is a flag used to reset salt. Salt is a random 4 character (alphanumeric) used in Arc resource names for arc enabled virtual machines (for eg: vm-group-node-salt-001). In case, user wants to regenerate the salt, they can run "Set-ArcConfig -ResetSalt" which will override existing salt with a new one in the arc.json and this will be used to create new VMs. This is useful in case of multiple clusters under same subscription using same resource group on a virtual setup. A 4 character (alphanumeric) Salt will be generated by default when user runs Set-ArcConfig the first time and stored in arc.json along with other arc configs. Within Set-ArcConfig, a quick test is done to check if atleast one vm exists with same salt in the resource group. If it does, it is regenerated.
5. By default, VMs with 2GB static memory and 1 processor count will be created.
Note: Please move the VHDX file into the collect folder. CSV Cache is also turned off by default.
We will now create our “fleet” of VMs by running:
```
New-Fleet -basevhd <PATH TO VHDX> -vms [ENTER_NUM_VMS] -adminpass [ENTER_ADMINPASS] -connectuser [ENTER_NODE_USER] -connectpass [ENTER_NODE_PASS]
```
-"adminpass" is the administrator password for the Server Core Image. This is the password you set on your Virtual Machine earlier.
-"connectuser" is a domain account with access to the cluster.
-"connectpass " is the password for the above domain account.
-"vms" is the number of VM's to create per node.
6. If "vms" parameter is not provided, the default is a 1:1 subscription ratio where the Number of VMs = Number of physical cores.
[Optional] You can consider modifying the VM hardware configuration. Run
```
Set-Fleet -ProcessorCount 1 -MemoryStartupBytes 2048mb -MemoryMaximumBytes 2048mb -MemoryMinimumBytes 2048mb
```
Note:
If you specify “MemoryMaximumBytes”, you must specify “MemoryMinimumBytes”, which implies that your VMs will have dynamic memory.
If you omit “MemoryMaximumBytes” or “MemoryMinimumBytes”, it implies that your VMs will have static memory.
If MemoryStartupBytes = MemoryMinimumBytes = MemoryMaximumBytes, that also denotes static memory.
“MemoryStartupBytes” is a mandatory parameter.
### Start Running VMFleet!
7. Open 2 PowerShell terminals. In the first one, run Watch-Cluster and in the second one, run Start-Fleet. This second function will turn on all the VMs in a “paused” state.
8. At this point you can run Start-FleetSweep [ENTER_PARAMETERS] or take this time to explore and run any of the other functions!
Here is a sample sweep command to help you get started: "Start-FleetSweep -b 4 -t 8 -o 8 -w 0 -d 300 -p r"
9. Done!
### Aftermath
Once you are done running VMFleet you can run Stop-Fleet to shut down all the virtual machines or run Remove-Fleet to completely delete all the virtual machines on your environment.
# From Setup to Measure-FleetCoreWorkload
This is a new workflow for setting up VMFleet and the predefined profile workloads (General, Peak, VDI, SQL).
## Prerequisites
Before we begin setting up VMFleet, there are a few prerequisites that you should have ready.
Ensure an HCI cluster is setup.
Ensure that you have 1 CSV per node
Within Storage Spaces Direct, CPU usage is based on the host. Therefore, it is recommended that you split the storage load by creating as many CSVs as there are host nodes. We can go ahead and create a CSV per node in the cluster.
In order to be precise about the CSV size, please use our new VMFleet command: "Get-FleetVolumeEstimate"
This will output a prescribed CSV size based on different resiliency types. We recommend you select a 2-way mirrored value or 3-way mirrored value depending on your node count.
1. You may run the following: (use the CSV size from Get-FleetVolumeEstimate)
```
Get-ClusterNode |% {
New-Volume -StoragePoolFriendlyName SU1_Pool* -FriendlyName $_ -FileSystem CSVFS_ReFS -Size <DESIRED SIZE>
}
```
Ensure that you create a “collect” volume.
2. You may run the following:
```
New-Volume -StoragePoolFriendlyName SU1_Pool* -FriendlyName collect -FileSystem CSVFS_ReFS -Size 200GB
```
3. If you have ran VMFleet in the past, please ensure that prior VMFleet directories are completely removed from existing volumes.
Retrieve or install a Server Core VHDX file. If you do not have one handy, we can create a new one by following the below instructions.
Download WS2019 Server Core ISO from the public website.
Open Hyper-V Manager.
Click “New”, then “Virtual Machine”.
Navigate through the prompts and pick a location to store your VM.
Once your VM is created, boot up the VM and follow the instructions. This is where you will decide your VM password, or what we will later call, “adminpass”.
This is important as we will use this later, so make sure you write this down.
Log out of the VM, and navigate to where you stored your VM. You should find a “Virtual Hard Disks” folder. Inside, you should find your new Server Core VHDX file.
Rename it to “Base1.vhdx”.
Copy or move the file to the cluster environment that you want to run VMFleet in.
4. Done!
## Deployment
5. Lets begin deploying VMFleet. First, we need to install the new PowerShell Module from the PowerShell Gallery and then load it into the terminal. Run the following:
```
Install-Module -Name “VMFleet”
Import-Module VMFleet
Set-ClusterStorageSpacesDirect -CacheState Disabled;
```
## Sanity Check:
6. Run "Get-Module VMFleet" to confirm the module exists.
7. Run "Get-Command -Module VMFleet" to obtain a list of commands included in the module.
8. We will now set up the directory structure within the “Collect” CSV that we created earlier. Run "Install-Fleet"
9. You need to create a new or use existing Resource group under the same subscription as the Resource bridge VM is under. Set-ArcConfig will take care of creating one if not already present.
10. Setup configuration required for creating Arc enabled Virtual Machines.
```
Set-ArcConfig -ResourceGroup [ENTER_RESOURCE_GROUP] -AzureRegistrationUser [ENTER_AZURE_REGUSER] -AzureRegistrationPassword [ENTER_AZURE_REGPASS]-StoragePathCsv [ENTER_CSV_PATH] -Enabled $true -StoragePathName [ENTER_StoragePath_Name] -ImageName [ENTER_Image_Name] -ResetSalt
```
-"ResourceGroup" is the resource group where ARC VMs will be deployed.
-"AzureRegistrationUser" and "AzureRegistrationPassword" are the azure account credentials.
-"StoragePathCsv" (optional) is the csv path where storage path resource will be created. If not provided, one of the existing csvs which were created earlier will be used by default.
-"StoragePathName" (optional) is the storage path name which will be used to create gallery image. If not provided, default name will be used.
-"ImageName" (optional) is the gallery image name which will be used to create Arc-enabled virtual machines. If not provided, default name will be used.
-"Enabled" (optional) is set to $false by default. Set to $true if Arc-enabled virtual machines are to be created.
-"ResetSalt" (optional) is a flag used to reset salt. Salt is a random 4 character (alphanumeric) used in Arc resource names for arc enabled virtual machines (for eg: vm-group-node-salt-001). In case, user wants to regenerate the salt, they can run "Set-ArcConfig -ResetSalt" which will override existing salt with a new one in the arc.json and this will be used to create new VMs. This is useful in case of multiple clusters under same subscription using same resource group on a virtual setup. A 4 character (alphanumeric) Salt will be generated by default when user runs Set-ArcConfig the first time and stored in arc.json along with other arc configs. Within Set-ArcConfig, a quick test is done to check if atleast one vm exists with same salt in the resource group. If it does, it is regenerated.
11. By default, VMs with 2GB static memory and 1 processor count will be created.
Note: Please move the VHDX file into the collect folder. CSV Cache is also turned off by default.
We will now create our “fleet” of VMs by running:
```
New-Fleet -basevhd <PATH TO VHDX> -vms [ENTER_NUM_VMS] -adminpass [ENTER_ADMINPASS] -connectuser [ENTER_NODE_USER] -connectpass [ENTER_NODE_PASS]
```
-"adminpass" is the administrator password for the Server Core Image. This is the password you set on your Virtual Machine earlier.
-"connectuser" is a domain account with access to the cluster.
-"connectpass " is the password for the above domain account.
-"vms" is the number of VM's to create per node.
12. Measure-FleetCoreWorkload also collects diagnostic data (Get-SDDCDiagnosticInfo). Therefore, before running the command, we must also install the NuGet Package if you have not previously done so. In doing so, we also need to temporairly set the PSGallery as a trusted repository source (Note: This will temporarily relax the security boundary).
```
$repo = Get-PSRepository -Name PSGallery
if ($null -eq $repo) { Write-Host "The PSGallery is not configured on this system, please address this before continuing" }
else {
if ($repo.InstallationPolicy -ne 'Trusted') {
Write-Host "Setting the PSGallery repository to Trusted, original InstallationPolicy: $($repo.InstallationPolicy)"
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
}
### Installing the pre-requisite modules
Install-PackageProvider NuGet -Force
Install-Module -Name PrivateCloud.DiagnosticInfo -Force
Install-Module -Name MSFT.Network.Diag -Force
if ($repo.InstallationPolicy -ne 'Trusted') {
Write-Host "Resetting the PSGallery repository to $($repo.InstallationPolicy)"
Set-PSRepository -Name PSGallery -InstallationPolicy $repo.InstallationPolicy
}
}
```
## Run Measure-FleetCoreWorkload!
13. We can now run Measure-FleetCoreWorkload. Running the command below will automatically run all 4 workloads (General, Peak, VDI, SQL) and place the individual outputs in the result directory. IMPORTANT: If you plan on running another test, please clear the result directory.
```
Measure-FleetCoreWorkload
```
14. Congratulations! Youre done! All you need to do is wait for the test to complete.
Note: If you ever run into an error and need to rerun Measure-FleetCoreWorkload, dont be afraid to do so! It is smart enough to pick up from where it last stopped and continue the test without starting from scratch.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,728 @@
Describe "TestModulePresent" {
BeforeAll {
$script:e = $null
}
if (Test-Path .\VMFleet.psd1)
{
It "ShouldLoad-Local" {
{ Import-Module .\VMFleet.psd1 -Force -ErrorVariable script:e } | Should Not Throw
$script:e | Should BeNullOrEmpty Because "this should be successful"
}
}
else
{
It "ModuleExists-Installed" {
Get-Module VMFleet -ListAvailable -ErrorVariable script:e | Should Not BeNullOrEmpty Because "module should be installed on system"
$script:e | Should BeNullOrEmpty Because "this should be successful"
}
It "ShouldLoad-Installed" {
{ Import-Module VMFleet -Force -ErrorVariable script:e } | Should Not Throw
$script:e | Should BeNullOrEmpty Because "this should be successful"
}
}
It "ShouldBeLoaded" {
Get-Module VMFleet -ErrorVariable script:e | Should Not BeNullOrEmpty Because "module should now be loaded"
$script:e | Should BeNullOrEmpty Because "this should be successful"
}
}
InModuleScope VMFleet {
Describe "GetDistributedShift" {
BeforeAll {
$a = @('a', 'b', 'c', 'd', 'e')
$b = @('f', 'g', 'h', 'i', 'j')
$c = @('k', 'l', 'm', 'n', 'o')
$d = @('p', 'q', 'r', 's', 't')
$bad = @('z')
$a20 = 1..20 |% { "a$_" }
$b20 = 1..20 |% { "b$_" }
$c20 = 1..20 |% { "c$_" }
$d20 = 1..20 |% { "d$_" }
}
It "ShouldRequireListOfLists" {
GetDistributedShift -Group $a -N 1 | Should Throw
}
It "ShouldRequireMoreThanOne" {
GetDistributedShift -Group @($a) -N 1 | Should Throw
}
It "ShouldRequireAllSameSize" {
GetDistributedShift -Group $a,$b,$bad -N 1 | Should Throw
}
It "ShouldRequirePositiveRotation" {
GetDistributedShift -Group $a,$b -N 0 | Should Throw
}
It "ShouldNotShiftTooMany" {
GetDistributedShift -Group $a,$b -N 6 | Should Throw
}
#
# Two group rotations
#
It "Rotate2-1" {
{ GetDistributedShift -Group $a,$b -N 1 } | Should Not Throw
$a1, $b1 = GetDistributedShift -Group $a,$b -N 1
$a1.Count | Should Be 5
$b1.Count | Should Be 5
($a1 -join '') | Should Be "abcdj"
($b1 -join '') | Should Be "fghie"
}
It "Rotate2-2" {
{ GetDistributedShift -Group $a,$b -N 2 } | Should Not Throw
$a1, $b1 = GetDistributedShift -Group $a,$b -N 2
$a1.Count | Should Be 5
$b1.Count | Should Be 5
($a1 -join '') | Should Be "abcij"
($b1 -join '') | Should Be "fghde"
}
It "Rotate2-3" {
{ GetDistributedShift -Group $a,$b -N 3 } | Should Not Throw
$a1, $b1 = GetDistributedShift -Group $a,$b -N 3
$a1.Count | Should Be 5
$b1.Count | Should Be 5
($a1 -join '') | Should Be "abhij"
($b1 -join '') | Should Be "fgcde"
}
It "Rotate2-4" {
{ GetDistributedShift -Group $a,$b -N 4 } | Should Not Throw
$a1, $b1 = GetDistributedShift -Group $a,$b -N 4
$a1.Count | Should Be 5
$b1.Count | Should Be 5
($a1 -join '') | Should Be "aghij"
($b1 -join '') | Should Be "fbcde"
}
It "Rotate2-5" {
{ GetDistributedShift -Group $a,$b -N 5 } | Should Not Throw
$a1, $b1 = GetDistributedShift -Group $a,$b -N 5
$a1.Count | Should Be 5
$b1.Count | Should Be 5
($a1 -join '') | Should Be "fghij"
($b1 -join '') | Should Be "abcde"
}
#
# Three group rotations
#
It "Rotate3-1" {
{ GetDistributedShift -Group $a,$b,$c -N 1 } | Should Not Throw
$a1, $b1, $c1 = GetDistributedShift -Group $a,$b,$c -N 1
$a1.Count | Should Be 5
$b1.Count | Should Be 5
$c1.Count | Should Be 5
($a1 -join '') | Should Be "abcdo"
($b1 -join '') | Should Be "fghie"
($c1 -join '') | Should Be "klmnj"
}
It "Rotate3-2" {
{ GetDistributedShift -Group $a,$b,$c -N 2 } | Should Not Throw
$a1, $b1, $c1 = GetDistributedShift -Group $a,$b,$c -N 2
$a1.Count | Should Be 5
$b1.Count | Should Be 5
$c1.Count | Should Be 5
($a1 -join '') | Should Be "abcio"
($b1 -join '') | Should Be "fghne"
($c1 -join '') | Should Be "klmdj"
}
It "Rotate3-3" {
{ GetDistributedShift -Group $a,$b,$c -N 3 } | Should Not Throw
$a1, $b1, $c1 = GetDistributedShift -Group $a,$b,$c -N 3
$a1.Count | Should Be 5
$b1.Count | Should Be 5
$c1.Count | Should Be 5
($a1 -join '') | Should Be "abmio"
($b1 -join '') | Should Be "fgcne"
($c1 -join '') | Should Be "klhdj"
}
It "Rotate3-4" {
{ GetDistributedShift -Group $a,$b,$c -N 4 } | Should Not Throw
$a1, $b1, $c1 = GetDistributedShift -Group $a,$b,$c -N 4
$a1.Count | Should Be 5
$b1.Count | Should Be 5
$c1.Count | Should Be 5
($a1 -join '') | Should Be "agmio"
($b1 -join '') | Should Be "flcne"
($c1 -join '') | Should Be "kbhdj"
}
It "Rotate3-5" {
{ GetDistributedShift -Group $a,$b,$c -N 5 } | Should Not Throw
$a1, $b1, $c1 = GetDistributedShift -Group $a,$b,$c -N 5
$a1.Count | Should Be 5
$b1.Count | Should Be 5
$c1.Count | Should Be 5
($a1 -join '') | Should Be "kgmio"
($b1 -join '') | Should Be "alcne"
($c1 -join '') | Should Be "fbhdj"
}
It "RotateLarge4-1" {
{ GetDistributedShift -Group $a20,$b20,$c20,$d20 -N 1 } | Should Not Throw
$g = GetDistributedShift -Group $a20,$b20,$c20,$d20 -N 1
# note last group rotates 1
$g[0] -join '' | Should Be "a1a2a3a4a5a6a7a8a9a10a11a12a13a14a15a16a17a18a19d20"
$g[1] -join '' | Should Be "b1b2b3b4b5b6b7b8b9b10b11b12b13b14b15b16b17b18b19a20"
$g[2] -join '' | Should Be "c1c2c3c4c5c6c7c8c9c10c11c12c13c14c15c16c17c18c19b20"
$g[3] -join '' | Should Be "d1d2d3d4d5d6d7d8d9d10d11d12d13d14d15d16d17d18d19c20"
}
It "RotateLarge4-2" {
{ GetDistributedShift -Group $a20,$b20,$c20,$d20 -N 2 } | Should Not Throw
$g = GetDistributedShift -Group $a20,$b20,$c20,$d20 -N 2
# note last group rotates 1, next 2
$g[0] -join '' | Should Be "a1a2a3a4a5a6a7a8a9a10a11a12a13a14a15a16a17a18c19d20"
$g[1] -join '' | Should Be "b1b2b3b4b5b6b7b8b9b10b11b12b13b14b15b16b17b18d19a20"
$g[2] -join '' | Should Be "c1c2c3c4c5c6c7c8c9c10c11c12c13c14c15c16c17c18a19b20"
$g[3] -join '' | Should Be "d1d2d3d4d5d6d7d8d9d10d11d12d13d14d15d16d17d18b19c20"
}
It "RotateLarge4-3" {
{ GetDistributedShift -Group $a20,$b20,$c20,$d20 -N 3 } | Should Not Throw
$g = GetDistributedShift -Group $a20,$b20,$c20,$d20 -N 3
# note last group rotates 1, then 2, 3
$g[0] -join '' | Should Be "a1a2a3a4a5a6a7a8a9a10a11a12a13a14a15a16a17b18c19d20"
$g[1] -join '' | Should Be "b1b2b3b4b5b6b7b8b9b10b11b12b13b14b15b16b17c18d19a20"
$g[2] -join '' | Should Be "c1c2c3c4c5c6c7c8c9c10c11c12c13c14c15c16c17d18a19b20"
$g[3] -join '' | Should Be "d1d2d3d4d5d6d7d8d9d10d11d12d13d14d15d16d17a18b19c20"
}
It "RotateLarge4-4" {
{ GetDistributedShift -Group $a20,$b20,$c20,$d20 -N 4 } | Should Not Throw
$g = GetDistributedShift -Group $a20,$b20,$c20,$d20 -N 4
# note last group rotates 1, then 2, 3, and back to 1
$g[0] -join '' | Should Be "a1a2a3a4a5a6a7a8a9a10a11a12a13a14a15a16d17b18c19d20"
$g[1] -join '' | Should Be "b1b2b3b4b5b6b7b8b9b10b11b12b13b14b15b16a17c18d19a20"
$g[2] -join '' | Should Be "c1c2c3c4c5c6c7c8c9c10c11c12c13c14c15c16b17d18a19b20"
$g[3] -join '' | Should Be "d1d2d3d4d5d6d7d8d9d10d11d12d13d14d15d16c17a18b19c20"
}
}
Describe "FilterObject" {
BeforeAll {
$o12 = [PSCustomObject]@{
K1 = 1
K2 = 2
}
$o13 = [PSCustomObject]@{
K1 = 1
K2 = 3
}
}
It "PassWithEmpty" {
$o12 | FilterObject -Filter @{} | Should Not BeNullOrEmpty
}
It "PassWithOneK" {
$o12 | FilterObject -Filter @{ K1 = 1 } | Should Not BeNullOrEmpty
}
It "PassWithTwoK" {
$o12 | FilterObject -Filter @{ K1 = 1; K2 = 2 } | Should Not BeNullOrEmpty
}
It "PassWithOneKDiffType" {
$o12 | FilterObject -Filter @{ K1 = '1' } | Should Not BeNullOrEmpty
}
It "PassWithTwoKDiffType" {
$o12 | FilterObject -Filter @{ K1 = '1'; K2 = '2' } | Should Not BeNullOrEmpty
}
It "NotPassMismatchSameType" {
$o12 | FilterObject -Filter @{ K1 = 2 } | Should BeNullOrEmpty
}
It "NotPassMismatchDiffType" {
$o12 | FilterObject -Filter @{ K1 = '2' } | Should BeNullOrEmpty
}
It "ShouldPassMultipleMatch" {
$r = @($o12,$o12 | FilterObject -Filter @{ K1 = 1 })
$r.Count | Should Be 2
}
It "ShouldPassTheMatch" {
$r = @($o12,$o13 | FilterObject -Filter @{ K2 = 2 })
$r.Count | Should Be 1
$r.K1 | Should Be 1
$r.K2 | Should Be 2
}
}
}
Describe "SetFleetProfile" {
BeforeAll {
It "ShouldBeExpectedSqlProfile" {
{ Get-FleetprofileXml -Name SQL } | Should Not Throw
$x = Get-FleetprofileXml -Name SQL
$x.Profile.TimeSpans.TimeSpan.Targets.Target[0].Throughput.InnerText | Should Be 1500
$x.Profile.TimeSpans.TimeSpan.Targets.Target[1].Throughput.InnerText | Should Be 300
}
$x = Get-FleetprofileXml -Name SQL
$oWarmup = $x.Profile.TimeSpans.TimeSpan.Warmup
$oDuration = $x.Profile.TimeSpans.TimeSpan.Duration
$oCooldown = $x.Profile.TimeSpans.TimeSpan.Cooldown
}
It "ShouldSetByParam" {
{ Set-FleetProfile -ProfileXml $x -Throughput 1000 } | Should Not Throw
$xn = Set-FleetProfile -ProfileXml $x -Throughput 1000
# original composition is 1500*4 threads + 300*1 thread = 6300
# 1000 * 6000/6300 / 4 threads = 238
# 1000 * 300/6300 / 1 thread = 48
$xn.Profile.TimeSpans.TimeSpan.Targets.Target[0].Throughput.InnerText | Should Be 238
$xn.Profile.TimeSpans.TimeSpan.Targets.Target[1].Throughput.InnerText | Should Be 48
}
It "ShouldSetByPipeline" {
{ $x | Set-FleetProfile -Throughput 1000 } | Should Not Throw
$xn = $x | Set-FleetProfile -Throughput 1000
$xn.Profile.TimeSpans.TimeSpan.Targets.Target[0].Throughput.InnerText | Should Be 238
$xn.Profile.TimeSpans.TimeSpan.Targets.Target[1].Throughput.InnerText | Should Be 48
}
It "ShouldSetUnbounded" {
{ $x | Set-FleetProfile -Throughput 0 } | Should Not Throw
$xn = $x | Set-FleetProfile -Throughput 0
$xn.Profile.TimeSpans.TimeSpan.ThreadCount | Should be 5
$xn.Profile.TimeSpans.TimeSpan.RequestCount | Should be 32
$xn.Profile.TimeSpans.SelectNodes("TimeSpan/Targets/Target/ThreadsPerFile") | Should BeNullOrEmpty
$xn.Profile.TimeSpans.SelectNodes("TimeSpan/Targets/Target/RequestCount") | Should BeNullOrEmpty
$xn.Profile.Timespans.TimeSpan.Targets.Target[1].InterlockedSequential | Should Be 'true'
}
It "ShouldSetWarmup" {
$oWarmup | Should Not Be 33
{ $x | Set-FleetProfile -Warmup 33 } | Should Not Throw
$xn = $x | Set-FleetProfile -Warmup 33
$xn.Profile.TimeSpans.TimeSpan.Warmup | Should Be 33
$xn.Profile.TimeSpans.TimeSpan.Duration | Should Be $oDuration
$xn.Profile.TimeSpans.TimeSpan.Cooldown | Should Be $oCooldown
}
It "ShouldSetDuration" {
$oDuration | Should Not Be 33
{ $x | Set-FleetProfile -Duration 33 } | Should Not Throw
$xn = $x | Set-FleetProfile -Duration 33
$xn.Profile.TimeSpans.TimeSpan.Warmup | Should Be $oWarmup
$xn.Profile.TimeSpans.TimeSpan.Duration | Should Be 33
$xn.Profile.TimeSpans.TimeSpan.Cooldown | Should Be $oCooldown
}
It "ShouldSetCooldown" {
$oCooldown | Should Not Be 33
{ $x | Set-FleetProfile -Cooldown 33 } | Should Not Throw
$xn = $x | Set-FleetProfile -Cooldown 33
$xn.Profile.TimeSpans.TimeSpan.Warmup | Should Be $oWarmup
$xn.Profile.TimeSpans.TimeSpan.Duration | Should Be $oDuration
$xn.Profile.TimeSpans.TimeSpan.Cooldown | Should Be 33
}
}
Describe "GetFleetProfileXml" {
It "HasPeak" {
{ $peak = Get-FleetProfileXml -Name Peak -BlockSize 8KB -WriteRatio 0 } | Should Not Throw
$peak = Get-FleetProfileXml -Name Peak -BlockSize 8KB -WriteRatio 0
$peak | Should Not BeNullOrEmpty
}
It "PeakDoesNotDefineBaseMax" {
$peak = Get-FleetProfileXml -Name Peak -BlockSize 8KB -WriteRatio 0
$peak.SelectNodes('Profile/TimeSpans/TimeSpan/Targets/Target/BaseFileOffset') | Should BeNullOrEmpty
$peak.SelectNodes('Profile/TimeSpans/TimeSpan/Targets/Target/MaxFileSize') | Should BeNullOrEmpty
}
It "PeakShouldAcceptBase" {
$peak = Get-FleetProfileXml -Name Peak -BlockSize 8KB -WriteRatio 0 -BaseOffset 1GB
$peak.SelectNodes('Profile/TimeSpans/TimeSpan/Targets/Target/MaxFileSize') | Should BeNullOrEmpty
$n = $peak.SelectNodes('Profile/TimeSpans/TimeSpan/Targets/Target/BaseFileOffset')
$n | Should Not BeNullOrEmpty
$n.Count | Should Be 1
$n.Item(0).InnerText | Should Be ([string] 1GB)
}
It "PeakShouldAcceptMax" {
$peak = Get-FleetProfileXml -Name Peak -BlockSize 8KB -WriteRatio 0 -MaxOffset 2GB
$peak.SelectNodes('Profile/TimeSpans/TimeSpan/Targets/Target/BaseFileOffset') | Should BeNullOrEmpty
$n = $peak.SelectNodes('Profile/TimeSpans/TimeSpan/Targets/Target/MaxFileSize')
$n | Should Not BeNullOrEmpty
$n.Count | Should Be 1
$n.Item(0).InnerText | Should Be ([string] 2GB)
}
It "PeakShouldAcceptBaseMax" {
$peak = Get-FleetProfileXml -Name Peak -BlockSize 8KB -WriteRatio 0 -BaseOffset 1GB -MaxOffset 2GB
$n = $peak.SelectNodes('Profile/TimeSpans/TimeSpan/Targets/Target/BaseFileOffset')
$n | Should Not BeNullOrEmpty
$n.Count | Should Be 1
$n.Item(0).InnerText | Should Be ([string] 1GB)
$n = $peak.SelectNodes('Profile/TimeSpans/TimeSpan/Targets/Target/MaxFileSize')
$n | Should Not BeNullOrEmpty
$n.Count | Should Be 1
$n.Item(0).InnerText | Should Be ([string] 2GB)
}
It "PeakShouldAcceptThreads" {
$peak = Get-FleetProfileXml -Name Peak -BlockSize 8KB -WriteRatio 0 -ThreadsPerTarget 2
$n = $peak.SelectNodes('Profile/TimeSpans/TimeSpan/Targets/Target/ThreadsPerFile')
$n | Should Not BeNullOrEmpty
$n.Count | Should Be 1
$n.Item(0).InnerText | Should Be ([string] 2)
}
}
InModuleScope VMFleet {
Describe "GetNextSplit" {
It "ShouldBeLargest@End" {
$o = [PSCustomObject]@{
Value = 0
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 5
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 15
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 40
CutoffType =[CutoffType]::No
}
$p1, $p2 = GetNextSplit $o 'Value' -OrderBy 'Value'
$p1.Value | Should Be 40
$p2.Value | Should Be 15
}
It "ShouldBeLargest@Begin" {
$o = [PSCustomObject]@{
Value = 40
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 5
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 15
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 0
CutoffType =[CutoffType]::No
}
$p1, $p2 = GetNextSplit $o 'Value' -OrderBy 'Value'
$p1.Value | Should Be 40
$p2.Value | Should Be 15
}
It "ShouldBeFirstOfEqual" {
$o = [PSCustomObject]@{
Value = 0
Order = 0
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 10
Order = 1
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 20
Order = 2
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 30
Order = 3
CutoffType =[CutoffType]::No
}
$p1, $p2 = GetNextSplit $o 'Value' -OrderBy 'Value'
$p1.Value | Should Be 10
$p2.Value | Should Be 0
}
It "ShouldRespectOrder" {
$o = [PSCustomObject]@{
Value = 0
Order = 0
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 10
Order = 2
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 20
Order = 3
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 30
Order = 1
CutoffType =[CutoffType]::No
}
$p1, $p2 = GetNextSplit $o 'Value' -OrderBy 'Order'
$p1.Value | Should Be 30
$p2.Value | Should Be 0
}
It "ShouldRespectCutoff" {
$o = [PSCustomObject]@{
Value = 0
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 10
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 30
CutoffType =[CutoffType]::Scale
},
[PSCustomObject]@{
Value = 60
CutoffType =[CutoffType]::Scale
}
$p1, $p2 = GetNextSplit $o 'Value' -OrderBy 'Value'
$p1.Value | Should Be 30
$p2.Value | Should Be 10
}
It "ShouldRespectCutoffOrdered" {
$o = [PSCustomObject]@{
Value = 0
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 30
CutoffType =[CutoffType]::Scale
},
[PSCustomObject]@{
Value = 10
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 60
CutoffType =[CutoffType]::Scale
}
$p1, $p2 = GetNextSplit $o 'Value' -OrderBy 'Value'
$p1.Value | Should Be 30
$p2.Value | Should Be 10
}
}
Describe "GetUpperAnchor" {
It "ShouldReturnFinalIfNotCutoff" {
$o = [PSCustomObject]@{
Value = 0
Order = 0
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 10
Order = 2
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 20
Order = 3
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 30
Order = 1
CutoffType =[CutoffType]::No
}
$p1,$p2 = GetUpperAnchor $o -OrderBy 'Order'
$p1.Value | Should Be 20
$p2.Value | Should Be 10
}
It "ShouldReturnFirstCutoff" {
$o = [PSCustomObject]@{
Value = 0
Order = 0
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 10
Order = 2
CutoffType =[CutoffType]::Scale
},
[PSCustomObject]@{
Value = 20
Order = 3
CutoffType =[CutoffType]::No
},
[PSCustomObject]@{
Value = 30
Order = 1
CutoffType =[CutoffType]::No
}
$p1,$p2 = GetUpperAnchor $o -OrderBy 'Order'
$p1.Value | Should Be 10
$p2.Value | Should Be 30
}
}
Describe "IsProfileThroughputLimited" {
It "PeakIsNot" {
$x = Get-FleetProfileXml -Name Peak -BlockSize 4KB -WriteRatio 0
IsProfileThroughputLimited -ProfileXml $x | Should Be $false
}
It "SqlIs" {
$x = Get-FleetProfileXml -Name Sql
IsProfileThroughputLimited -ProfileXml $x | Should Be $true
}
}
Describe "IsProfileSingleTimespan" {
It "PeakIs" {
$x = Get-FleetProfileXml -Name Peak -BlockSize 4KB -WriteRatio 0
IsProfileSingleTimespan -ProfileXml $x | Should Be $true
}
}
Describe "IsProfileSingleTarget" {
It "PeakIs" {
$x = Get-FleetProfileXml -Name Peak -BlockSize 4KB -WriteRatio 0
IsProfileSingleTarget -ProfileXml $x | Should Be $true
}
It "SqlIsNot" {
$x = Get-FleetProfileXml -Name Sql
IsProfileSingleTarget -ProfileXml $x | Should Be $false
}
}
Describe "GetFleetProfileFootprint" {
BeforeAll {
$sql = Get-FleetProfileXml -Name SQL
$vdi = Get-FleetProfileXml -Name VDI
}
It "CheckVDI" {
$vdi | Should Not BeNullOrEmpty
$f = GetFleetProfileFootprint -ProfileXml $vdi
$f.Count | Should Be 1
$f['*1'].BaseOffset | Should Be 0
$f['*1'].MaxOffset | Should Be (10GB)
}
It "CheckVDIRead" {
$vdi | Should Not BeNullOrEmpty
$f = GetFleetProfileFootprint -ProfileXml $vdi -Read
$f.Count | Should Be 1
$f['*1'].BaseOffset | Should Be 0
$f['*1'].MaxOffset | Should Be (8GB)
}
It "CheckSQL" {
$sql | Should Not BeNullOrEmpty
$f = GetFleetProfileFootprint -ProfileXml $sql
$f.Count | Should Be 1
$f['*1'].BaseOffset | Should Be 0
$f['*1'].MaxOffset | Should Be 0
}
It "CheckSQLRead" {
$sql | Should Not BeNullOrEmpty
$f = GetFleetProfileFootprint -ProfileXml $sql -Read
$f.Count | Should Be 1
$f['*1'].BaseOffset | Should Be (5GB)
$f['*1'].MaxOffset | Should Be 0
}
}
Describe "TimespanToString" {
BeforeAll {
$t0 = [datetime] '7/31/1996 12:00PM'
}
It "Seconds" {
TimespanToString ($t0.AddMilliseconds(1500) - $t0) | Should Be "01.5s"
}
It "Minutes" {
TimespanToString ($t0.AddMinutes(15) - $t0) | Should Be "15m:00.0s"
}
It "Hours" {
TimespanToString ($t0.AddHours(15) - $t0) | Should Be "15h:00m:00.0s"
}
It "Days" {
TimespanToString ($t0.AddDays(15) - $t0) | Should Be "15d.00h:00m:00.0s"
}
}
}
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<ViewDefinitions>
<!--
//
// VMFLeet.VolumeEstimate
//
-->
<View>
<Name>StorageBusBindingTableView</Name>
<ViewSelectedBy>
<TypeName>VolumeEstimate</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>VolumeType</Label>
</TableColumnHeader>
<TableColumnHeader>
<Label>MirrorSize</Label>
</TableColumnHeader>
<TableColumnHeader>
<Label>MirrorTierName</Label>
</TableColumnHeader>
<TableColumnHeader>
<Label>ParitySize</Label>
</TableColumnHeader>
<TableColumnHeader>
<Label>ParityTierName</Label>
</TableColumnHeader>
<TableColumnHeader>
<Label>Size</Label>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>VolumeType</PropertyName>
</TableColumnItem>
<TableColumnItem>
<Alignment>Right</Alignment>
<ScriptBlock>
$v = $_.MirrorSize;
$postfixes = @( "B", "KB", "MB", "GB", "TB", "PB" )
for ($i=0; $v -ge 1024 -and $i -lt $postfixes.Length; $i++) { $v /= 1024; }
return "" + [System.Math]::Round($v,2) + " " + $postfixes[$i];
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>MirrorTierName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<Alignment>Right</Alignment>
<ScriptBlock>
$v = $_.ParitySize;
$postfixes = @( "B", "KB", "MB", "GB", "TB", "PB" )
for ($i=0; $v -ge 1024 -and $i -lt $postfixes.Length; $i++) { $v /= 1024; }
return "" + [System.Math]::Round($v,2) + " " + $postfixes[$i];
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>ParityTierName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<Alignment>Right</Alignment>
<ScriptBlock>
$v = $_.Size;
$postfixes = @( "B", "KB", "MB", "GB", "TB", "PB" )
for ($i=0; $v -ge 1024 -and $i -lt $postfixes.Length; $i++) { $v /= 1024; }
return "" + [System.Math]::Round($v,2) + " " + $postfixes[$i];
</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
@@ -0,0 +1,197 @@
<#
VM Fleet
Copyright(c) Microsoft Corporation
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#>
#
# Module manifest for module 'VMFleet'
#
# Generated on: 1/30/2021
#
@{
# Script module or binary module file associated with this manifest.
RootModule = 'VMFleet.psm1'
# Version number of this module. Even build# is release, odd pre-release (Mj.Mn.Bd.Rv)
ModuleVersion = '2.1.0.0'
# Supported PSEditions
# CompatiblePSEditions = @()
# ID used to uniquely identify this module
GUID = '753ad5da-01b3-4cc8-a475-16a09a021384'
# Author of this module
Author = 'Microsoft Corporation'
# Company or vendor of this module
CompanyName = 'Microsoft Corporation'
# Copyright statement for this module
Copyright = '(c) 2021 Microsoft Corporation. All rights reserved.'
# Description of the functionality provided by this module
Description = 'VM Fleet is a performance characterization and analyst framework for exploring the storage capabilities of Windows Server Hyper-Converged environments with Storage Spaces Direct'
# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '5.1'
# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''
# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# CLRVersion = ''
# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()
# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()
# Format files (.ps1xml) to be loaded when importing this module
FormatsToProcess = @( 'VMFleet.format.ps1xml' )
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
NestedModules = @(
'Profile.psm1',
'WatchCluster.psm1',
'WatchCPU.psm1'
)
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @(
'Clear-FleetPause',
'Clear-FleetRunState',
'Convert-FleetXmlToString',
'Get-FleetComputeTemplate',
'Get-FleetDisk',
'Get-FleetDataDiskEstimate',
'Get-FleetPath',
'Get-FleetPause',
'Get-FleetPolynomialFit',
'Get-FleetPowerScheme',
'Get-FleetProfileXml',
'Get-FleetResultLog',
'Get-FleetVersion',
'Get-FleetVM',
'Get-FleetVolumeEstimate',
'Install-Fleet',
'Measure-FleetCoreWorkload',
'Move-Fleet',
'New-Fleet',
'Remove-Fleet',
'Repair-Fleet',
'Set-ArcConfig',
'Set-Fleet',
'Set-FleetPause',
'Set-FleetPowerScheme',
'Set-FleetProfile',
'Set-FleetQoS',
'Set-FleetRunProfileScript',
'Show-Fleet',
'Show-FleetCpuSweep',
'Show-FleetPause',
'Start-Fleet',
'Start-FleetReadCacheWarmup',
'Start-FleetResultRun',
'Start-FleetRun',
'Start-FleetSweep',
'Start-FleetWriteWarmup',
'Stop-Fleet',
'Test-FleetResultRun',
'Use-FleetPolynomialFit',
'Watch-FleetCluster',
'Watch-FleetCPU',
'Initialize-ArcVMs'
)
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @(
)
# Variables to export from this module
VariablesToExport = $null
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @()
# DSC resources to export from this module
# DscResourcesToExport = @()
# List of all modules packaged with this module
# ModuleList = @()
# List of all files packaged with this module
# FileList = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
# Tags = @()
# A URL to the license for this module.
# LicenseUri = ''
# A URL to the main website for this project.
ProjectUri = 'https://www.github.com/microsoft/diskspd'
# A URL to an icon representing this module.
# IconUri = ''
# ReleaseNotes of this module
# ReleaseNotes = ''
} # End of PSData hashtable
} # End of PrivateData hashtable
# HelpInfo URI of this module
# HelpInfoURI = ''
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,282 @@
<#
DISKSPD - VM Fleet
Copyright(c) Microsoft Corporation
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#>
Set-StrictMode -Version 3.0
function Watch-FleetCPU
{
[CmdletBinding()]
param(
[Parameter()]
[string]
$ComputerName = $env:COMPUTERNAME,
[Parameter()]
[switch]
$Guest,
[Parameter()]
[int]
$SampleInterval = 2
)
function div-to-width(
[int] $div
)
{
# 0 - 100 scale
# ex: 4 -> 100/4 = 25 buckets + 1 more for == 100
1+100/$div
}
function center-pad(
[string] $s,
[int] $width
)
{
if ($width -le $s.length) {
$s
} else {
(' ' * (($width - $s.length)/2) + $s)
}
}
function get-legend(
[string] $label,
[int] $width,
[int] $div
)
{
# now produce the scale, a digit at a time in vertical orientation
# at each multiple of 10% which aligns with a measurement bucket.
# the width is the width of the measured values
#
# 0 5 1
# 0 0
# 0
$lines = @()
$lines += '-' * $width
foreach ($dig in 0..2) {
$o = foreach ($pos in 0..($width - 1)) {
$val = $pos * $div
if ($val % 10 -eq 0) {
switch ($dig) {
0 { if ($val -eq 100) { 1 } else { $val / 10 }}
1 { if ($val -ne 0) { 0 } else { ' ' }}
2 { if ($val -eq 100) { 0 } else { ' ' }}
}
} else { ' ' }
}
$lines += $o -join ''
}
# trailing comments (horizontal scale name)
$lines += center-pad $label $width
$lines
}
# minimum clip, the vertical height available for the cpu core bars
$minClip = 10
# these are the valid divisions, in units of percentage width.
# they must evenly divide 100% and either 10% or 20% for scale markings.
# determine which is the best fit based on window width.
$div = 0
$divs = 1,2,4,5
foreach ($i in $divs) {
if ((div-to-width $i) -le [console]::WindowWidth) {
$div = $i
break
}
}
# if nothing fit ... ask for minimum
# in practice this is not possible, but we check anyway
if ($div -eq 0) {
Write-Error "Window width must be at least $(div-to-width $divs[-1]) columns"
return
}
$width = div-to-width $div
# which processor counterset should we use?
# pi is only the root partition if hv is active; when hv is active:
# hvlp is the host physical processors
# hvvp is the guest virtual processors
# via ctrs, hv is active iff hvlp is present and has multiple instances
$cs = Get-Counter -ComputerName $ComputerName -ListSet 'Hyper-V Hypervisor Logical Processor' -ErrorAction SilentlyContinue
$hvactive = $null -ne $cs -and $cs.CounterSetType -eq [Diagnostics.PerformanceCounterCategoryType]::MultiInstance
if ($Guest -and -not $hvactive) {
Write-Error "Hyper-V is not active on $ComputerName"
return
}
if ($hvactive) {
if ($Guest) {
$cpuset = "\Hyper-V Hypervisor Virtual Processor(*)\% Guest Run Time"
$legend = get-legend "Percent Guest VCPU Utilization" $width $div
} else {
$cpuset = '\Hyper-V Hypervisor Logical Processor(*)\% Total Run Time'
$legend = get-legend "Percent Host LP Utilization" $width $div
}
} else {
$cpuset = '\Processor Information(*)\% Processor Time'
$legend = get-legend "Percent Processor Utilization" $width $div
}
# processor performance counter (turbo/speedstep)
# this is used to normalize the total cpu utilization (can be > 100%)
$ppset = '\Processor Information(_Total)\% Processor Performance'
# account for the constant legend in the window height
# use the remaining height for the vertical cpu core bars.
$clip = [console]::WindowHeight - ($legend.Count + 1)
# insist on a minimum amount of space
if ($clip -lt $minClip) {
$minWindowHeight = $minClip + $legend.Count + 1
Write-Error "Window height must be at least $minWindowHeight lines"
return
}
# set window and buffer size simultaneously so we don't have extra scrollbars
Clear-Host
[console]::SetWindowSize($width, [console]::WindowHeight)
[console]::BufferWidth = [console]::WindowWidth
[console]::BufferHeight = [console]::WindowHeight
# common params for Get-Counter
$gcParam = @{
SampleInterval = $SampleInterval
Counter = $cpuset,$ppset
ErrorAction = 'SilentlyContinue'
}
while ($true) {
# reset measurements
# these are the vertical height of a cpu bar in each column, e.g. 2 = 2 cpus
$m = @([int] 0) * $width
# avoid remoting for the local case
if ($ComputerName -eq $env:COMPUTERNAME) {
$ctrs = Get-Counter @gcParam
} else {
$ctrs = Get-Counter @gcParam -ComputerName $ComputerName
}
# if more than one countersample was returned (ppset + something more), we have data
if ($null -ne $ctrs -and $ctrs.Countersamples.Count -gt 1)
{
# get all specific instances and count them into appropriate measurement bucket
$ctrs.Countersamples |% {
# get scaling factor for total utility
if ($_.Path -like "*$ppset") {
$pperf = $_.CookedValue/100
}
# a cpu: count into appropriate measurement bucket
# (ignore total and/or and per-numa total)
elseif ($_.InstanceName -notlike '*_Total') {
$m[[math]::Floor($_.CookedValue/$div)] += 1
}
# get total
#
elseif ($_.InstanceName -eq '_Total') {
$total = $_.CookedValue
}
}
# now produce the bar area as a series of lines
# work down the veritical altitude of each strip, starting at the clip/top
$altitude = $clip
$lines = do {
$($m |% {
# top line - if we are potentially clipped, handle
if ($altitude -eq $clip) {
# clipped?
if ($_ -gt $altitude) { 'x' }
# unclipped but at clip?
elseif ($_ -eq $altitude) { '*' }
# nothing, bar less than altitude
else { ' ' }
} else {
# below top line
# >=, output bar
if ($_ -ge $altitude) { '*' }
# <, nothing
else { ' ' }
}
}) -join ''
} while (--$altitude)
$totalStr = "{0:0.0}%" -f $total
$normalStr = "{0:0.0}%" -f ($total*$pperf)
# move the cursor to indicate average utilization
# column number is zero based, width is 1-based
$cpos = [math]::Floor(($width - 1)*$total/100)
}
else
{
# Center no data message vertically and horizontally in the frame
$vpre = [math]::Floor($clip/2) - 1
$vpost = [math]::Floor($clip/2)
$lines = @('') * $vpre
$lines += center-pad "No Data Available" $width
$lines += @('') * $vpost
$totalStr = $normalStr = "---"
# zero cursor
$cpos = 0
}
Clear-Host
Write-Host -NoNewline ($lines + $legend -join "`n")
Write-Host -NoNewLine ("`n" + (center-pad "$ComputerName Total: $totalStr Normalized: $normalStr" $width))
# move the cursor to indicate average utilization
# column number is zero based, width is 1-based
[console]::SetCursorPosition($cpos,[console]::CursorTop-$legend.Count)
}
}
@@ -0,0 +1,552 @@
<#
DISKSPD - VM Fleet
Copyright(c) Microsoft Corporation
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#>
Set-StrictMode -Version 3.0
# display name
# ctr name
# display order
# format string
# scalar multiplier
class CounterColumn {
[string] $displayname
[string] $setname
[string[]] $ctrname
[int] $width
[string] $fmt
[decimal] $multiplier
[ValidateSet("Average","AverageAggregate","Sum")][string] $aggregate
[boolean] $divider
CounterColumn(
[string] $displayname,
[string] $setname,
[string[]] $ctrname,
[int] $width,
[string] $fmt,
[decimal] $multiplier,
[string] $aggregate,
[boolean] $divider
)
{
$this.displayname = $displayname
$this.setname = $setname
$this.ctrname = $ctrname
$this.width = $width
$this.fmt = $fmt
$this.multiplier = $multiplier
$this.aggregate = $aggregate
$this.divider = $divider
if ($this.width -lt $this.displayname.length + 1) {
$this.width = $this.displayname.length + 1
}
}
}
class CounterColumnSet {
[CounterColumn[]] $columns
[string[]] $counters
[string] $topfmt
[string] $linfmt
[string] $name
[string] $totalline
[string[]] $nodelines
CounterColumnSet($name)
{
$this.columns = $null
$this.name = $name
}
[void] Add([CounterColumn] $c)
{
$this.columns += $c
}
[void] Seal()
{
# assemble the top-line fmt and per-line fmt strings
# top-line is just strings/width
# per-line adds the (likely numeric) format specifier
$n = 1
$this.topfmt = $this.linfmt = "{0,-16}"
foreach ($col in $this.columns) {
$str = ''
if ($col.divider) {
$str += '| '
}
$str += "{$n,-$($col.width)"
$this.topfmt += "$str}"
$this.linfmt += "$($str):$($col.fmt)}"
$n += 1
}
# assemble the list of counter instances that will be needed
# note that some may be internally aggregated (i.e., total = read + write)
# in cases where a counterset does not provide an explicit total
$this.counters = ($this.columns |% {
$s = $_.setname
$_.ctrname |% { "\$($s)(_Total)\$($_)" }
} | group -NoElement).Name
}
[void] DisplayPre(
[hashtable] $samples,
[hashtable] $psamples
)
{
# aggregate each column across all live sampled nodes
$this.totalline = $this.linfmt -f $(
"Total"
foreach ($col in $this.columns) {
# average aggreate doesn't work across nodes if the base is not consistent
# for instance: cannot average latency safely, but can average cpu utilization
if ($col.aggregate -ne 'Average' -or $col.aggregate -eq 'AverageAggregate') {
$(foreach ($node in $psamples.keys) {
get-samples $psamples $node $col
}) | get-aggregate $col
} else {
$null
}
}
)
# individual nodes
# flags downed/non-responsive nodes by noting which are not
# present in the processed samples
$this.nodelines = foreach ($node in $samples.keys | sort) {
if ($psamples.ContainsKey($node)) {
$this.linfmt -f $(
$node
foreach ($col in $this.columns) {
$s = get-samples $psamples $node $col
if ($null -ne $s) {
$a = $s | get-aggregate $col
$a
} else {
"-"
}
}
)
} else {
$this.topfmt -f $(
$node
0..($this.columns.count - 1) |% { "-" }
)
}
}
}
[void] Display()
{
write-host ($this.topfmt -f (,$this.name + $this.columns.displayname))
write-host -fore green $this.totalline
$this.nodelines |% { write-host $_ }
}
}
function Watch-FleetCluster
{
[CmdletBinding()]
param(
[Parameter()]
[string]
$Cluster = ".",
[Parameter()]
[int]
$SampleInterval = 2,
[Parameter()]
[ValidateSet("CSV FS","SSB Cache","SBL","SBL Local","SBL Remote","SBL*","S2D BW","Hyper-V LCPU","SMB SRV","SMB Transport","*")]
[string[]] $Sets = "CSV FS",
[Parameter()]
[string]
$LogFile
)
if ($PSBoundParameters.ContainsKey('LogFile'))
{
$script:log = $LogFile
Remove-Item -Force $log -ErrorAction SilentlyContinue
}
function WriteLog(
[Parameter(ValueFromPipeline = $true)]
[string[]]
$String
)
{
PROCESS {
if ($null -ne $script:log) {
"$(get-date) $String" | Out-File -Append -FilePath $script:log -Width 9999 -Encoding ascii
}
}
}
function get-aggregate(
[CounterColumn] $col
)
{
BEGIN {
$n = 0
$v = 0
}
PROCESS {
$n += 1
$v += $_
}
END {
if ($n -gt 0) {
switch -wildcard ($col.aggregate) {
'Sum' {
#write-host $col.displayname $col.multipler $v
$col.multiplier * $v
}
'Average*' {
#write-host $col.displayname $col.multiplier $v $n
$col.multiplier * $v / $n
}
}
} else {
$null
}
}
}
# get samples out of per-node hash of ctr hashes
function get-samples(
[hashtable] $h,
[string] $node,
[CounterColumn] $col
)
{
foreach ($i in $col.ctrname) {
$k = "$($col.setname)+$($i)"
if ($h.ContainsKey($node)) {
if ($h[$node].ContainsKey($k)) {
$h[$node][$k]
} else {
WriteLog "missing $node[$k] : $($h[$node].Keys.Count) total keys"
}
} else {
WriteLog "missing $node"
}
}
}
$allctrs = @()
###
$c = [CounterColumnSet]::new("CSV FS")
$c.Add([CounterColumn]::new("IOPS", "Cluster CSVFS", @("Reads/sec","Writes/sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Reads", "Cluster CSVFS", @("Reads/sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Writes", "Cluster CSVFS", @("Writes/sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("BW (MB/s)", "Cluster CSVFS", @("Read Bytes/sec","Write Bytes/sec"), 13, '#,#', 0.000001, 'Sum', $true))
$c.Add([CounterColumn]::new("Read", "Cluster CSVFS", @("Read Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Write", "Cluster CSVFS", @("Write Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Read Lat (ms)", "Cluster CSVFS", @("Avg. sec/Read"), 15, '0.000', 1000, 'Average', $true))
$c.Add([CounterColumn]::new("Write", "Cluster CSVFS", @("Avg. sec/Write"), 8, '0.000', 1000, 'Average', $false))
$c.Add([CounterColumn]::new("Read QAvg", "Cluster CSVFS", @("Avg. Read Queue Length"), 11, '0.000', 1, 'Average', $true))
$c.Add([CounterColumn]::new("Write", "Cluster CSVFS", @("Avg. Write Queue Length"), 8, '0.000', 1, 'Average', $false))
$c.Seal()
$allctrs += $c
###
$c = [CounterColumnSet]::new("SSB Cache")
$c.Add([CounterColumn]::new("Hit/Sec", "Cluster Storage Hybrid Disks", @("Cache Hit Reads/Sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Miss/Sec", "Cluster Storage Hybrid Disks", @("Cache Miss Reads/Sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Remap/Sec" ,"Cluster Storage Cache Stores", @("Page ReMap/sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Cache (MB/s)", "Cluster Storage Hybrid Disks", @("Cache Populate Bytes/sec","Cache Write Bytes/sec"), 13, '#,#', 0.000001, 'Sum', $true))
$c.Add([CounterColumn]::new("RdPop", "Cluster Storage Hybrid Disks", @("Cache Populate Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("WrPop", "Cluster Storage Hybrid Disks", @("Cache Write Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Destage (MB/s)", "Cluster Storage Cache Stores", @("Destage Bytes/sec"), 15, '#,#', 0.000001, 'Sum', $true))
$c.Add([CounterColumn]::new("Update", "Cluster Storage Cache Stores", @("Update Bytes/sec"), 7, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Total (Pgs)", "Cluster Storage Cache Stores", @("Cache Pages"), 11, '0.00E+0', 1, 'Sum', $true))
$c.Add([CounterColumn]::new("Standby", "Cluster Storage Cache Stores", @("Cache Pages StandBy"), 9, '0.00E+0', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("L0", "Cluster Storage Cache Stores", @("Cache Pages StandBy L0"), 9, '0.00E+0', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("L1", "Cluster Storage Cache Stores", @("Cache Pages StandBy L1"), 9, '0.00E+0', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("L2", "Cluster Storage Cache Stores", @("Cache Pages StandBy L2"), 9, '0.00E+0', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Dirty", "Cluster Storage Cache Stores", @("Cache Pages Dirty"), 9, '0.00E+0', 1, 'Sum', $false))
$c.Seal()
$allctrs += $c
###
foreach ($subset in '','Local','Remote') {
$name = 'SBL'
$prefix = ''
if ($subset.Length) {
$name += " $subset"
$prefix = "$($subset): "
}
$c = [CounterColumnSet]::new($name)
$c.Add([CounterColumn]::new("IOPS", "Cluster Disk Counters", @(($prefix + "Read/sec"),($prefix + "Writes/sec")), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Reads", "Cluster Disk Counters", @($prefix + "Read/sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Writes", "Cluster Disk Counters", @($prefix + "Writes/sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("BW (MB/s)", "Cluster Disk Counters", @(($prefix + "Read - Bytes/sec"),($prefix + "Write - Bytes/sec")), 13, '#,#', 0.000001, 'Sum', $true))
$c.Add([CounterColumn]::new("Read", "Cluster Disk Counters", @($prefix + "Read - Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Write", "Cluster Disk Counters", @($prefix + "Write - Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Read Lat (ms)", "Cluster Disk Counters", @($prefix + "Read Latency"), 15, '0.000', 1000, 'Average', $true))
$c.Add([CounterColumn]::new("Write", "Cluster Disk Counters", @($prefix + "Write Latency"), 8, '0.000', 1000, 'Average', $false))
$c.Add([CounterColumn]::new("Read QAvg", "Cluster Disk Counters", @($prefix + "Read Avg. Queue Length"), 11, '0.000', 1, 'Average', $true))
$c.Add([CounterColumn]::new("Write", "Cluster Disk Counters", @($prefix + "Write Avg. Queue Length"), 8, '0.000', 1, 'Average', $false))
$c.Seal()
$allctrs += $c
}
###
$c = [CounterColumnSet]::new("SMB SRV")
$c.Add([CounterColumn]::new("IOPS", "SMB Server Shares", @("Data Requests/sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Reads", "SMB Server Shares", @("Read Requests/sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Writes", "SMB Server Shares", @("Write Requests/sec"), 12, '#,#', 1, 'Sum', $false))
$c.Add([CounterColumn]::new("Data BW (MB/s)", "SMB Server Shares", @("Data Bytes/sec"), 13, '#,#', 0.000001, 'Sum', $true))
$c.Add([CounterColumn]::new("Read", "SMB Server Shares", @("Read Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Write", "SMB Server Shares", @("Write Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Total BW (MB/s)", "SMB Server Shares", @("Transferred Bytes/sec"), 13, '#,#', 0.000001, 'Sum', $true))
$c.Add([CounterColumn]::new("Rcv", "SMB Server Shares", @("Received Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Snd", "SMB Server Shares", @("Sent Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Seal()
$allctrs += $c
##
$c = [CounterColumnSet]::new("S2D BW")
$c.Add([CounterColumn]::new("CSV (MB/s)", "Cluster CSVFS", @("Read Bytes/sec","Write Bytes/sec"), 10, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Read", "Cluster CSVFS", @("Read Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Write", "Cluster CSVFS", @("Write Bytes/sec"), 8 ,'#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("SBL (MB/s)", "Cluster Disk Counters", @("Read - Bytes/sec","Write - Bytes/sec"), 10, '#,#', 0.000001, 'Sum', $true))
$c.Add([CounterColumn]::new("Read", "Cluster Disk Counters", @("Read - Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Write", "Cluster Disk Counters", @("Write - Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Cache (MB/s)", "Cluster Storage Hybrid Disks", @("Cache Hit Read Bytes/sec","Cache Write Bytes/sec"), 12, '#,#', 0.000001, 'Sum', $true))
$c.Add([CounterColumn]::new("Read", "Cluster Storage Hybrid Disks", @("Cache Hit Read Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Write", "Cluster Storage Hybrid Disks", @("Cache Write Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Disk (MB/s)", "Cluster Storage Hybrid Disks", @("Disk Read Bytes/sec","Disk Write Bytes/sec"), 11, '#,#', 0.000001, 'Sum', $true))
$c.Add([CounterColumn]::new("Read", "Cluster Storage Hybrid Disks", @("Disk Read Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Add([CounterColumn]::new("Write", "Cluster Storage Hybrid Disks", @("Disk Write Bytes/sec"), 8, '#,#', 0.000001, 'Sum', $false))
$c.Seal()
$allctrs += $c
##
$c = [CounterColumnSet]::new("Hyper-V LCPU")
$c.Add([CounterColumn]::new("Logical Total%", "Hyper-V Hypervisor Logical Processor", @("% Total Run Time"), 8, "0.00", 1, 'AverageAggregate', $false))
$c.Add([CounterColumn]::new("Guest%", "Hyper-V Hypervisor Logical Processor", @("% Guest Run Time"), 8, "0.00", 1, 'AverageAggregate', $false))
$c.Add([CounterColumn]::new("Hypervisor%", "Hyper-V Hypervisor Logical Processor", @("% Hypervisor Run Time"), 13, "0.00", 1, 'AverageAggregate', $false))
$c.Add([CounterColumn]::new("Root Total%", "Hyper-V Hypervisor Root Virtual Processor", @("% Total Run Time"), 12, "0.00", 1, 'AverageAggregate', $true))
$c.Add([CounterColumn]::new("Guest%", "Hyper-V Hypervisor Root Virtual Processor", @("% Guest Run Time"), 8, "0.00", 1, 'AverageAggregate', $false))
$c.Add([CounterColumn]::new("Hypervisor%", "Hyper-V Hypervisor Root Virtual Processor", @("% Hypervisor Run Time"), 12, "0.00", 1, 'AverageAggregate', $false))
$c.Add([CounterColumn]::new("Remote%", "Hyper-V Hypervisor Root Virtual Processor", @("% Remote Run Time"), 7, "0.00", 1, 'AverageAggregate', $false))
$c.Seal()
$allctrs += $c
##
$c = [CounterColumnSet]::new("SMB Transport")
$c.add([CounterColumn]::new("Read IOPS", "SMB Client Shares", @("Read Requests/sec"), 11, "#,#", 1, 'Sum', $false))
$c.add([CounterColumn]::new("Write", "SMB Client Shares", @("Write Requests/sec"), 8, "#,#", 1, 'Sum', $false))
$c.add([CounterColumn]::new("RDMA Read", "SMB Client Shares", @("Read Requests transmitted via SMB Direct/sec"), 11, "#,#", 1, 'Sum', $true))
$c.add([CounterColumn]::new("Write", "SMB Client Shares", @("Write Requests transmitted via SMB Direct/sec"), 8, "#,#", 1, 'Sum', $false))
$c.Seal()
$allctrs += $c
##
if ($Sets.Count -eq 1 -and $Sets[0] -eq '*') {
$ctrs = $allctrs
} else {
$ctrs = $Sets |% {
$s = $_
$allctrs |? { $_.name -like $s } # allows the SBL* wildcard
}
}
function start-sample(
[CounterColumnSet[]] $ctrs,
[int] $SampleInterval
)
{
# clear any previous job instance
Get-Job -Name watch-cluster -ErrorAction SilentlyContinue | Stop-Job
Get-Job -Name watch-cluster -ErrorAction SilentlyContinue | Remove-Job
# flatten list of counters and uniquify for the total counter set
# some display counter sets may repeat specific values (which is fine)
$counters = ($ctrs.counters |% { $_ |% { $_ }} | group -NoElement).Name
icm -AsJob -JobName watch-cluster (Get-ClusterNode -Cluster $Cluster) {
# extract countersamples, the object does not survive transfer between powershell sessions
# extract as a list, not as the individual counters
get-counter -Continuous -SampleInterval $using:SampleInterval $using:ctrs.counters |% {
,$_.countersamples
}
}
}
# start the first sample job and allow frame draw the first time through
$j = start-sample $ctrs $SampleInterval
$downtime = $null
$skipone = $false
$loops = 0
$restart = $false
# hash of most recent samples/node
$samples = @{}
Get-ClusterNode -Cluster $Cluster |% { $samples[$_.Name] = $null }
while ($true) {
if (-not $restart) {
Start-Sleep -Seconds $SampleInterval
# sleep again if needed to prime the sample pipeline;
# there are no samples if we just restarted the sampling jobs
if ($skipone) {
$skipone = $false
continue
}
# receive updates into the per-node hash
foreach ($child in $j.ChildJobs) {
$samples[$child.Location] = $child | receive-job -ErrorAction SilentlyContinue
}
# null out downed nodes and remember first time we saw one drop out
$down = 0
$j.ChildJobs |? State -ne Running |% {
$samples[$_.Location] = $null
$down += 1
}
if ($down -and $null -eq $downtime) {
$downtime = get-date
}
# if everything is down, we will attempt restart
if ($down -eq $j.ChildJobs.Count) {
$restart = $true
break
}
}
# if explicit restart is required, or it has been 30 seconds with a downed node, restart the jobs to retry
if ($restart -or ($null -ne $downtime -and ((get-date)-$downtime).totalseconds -gt 30)) {
$j | stop-job
$j | remove-job
$j = start-sample $ctrs $SampleInterval
# force gc to clear out accumulated job state quickly
[system.gc]::Collect()
$downtime = $null
$skipone = $true
$restart = $false
continue
}
# now process samples into per-node hashes of set/ctr containing lists of the
# cooked values acrosss the (possible) multiple instances
$psamples = @{}
foreach ($node in $samples.keys) {
if ($samples[$node]) {
$nsamples = @{}
# flatten samples - if we are lagging, we'll have a list
# of consecutive (increasing by timestamp) samples
# we could try to be more efficient by dumping all but the
# final sample, but later ...
$samples[$node] |% { $_ } |% {
($setinst,$ctr) = $($_.path -split '\\')[3..4]
$set = ($setinst -split '\(')[0]
$k = "$set+$ctr"
$nsamples[$k] = $_.cookedvalue
}
$psamples[$node] = $nsamples
}
}
# post-process the samples into the counterset, then clear and dump
$ctrs.DisplayPre($samples, $psamples)
Clear-Host
$drawsep = $false
$ctrs |% {
if ($drawsep) {
write-host -fore Green $('-'*20)
}
$drawsep = $true
$_.Display()
}
# restart the jobs every so many loops to prevent resource growth
$loops += 1
if ($loops -gt 100) {
$loops = 0
$restart = $true
}
}
}