Finding OUs with Workstations – Powershell performance is weird

I was recently asked to implement LAPS for a customer. Nothing unusual there. This particular environment was new to me which meant it was time to investigate their OU structure.

Why does OU structure matter?

When implementing laps there are a few things to keep in mind. First, you need to extend the schema. Then you need to grant computers permission to access the newly created attributes… Well to accomplish this you need to assign the self permission in your root computers OU but are you sure you got them all? What if you’re a global company and break down your computers and users into country OU’s?

So how do we find all of the OU’s that have workstations in them?

Enter PowerShell

Gathering information from Active Directory can range from very simple to very complicated. Fortunately for us, we just want to know what OU all computer objects, with a workstation OS can be found in. This should be a relatively simple proposition.

Get-ADComputer -Identity WARMACHINE

Quick look and we find that we have the distinguished name property but it includes the name. Hrm. OK we can solve that right all we need to do is find the first ‘,’ and absorb the rest right?

Well, here is where PowerShell and its associated methodologies gets weird.

Get-ADComputer -filter * | ForEach-Object{$_.DistinguishedName.Substring($_.IndexOf(',') +1)}
Get-ADComputer -filter * | Select-object DistinguishedName | ForEach-Object{$_.Substring($_.IndexOf(',') +1)}
$(Get-ADComputer -filter *).DistinguishedName | ForEach-Object{$_.Substring($_.IndexOf(',') +1)}

You would expect that all three of the above commands should return the same results. Not so much. The first two versions will fail spectacularly because the parser retains the object type information of the “DistinguishedName” attribute. However, the third one enters the property, expanding it and returning its string value. Simple tweak, and we can get only WORKSTATIONS instead.

$(Get-ADComputer -filter 'OperatingSystem -notlike "Windows server*"').DistinguishedName | ForEach-Object{$_.Substring($_.IndexOf(',') +1)}

Which will return some nice shiny results like this:

Simple, direct and using only methods in the native pipeline. However, it’s not the only method of getting this done. I shared my solution with some friends in the WinAdmins Discord server because man substring and index of is pretty ugly but it works and I was curious what other peoples 5 minutes or less solution would be.

Enter the Hash Splat

Chris Dent, as soon as he saw the code went right to using the splat methodology:

Get-ADOrganizationalUnit -Filter * | Where-Object {
    $params = @{
        Filter        = 'operatingSystem -notlike "*server*"'
        SearchBase    = $_.DistinguishedName
        SearchScope   = 'OneLevel'
        ResultSetSize = 1
    }
    Get-ADComputer @params
}

Typically using where-Object and trying to limit specific properties using a splat SHOULD optimize the search and make it run significantly faster especially as machine counts scale. However, what I found after testing was interesting. I made an assumption that in the case of my lab the foreach statement would be more performant and no surprise – it was in fact almost 50% more efficient.

Now, I assumed that we would start to see some performance gains at around 100 machines. So I did another test against about 600 devices, with 400 or so workstations.

Don’t mind the red… I just can’t spell count.

Holy cow, ForEach-Object was MORE efficient in this use case. So I went a few steps further in larger and larger sites, and it seems that around 4K devices is the magic number where suddenly a SPLAT method becomes more efficient but even then its only barely.

Environment with just over 4K devices

What about using .NET

Not one to be out done, another friend Cody Mathis was now curious. What if we used .NET:

Measure-Command {

$AllWorkstationSearcher = [System.DirectoryServices.DirectorySearcher]::new('(&(!(OperatingSystem=Microsoft Windows *Server*)(objectCategory=computer)))', @('distinguishedName'), [System.DirectoryServices.SearchScope]::Subtree)
$AllWorkstationSearcher.PageSize = 1000
$AllWorkstations = $AllWorkstationSearcher.FindAll()

$OUsWithWorkstations = $(foreach ($d in $AllWorkstations) {
        ($d.properties['distinguishedName'][0] -replace '^[^,]+,')
    }) | Select-Object -Unique
}

This example was particularly interesting, because with the Double Wild card around “server” it performed consistently WORSE than both of the other methods used. However, when only a single wildcard was used (which generates the same results). It absolutely stomped the other two methods used.

OK So what was the point of this again?

PowerShell performance is almost always a subject for hot debate. Especially when you get into discussions around it with experts. This particular example use case study just happened to really highlight how depending on what method you use to retrieve data you can experience rather drastically different types of performance hits.

The big thing to remember is to code for YOUR environment. A lot of the great PowerShell writers out there are used to writing code AT SCALE. That means they write things optimized for dealing with 50 – 200K objects when interacting with Active Directory. That means their code may not be the right fit for YOUR organization.

Happy scripting!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: