TL;DR – The native Add-CMDeviceCollectionDirectMembershipRule PowerShell cmdlet sucks for adding more than 100 devices, use this replacement script instead.
How fast is good enough? When is the default, too slow?
I guess most of us have been spoiled with modern machines: Quad Xeon Procesors, couple hundred GB of ram, NVME cache drives, and Petabytes of storage at our command.
And don’t get me started with modern database indexing, you want to know what the average annual rainfall on the Spanish Plains are? If I don’t get 2 million responses within a half a second, I’ll be surprised, My Fair Lady.
But sometimes as a developer we need to account for actual performance, we can’t just use the default process and expect it to work in all scenarios to scale.
Background
Been working on a ConfigMgr project in an environment with a machine count well over ~300,000 devices. And we were prototyping a project that involved creating Device Collections and adding computers to the Collections using Direct Membership Rules.
Our design phase was complete, when one of our engineers mentioned that Direct Memberships are generally not optimal at scale. We figured that during the lifecycle of our project we might need to add 5000 arbitrary devices to a collection. What would happen then?
My colleague pointed to this article: http://rzander.azurewebsites.net/collection-scenarios Which discussed some of the pitfalls of Direct Memberships, but didn’t go into the details of why, or discuss what the optimal solution would be for our scenario.
I went to our NWSCUG meeting last week, and there was a knowledgeable Microsoft fella there so I asked him during Lunch. He mentioned that there were no on-going performance problems with Direct Membership collections, however there might be some performance issues when creating/adding to the collection, especially within the Console (Load up the large collection in memory, then add a single device, whew!). He recommended, of course, running our own performance analysis, to find out what worked for us.
OK, so the hard way…
The Test environment
So off to my Standard home SCCM test environment: I’m using the ever efficient Microsoft 365 Powered Device Lab Kit. It’s a bit big, 50GB, but once downloaded, I’ll have a fully functional SCCM Lab environment with a Domain Controller, MDT server, and a SCCM Server, all running within a Virtual Environment, within Seconds!
My test box is an old Intel Motherboard circa 2011, with a i7-3930k processor, 32GB of ram, and running all Virtual Machines running off a Intel 750 Series NVME SSD Drive!
First step was to create 5000 Fake computers. That was fairly easy with a CSV file and the SCCM PowerShell cmdlet Import-CMComputerInformation. Done!
Using the native ConfigMgr PowerShell cmdlets
OK, lets write a script to create a new Direct Membership rule in ConfigMgr, and write some Device Objects to the Collection.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
Example of how to create a Device Collection and populate it with computer objects | |
The Slow way. <Yuck> | |
#> | |
[cmdletbinding()] | |
param( | |
$CollBaseName = 'MyTestCol_03_{0:D4}', | |
$name = 'PCTest*' | |
) | |
foreach ( $Count in 5,50 ) { | |
$CollName = $CollBaseName -f $Count | |
write-verbose "Create a collection called '$CollName'" | |
New-CMDeviceCollection -LimitingCollectionName 'All Systems' -Name $CollName | % name | write-Host | |
Measure-Command { | |
Write-Verbose "Find all Devices that match [$Name], grab only the first $Count, and add to Collection [$CollName]" | |
get-cmdevice -name $name -Fast | | |
Select-Object -first $count | | |
Foreach-Object { | |
Add-CMDeviceCollectionDirectMembershipRule -CollectionName $CollName -ResourceId $_.ResourceID -verbose:$False | |
} | |
} | % TotalSeconds | write-Host | |
} |
Unfortunately the native Add-CMDeviceCollectionDirectMembershipRule cmdlet, doesn’t support adding devices using a pipe, and won’t let us add more than one Device at a time. Gee… I wonder if *that* will affect performance. Query the Collection, add a single device, and write back to the server, for each device added. Hum….
Well the performance numbers weren’t good:
Items to add | Number of Seconds to add all items |
5 | 4.9 |
50 | 53 |
As you can see the number of seconds increased proportionally to the number of items added. If I wanted to add 5000 items, were talking about 5000 seconds, or an hour and a half. Um… no.
In fact a bit of decompiling of the native function in CM suggests that it’s not really designed for scale, best for adding only one device at a time.
Yuck!
The WMI way
I decided to see if we could write a functional replacement to the Add-CMDeviceCollectionDirectMembershipRule cmdlet that made WMI calls instead.
I copied some code from Kadio on http://cm12sdk.net (sorry the site is down at the moment), and tried playing around with the function.
Turns out that the SMS_Collection WMI collection has AddMembershipRule() <Singular> and a AddMembershipRules() <multiple> function. Hey, Adding more than once one device at a time sounds… better!
<Insert several hours of coding pain here>
And finally got something that I think works pretty well:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
Example of how to create a Device Collection and populate it with computer objects | |
The Faster way. <Yea!> | |
#> | |
[cmdletbinding()] | |
param( | |
$CollBaseName = 'MyTestCol_0C_{0:D4}', | |
$name = 'PCTest*' | |
) | |
#region Replacement function | |
Function Add-ResourceToCollection { | |
[CmdLetBinding()] | |
Param( | |
[string] $SiteCode = 'CHQ', | |
[string] $SiteServer = $env:computerName, | |
[string] $CollectionName, | |
[parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
$System | |
) | |
begin { | |
$WmiArgs = @{ NameSpace = "Root\SMS\Site_$SiteCode"; ComputerName = $SiteServer } | |
$CollectionQuery = Get-WmiObject @WMIArgs -Class SMS_Collection -Filter "Name = '$CollectionName' and CollectionType='2'" | |
$InParams = $CollectionQuery.PSBase.GetMethodParameters('AddMembershipRules') | |
$Cls = [WMIClass]"Root\SMS\Site_$($SiteCode):SMS_CollectionRuleDirect" | |
$Rules = @() | |
} | |
process { | |
foreach ( $sys in $System ) { | |
$NewRule = $cls.CreateInstance() | |
$NewRule.ResourceClassName = "SMS_R_System" | |
$NewRule.ResourceID = $sys.ResourceID | |
$NewRule.Rulename = $sys.Name | |
$Rules += $NewRule.psobject.BaseObject | |
} | |
} | |
end { | |
$InParams.CollectionRules += $Rules.psobject.BaseOBject | |
$CollectionQuery.PSBase.InvokeMethod('AddMembershipRules',$InParams,$null) | Out-null | |
$CollectionQuery.RequestRefresh() | out-null | |
} | |
} | |
#endregion | |
foreach ( $Count in 5,50,500,5000 ) { | |
$CollName = $CollBaseName -f $Count | |
write-verbose "Create a collection called '$CollName'" | |
New-CMDeviceCollection -LimitingCollectionName 'All Systems' -Name $CollName | % name | write-Host | |
Measure-Command { | |
Write-Verbose "Find all Devices that match [$Name], grab only the first $Count, and add to Collection [$CollName]" | |
get-cmdevice -name $name -Fast | | |
Select-Object -first $count | | |
Add-ResourceToCollection -CollectionName $CollName | |
} | % TotalSeconds | write-Host | |
} |
Performance numbers look much better:
Items to add | Number of Seconds to add all items |
5 | 1.1 |
50 | 1.62 |
500 | 8.06 |
5000 | 61.65 |
Takes about the same amount of time to add 5000 devices using my function as it takes to add 50 devices using the native CM function. Additionally some code testing suggests that about half of the time for each group is being performed creating each rule ( the process {} block ), and the remaining half in the call to AddMembershipRules(), my guess is that should be better for our production CM environment.
Note that this isn’t just a PowerShell Function, it’s operating like a PowerShell Cmdlet. The function will accept objects from the pipeline and process them as they arrive, as quickly as Get-CMDevice can feed them through the pipeline.
However more testing continues.
-k