<# .SYNOPSIS Wertet ein PSProfiler-Ergebnis (Measure-Script, Typ ScriptLineMeasurement) aus. .DESCRIPTION Angepasst an die tatsaechliche PSProfiler-Ausgabe: LineNo [int] - Zeilennummer ExecutionTime [TimeSpan] - Gesamtzeit dieser Zeile (Summe aller Treffer) TimeLine.TimeSpans [List] - Einzelmessungen; deren Anzahl = HitCount Line [string] - Quellcode-Text der Zeile SourceScript [string] - Pfad der Quelldatei Top [bool] WICHTIG: Measure-Script instrumentiert NUR die uebergebene Datei. Zur Laufzeit dot-gesourcte Dateien (Utility/Base/Work/R125) erscheinen NICHT mit eigenen Zeilen - ihre Zeit wird der jeweiligen Aufrufzeile im Rahmenskript zugeschlagen. Fuer Hotspots INNERHALB von R125 muss R125 separat gemessen werden (siehe Measure-R125Direct.ps1). .PARAMETER XmlPath Pfad zur Ergebnis-XML (Export-Clixml). .PARAMETER Result Alternativ das Measure-Script-Ergebnisobjekt direkt. .PARAMETER Top Anzahl Top-Zeilen. Default 25. .PARAMETER FocusFile Optionales Dateinamen-Muster, auf das das Detail eingeschraenkt wird (z.B. '*Work-R125.ps1', wenn R125 direkt gemessen wurde). .EXAMPLE ./Analyze-ProfileResult.ps1 -XmlPath ./profile-R125-20260624-195753-run1.xml #> [CmdletBinding(DefaultParameterSetName = 'FromXml')] param( [Parameter(ParameterSetName = 'FromXml', Mandatory = $true, Position = 0)] [string]$XmlPath, [Parameter(ParameterSetName = 'FromObject', Mandatory = $true)] [object]$Result, [int]$Top = 25, [string]$FocusFile = $null, # Wird von den gemessenen Zeilennummern abgezogen, um auf die Original- # Zeilen der Zieldatei zurueckzurechnen (z.B. wenn ein Mess-Header vorangestellt # wurde). Wird automatisch aus der .meta.xml-Sidecar gelesen, falls vorhanden. [int]$LineOffset = 0 ) $ErrorActionPreference = "Stop" if ($PSCmdlet.ParameterSetName -eq 'FromXml') { if (-not (Test-Path $XmlPath)) { throw "XML nicht gefunden: $XmlPath" } $Result = Import-Clixml -Path $XmlPath # Offset automatisch aus Sidecar lesen, falls nicht manuell gesetzt. if ($LineOffset -eq 0) { $metaPath = $XmlPath -replace '\.xml$', '.meta.xml' if (Test-Path $metaPath) { try { $meta = Import-Clixml -Path $metaPath if ($meta.HeaderOffset) { $LineOffset = [int]$meta.HeaderOffset Write-Host ("Sidecar gefunden: Zeilen-Offset {0} (Quelle: {1})" -f $LineOffset, $meta.SourceFile) -ForegroundColor DarkGray } } catch { } } } } # Flache Liste der ScriptLineMeasurement-Objekte $rows = @($Result | Where-Object { $_.PSObject.Properties['LineNo'] }) if ($rows.Count -eq 0) { throw "Keine ScriptLineMeasurement-Objekte gefunden (Feld 'LineNo' fehlt)." } # --- Normalisierung -------------------------------------------------------- # ExecutionTime ist ein TimeSpan -> ms. HitCount = Anzahl Eintraege in # TimeLine.TimeSpans (Fallback: 1, wenn ExecutionTime > 0). function Get-HitCount { param($row) $tl = $row.PSObject.Properties['TimeLine'] if ($tl -and $tl.Value) { $ts = $tl.Value.PSObject.Properties['TimeSpans'] if ($ts -and $ts.Value) { try { return @($ts.Value).Count } catch { } } } if ($row.ExecutionTime -and $row.ExecutionTime.TotalMilliseconds -gt 0) { return 1 } return 0 } $norm = foreach ($row in $rows) { $ms = 0.0 if ($row.ExecutionTime -is [TimeSpan]) { $ms = $row.ExecutionTime.TotalMilliseconds } $rawLine = [int]$row.LineNo $origLine = $rawLine - $LineOffset [pscustomobject]@{ File = [string]$row.SourceScript Leaf = if ($row.SourceScript) { Split-Path $row.SourceScript -Leaf } else { '' } LineNo = $origLine # auf Original-R125 zurueckgerechnet RawLine = $rawLine # Zeile in der gemessenen (temporaeren) Datei IsHeader = ($origLine -le 0) Ms = [double]$ms Hits = [int](Get-HitCount $row) Code = ([string]$row.Line).Trim() Top = [bool]$row.Top } } # Header-Zeilen (Mess-Geruest) aus der Hotspot-Betrachtung nehmen, sofern ein # Offset aktiv ist - sie gehoeren nicht zum Funktionskoerper. if ($LineOffset -gt 0) { $norm = $norm | Where-Object { -not $_.IsHeader } } if ($FocusFile) { $norm = $norm | Where-Object { $_.Leaf -like $FocusFile -or $_.File -like $FocusFile } } $totalMs = ($norm | Measure-Object Ms -Sum).Sum function Fmt-Dur { param([double]$ms) if ($ms -ge 60000) { return ('{0:n1} min' -f ($ms / 60000)) } if ($ms -ge 1000) { return ('{0:n1} s' -f ($ms / 1000)) } return ('{0:n1} ms' -f $ms) } # --- Uebersicht ------------------------------------------------------------ Write-Host "" Write-Host "================ UEBERSICHT ================" -ForegroundColor Cyan Write-Host ("Gemessene Zeilen : {0}" -f $norm.Count) Write-Host ("Gesamtzeit : {0} ({1:n0} ms)" -f (Fmt-Dur $totalMs), $totalMs) $files = @($norm | Select-Object -ExpandProperty Leaf -Unique) Write-Host ("Quelldateien : {0}" -f ($files -join ', ')) if ($files.Count -eq 1) { Write-Host "" Write-Host "HINWEIS: Nur eine Quelldatei instrumentiert. Dot-gesourcte Dateien" -ForegroundColor Yellow Write-Host " (R125 etc.) sind hier NICHT zeilenweise enthalten - ihre Zeit" -ForegroundColor Yellow Write-Host " steckt in den Aufrufzeilen. Fuer R125-interne Hotspots R125" -ForegroundColor Yellow Write-Host " direkt messen (Measure-R125Direct.ps1)." -ForegroundColor Yellow } # --- Ranking pro Datei (nur sinnvoll bei mehreren Dateien) ----------------- if ($files.Count -gt 1) { Write-Host "" Write-Host "================ RANKING PRO DATEI ================" -ForegroundColor Cyan $norm | Group-Object Leaf | ForEach-Object { $sum = ($_.Group | Measure-Object Ms -Sum).Sum [pscustomobject]@{ Datei = $_.Name Zeit = Fmt-Dur $sum Ms = [math]::Round($sum, 1) Anteil = ('{0:n1}%' -f ($(if ($totalMs) { 100 * $sum / $totalMs } else { 0 }))) Zeilen = $_.Count } } | Sort-Object Ms -Descending | Format-Table Datei, Zeit, Anteil, Zeilen -AutoSize | Out-Host } # --- Top-Hotspots nach Zeit ------------------------------------------------ Write-Host "" Write-Host "================ TOP $Top NACH ZEIT ================" -ForegroundColor Cyan $norm | Sort-Object Ms -Descending | Select-Object -First $Top | ForEach-Object { [pscustomobject]@{ Datei = $_.Leaf Zeile = $_.LineNo Zeit = Fmt-Dur $_.Ms Anteil = ('{0:n1}%' -f ($(if ($totalMs) { 100 * $_.Ms / $totalMs } else { 0 }))) Hits = $_.Hits Code = if ($_.Code.Length -gt 75) { $_.Code.Substring(0, 75) + '...' } else { $_.Code } } } | Format-Table -AutoSize | Out-Host # --- Top-Hotspots nach HitCount (Wiederholungs-Hotspots) ------------------- $haveHits = ($norm | Where-Object { $_.Hits -gt 1 } | Measure-Object).Count -gt 0 if ($haveHits) { Write-Host "" Write-Host "================ TOP $Top NACH HITCOUNT ================" -ForegroundColor Cyan Write-Host "(relevant fuer die grosse if/elseif-StartsWith-Kette in R125)" -ForegroundColor DarkGray $norm | Sort-Object Hits -Descending | Select-Object -First $Top | ForEach-Object { [pscustomobject]@{ Datei = $_.Leaf Zeile = $_.LineNo Hits = $_.Hits Zeit = Fmt-Dur $_.Ms Code = if ($_.Code.Length -gt 75) { $_.Code.Substring(0, 75) + '...' } else { $_.Code } } } | Format-Table -AutoSize | Out-Host } else { Write-Host "" Write-Host "HINWEIS: Alle Zeilen haben HitCount<=1 (TimeSpans nicht pro Treffer" -ForegroundColor DarkGray Write-Host " erfasst). HitCount-Ranking daher uebersprungen." -ForegroundColor DarkGray }