Scripts/compare-blg-report.ps1

#Requires -Version 5.1
# Run on FILESERVER (powershell.exe 5.1). Stop the Perfmon set first.
# Verifies SMBeat CIM (SMB-Perf + NIC) against Perfmon .blg in the same window.
#
# powershell.exe -NoProfile -File .\compare-blg-report.ps1
# powershell.exe -NoProfile -File .\compare-blg-report.ps1 -BlgPath 'C:\PerfLogs\Admin\SMB_MBF'
# powershell.exe -NoProfile -File .\compare-blg-report.ps1 -Start '2026-08-30 13:49' -End '2026-08-30 14:49'

[CmdletBinding()]
param(
    [string]$BlgPath = 'C:\PerfLogs\Admin\SMB_MBF',
    [datetime]$Start,
    [datetime]$End
)

$ErrorActionPreference = 'Stop'

function ConvertTo-SMBeatCompareGB {
    param([double]$Bytes)
    [math]::Round(($Bytes / 1GB), 3)
}

function ConvertFrom-SMBeatCompareNumber {
    param([string]$Text)
    if ([string]::IsNullOrWhiteSpace($Text)) {
        return $null
    }
    $s = $Text.Trim().Trim('"')
    if ($s.Length -eq 0 -or $s -eq '-' -or $s -eq ' ') {
        return $null
    }
    $n = 0.0
    if ([double]::TryParse($s, [System.Globalization.NumberStyles]::Float, [cultureinfo]::GetCultureInfo('de-DE'), [ref]$n)) {
        return $n
    }
    if ([double]::TryParse($s, [System.Globalization.NumberStyles]::Float, [cultureinfo]::InvariantCulture, [ref]$n)) {
        return $n
    }
    return $null
}

function ConvertFrom-SMBeatCompareWhen {
    param([string]$Text)
    $s = $Text.Trim().Trim('"')
    $styles = [System.Globalization.DateTimeStyles]::AllowWhiteSpaces
    $dto = [datetime]::MinValue
    foreach ($cult in @([cultureinfo]::GetCultureInfo('de-DE'), [cultureinfo]::InvariantCulture, [cultureinfo]::GetCultureInfo('en-US'))) {
        if ([datetime]::TryParse($s, $cult, $styles, [ref]$dto)) {
            return $dto
        }
    }
    return $null
}

function Test-SMBeatCompareHeader {
    param(
        [string]$Header,
        [string]$Kind
    )

    $h = $Header.ToLowerInvariant()
    $isShare = ($h -like '*freigabe*' -or $h -like '*share*')
    $isSmb = ($h -like '*smb-server*' -or $h -like '*smb server*' -or $h -like '*smbserver*')
    $isNic = ($h -like '*netzwerkschnittstelle*' -or $h -like '*network interface*')
    $isSent = ($h -like '*gesendete*' -or $h -like '*bytes sent*' -or $h -like '*bytes gesendet*' -or $h -like '*sent bytes*')
    $isRecv = ($h -like '*empfangene*' -or $h -like '*bytes received*' -or $h -like '*bytes empfangen*' -or $h -like '*received bytes*')
    $isPrivat = ($h -like '*privat*')
    $isAdmin = ($h -like '*admin$*' -or $h -like '*ipc$*')

    if ($Kind -eq 'smbServerSent') { return ($isSmb -and -not $isShare -and $isSent) }
    if ($Kind -eq 'smbServerRecv') { return ($isSmb -and -not $isShare -and $isRecv) }
    if ($Kind -eq 'sharePrivatSent') { return ($isShare -and $isPrivat -and $isSent -and -not $isAdmin) }
    if ($Kind -eq 'sharePrivatRecv') { return ($isShare -and $isPrivat -and $isRecv -and -not $isAdmin) }
    if ($Kind -eq 'nicSent') { return ($isNic -and $isSent) }
    if ($Kind -eq 'nicRecv') { return ($isNic -and $isRecv) }
    return $false
}

function Get-SMBeatCompareRowRate {
    param(
        [int[]]$Indexes,
        [string[]]$Cells
    )
    $rate = 0.0
    $any = $false
    if ($null -eq $Indexes) {
        return $null
    }
    foreach ($i in $Indexes) {
        if ($i -ge $Cells.Count) { continue }
        $n = ConvertFrom-SMBeatCompareNumber -Text $Cells[$i]
        if ($null -ne $n) {
            $rate += [double]$n
            $any = $true
        }
    }
    if (-not $any) {
        return $null
    }
    $rate
}

function Get-SMBeatCompareBlgFiles {
    param(
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        throw ('BLG path not found: {0}' -f $Path)
    }

    $item = Get-Item -LiteralPath $Path
    if (-not $item.PSIsContainer) {
        if ($item.Extension -ne '.blg') {
            throw ('Not a .blg file: {0}' -f $Path)
        }
        return @($item)
    }

    $list = @(Get-ChildItem -LiteralPath $Path -Filter '*.blg' -File -Recurse | Sort-Object LastWriteTime, Name)
    if ($list.Count -eq 0) {
        throw ('No .blg under {0}.' -f $Path)
    }
    $list
}

function ConvertFrom-SMBeatCompareBlg {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Blg,
        [Parameter(Mandatory = $true)]
        [string]$Relog
    )

    $empty = [PSCustomObject]@{
        File    = $Blg
        Headers = @()
        Columns = @{}
        Samples = @()
        Skipped = $true
        Note    = ''
    }

    $csv = Join-Path $env:TEMP ('smbeat-blg-{0}.csv' -f [guid]::NewGuid().ToString('N'))
    try {
        Write-Host ('relog {0}' -f $Blg)
        & $Relog $Blg -f csv -o $csv | Out-Host
        $csvOk = $false
        if ((Test-Path -LiteralPath $csv) -and ((Get-Item -LiteralPath $csv).Length -gt 0)) {
            $csvOk = $true
        }
        if (-not $csvOk) {
            Write-Host ('SKIP (locked or unreadable, often the live BLG): {0}' -f $Blg)
            $empty.Note = 'relog failed (live or locked)'
            return $empty
        }

        $lines = @(Get-Content -LiteralPath $csv -Encoding Default | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
        if ($lines.Count -lt 2) {
            $empty.Skipped = $false
            $empty.Note = 'no samples'
            return $empty
        }

        $headerLine = $lines[0]
        $sep = ','
        if ($headerLine.IndexOf(';') -ge 0 -and ($headerLine.Split(';').Count -gt $headerLine.Split(',').Count)) {
            $sep = ';'
        }

        $headers = @($headerLine.Split($sep))
        $idx = @{
            smbSent  = @()
            smbRecv  = @()
            privSent = @()
            privRecv = @()
            nicSent  = @()
            nicRecv  = @()
        }
        for ($i = 1; $i -lt $headers.Count; $i++) {
            $name = $headers[$i]
            if (Test-SMBeatCompareHeader -Header $name -Kind 'smbServerSent') { $idx.smbSent += $i }
            if (Test-SMBeatCompareHeader -Header $name -Kind 'smbServerRecv') { $idx.smbRecv += $i }
            if (Test-SMBeatCompareHeader -Header $name -Kind 'sharePrivatSent') { $idx.privSent += $i }
            if (Test-SMBeatCompareHeader -Header $name -Kind 'sharePrivatRecv') { $idx.privRecv += $i }
            if (Test-SMBeatCompareHeader -Header $name -Kind 'nicSent') { $idx.nicSent += $i }
            if (Test-SMBeatCompareHeader -Header $name -Kind 'nicRecv') { $idx.nicRecv += $i }
        }

        $samples = New-Object System.Collections.Generic.List[object]
        foreach ($line in ($lines | Select-Object -Skip 1)) {
            $cells = @($line.Split($sep))
            if ($cells.Count -lt 2) { continue }
            $when = ConvertFrom-SMBeatCompareWhen -Text $cells[0]
            if ($null -eq $when) { continue }
            $samples.Add([PSCustomObject]@{
                    When     = $when
                    File     = $Blg
                    smbSent  = (Get-SMBeatCompareRowRate -Indexes $idx.smbSent -Cells $cells)
                    smbRecv  = (Get-SMBeatCompareRowRate -Indexes $idx.smbRecv -Cells $cells)
                    privSent = (Get-SMBeatCompareRowRate -Indexes $idx.privSent -Cells $cells)
                    privRecv = (Get-SMBeatCompareRowRate -Indexes $idx.privRecv -Cells $cells)
                    nicSent  = (Get-SMBeatCompareRowRate -Indexes $idx.nicSent -Cells $cells)
                    nicRecv  = (Get-SMBeatCompareRowRate -Indexes $idx.nicRecv -Cells $cells)
                }) | Out-Null
        }

        [PSCustomObject]@{
            File    = $Blg
            Headers = $headers
            Columns = $idx
            Samples = $samples.ToArray()
            Skipped = $false
            Note    = ''
        }
    }
    finally {
        Remove-Item -LiteralPath $csv -Force -ErrorAction SilentlyContinue
    }
}

function Show-SMBeatCompareCols {
    param(
        [string]$Label,
        [int[]]$Indexes,
        [string[]]$Headers
    )
    $text = '(none)'
    if ($Indexes -and $Indexes.Count -gt 0) {
        $text = ($Indexes | ForEach-Object { $Headers[$_] }) -join ' | '
    }
    Write-Host ('{0}: {1}' -f $Label, $text)
}

function Add-SMBeatCompareVolume {
    param(
        [hashtable]$Map,
        [string]$Key,
        $Rate,
        [double]$Seconds
    )
    if ($null -eq $Rate) {
        return
    }
    $Map[$Key] = $Map[$Key] + ([double]$Rate * $Seconds)
}

if ($env:OS -ne 'Windows_NT') {
    throw 'Needs Windows relog.exe. Run on FILESERVER.'
}

$relog = Join-Path $env:SystemRoot 'System32\relog.exe'
if (-not (Test-Path -LiteralPath $relog)) {
    throw 'relog.exe not found.'
}

$files = @(Get-SMBeatCompareBlgFiles -Path $BlgPath)
Write-Host ('Path : {0}' -f $BlgPath)
Write-Host ('BLGs : {0}' -f $files.Count)
foreach ($f in $files) {
    Write-Host (' {0}' -f $f.FullName)
}

$parsed = New-Object System.Collections.Generic.List[object]
foreach ($f in $files) {
    $parsed.Add((ConvertFrom-SMBeatCompareBlg -Blg $f.FullName -Relog $relog)) | Out-Null
}

$firstWithCols = $null
foreach ($pack in $parsed) {
    if ($pack.Headers.Count -gt 0) {
        $firstWithCols = $pack
        break
    }
}

Write-Host ''
Write-Host '=== columns (first blg with samples) ==='
if ($null -eq $firstWithCols) {
    throw 'No samples in any BLG CSV.'
}
Show-SMBeatCompareCols -Label 'SMB-Server Out' -Indexes $firstWithCols.Columns.smbSent -Headers $firstWithCols.Headers
Show-SMBeatCompareCols -Label 'SMB-Server In ' -Indexes $firstWithCols.Columns.smbRecv -Headers $firstWithCols.Headers
Show-SMBeatCompareCols -Label 'Privat Out ' -Indexes $firstWithCols.Columns.privSent -Headers $firstWithCols.Headers
Show-SMBeatCompareCols -Label 'Privat In ' -Indexes $firstWithCols.Columns.privRecv -Headers $firstWithCols.Headers
Show-SMBeatCompareCols -Label 'NIC Out ' -Indexes $firstWithCols.Columns.nicSent -Headers $firstWithCols.Headers
Show-SMBeatCompareCols -Label 'NIC In ' -Indexes $firstWithCols.Columns.nicRecv -Headers $firstWithCols.Headers

Write-Host ''
Write-Host '=== per blg ==='
$fileRows = New-Object System.Collections.Generic.List[object]
foreach ($pack in $parsed) {
    $from = $null
    $to = $null
    if ($pack.Samples.Count -gt 0) {
        $from = ($pack.Samples | Measure-Object -Property When -Minimum).Minimum
        $to = ($pack.Samples | Measure-Object -Property When -Maximum).Maximum
    }
    $folder = Split-Path -Leaf (Split-Path -Parent $pack.File)
    $note = ''
    if ($pack.PSObject.Properties['Note'] -and $pack.Note) {
        $note = [string]$pack.Note
    }
    $fileRows.Add([PSCustomObject]@{
            File    = ('{0}\{1}' -f $folder, [System.IO.Path]::GetFileName($pack.File))
            Samples = $pack.Samples.Count
            From    = $from
            To      = $to
            Note    = $note
        }) | Out-Null
}
$fileRows | Format-Table -AutoSize

$merged = New-Object System.Collections.Generic.List[object]
$seen = @{}
foreach ($pack in $parsed) {
    foreach ($row in $pack.Samples) {
        $key = $row.When.ToString('o')
        if ($seen.ContainsKey($key)) {
            continue
        }
        $seen[$key] = $true
        $merged.Add($row) | Out-Null
    }
}

$ordered = @($merged | Sort-Object When)
$sum = @{
    smbSent  = 0.0
    smbRecv  = 0.0
    privSent = 0.0
    privRecv = 0.0
    nicSent  = 0.0
    nicRecv  = 0.0
}
$used = 0
$first = $null
$last = $null
$prevWhen = $null
$hasStart = $PSBoundParameters.ContainsKey('Start')
$hasEnd = $PSBoundParameters.ContainsKey('End')

foreach ($row in $ordered) {
    $when = $row.When
    if ($hasStart -and $when -lt $Start) { continue }
    if ($hasEnd -and $when -gt $End) { continue }

    $dt = 60.0
    if ($null -ne $prevWhen) {
        $gap = ($when - $prevWhen).TotalSeconds
        if ($gap -gt 0 -and $gap -le 3600) { $dt = $gap }
    }
    $prevWhen = $when
    if ($null -eq $first) { $first = $when }
    $last = $when
    $used++
    Add-SMBeatCompareVolume -Map $sum -Key 'smbSent' -Rate $row.smbSent -Seconds $dt
    Add-SMBeatCompareVolume -Map $sum -Key 'smbRecv' -Rate $row.smbRecv -Seconds $dt
    Add-SMBeatCompareVolume -Map $sum -Key 'privSent' -Rate $row.privSent -Seconds $dt
    Add-SMBeatCompareVolume -Map $sum -Key 'privRecv' -Rate $row.privRecv -Seconds $dt
    Add-SMBeatCompareVolume -Map $sum -Key 'nicSent' -Rate $row.nicSent -Seconds $dt
    Add-SMBeatCompareVolume -Map $sum -Key 'nicRecv' -Rate $row.nicRecv -Seconds $dt
}

Write-Host '=== window (all blgs) ==='
if ($null -eq $first) {
    Write-Host 'No samples in the selected window.'
    return
}
Write-Host ('From : {0}' -f $first)
Write-Host ('To : {0}' -f $last)
Write-Host ('Samples : {0}' -f $used)
Write-Host ('Seconds : {0:N0}' -f (($last - $first).TotalSeconds))

Write-Host ''
Write-Host '=== volume (rate * interval, all blgs) ==='
@(
    [PSCustomObject]@{ Source = 'SMB-Server Out'; Bytes = [int64]$sum.smbSent; GB = (ConvertTo-SMBeatCompareGB -Bytes $sum.smbSent) }
    [PSCustomObject]@{ Source = 'SMB-Server In';  Bytes = [int64]$sum.smbRecv; GB = (ConvertTo-SMBeatCompareGB -Bytes $sum.smbRecv) }
    [PSCustomObject]@{ Source = 'Privat Out';     Bytes = [int64]$sum.privSent; GB = (ConvertTo-SMBeatCompareGB -Bytes $sum.privSent) }
    [PSCustomObject]@{ Source = 'Privat In';      Bytes = [int64]$sum.privRecv; GB = (ConvertTo-SMBeatCompareGB -Bytes $sum.privRecv) }
    [PSCustomObject]@{ Source = 'NIC Out';        Bytes = [int64]$sum.nicSent; GB = (ConvertTo-SMBeatCompareGB -Bytes $sum.nicSent) }
    [PSCustomObject]@{ Source = 'NIC In';         Bytes = [int64]$sum.nicRecv; GB = (ConvertTo-SMBeatCompareGB -Bytes $sum.nicRecv) }
) | Format-Table -AutoSize

Write-Host 'Paste this output. HTML is compared in chat, not in this script.'