Foreach object is more faster than foreach -parallel? - multithreading

I am just starting using powershell and I wonder why my parallel srcipt is more slower than my normal foreach object script?
My script for my normal foreachobject:
function Get-ADUsers { #get all users in nested groups }
function Get-NestedGroupUsers {
param (
[Parameter(Mandatory = $true)][String]$FileName,
[Parameter(Mandatory = $true)][String]$searchFileURL
)
$storageHolder = #()
# $storageHolder | Export-Csv -Path "C:\Users\demandx\Desktop\AD User Lists\$FileName.csv" -NoTypeInformation -Force
$groupList = Get-Content $searchFileURL
$groupList | ForEach-Object {
$allusers = Get-ADUsers -GroupName $_
$storageHolder += $allusers
}
$storageHolder | select ParentGroup, Name, EmployeeNumber, Enabled, LastLogonDate, PasswordLastSet |Export-Csv -Path "C:\Users\demandx\Desktop\$FileName.csv" -NoTypeInformation -Force
}
My script for foreach -parallel (I store the function inside psm1 then import here.)
Function Get-Members {
param (
[Parameter(Mandatory = $true)][String]$FileName,
[Parameter(Mandatory = $true)][String]$searchFileURL
)
$groupList = Get-Content $searchFileURL
$storageHolder = $groupList | ForEach-Object -Parallel {
Import-Module -Name "C:\Users\demandx\Desktop\Get-ADUserMembers.psm1"
Get-ADUserMembers -GroupName $_ | Select-Object ParentGroup, Name, EmployeeNumber, Enabled, LastLogonDate, PasswordLastSet
} -ThrottleLimit 5
$storageHolder | Export-Csv -Path "C:\Users\demandx\Desktop\AD User Lists\$FileName.csv" -NoTypeInformation -Force
}
The script or my get-adusers (get all the users in nested groups)
function Get-ADUsers {
param (
[Parameter(ValuefromPipeline = $true, mandatory = $true)][String] $GroupName
)
[int]$circular = $null
# result holder
$resultHolder = #()
$table = $null
$nestedmembers = $null
$adgroupname = $null
function Get-ADUsers {
param (
[Parameter(ValuefromPipeline = $true, mandatory = $true)][String] $GroupName
)
[int]$circular = $null
# result holder
$resultHolder = #()
$table = $null
$nestedmembers = $null
$adgroupname = $null
# get members of the group and member of
$ADGroupname = get-adgroup $groupname -properties memberof, members
# list all members as list (no headers) and save to var
$memberof = $adgroupname | select -expand memberof
if ($adgroupname) {
if ($circular) {
$nestedMembers = Get-ADGroupMember -Identity $GroupName -recursive
$circular = $null
}
else {
$nestedMembers = Get-ADGroupMember -Identity $GroupName | sort objectclass -Descending
# if get adgroupmember returns nothing, it uses the members for ordinary getADGroup
if (!($nestedmembers)) {
$unknown = $ADGroupname | select -expand members
if ($unknown) {
$nestedmembers = #()
foreach ($member in $unknown) {
$nestedmembers += get-adobject $member
}
}
}
}
# loops through each member
ForEach($nestedmember in $nestedmembers){
# creates the properties into a custom object.
$Props = #{
Type = $nestedmember.objectclass;
Name = $nestedmember.name;
DisplayName = "";
ParentGroup = $ADgroupname.name;
Enabled = "";
Nesting = $nesting;
DN = $nestedmember.distinguishedname;
Comment = ""
EmployeeNumber = "";
LastLogonDate = "";
PasswordLastSet = "";
}
# if member object is a user
if ($nestedmember.objectclass -eq "user") {
# saves all the properties in the table.
$nestedADMember = get-aduser $nestedmember.Name -properties enabled, displayname, EmployeeNumber, LastLogonDate, PasswordLastSet
$table = new-object psobject -property $props
$table.enabled = $nestedadmember.enabled
$table.name = $nestedadmember.samaccountname
$table.displayname = $nestedadmember.displayname
$table.EmployeeNumber = $nestedadmember.EmployeeNumber
$table.LastLogonDate = $nestedadmember.LastLogonDate
$table.PasswordLastSet = $nestedadmember.PasswordLastSet
#save all in 1 storage
$resultHOlder += $table | select type, name, displayname, parentgroup, nesting, enabled, dn, comment , EmployeeNumber, LastLogonDate, PasswordLastSet
}
# if member object is group
elseif ($nestedmember.objectclass -eq "group") {
$table = new-object psobject -Property $props
# if circular, meaning the groups member of list contains one of its members.
# e.g. if group 2 is a member of group 1 and group 1 is a member of grou 2
if ($memberof -contains $nestedmember.distinguishedname) {
$table.comment = "Circular membership"
$circular = 1
}
# for circular output
#$table | select type, name, displayname, parentgroup, nesting, enabled, dn, comment
#calling function itself
$resultHOlder += Get-ADUsers -GroupName $nestedmember.distinguishedName
}
else {
if ($nestedmember) {
$table = new-object psobject -property $props
$resultHolder += $table | select type, name, displayname, parentgroup, nesting, enabled, dn, comment, EmployeeNumber, LastLogonDate, PasswordLastSet
}
}
}
}
return $resultHOlder
}
function Get-NestedGroupUsers {
param (
[Parameter(Mandatory = $true)][String]$FileName,
[Parameter(Mandatory = $true)][String]$searchFileURL
)
$storageHolder = #()
# $storageHolder | Export-Csv -Path "C:\Users\demandx\Desktop\AD User Lists\$FileName.csv" -NoTypeInformation -Force
$groupList = Get-Content $searchFileURL #| ForEach-Object { $_ }
$groupList | ForEach-Object {
$allusers = Get-ADUsers -GroupName $_
$storageHolder += $allusers
}
$storageHolder | select ParentGroup, Name, EmployeeNumber, Enabled, LastLogonDate, PasswordLastSet |Export-Csv -Path "C:\Users\demandx\Desktop\$FileName.csv" -NoTypeInformation -Force
}
# get members of the group and member of
$ADGroupname = get-adgroup $groupname -properties memberof, members
# list all members as list (no headers) and save to var
$memberof = $adgroupname | select -expand memberof
if ($adgroupname) {
if ($circular) {
$nestedMembers = Get-ADGroupMember -Identity $GroupName -recursive
$circular = $null
}
else {
$nestedMembers = Get-ADGroupMember -Identity $GroupName | sort objectclass -Descending
# if get adgroupmember returns nothing, it uses the members for ordinary getADGroup
if (!($nestedmembers)) {
$unknown = $ADGroupname | select -expand members
if ($unknown) {
$nestedmembers = #()
foreach ($member in $unknown) {
$nestedmembers += get-adobject $member
}
}
}
}
# loops through each member
ForEach($nestedmember in $nestedmembers){
# creates the properties into a custom object.
$Props = #{
Type = $nestedmember.objectclass;
Name = $nestedmember.name;
DisplayName = "";
ParentGroup = $ADgroupname.name;
Enabled = "";
Nesting = $nesting;
DN = $nestedmember.distinguishedname;
Comment = ""
EmployeeNumber = "";
LastLogonDate = "";
PasswordLastSet = "";
}
# if member object is a user
if ($nestedmember.objectclass -eq "user") {
# saves all the properties in the table.
$nestedADMember = get-aduser $nestedmember.Name -properties enabled, displayname, EmployeeNumber, LastLogonDate, PasswordLastSet
$table = new-object psobject -property $props
$table.enabled = $nestedadmember.enabled
$table.name = $nestedadmember.samaccountname
$table.displayname = $nestedadmember.displayname
$table.EmployeeNumber = $nestedadmember.EmployeeNumber
$table.LastLogonDate = $nestedadmember.LastLogonDate
$table.PasswordLastSet = $nestedadmember.PasswordLastSet
#save all in 1 storage
$resultHOlder += $table | select type, name, displayname, parentgroup, nesting, enabled, dn, comment , EmployeeNumber, LastLogonDate, PasswordLastSet
}
# if member object is group
elseif ($nestedmember.objectclass -eq "group") {
$table = new-object psobject -Property $props
# if circular, meaning the groups member of list contains one of its members.
# e.g. if group 2 is a member of group 1 and group 1 is a member of grou 2
if ($memberof -contains $nestedmember.distinguishedname) {
$table.comment = "Circular membership"
$circular = 1
}
# for circular output
#$table | select type, name, displayname, parentgroup, nesting, enabled, dn, comment
#calling function itself
$resultHOlder += Get-ADUsers -GroupName $nestedmember.distinguishedName
}
else {
if ($nestedmember) {
$table = new-object psobject -property $props
$resultHolder += $table | select type, name, displayname, parentgroup, nesting, enabled, dn, comment, EmployeeNumber, LastLogonDate, PasswordLastSet
}
}
}
}
return $resultHOlder
}
Parallel result
-------------------------------------------
Days : 0
Hours : 0
Minutes : 1
Seconds : 2
Milliseconds : 283
Ticks : 622833415
TotalDays : 0.000720872008101852
TotalHours : 0.0173009281944444
TotalMinutes : 1.03805569166667
TotalSeconds : 62.2833415
TotalMilliseconds : 62283.3415
Non parallel Result
-------------------------------------------
Days : 0
Hours : 0
Minutes : 0
Seconds : 35
Milliseconds : 322
Ticks : 353221537
TotalDays : 0.00040882122337963
TotalHours : 0.00981170936111111
TotalMinutes : 0.588702561666667
TotalSeconds : 35.3221537
TotalMilliseconds : 35322.1537

TLDR:
There are 3 reasons:
To take full advantage of ForEach-Object -Parallel performance, the processing time of the Script Block needs to be significantly larger than the time to set up the thread and environment.
The Import-Module will introduce an overhead.
Both these factors individually are small, but multiply them by 1000 or a larger number, and they become big.
ForEach-Object -Parallel runs very different than a normal ForEach-Object.
First, a normal ForEach-Object runs inside your current PowerShell thread with access to all the variables, loaded memory, and pipelining. This is fine for 98% of all the jobs we run, and 1 second execution times are ok. In the 2% of times that we have a process that is super CPU intensive that maxes out a single CPU Core and runs forever, or we need to wait for responses (e.g. API requests) when other execution can take place, then -Parallel is what we need to look at.
The idea behind Parallel execution is to take advantage of your brand new AMD Ryzen™ Threadripper™ 3990X with 64 Cores/128 Threads, and split your process into separate "Jobs" that cand run across multiple CPU Cores and multiple threads at the same time. This could increase your speeds by orders of magnitude e.g. potentially 128 times faster.
To achieve this, ForEach-Object -Parallel creates a new "Job" for each script block you execute, and starts spreading the Jobs across CPU Cores for execution. This is great when you have long running CPU bound processes, but when you have very short and small Jobs you hit the crux of Parallel execution, where the setup takes more time than the actual execution. ForEach-Object -Parallel has to completely set up your environment for each "Job" you run, e.g. it has to spin up multiple new threads and multiple new PowerShell instances for every Job to run in.
To illustrate the amount of setup time needed, If we wrote "Hello World" once to the current thread it takes 1 milisecond:
PS C:\> Measure-Command { Write-Host "Hello World" }
Hello World
Seconds : 0
Milliseconds : 1
TotalMilliseconds : 1.9798
To run 1 single "Hello World" in Parallel takes 26 miliseconds:
PS C:\> Measure-Command { 1 | ForEach-Object -Parallel { Write-Host "Hello World" } }
Hello World
Seconds : 0
Milliseconds : 26
TotalMilliseconds : 26.052
That means that it spent about 25ms spinning up a new thread, and setting up the environment and 1 ms of actual work.
To write it 100 times on the currently running thread takes about 83ms:
PS C:\> Measure-Command { 1..100 | ForEach-Object { Write-Host "Hello World" } }
Hello World
...
Hello World
Hello World
Seconds : 0
Milliseconds : 83
TotalMilliseconds : 83.1846
Running in -Parallel with a -ThrottleLimit 5 takes 294ms:
PS C:\> Measure-Command { 1..100 | ForEach-Object -Parallel { Write-Host "Hello World" } -ThrottleLimit 5 }
Hello World
...
Hello World
Hello World
Seconds : 0
Milliseconds : 294
TotalMilliseconds : 294.3205
This goes to show how running in Parallel can be bad for tiny individual operations. But on the flip side, if you have something that takes 1 second to run, you can start to see how it works better:
e.g. run 5 processes that take 1 second each. First on a single thread:
PS C:\> Measure-Command { 1..5 | ForEach-Object { Start-Sleep -Seconds 1 } }
Seconds : 5
Milliseconds : 46
TotalSeconds : 5.046348
TotalMilliseconds : 5046.348
As expected, it takes just over 5 seconds. Now, in Parallel:
PS C:\> Measure-Command { 1..5 | ForEach-Object -Parallel { Start-Sleep -Seconds 1 } -ThrottleLimit 5 }
Seconds : 1
Milliseconds : 73
TotalSeconds : 1.0732423
TotalMilliseconds : 1073.2423
It completes in just over a second. If the processing time takes significantly more time than the setup time, then -Parallel is useful.
Also, in your case, not only do you have extra overhead of the setup time, but loading a module (needed to set up the new environment), adds significantly more time to the ForEach-Object -Parallel version.
For example, lets import a module AzureAD inside our ForEach-Object script 5 times:
PS C:\> Measure-Command { 1..5 | ForEach-Object { Import-Module AzureAD } }
Seconds : 0
Milliseconds : 18
TotalSeconds : 0.0185406
TotalMilliseconds : 18.5406
And now with ForEach-Object -Parallel:
PS C:\> Measure-Command { 1..5 | ForEach-Object -Parallel { Import-Module AzureAD } -ThrottleLimit 5 }
Seconds : 0
Milliseconds : 125
TotalSeconds : 0.1256923
TotalMilliseconds : 125.6923
We can see that there is a significant difference because it has to load the module 5 times as opposed to only a single time inside the thread, then noticing that it is still loaded, and not re-loading it.

Related

Is it possible to share module between runspaces or assign a module to a runspace pool in Powershell?

I'm writing a multithreaded script which was leveraging the ability to multithread with runspaces and saving time until I had to use the Exchange module. It takes so long to load the Exchange module, it's almost as slow or slower than synchronous processing.
Below is the code I am working with, to repro you'll need to populate the servers array and the Exchange uri. At the bottom in quotes you can see the errors I'm receiving indicating the Exchange module is not being loaded and the Exchange cmdlets are not accessible.
Is there a way to copy the module to each new runspace and only initialize it one time or another way that I can significantly save time and reap the benefits of multiple runspaces with Exchange Powershell?
Get-Module | Remove-Module;
#Region Initial sessionstate
$servers = #("XXXXX", "XXXXX", "XXXXX");
$uri = 'https://XXXXX/powershell?ExchClientVer=15.1';
$session = new-pssession -ConfigurationName Microsoft.Exchange -ConnectionUri $uri -Authentication Negotiate;
$sessionImported = Import-PSSession $session -AllowClobber;
$sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::Create($sessionImported);
$modules = Get-Module ;
foreach ($module in $modules)
{
if ($module.ExportedCommands.Keys -contains "get-mailbox")
{
$exchangeModule = $module;
}
}
$initialSessionState = [initialsessionstate]::CreateDefault()
$initialSessionState.ImportPSModule($exchangeModule)
#EndRegion Initial sessionstate
$RunspacePool = [runspacefactory]::CreateRunspacePool($initialSessionState);
$RunspacePool.Open();
foreach ($server in $servers)
{
$threads = New-Object System.Collections.ArrayList;
$PowerShell = [powershell]::Create($sessionState);
$PowerShell.RunspacePool = $RunspacePool;
[void]$PowerShell.AddScript({
Param ($server)
[pscustomobject]#{
server = $server
} | Out-Null
Set-ADServerSettings -ViewEntireForest:$true;
$mbs = Get-MailboxDatabase -Server $server -Status;
return $mbs;
}) # end of add script
$PowerShell.AddParameter('server', $server) | Out-Null;
$returnVal = $PowerShell.BeginInvoke();
$temp = "" | Select PowerShell,returnVal;
$temp.PowerShell = $PowerShell;
$temp.returnVal = $returnVal;
$threads.Add($Temp) | Out-Null;
} #foreach server
$completed = $false;
$threadsCompleted = New-Object System.Collections.ArrayList;
while ($completed -eq $false)
{
$completed = $true;
$threadsCompleted.Clear();
foreach ($thread in $threads)
{
$endInvoke = $thread.PowerShell.EndInvoke($thread.returnVal);
#$endInvoke;
$threadHandle = $thread.returnVal.AsyncWaitHandle.Handle;
$threadIsCompleted = $thread.returnVal.IsCompleted;
if ($threadIsCompleted -eq $false)
{
$completed = $false;
}
else
{
$threadsCompleted.Add($threadHandle) | Out-Null;
}
}
Write-Host "Threads completed count: " $threadsCompleted.Count;
foreach ($threadCompleted in $threadsCompleted)
{
Write-Host "Completed thread handle -" $threadCompleted;
}
#Write-Host "";
sleep -Milliseconds 1000;
} # while end
Write-Host "Seconds elapsed:" $stopwatch.Elapsed.TotalSeconds;
Write-Host "";
$temp.PowerShell.Streams.Error;
return;
Set-ADServerSettings : The term 'Set-ADServerSettings' is not
recognized as the name of a cmdlet, function, script file, or operable
program. Check the spelling of the name, or if a path was included,
verify that the path is correct and try again. At line:9 char:3
Set-ADServerSettings -ViewEntireForest:$true;
~~~~~~~~~~~~~~~~~~~~
CategoryInfo : ObjectNotFound: (Set-ADServerSettings:String) [], CommandNotFoundException
FullyQualifiedErrorId : CommandNotFoundException Get-MailboxDatabase : The term 'Get-MailboxDatabase' is not recognized
as the name of a cmdlet, function, script file, or operable program.
Check the spelling of the name, or if a path was included, verify that
the path is correct and try again. At line:10 char:10
$mbs = Get-MailboxDatabase -Server $server -Status;
~~~~~~~~~~~~~~~~~~~
CategoryInfo : ObjectNotFound: (Get-MailboxDatabase:String) [], CommandNotFoundException
FullyQualifiedErrorId : CommandNotFoundException
So the answer is yes, I found out that you can use a property from initialsessionstate called ImportPSModule (which can accept a known module name from a static module or a file path to a dynamic or custom module), the tricky thing with Exchange is that that the module is created dynamically. There is hope though, if you capture the module the way I do below, there is a property to it called "Path" which gives you a local file path to where the module is temporarily stored on the machine after it is created the first time during that particular session.
I also discovered that you can specify a list of commands you want to import (commented out below) which saves an enormous amount of time with importing the Exchange module if you only need a few commands. Literally the progress bar for the Exchange command import only flashes when you filter on only a few commands and if you were to blink you'd miss it. If you use the specific command code make sure to import the get-mailbox command as that's how I filter on which module is for Exchange on prem, and configure the uri to your environment.
The benchmark for the single threaded version of this was 106 seconds, the fastest I've seen my code process with runspaces and multithreading is 9.8 seconds, so the potential for processing time savings is significant.
$WarningPreference = "SilentlyContinue";
Get-Module | Remove-Module;
#Region Initial sessionstate
$uri = 'https://xxxxx/powershell?ExchClientVer=15.1';
$session = new-pssession -ConfigurationName Microsoft.Exchange -ConnectionUri $uri -Authentication Negotiate;
Import-PSSession $session -AllowClobber > $null;
#Import-PSSession $session -AllowClobber -CommandName Get-Mailbox, Get-MailboxDatabaseCopyStatus, Get-MailboxDatabase, Get-DatabaseAvailabilityGroup > $null;
$modules = Get-Module;
foreach ($module in $modules)
{
if ($module.ExportedCommands.Keys -contains "get-mailbox")
{
$exchangeModule = $module;
}
}
$maxThreads = 17;
$iss = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
[void]$iss.ImportPSModule($exchangeModule.Path)
$runspacePool = [runspacefactory]::CreateRunspacePool(1, $maxThreads, $iss, $host)
$runspacePool.Open()
#Region Get dag members
$PowerShell = [powershell]::Create($iss); # may not need a runspace here
$PowerShell.RunspacePool = $RunspacePool;
$PowerShell.AddCommand("get-databaseavailabilitygroup") | Out-Null;
$PowerShell.AddParameter("identity", $DagName) | Out-Null;
$returnVal = $PowerShell.Invoke(); # writes to screen
$servers = New-Object System.Collections.ArrayList;
$serversUnsorted = $returnVal.Servers | sort;
foreach ($server in $serversUnsorted)
{
$servers.Add($server) | Out-Null;
}
#EndRegion Get dag members
#EndRegion Initial sessionstate
foreach ($server in $servers)
{
$PowerShell = [powershell]::Create($iss);
$PowerShell.RunspacePool = $RunspacePool;
[void]$PowerShell.AddScript({
Param ($server)
[pscustomobject]#{
server = $server
} | Out-Null
Set-ADServerSettings -ViewEntireForest:$true;
$copyStatus = Get-MailboxDatabaseCopyStatus -Server $server;
return $copyStatus;
}) # end of add script
$PowerShell.AddParameter('server', $server) | Out-Null;
$returnVal = $PowerShell.BeginInvoke();
$temp = "" | Select PowerShell,returnVal;
$temp.PowerShell = $PowerShell;
$temp.returnVal = $returnVal;
$threads.Add($Temp) | Out-Null;
} #foreach server

How to output hash table query result into Out-GridView?

I wish to export a hashtable result into Out-GridView using the Powershell.
The purpose of the below script is to export the Azure VM tags to Out-GridView, it throws error like the below blank result:
Error on the console:
Out-GridView : Syntax error in PropertyPath 'Syntax error in Binding.Path '[ Product] ' ... '(Tag)'.'.
At line:46 char:19
+ $Output | Out-GridView #Export-Csv -Path c:\temp\1a.csv -appe ...
+ ~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [Out-GridView], InvalidOperationException
+ FullyQualifiedErrorId : ManagementListInvocationException,Microsoft.PowerShell.Commands.OutGridViewCommand
This is the actual script which was executed under the Global Administrator role:
<#
.AUTHOR: https://stackoverflow.com/users/13390556/lukasz-g
#>
$Subscription = Get-AzSubscription | Out-GridView -Title 'Select subscription' -OutputMode 'Multiple'
# Initialise output array
$Output = #()
if ($Subscription) {
foreach ($item in $Subscription) {
$item | Select-AzSubscription
# Collect all the resources or resource groups (comment one of below)
$Resource = Get-AzResource
#$Resource = Get-AzResourceGroup
# Obtain a unique list of tags for these groups collectively
$UniqueTags = $Resource.Tags.GetEnumerator().Keys | Get-Unique -AsString | Sort-Object | Select-Object -Unique | Where-Object { $_ -notlike "hidden-*" }
# Loop through the resource groups
foreach ($ResourceGroup in $Resource) {
# Create a new ordered hashtable and add the normal properties first.
$RGHashtable = New-Object System.Collections.Specialized.OrderedDictionary
$RGHashtable.Add("Name", $ResourceGroup.ResourceGroupName)
$RGHashtable.Add("Location", $ResourceGroup.Location)
$RGHashtable.Add("Id", $ResourceGroup.ResourceId)
$RGHashtable.Add("ResourceType", $ResourceGroup.ResourceType)
# Loop through possible tags adding the property if there is one, adding it with a hyphen as it's value if it doesn't.
if ($ResourceGroup.Tags.Count -ne 0) {
$UniqueTags | Foreach-Object {
if ($ResourceGroup.Tags[$_]) {
$RGHashtable.Add("[$_] (Tag)", $ResourceGroup.Tags[$_])
}
else {
$RGHashtable.Add("[$_] (Tag)", "-")
}
}
}
else {
$UniqueTags | Foreach-Object { $RGHashtable.Add("[$_] (Tag)", "-") }
}
# Update the output array, adding the ordered hashtable we have created for the ResourceGroup details.
$Output += New-Object psobject -Property $RGHashtable
}
# Sent the final output to CSV
$Output | Out-GridView #Export-Csv -Path c:\temp\1a.csv -append -NoClobber -NoTypeInformation -Encoding UTF8 -Force
}
}
$RGHashtable.Add("[$_] (Tag)"
In above code, You are trying to add something like below :
In the output
Removed everthing and I tested with simple statements
$Output = #()
$RGHashtable = New-Object System.Collections.Specialized.OrderedDictionary
$RGHashtable.Add("[Testing] (Name)", "Temporary")
$Output += New-Object psobject -Property $RGHashtable
$Output | Out-GridView
I was provided with the same error.
After couple of testing, understood the error only occurs when there is a combination "[SomeString](SomeString)" --- [...](....) in the string.
The Out-GridView is trying to parse the "[<SomeString>](<SomeString>)" and hence the error.
You could try any 1 of the below combination in your code :
$RGHashtable.Add("[$_] [Tag]", $ResourceGroup.Tags[$_])
OR
$RGHashtable.Add("{$_} (Tag)", $ResourceGroup.Tags[$_])
OR
$RGHashtable.Add("[$_] [Tag]", $ResourceGroup.Tags[$_])
This should resolve your issue.
you will have change in 3 instances in your code if I am not wrong.

PoshRsJob Performance Issue

Why is using Multithreading in PowerShell so unbelievable slow. Am I doing anything wrong? I am using the PoshRsJob Module.
RSJobs:
(Measure-Command {
$output = Start-RSJob -InputObject $shortDump -ScriptBlock {
Param($out, $shortDump)
$retObj = [pscustomobject]#{
UserMail = $_.Mail
Type = $_.Type
}
# return $retObj
$retObj
} | Wait-RSJob
$out.Add( $( Get-RSJob | Receive-RSJob) )
# $out += $( Get-RSJob | Receive-RSJob )
}).TotalSeconds
and
Standard foreach:
(Measure-Command {
foreach ($obj in $shortDump) {
$retObj = [pscustomobject]#{
UserMail =$obj.Mail
Type = $obj.Type
}
# $out+= $retObj
$out.Add($retObj)
}
}).TotalSeconds
My goal is to build objects faster, because i have ~ 300.000 objects to build.
edit: Here is a another example. It's totally slow!
fast
$out = New-Object System.Collections.ArrayList
"default"
(Measure-Command {
for ($x = 0; $x -lt 100000; $x++)
{
$retObj = [pscustomobject]#{
UserMail = 'test'
Type = 'test2'
Test = 'default'
}
$out.Add($retObj)
}
}).TotalSeconds
$out2 = $out
horribly slow
$out = New-Object System.Collections.ArrayList
$Test = `"RSJobs"`
"RSJobs"
$ScriptBlock = {
[pscustomobject]#{
UserMail = 'test'
Type = 'test2'
Test = $Using:Test
}
}
(Measure-Command {
1..100000 | Start-RSJob -Name {$_} -ScriptBlock $ScriptBlock
$out.Add( $( Get-RSJob | Receive-RSJob) )
}).TotalSeconds
Creating a new runspace has overhead. So with many small jobs then you are adding the overhead every single time.
(measure-command {[pscustomobject]#{'a'='b'}}).totalmilliseconds
0.1773
{start-rsjob -scriptblock {[pscustomobject]#{'a'='b'}}}).totalmilliseconds
93.0173
Then you are adding even more overhead retrieving all of the returned data from the individual jobs into one object, which was basically your goal in the first place.
Basically, build 1 object from 100,000 objects vs create a runspace 100,000 times each creating 1 object then return all of these objects to build 1 object from 100,000 objects.
I don't see how you are going to get any gain in efficiency using runspaces in this application. If there was an expensive calculation to determine each object, and then you made just a few runspaces and ran a subset of your array in each, maybe.

Create a blank object for Test-Connection

I recently answered a SO post about Test-Connection Powershell script: create loop for ResponseTime
When a Test-Connection cannot connect it will return a System.Net.NetworkInformation.PingException which is fine but I would like to record that as an empty object in output instead of skipping over it. I am aware that I could just select the properties I want and just create a custom object to output on the command line. That is how I approached the linked question but I feel I could do better.
My desire is to have output like this
Source Destination IPV4Address IPV6Address Bytes Time(ms)
------ ----------- ----------- ----------- ----- --------
WYVERN localhost 127.0.0.1 ::1 32 0
failed host 169.254.158.1
WYVERN localhost 127.0.0.1 ::1 32 0
The two returns are proper from Test-Connection with a dummy line inserted. It has all the properties of a proper return from Test-Connection but, since it failed, only some of the properties have values. The only approach that I tried to accomplish this was to create another object of a similar type. Note (Test-Connection -Count 1 localhost).GetType().FullName returned System.Management.ManagementObject
$servers = "10.50.10.100","169.254.54.1"
$servers | ForEach-Object{
Test-Connection $_ -Count 1 -ErrorAction SilentlyContinue
If(!$testconnection){
$blank = New-Object -TypeName System.Management.ManagementObject
$blank.Destination = $_
}
}
Test-Connection returns more than just a basic System.Management.ManagementObject. So the problem is that a new-object will not have the same properties and, as a result, $blank.Destination = $_ will fail since "'Destination' cannot be found on this object". I also experimented with Test-Connection -Count 1 127.0.0.1 | gm -MemberType Property to try and create a property collection that I could use to build my blank object but that was not bearing an fruit. Most likely since I am not doing it right.
FYI
I am hoping to apply this logic in other places in my scripts. While test-connection is the cmdlet I am dwelling on in this question I am hunting for a broader solution.
Attempt
I have tried, unsuccessfully, something like this but the object are not being outputted together.
$props = #{}
Test-Connection -Count 1 127.0.0.1 | gm -MemberType Property | %{$props.($_.Name) = ""}
$props.destination = "FailedHostAddress"
New-Object -TypeName PSCustomObject -Property $props
Not sure if this helps or not:
$base = Test-Connection 127.0.0.1 -Count 1
$blank = $base | select *
foreach ($prop in $base.psobject.Properties.Name)
{$blank.$prop = $null}
The select * will keep all of the properties, but convert them to note properties so they will be writeable, and you can set they to whatever you want them to be.
I would probably do something like this:
$servers = '10.50.10.100', '169.254.54.1', 'somehost'
$servers | % {
$dst = $_
try {
Test-Connection $dst -Count 1 -ErrorAction Stop | % {
$props = [ordered]#{
'Source' = $env:COMPUTERNAME
'Destination' = $dst
'IPv4Address' = $_.IPV4Address
'IPv6Address' = $_.IPV6Address
'Available' = $true
}
}
} catch {
try {
$addr = [ipaddress]$dst
$props = [ordered]#{
'Source' = $env:COMPUTERNAME
'Destination' = $dst
'IPv4Address' = $addr.MapToIPv4()
'IPv6Address' = $addr.MapToIPv6()
'Available' = $false
}
} catch {
$props = [ordered]#{
'Source' = $env:COMPUTERNAME
'Destination' = $dst
'IPv4Address' = $null
'IPv6Address' = $null
'Available' = $false
}
}
}
New-Object -Type PSObject -Property $props
}
The [ordered] hashes make the properties appear in the given order.
With PowerShell v3 or newer it can be simplified to this:
$servers | % {
$dst = $_
try {
Test-Connection $dst -Count 1 -ErrorAction Stop | % {
[PSCustomObject]#{
'Source' = $env:COMPUTERNAME
'Destination' = $dst
'IPv4Address' = $_.IPV4Address
'IPv6Address' = $_.IPV6Address
'Available' = $true
}
}
} catch {
try {
$addr = [ipaddress]$dst
[PSCustomObject]#{
'Source' = $env:COMPUTERNAME
'Destination' = $dst
'IPv4Address' = $addr.MapToIPv4()
'IPv6Address' = $addr.MapToIPv6()
'Available' = $false
}
} catch {
[PSCustomObject]#{
'Source' = $env:COMPUTERNAME
'Destination' = $dst
'IPv4Address' = $null
'IPv6Address' = $null
'Available' = $false
}
}
}
}
The output will look somewhat like this:
Source Destination IPv4Address IPv6Address Available
------ ----------- ----------- ----------- ---------
WYVERN 10.50.10.100 10.50.10.100 fe80::3a8f:4854:248d:787f%11 True
WYVERN 169.254.54.1 169.254.54.1 ::ffff:169.254.54.1 False
WYVERN somehost False

Utilize Results from Synchronized Hashtable (Runspacepool 6000+ clients)

Adapting a script to do multiple functions, starting with test-connection to gather data, will be hitting 6000+ machines so I am using RunspacePools adapted from the below site;
http://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/
The data comes out as below, I would like to get it sorted into an array (I think that's the terminology), so I can sort the data via results. This will be adapted to multiple other functions pulling anything from Serial Numbers to IAVM data.
Is there any way I can use the comma delimited data and have it spit the Values below into columns? IE
Name IPAddress ResponseTime Subnet
x qwe qweeqwe qweqwe
The added values aren't so important at the moment, just the ability to add the values and pull them.
Name Value
—- —–
x-410ZWG \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410ZWG",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-47045Q \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-47045Q",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-440J26 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-440J26",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-410Y45 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410Y45",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DJKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DJKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
nonexistant
x-DDMVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DDMVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-470481 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-470481",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DHKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DHKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-430XXF \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-430XXF",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DLKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DLKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-410S86 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410S86",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-SCH004 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-SCH004",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-431KMS
x-440J22 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-440J22",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
Thank for any help!
Code currently
Function Get-RunspaceData {
[cmdletbinding()]
param(
[switch]$Wait
)
Do {
$more = $false
Foreach($runspace in $runspaces) {
If ($runspace.Runspace.isCompleted) {
$runspace.powershell.EndInvoke($runspace.Runspace)
$runspace.powershell.dispose()
$runspace.Runspace = $null
$runspace.powershell = $null
} ElseIf ($runspace.Runspace -ne $null) {
$more = $true
}
}
If ($more -AND $PSBoundParameters['Wait']) {
Start-Sleep -Milliseconds 100
}
#Clean out unused runspace jobs
$temphash = $runspaces.clone()
$temphash | Where {
$_.runspace -eq $Null
} | ForEach {
Write-Verbose ("Removing {0}" -f $_.computer)
$Runspaces.remove($_)
}
Write-Host ("Remaining Runspace Jobs: {0}" -f ((#($runspaces | Where {$_.Runspace -ne $Null}).Count)))
} while ($more -AND $PSBoundParameters['Wait'])
}
#Begin
#What each runspace will do
$ScriptBlock = {
Param ($computer,$hash)
$Ping = test-connection $computer -count 1 -ea 0
$hash[$Computer]= $Ping
}
#Setup the runspace
$Script:runspaces = New-Object System.Collections.ArrayList
# Data table for all of the runspaces
$hash = [hashtable]::Synchronized(#{})
$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$runspacepool = [runspacefactory]::CreateRunspacePool(1, 100, $sessionstate, $Host)
$runspacepool.Open()
#Process
ForEach ($Computer in $Computername) {
#Create the powershell instance and supply the scriptblock with the other parameters
$powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($computer).AddArgument($hash)
#Add the runspace into the powershell instance
$powershell.RunspacePool = $runspacepool
#Create a temporary collection for each runspace
$temp = "" | Select-Object PowerShell,Runspace,Computer
$Temp.Computer = $Computer
$temp.PowerShell = $powershell
#Save the handle output when calling BeginInvoke() that will be used later to end the runspace
$temp.Runspace = $powershell.BeginInvoke()
Write-Verbose ("Adding {0} collection" -f $temp.Computer)
$runspaces.Add($temp) | Out-Null
}
# Wait for all runspaces to finish
#End
Get-RunspaceData -Wait
$stoptimer = Get-Date
#Display info, and display in GridView
Write-Host
Write-Host "Availability check complete!" -ForegroundColor Cyan
"Execution Time: {0} Minutes" -f [math]::round(($stoptimer – $starttimer).TotalMinutes , 2)
$hash | ogv
When you use runspaces, you write the scriptblock for the runspace pretty much the same way you would for a function. You write whatever you want the return to be to the pipeline, and then either assign it to a variable, pipe it to another cmdlet or function, or just let it output to the console. The difference is that while the function returns it's results automatically, with the runspace they collect in the runspace output buffer and aren't returned until you do the .EndInvoke() on the runspace handle.
As a general rule, the objective of a Powershell script is (or should be) to create objects, and the objective of using the runspaces is to speed up the process by multi-threading. You could return string data from the runspaces back to the main script and then use that to create objects there, but that's going to be a single threaded process. Do your object creation in the runspace, so that it's also multi-threaded.
Here's a sample script that uses a runspace pool to do a pingsweep of a class C subnet:
Param (
[int]$timeout = 200
)
$scriptPath = (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent)
While (
($network -notmatch "\d{1,3}\.\d{1,3}\.\d{1,3}\.0") -and -not
($network -as [ipaddress])
)
{ $network = read-host 'Enter network to scan (ex. 10.106.31.0)' }
$scriptblock =
{
Param (
[string]$network,
[int]$LastOctet,
[int]$timeout
)
$options = new-object system.net.networkinformation.pingoptions
$options.TTL = 128
$options.DontFragment = $false
$buffer=([system.text.encoding]::ASCII).getbytes('a'*32)
$Address = $($network.trim("0")) + $LastOctet
$ping = new-object system.net.networkinformation.ping
$reply = $ping.Send($Address,$timeout,$buffer,$options)
Try { $hostname = ([System.Net.Dns]::GetHostEntry($Address)).hostname }
Catch { $hostname = 'No RDNS' }
if ( $reply.status -eq 'Success' )
{ $ping_result = 'Yes' }
else { $ping_result = 'No' }
[PSCustomObject]#{
Address = $Address
Ping = $ping_result
DNS = $hostname
}
}
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(100,100)
$RunspacePool.Open()
$Jobs =
foreach ( $LastOctet in 1..254 )
{
$Job = [powershell]::Create().
AddScript($ScriptBlock).
AddArgument($Network).
AddArgument($LastOctet).
AddArgument($Timeout)
$Job.RunspacePool = $RunspacePool
[PSCustomObject]#{
Pipe = $Job
Result = $Job.BeginInvoke()
}
}
Write-Host 'Working..' -NoNewline
Do {
Write-Host '.' -NoNewline
Start-Sleep -Seconds 1
} While ( $Jobs.Result.IsCompleted -contains $false)
Write-Host ' Done! Writing output file.'
Write-host "Output file is $scriptPath\$network.Ping.csv"
$(ForEach ($Job in $Jobs)
{ $Job.Pipe.EndInvoke($Job.Result) }) |
Export-Csv $scriptPath\$network.ping.csv -NoTypeInformation
$RunspacePool.Close()
$RunspacePool.Dispose()
The runspace script does a ping on each address, and if it gets successful ping attempts to resolve the host name from DNS. Then it builds a custom object from that data, which is output to the pipeline. At the end, those objects are returned when the .EndInvoke() is done on the runspace jobs and piped directly into Export-CSV, but it could just as easily be output to the console, or saved into a variable.

Resources