SparKURS.DAW/Analyze-ProfileResult.ps1
2026-07-05 13:11:37 +02:00

201 lines
7.9 KiB
PowerShell
Executable file

<#
.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 { '<unknown>' }
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
}