Trace-Script.ps1
$script:totals = [System.Collections.Queue]::new() function Trace-Script { [CmdletBinding()] param( [Parameter(Mandatory)] [ScriptBlock] $ScriptBlock, [string[]] $FilterPath, [uint32] $Preheat = 0, [Switch] $DisableWarning, [Hashtable] $Feature, [Switch] $Before, [Switch] $After, [Switch] $UseNativePowerShell7Profiler ) if ($Before -and $After) { Write-Warning "You cannot use -Before and -After together, using -After." } if ($After) { $Before = $false } $invokedAs = $MyInvocation.Line $trace = Trace-ScriptInternal -ScriptBlock $ScriptBlock -Preheat $Preheat -DisableWarning:$DisableWarning -Feature $Feature -UseNativePowerShell7Profiler:$UseNativePowerShell7Profiler -Before:$Before Write-Host -ForegroundColor Magenta "Processing $($trace.Count) trace events. $(if (1000000 -lt $trace.Count) { " This might take a while..."})" if ($null -ne $FilterPath -and 0 -lt @($FilterPath).Count) { $files = $Path | ForEach-Object { (Resolve-Path $_).Path } } else { $unique = [Collections.Generic.HashSet[string]]::new() $null = foreach ($f in $Trace) { $unique.Add($f.Path) } $files = foreach ($v in $unique.GetEnumerator()) { $v } } Write-Host -ForegroundColor Magenta "Loading sources for $($files.Count) files." $fileMap = @{} foreach ($file in $files) { if ($file -and -not $fileMap.ContainsKey($file)) { if (-not (Test-Path $file)) { Write-Host "$file no longer exists, skipping." continue } $name = [IO.Path]::GetFileName($file) $lines = Get-Content $file # each line in this file will gets its own object $lineProfiles = [Collections.Generic.List[object]]::new($lines.Length) $index = 0 foreach ($line in $lines) { $lineProfile = [PSCustomObject] @{ Percent = 0 HitCount = 0 Duration = [TimeSpan]::Zero Average = [TimeSpan]::Zero Name = $name Line = ++$index # start from 1 as in file Text = $line Path = $file Hits = [Collections.Generic.List[object]]::new() CommandHits = @{} } $lineProfiles.Add($lineProfile) } $fileMap.Add($file, $lineProfiles) } else { # skip the file, because we already processed it, this is an alternative to sorting the list # of files and getting unique, but that would be slower } } Write-Host -ForegroundColor Magenta "Sorting trace events to file lines." foreach ($hit in $trace) { if (-not $hit.Path -or -not ($fileMap.Contains($hit.Path))) { continue } # get the object that describes this particular file $lineProfiles = $fileMap[$hit.Path] $lineProfile = $lineProfiles[$hit.Line - 1] # array indexes from 0, but lines from 1 $lineProfile.Duration += $hit.Duration $lineProfile.HitCount++ $lineProfile.Hits.Add($hit) # add distinct entries per column when there are more commands # on the same line (like we did it with the Group-Object on foreach ($i in 1...1000)) if ($lineProfile.CommandHits.ContainsKey($hit.Column)) { $commandHit = $lineProfile.CommandHits[$hit.Column] $commandHit.Duration += $hit.Duration $commandHit.HitCount++ } else { $commandHit = [PSCustomObject] @{ Line = $hit.Line # start from 1 as in file Column = $hit.Column Duration = $hit.Duration HitCount = 1 Text = $hit.Text } $lineProfile.CommandHits.Add($hit.Column, $commandHit) } } $total = [TimeSpan]::FromTicks($trace[-1].Timestamp - $trace[0].Timestamp) Write-Host -ForegroundColor Magenta "Counting averages and percentages." # this is like SelectMany, it joins the arrays of arrays # into a single array $all = $fileMap.Values | Foreach-Object { $_ } | ForEach-Object { $_.Average = if ($_.HitCount -eq 0) { [TimeSpan]::Zero } else { [TimeSpan]::FromTicks($_.Duration.Ticks / $_.HitCount) } $_.Percent = [Math]::Round($_.Duration.Ticks / $total.Ticks, 5, [System.MidpointRounding]::AwayFromZero) * 100 $_ } Write-Host -ForegroundColor Magenta "Getting Top50 that take most percent of the run." $top50Percent = $all | Where-Object Percent -gt 0 | Sort-Object -Property Percent -Descending | Select-Object -First 50 Write-Host -ForegroundColor Magenta "Getting Top50 with the longest average duration." $top50Average = $all | Where-Object Average -gt 0 | Sort-Object -Property Average -Descending | Select-Object -First 50 Write-Host -ForegroundColor Magenta "Getting Top50 with the longest duration." $top50Duration = $all | Where-Object Duration -gt 0 | Sort-Object -Property Duration -Descending | Select-Object -First 50 Write-Host -ForegroundColor Magenta "Getting Top50 with the most hits." $top50HitCount = $all | Where-Object HitCount -gt 0 | Sort-Object -Property HitCount -Descending | Select-Object -First 50 $script:processedTrace = [PSCustomObject] @{ Top50 = $top50Percent Top50Average = $top50Average Top50Duration = $top50Duration Top50HitCount = $top50HitCount TotalDuration = $total All = $all Events = $trace Files = foreach ($pair in $fileMap.GetEnumerator()) { [PSCustomObject]@{ Path = $pair.Key Profile = $pair.Value } } } $script:processedTrace $variable = if ($invokedAs -match "(\$\S*)\s*=\s*Trace-Script") { "$($matches[1])" } if ($null -eq $variable) { Write-Host -ForegroundColor Yellow "Looks like you did not assign the output to a variable. Use Get-LatestTrace to retrieve the trace, e.g.: `$trace = Get-LatestTrace" } $previous = if (0 -lt $totals.Count) { $totals.ToArray()[-1] } $diff = if ($null -eq $previous) { 0 } else { [int] ($total.TotalMilliseconds - $previous.Total.TotalMilliseconds) } $color = if (0 -eq $diff) { "Yellow" } elseif (0 -gt $diff) { "Green" } else { "Red" } $totals.Enqueue(@{ Color = $color; Diff = $diff ; Total = $total; Before = $Before } ) while ($totals.Count -gt 5) { $null = $totals.Dequeue() } if (1 -lt $totals.Count) { Write-Host -ForegroundColor Magenta "Progress: " -NoNewline $i = 0 foreach ($t in $totals) { $last = $i -eq ($totals.Count - 1) Write-Host -ForegroundColor $t.Color "$(if ($t.Before) { "B:" } else {"A:" }) $($t.Total) ($($t.Diff) ms)$(if (-not $last) {" -> "} else { "`n" })" -NoNewline $i++ } } else { Write-Host -ForegroundColor Magenta "Duration: $(if ($Before) { "B:" } else {"A:" }) " -NoNewline Write-Host -ForegroundColor Yellow $total } Write-Host -ForegroundColor Magenta "Done. Try $(if ($variable) { "$($variable)" } else { '$yourVariable' }).Top50 | Format-Table to get the report. There are also Top50Average, Top50Duration, Top50HitCount, and Files." } function Get-LatestTrace { if ($script:processedTrace) { $script:processedTrace } else { Write-Warning "But there is no trace yet. This command is here to get the trace when you forgot to output it to variable. Run Trace-Script, ideally $trace = Trace-Script -ScriptBlock { & yourfile.ps1 }, and you won't need this cmdlet at all." } } |