2

How would I speed up the processing of my PowerShell script?

It pulls out information from a apps.csv file (containing 2 apps - a Windows XP and a Windows 7 version) and information from a hosts.csv file.

The app locations are tested across different hostnames (I can tell whether they're Windows XP/Windows 7 machines), to see if they are installed by checking for fingerprints.

At the moment it takes about 5-10 minutes to run 40+ records, really need to speed it up.

clear all

$Applications = @{}
$ComputerObjects = @()

# Gets OS
function Get-OS ([string]$hostname) {
    $os = "offline"

    gwmi win32_operatingsystem -computername $hostname -ea silentlycontinue | ForEach-Object {
        if($_.Version.ToString().StartsWith("6.1")) {
            $os = "Windows 7"
        } elseif ($_.Version.ToString().StartsWith("5.1")){
            $os = "Windows XP"
        } else {
            $os = "N/A"
        }
    } > $null

    return $os
}

# Sorts out the application information, breaking it into OS, LOB and then finally Location
Import-CSV C:\RDC\apps.csv | ForEach-Object {
    $appname = $_.appname.ToLower()
    $os = $_.os.ToLower()
    $lob = $_.lob.ToLower()
    $location = $_.location.ToLower()

    if ($Applications.Keys -notcontains $appname) {
        $WindowsOS=@{}

        # hashtable for windows xp and windows 7 applications
        $WindowsOS["windows xp"]=@{}
        $WindowsOS["windows 7"]=@{}
        $Applications[$appname]=$WindowsOS
    } 

    if ($Applications[$appname][$os].Keys -notcontains $lob) {
        $Applications[$appname][$os][$lob]=@()
    }

    if ($Applications[$appname][$os][$lob].Keys -notcontains $location) {
        $Applications[$appname][$os][$lob]+=$location
    }
}

# Sorts the Hostnames out and tests all Application Locations
Import-CSV C:\RDC\hosts.csv | ForEach-Object {
    $Properties = @{}
    $Properties["hostname"]=$_.hostname.ToLower()
    $Properties["lob"]=$_.type.ToLower()
    $Properties["os"]=Get-OS $Properties["hostname"]

    $Applications.Keys | ForEach-Object {
        $currAppName = $_
        $Properties[$currAppName]=$false;

        if ($Applications[$currAppName].Keys -contains $Properties["os"] -and $Applications[$currAppName][$Properties["os"]].Keys -contains $Properties["lob"]) {
            $Applications[$currAppName][$Properties["os"]][$Properties["lob"]] | ForEach-Object {
                $Properties[$currAppName]=$Properties[$currAppName] -or (Test-Path "\\$($Properties["hostname"])\$($_)")
            }
        }
    }

    $HostObject = New-Object PSObject -Property $Properties
    $ComputerObjects += $HostObject
}

$ComputerObjects | ft

$a = [int][double]::Parse((Get-Date -UFormat %s))

#$ComputerObjects | Export-csv "C:\Working\results_$a.csv" -NoTypeInformation
3
  • 1
    you should really take a look at the *-job cmdlets (technet.microsoft.com/en-us/library/hh847783(v=wps.620).aspx) Commented Jan 31, 2014 at 6:18
  • The first thing I would do is create a variable out of the CSV files you are importing, instead of directly importing them into a foreach loop. Commented Jan 31, 2014 at 6:57
  • Thanks guys - any suggestions on how to improve my code would really help Commented Jan 31, 2014 at 7:32

5 Answers 5

3

here is an example of using jobs :

param( $computername=$fullnameRODC)
Invoke-Command -ComputerName $computername -AsJob -JobName checkOS -ScriptBlock {
    $o=@{}
    $os = "offline"

    gwmi win32_operatingsystem  -ea silentlycontinue | ForEach-Object {
        if($_.Version.ToString().StartsWith("6.1")) {
            $os = "Windows 7"
        } elseif ($_.Version.ToString().StartsWith("5.1")){
            $os = "Windows XP"
        } else {
            $os = "N/A"
        }
    }
    $o["os"]=$os
    $r=new-Object -TypeName psObject -Property $o
    $r
    }

wait-Job checkOS
receive-Job checkOS
remove-job checkOS

using Measure-Command -Expression {c:\temp\cheskOS.ps1} this script is executed in about 10 sec.

Without the job

param( $computername=$fullnameRODC)


    gwmi win32_operatingsystem -computername $computername  -ea silentlycontinue | ForEach-Object {
        if($_.Version.ToString().StartsWith("6.1")) {
            $os = "Windows 7"
        } elseif ($_.Version.ToString().StartsWith("5.1")){
            $os = "Windows XP"
        } else {
            $os = "N/A"
        }
    }
    $os

it takes 4min, 24X slower !


Update for your script. I think the simpliest thing is to use job to check os and store the results to an array. Then replace your get-os function . That is :

param( $computername=@("computer1","computer2"))
$results=@()
Invoke-Command -ComputerName $computername -AsJob -JobName checkOS -ScriptBlock {
    $o=@{}
    $os = "offline"
    gwmi win32_operatingsystem  -ea silentlycontinue | ForEach-Object {
        if($_.Version.ToString().StartsWith("6.1")) {
            $os = "Windows 7"
        } elseif ($_.Version.ToString().StartsWith("5.1")){
            $os = "Windows XP"
        } else {
            $os = "N/A"
        }
    }
    $o["os"]=$os
    $r=new-Object -TypeName psObject -Property $o
    $r
    }

wait-Job checkOS
$results+=receive-Job checkOS
remove-job checkOS

function Get-OS ([string]$computername) {
$script:results | ?{$_.pscomputername -eq $computername} | select os
}

then you can do

get-os "computer2"
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for this. How can I incorporate this into my code as a whole? What I mean is - what would the updated code look like with jobs?
Thanks for the reply. I still want to pull the computernames from a hosts.csv file and the apps from the apps.csv file - this doesn't seem to be happening anywhere in your code. Can you please include these details?
I will not copy-paste, I've gave you all the elements. All you have to do is paste my code on top of your script and remove your get-os function
3

When your script takes several minutes to process 40 rows, there something more going on than re-reading a file every loop iteration.

From what I can gather you're only making one network call by invoking

gwmi win32_operatingsystem -computername $hostname -ea silentlycontinue

I suspect that this command is taking several seconds to complete, and is making your script slow. You can test this by seeing how long it takes to only invoke this command for all known hostnames, but I think it will be pretty much the same as the running time of your entire script.

I wouldn't know of a faster way to determine the OS version, but I can imagine that this information doesn't change every day, so maybe you could cache this information and have it refreshed on a per-day basis?

-- Edit: As David Martin pointed out, a better option might be to try to make these calls parallel. And apparently, Powershell has support for that through the ForEach -Parallel command.

2 Comments

Agee that this is where the time is being spent, if you can run this in parallel using a job you should see a good improvement.
@DavidMartin: Nice one, hadn't thought of that yet.
1

I looked over the your code and here's what I deduced:


Section One

You have one function with conditional logic

I see nothing wrong with this. It's simple and standard.

function Get-OS ([string]$hostname) {
    $os = "offline"

    gwmi win32_operatingsystem -computername $hostname -ea silentlycontinue | ForEach-Object {
        if($_.Version.ToString().StartsWith("6.1")) {
            $os = "Windows 7"
        } elseif ($_.Version.ToString().StartsWith("5.1")){
            $os = "Windows XP"
        } else {
            $os = "N/A"
        }
    } > $null

    return $os
}

Section Two

You have two foreach loops which read a CSV file from disk and convert the CSV file into an object[] array while looping through each object in that array.

The first foreach loop contains conditional logic. I think this is fine but it could be faster if you imported the CSV into memory before passing it to the pipeline/foreach loop.

Import-CSV C:\RDC\apps.csv | ForEach-Object {
    $appname = $_.appname.ToLower()
    $os = $_.os.ToLower()
    $lob = $_.lob.ToLower()
    $location = $_.location.ToLower()

    if ($Applications.Keys -notcontains $appname) {
        $WindowsOS=@{}

        # hashtable for windows xp and windows 7 applications
        $WindowsOS["windows xp"]=@{}
        $WindowsOS["windows 7"]=@{}
        $Applications[$appname]=$WindowsOS
    } 

    if ($Applications[$appname][$os].Keys -notcontains $lob) {
        $Applications[$appname][$os][$lob]=@()
    }

    if ($Applications[$appname][$os][$lob].Keys -notcontains $location) {
        $Applications[$appname][$os][$lob]+=$location
    }
}

Section Three

The second foreach loop contains:

  • another foreach loop
    • which contains conditional logic
      • which contains another foreach loop

Once again, you should read the CSV into memory(variable) before passing it to the loop.

The second thing I think you need to do here is either reduce the nesting by writing different code to do the same job, or at the very least separate the inside foreach loop from the if statement somehow.

Import-CSV C:\RDC\hosts.csv | ForEach-Object {
    $Properties = @{}
    $Properties["hostname"]=$_.hostname.ToLower()
    $Properties["lob"]=$_.type.ToLower()
    $Properties["os"]=Get-OS $Properties["hostname"]

    $Applications.Keys | ForEach-Object {
        $currAppName = $_
        $Properties[$currAppName]=$false;

        if ($Applications[$currAppName].Keys -contains $Properties["os"] -and $Applications[$currAppName][$Properties["os"]].Keys -contains $Properties["lob"]) {
            $Applications[$currAppName][$Properties["os"]][$Properties["lob"]] | ForEach-Object {
                $Properties[$currAppName]=$Properties[$currAppName] -or (Test-Path "\\$($Properties["hostname"])\$($_)")
            }
        }
    }

    $HostObject = New-Object PSObject -Property $Properties
    $ComputerObjects += $HostObject
}

Final Comments

Those are the main parts of your script that are going to take the most time.

You can also try wrapping each section in Measure-Command {} to see which section is taking up all/most of the time.

Comments

1

You can speed it up by multi-threading the Get-WMIObject (but don't try to do it with background jobs). Get-WMIObject will multi-thread itself if you pass it multiple computer names. It will take some re-factoring but you should collect all the host names first, then pass that collection to Get-WMIObject all at once. Sort the results into a hash table of hostname/os and then foreach through your csv, going back to the hash table to get the os for each host.

3 Comments

Hi mjolinor, could you develop your response please.I've tested using jobs it's 24x faster than passing an array of computername to gwmi ...
You're using remote jobs, which offloads and distributes the load to the target computers. That requires remoting to be enabled and configured on every remote system. Background jobs all run on the local system. Using remote jobs will speed it up, but spinning up a remote PS session on the target computer just to get it's OS version seems a little heavy-handed from a resource management perspective, IMHO.
Tank you. I see what you mean. In this case he just wanted the OS but maybe next time he will want to get much more info .... cheers
0

You can also speed up your script by not using foreach. this will invoke the whole object before you run the loop. using For($i=0; $i -lt $csv.count; $i){Commands} if you are only looking for certain data you should but it in a function and return the value inside the loop instead of waiting for it to finish.

If you you are working with a sorted csv file. you can jump down to a certain value of your csv by checking the input and setting $i to a certain line.

Im currently working on a project myself where i know Desktops shows up after line 15000. I then tell the loop $i = 15000; $i -lt $csv.count;$i++ this is basicly useless when working with small files, but with bigger files like in my case this is actully usefull.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.