How to Build a Hyper-V Performance Monitoring Tool with PowerShell

Save to My DOJO

How to Build a Hyper-V Performance Monitoring Tool with PowerShell

In a previous post, I guided you through some PowerShell techniques to get, format and display Hyper-V related performance counters. The potential downside is that some of those techniques relied on additional PowerShell modules which you may not feel like installing. With that in mind let me wrap up my short course on performance counters and demonstrate how you can create a visually useful monitoring tool with nothing more than the PowerShell console. Technically, today’s code will also work in the PowerShell ISE but I think it is more effective in the PowerShell console.

Determining Average Dynamic Memory Pressure

As I have mentioned before, you can use the techniques and concepts to work with all sorts of performance counter data. The technique I want to show now works best with a single counter for a single virtual machine. I’m going to use the Average Pressure counter from the Dynamic Memory counter set for a single VM.

I’m using my local Hyper-V setup on Windows 10 but you should get the similar results for any Hyper-V host, even remote ones.

$computer = $env:COMPUTERNAME
$VMName = "SRV2"

Get-Counter "Hyper-V Dynamic Memory VM($VMName)Average Pressure" -ComputerName $computer

Getting Hyper-V Dynamic Memory Average Pressure

Verifying you can get a single instance is a good first step. Now that I know my syntax is correct, I can extend Get-Counter to provide a number of samples at a set interval.

Get-Counter "Hyper-V Dynamic Memory VM($VMName)Average Pressure" -ComputerName $computer -MaxSamples 24 -SampleInterval 5

I could use some of the techniques from the previous article. But instead, I am going to turn to a cmdlet that most people don’t use, Write-Progress.

Setting Write-Progress as Output

I encourage you to take a few minutes at some point to read full help and examples for the cmdlet. You’ve probably seen this cmdlet in action in some commands that take a long time to run. You see a greenish (cyan) box with a text style progress bar. It turns out that you can control the features of the progress box from PowerShell. In other words, I can dynamically adjust the percent complete value to reflect any value I want. In this case, I can use it to display the value of Average Memory Pressure performance counter.

Get-Counter "Hyper-V Dynamic Memory VM($VMName)Average Pressure" -ComputerName $computer -MaxSamples 10 -SampleInterval 3 |
foreach -begin {
    $params = @{
    Activity = "Average Memory Pressure" 
    CurrentOperation = "" 
    Status = $VMName
    PercentComplete = 0
    }

} -process { 
    $params.CurrentOperation = "Value: $($_.countersamples.cookedvalue) [$($_.Timestamp) ]" 
    $params.PercentComplete = ($_.CounterSamples.cookedvalue)*.8
    Write-Progress @params
}

Displaying a live display of average memory pressure

PowerShell will display a live update on the Average Pressure value for virtual machine SRV2. My code snippet is updating the progress bar 10 times every 3 seconds. In order to fit in the performance counter value, especially if it gets really high, I’m displaying 80% of the value. The progress bar becomes more of a relative indicator. If I let this run long enough and the VM is stressed, I’ll see the progress bar increase and decrease as pressure ebbs.

Creating a Re-Usable Tool

With this premise in mind, I took it upon myself to create a PowerShell function that you can run at any time to get a similar display. Here it is!

Function Show-VMMemoryPressure {
[cmdletbinding(DefaultParameterSetName="interval")]
Param(
[Parameter(Position = 0, Mandatory,HelpMessage = "Enter the name of a virtual machine")]
[alias("Name")]
[string]$VMName,
[Parameter(HelpMessage = "The name of the Hyper-V Host")]
[Alias("CN","vmhost")]
[string]$Computername = $env:computername,
[Parameter(HelpMessage = "The sample interval in seconds")]
[int32]$Interval = 5,
[Parameter(HelpMessage = "The maximum number of samples",ParameterSetName="interval")]
[ValidateScript({$_ -gt 0})]
[int32]$MaxSamples=2,
[Parameter(HelpMessage = "Take continuous measurements.",ParameterSetName="continous")]
[switch]$Continuous

)

Try {
    #verify VM
    Write-Verbose "Verifying $VMName on $Computername"
    $vm = Get-VM -ComputerName $Computername -Name $VMName -ErrorAction stop
    if ($vm.state -ne 'running') {
        $msg = "The VM {0} on {1} is not running. Its current state is {2}." -f $vmname.Toupper(),$Computername,$vm.state
        Write-Warning $msg
     }
    else {
        $counterparams = @{
            Counter = "Hyper-V Dynamic Memory VM($($vm.vmname))Average Pressure" 
            ComputerName = $Computername 
            SampleInterval = $Interval
        }
        if ($Continuous) {
            $counterparams.Add("Continuous",$True)
        }
        else {
            $counterparams.Add("MaxSamples",$MaxSamples)
        }

        Write-Verbose "Getting counter data"
        $counterparams | Out-string | Write-Verbose
        Get-Counter @counterparams | foreach-Object {

        #scale values over 100
        $pct = ($_.CounterSamples.cookedvalue)*.8
        #if scaled value is over 100 then max out the percentage
        if ($pct -gt 100) {
            $pct = 100
        }

        $progparams = @{
            Activity = "Average Memory Pressure" 
            Status = $VM.vmname
            CurrentOperation = "Value: $($_.countersamples.cookedvalue) [$($_.Timestamp) ]" 
            PercentComplete = $pct
        }
        Write-Progress @progparams
        }

    } #else VM Verified
} #Try
Catch {
    Throw $_
} #Catch

}

Save a copy to a local folder and dot source the file in your PowerShell session.

. C:scriptsShow-VMMemoryPressure.ps1

To run, specify the name of a virtual machine along with the number of samples and sampling interval in seconds. The function will default to the local computer for the Hyper-V host so don’t forget to specify one if you are querying a remote machine. There is nothing in the function that requires the Hyper-V module so you can run this just about anywhere.

Show-VMMemoryPressure

Scaling Out

One drawback to this function is that it can only handle a single virtual machine. However, with a little PowerShell magic, you can fire up as many PowerShell windows as you find reasonable and monitor multiple virtual machines at once. Very handy if you have a multiple monitor setup. From within PowerShell run code like this:

"dom1","srv1","srv2" |
foreach {
start "powershell" -ArgumentList "-noexit -noprofile -command &{. c:scriptsshow-vmmemorypressure.ps1;show-vmmemorypressure -vmname $_ -continuous}"
}

Each of the piped in names is the name of a Hyper-V virtual machine. For each name, I’m starting a new PowerShell window, dot-sourcing the script and running the function against the VM in continuous mode. After manually resizing and re-arranging the windows I now have a great look at the memory pressures on these VMs.

Monitoring memory pressure with PowerShell on multiple VMs

I can manually close the windows when I’m finishing monitoring. I wouldn’t recommend this for a large Hyper-V farm, but if you have a few VMs you need to monitor or troubleshoot this could be a useful technique.

Or you can try this variation that lets you display multiple VMs in a single Write-Progress window. I would limit this to no more than 3 VMs in the console or maybe 4 if using the PowerShell ISE. As before you need to dot source the script file first.

#requires -version 5.0
#requires -runasAdministrator
#requires -module Hyper-V

#use max of 3 VMS in the console. You may also need to resize the console window.
#use max of 4 VMs in the ISE

#Show-MemoryPressureProgress
Function Show-VMMemoryPressure {

    [cmdletbinding(DefaultParameterSetName = "interval")]
    Param(
        [Parameter(Position = 0, Mandatory, HelpMessage = "Enter the name of a virtual machine.")]
        [alias("Name")]
        [string[]]$VMName,
        [Parameter(HelpMessage = "The name of the Hyper-V Host")]
        [Alias("CN", "vmhost")]
        [string]$Computername = $env:computername,
        [Parameter(HelpMessage = "The sample interval in seconds")]
        [int32]$Interval = 5,
        [Parameter(HelpMessage = "The maximum number of samples", ParameterSetName = "interval")]
        [ValidateScript( {$_ -gt 0})]
        [int32]$MaxSamples = 2,
        [Parameter(HelpMessage = "Take continuous measurements.", ParameterSetName = "continous")]
        [switch]$Continuous,
        [switch]$ClearHost
    )

    DynamicParam {
        if ($host.name -eq 'ConsoleHost') {

            #define a collection for attributes
            $attributes = New-Object System.Management.Automation.ParameterAttribute
            $attributes.Mandatory = $False
            $attributes.HelpMessage = "Enter a console color"
            $attributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $attributeCollection.Add($attributes)

            #define the dynamic param
            $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("ProgressBackground", [system.consolecolor], $attributeCollection)
            $dynParam1.Value = $host.PrivateData.ProgressBackgroundColor

            #define the dynamic param
            $dynParam2 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("ProgressForeground", [system.consolecolor], $attributeCollection)
            $dynParam2.Value = $host.PrivateData.ProgressForegroundColor

            #create array of dynamic parameters
            $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
            $paramDictionary.Add("ProgressBackground", $dynParam1)
            $paramDictionary.Add("ProgressForeground", $dynParam2)

            #use the array
            return $paramDictionary

        } #if consolehost
    } #close DynamicParam

    Begin {
        $counters = @()
        $vmhash = @{}
        $j = 0

        if ($ClearHost) {
            Clear-Host
        }
        
        #limit number of VMs
        if ($host.name -eq 'ConsoleHost') {
            $max = 3

            if ($PSBoundParameters.ContainsKey("ProgressBackground")) {
                $savedbg = $host.PrivateData.ProgressBackgroundColor
                 $host.PrivateData.ProgressBackgroundColor = $PSBoundParameters.ProgressBackground
                  Write-Verbose "Using progress background color $ProgressBackground"
            }
            if ($PSBoundParameters.ContainsKey("ProgressForeground")) {
                $savedfg = $host.PrivateData.ProgressForegroundColor
                $host.PrivateData.ProgressForegroundColor = $PSBoundParameters.ProgressForeground
                Write-Verbose "Using progress foreground color $Progressforeground"
            }           

        }
        elseif ($host.name -match "ISE") {
            $max = 4
        }
    } #begin

    Process {
        foreach ($item in $VMName[0..($max - 1)]) {
            Try {

                Write-Verbose "Verifying $item on $Computername"
                $vm = Get-VM -ComputerName $computername -Name $item -ErrorAction stop
                if ($vm.state -ne 'running') {
                    $msg = "The VM {0} on {1} is not running. Its current state is {2}." -f $item.Toupper(), $Computername, $vm.state
                    Write-Warning $msg
                }
                else {
                    Write-Verbose "Adding VM data"
                    $counters += "Hyper-V Dynamic Memory VM($($vm.vmname))Average Pressure"
                    #create a hash with VMNames and a number for their progress id
                    $j++
                    $Vmhash.Add($vm.vmname, $j)
                }

            } #Try
            Catch {
                Write-Warning $_.exception.message

            } #Catch
        } #foreach item

        if ($counters.count -gt 0) {

            $counterparams = @{
                Counter          = $counters
                ComputerName     = $Computername
                SampleInterval   = $Interval
                pipelinevariable = "pv"

            }
            if ($Continuous) {
                $counterparams.Add("Continuous", $True)
            }
            else {
                $counterparams.Add("MaxSamples", $MaxSamples)
            }

            Write-Verbose "Getting counter data"
            $counterparams | Out-String | Write-Verbose

            Get-Counter @counterparams | ForEach-Object {
                $_.countersamples | Sort-Object -property Instancename |
                    Group-Object -property instancename | ForEach-Object {

                    #scale values over 100
                    $pct = ($_.group.cookedvalue) * .8
                    #if scaled value is over 100 then max out the percentage
                    if ($pct -gt 100) {
                        $pct = 100
                    }

                    $progparams = @{
                        Activity         = $_.Name.ToUpper()
                        Status           = $($pv.Timestamp)
                        CurrentOperation = "Average Pressure: $($_.group.cookedvalue)"
                        PercentComplete  = $pct
                        id               = $vmhash[$_.Name]
                    }

                    Write-Progress @progparams

                } #for each value
            } #foreach countersample
        } #if counters
    } #process

    End {
        #set private data values back
        if ($savedbg) {
            $host.PrivateData.ProgressBackgroundColor = $savedbg
        }
        if ($savedfg) {
            $host.PrivateData.ProgressForegroundColor = $savedfg
        }
    } #end
} #close function

This version of the function will require the Hyper-V module and it needs to be run in an elevated session. In the PowerShell ISE you get output like this:

Monitoring memory pressure for multiple VMs in the PowerShell ISE

In addition to being able to specify multiple virtual machines, when you run in the console you can also specify a color scheme for the progress bar. If you don’t, then you get the defaults.

. C:scriptsShow-VMMemoryPressureProgress.ps1
Show-VMMemoryPressure -VMName SRV1,SRV2,DOM1 -Computername Bovine320 -Interval 3 -MaxSamples 10 -ProgressBackground yellow -ProgressForeground black

Displaying Hyper-V Memory Pressure in PowerShell

If you don’t see the progress bar for all the virtual machines you might need to resize your console window. For a typical PowerShell console window, I think 3 VMs should work just fine. If you run larger windows and need to see more VMs you can modify the code.

In any event, I now have a performance monitoring tool built in PowerShell that doesn’t require anything special.

What About You?

What do you think? Is this something you might use or take as a jumping off point for your own project? I’d love to hear what you think. By the way, if you are a beginner PowerShell pro looking to learn more about scripting, take a look at my PowerShell course Learn PowerShell Scripting in a Month of Lunches. If you have a bit more PowerShell experience and looking to improve your scripting game then I’d recommend The PowerShell Scripting and Toolmaking Book. Or find me on Twitter for additional tips and suggestions.

Until next time, keep pushing the limits of what you can do to manage Hyper-V with PowerShell.

Altaro Hyper-V Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

Leave a comment or ask a question

Your email address will not be published. Required fields are marked *

Your email address will not be published. Required fields are marked *

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.