简体   繁体   中英

Which part of this Powershell code snippet is making it take a long time to run?

I'm tasked with making a report of the last logon time for each user in our AD env, I obviously first asked mother google for something that I could repurpose but couldn't find anything that would check multiple Domain Controllers and reconcile the last one, and then spit out if it was past an arbitrarily set date/number of days.

Here's the code:

foreach ($user in $usernames) {
    $percentCmpUser = [math]::Truncate(($usernames.IndexOf($user)/$usernames.count)*100)
    Write-Progress -Id 3 -Activity "Finding Inactive Accounts" -Status "$($percentCmpUser)% Complete:" -PercentComplete $percentCmpUser
    $allLogons = $AllUsers | Where-Object {$_.SamAccountName -match $user}
    $finalLogon = $allLogons| Sort-Object LastLogon -Descending |Select-Object -First 1
    if ($finalLogon.LastLogon -lt $time.ToFileTime()) {
        $inactiveAccounts += $finalLogon
    } 
}

$usernames is a list of about 6000 usernames

$AllUsers is a list of 18000 users, it includes 10 different properties that I'd like to have access to in my final report. The way I got it was by hitting three of our 20 or so DC's for all users in specific OUs that I'm concerned with. The final script will actually be 6k*20 bec I do need to hit every DC to make sure I don't miss any user's logon.

Here's how $time is calculated:

$DaysInactive = 60
$todayDate = Get-Date
$time = ($todayDate).Adddays(-($DaysInactive))

Each variable is used elsewhere in the script, which is why I break it out like that.

Before you suggest LastLogonTimestamp , I was told it's not current enough and when I asked about changing the replication time to be more current I was told "no, not gonna happen".

Search-ADAccount also doesn't seem to offer an accurate view of inactive users.

I'm open to all suggestions about how to make this specific snippet run faster or on how to use a different methodology to achieve the same result in a fast time.

As of now hitting each DC for all users in specific OUs takes about 10-20sec per DC and then the above snippet takes 30-40 min.

Couple of things stand out, but likely the biggest performance killer here is these two statements:

$percentCmpUser = [math]::Truncate(($usernames.IndexOf($user)/$usernames.count)*100)
# and
$allLogons = $AllUsers | Where-Object {$_.SamAccountName -match $user}

... both of these statements will exhibit O(N^2) (or quadratic ) performance characteristics - that is, every time you double the input size, the time taken quadruples!


  1. Array.IndexOf() is effectively a loop

Let's look at the first one:

$percentCmpUser = [math]::Truncate(($usernames.IndexOf($user)/$usernames.count)*100)

It might not be self-evident, but this method-call: $usernames.IndexOf() might require iterating through the entire list of $usernames every time it executes - by the time you reach the last $user , it needs to go through and compare $user all 6000 items.

Two ways you can address this:

Use a regular for loop:

for($i = 0; $i -lt $usernames.Count; $i++) {
    $user = $usernames[$i]
    $percent = ($i / $usernames.Count) * 100
    # ...
}

Stop outputting progress altogether

Write-Progress is really slow - even if the caller suppresses Progress output (eg. $ProgressPreference = 'SilentlyContinue' ), using the progress stream still carries overhead, especially when called in every loop iteration.

Removing Write-Progress altogether would remove the requirement for calculating percentage :)

If you still need to output progress information you can shave off some overhead by only calling Write-Progress sometimes - for example once every 100 iterations:

for($i = 0; $i -lt $usernames.Count; $i++) {
    $user = $usernames[$i]
    if($i % 100 -eq 0){
        $percent = ($i / $usernames.Count) * 100
        Write-Progress -Id 3 -Activity "Finding Inactive Accounts" -PercentComplete $percent
    }
    # ...
}

  1. ... |Where-Object is also just a loop

Now for the second one:

$allLogons = $AllUsers | Where-Object {$_.SamAccountName -match $user}

... 6000 times, powershell has to enumerate all 18000 objects in $AllUsers and test them for the Where-Object filter.

Instead of using an array and Where-Object , consider loading all users into a hashtable:

# Only need to run this once, before the loop
$AllLogonsTable = @{}
$AllUsers |ForEach-Object {
    # Check if the hashtable already contains an item associated with the user name
    if(-not $AllLogonsTable.ContainsKey($_.SamAccountName)){
        # Before adding the first item, create an array we can add subsequent items to
        $AllLogonsTable[$_.SamAccountName] = @()
    }

    # Add the item to the array associated with the username
    $AllUsersTable[$_.SamAccountName] += $_
}

foreach($user in $users){
    # This will be _much faster_ than $AllUsers |Where-Object ...
    $allLogons = $AllLogonsTable[$user]
}

Hashtables have crazy-fast lookups - finding an object by key is much faster that using Where-Object on an array.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM