Introduction

A colleague of mine recently asked for some help with a script he was writing. We use vSphere tags in order to determine which backup schedule a given VM is in and he wanted to find all VMs which were not assigned a tag in the category backup-service-level, and therefore were not being backed up. His actual query was regarding filtering out certain VMs which will never be given a backup tag. However, in helping with this I realised that his script was taking over 20 minutes to complete! I decided to take a copy and see if I could speed things up a bit.

The Script

Here is the script as it was when I first took a look at it.

$VmNoTag = @()

$VMs = Get-VM

foreach ($VM in $VMs) {
    $Name = $VM.name
    $Tag = $($VM | Get-TagAssignment -Category 'backup-service-level').tag.name
    $Power = $VM.PowerState
        
    $Line = New-Object -TypeName psobject 
    $Line | Add-Member -MemberType NoteProperty VM -Value $Name
    $Line | Add-Member -MemberType NoteProperty Tag -Value $Tag
    $Line | Add-Member -MemberType NoteProperty PowerState -Value $Power
    $VmNoTag += $line
}

$VmNoTag | Where-Object {$_.Tag -eq $null}

It's functional and returns the required results. However, the first thing I noticed is that the Get-TagAssignment cmdlet is being called in every iteration of the foreach loop. This will take a very long time as it has to fetch results from the vCenter API each time it is called. Also, the last line of the script filters the results so that only VMs without a tag assigned are returned. This is iterating though all of the results in the $VMs array for a second time, which will also be contributing a small performance hit.

In order to effectively measure the performance of the script in its current state I used the Measure-Command cmdlet. As you can see the script takes 24 minutes to run in its current state.

Measure-Command {& .\Get-VMsWithNoTag.ps1}

Days              : 0
Hours             : 0
Minutes           : 24
Seconds           : 4
Milliseconds      : 361
Ticks             : 14443611146
TotalDays         : 0.0167171425300926
TotalHours        : 0.401211420722222
TotalMinutes      : 24.0726852433333
TotalSeconds      : 1444.3611146
TotalMilliseconds : 1444361.1146

Optimization

I was pretty sure that calling Get-TagAssignment over and over again was causing the slowness. To fix this, I call Get-TagAssignment once, before the foreach loop, and store the results in an array named $TagAssignments. Then within the foreach loop all I have to do is check whether the $TagAssignments array contains the VM's name. If it doesn't then the VM doesn't have a backup tag assigned and I add it to the $VmNoTag array of results.

$VmNoTag = @()

$VMs = Get-VM
$TagAssignments = Get-TagAssignment -Category 'backup-service-level'

foreach ($VM in $VMs) {
    if (!($TagAssignments.Entity.Name -contains $VM.Name)) {
        $VmNoTag += $VM    
    }
}

$VmNoTag

I retested the script using the Measure-Command cmdlet and the results were very interesting.

Measure-Command {& .\Get-VMsWithNoTag.ps1}


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 28
Milliseconds      : 833
Ticks             : 288335148
TotalDays         : 0.000333721236111111
TotalHours        : 0.00800930966666667
TotalMinutes      : 0.48055858
TotalSeconds      : 28.8335148
TotalMilliseconds : 28833.5148

That's right 28 seconds! Down from 24 minutes. I was expecting to see an improvement but this amazed me. It just goes to show how important it is to optimize your code.

Conclusion

This technique of storing a full set of results in an array, and then iterating through the array to find matching values can be a real time saver. I hope you have found it interesting and can use it to speed up some of your scripts.