Scripts/measure-report.ps1
|
#Requires -Version 5.1 # Measures the local Show-SMBeat report path without opening the report. <# .SYNOPSIS Measures the local SMBeat report pipeline by phase. .DESCRIPTION Uses the same private readers, aggregation, renderer, and optional CSV writer as Show-SMBeat. The report is never opened. By default, diagnostic HTML with an SMBeat-Measure prefix is written to the report directory. CSV only with -Csv. .PARAMETER NoExport Render HTML in memory but do not write HTML or CSV files. .PARAMETER Csv Also write diagnostic CSV files. Off by default. .PARAMETER ModulePath Manifest path or module name. Defaults to the manifest above this script, then to the installed SMBeat module. .EXAMPLE .\measure-report.ps1 .EXAMPLE .\measure-report.ps1 -LastDays 7 -NoExport .EXAMPLE .\measure-report.ps1 -Path 'D:\SMBeat' -OutputPath 'D:\SMBeat-Measure' #> [CmdletBinding()] param( [datetime]$Start, [datetime]$End, [int]$LastHours, [int]$LastDays, [int]$LastWeeks, [ValidateSet('Hour', 'Day', 'Week')] [string[]]$Granularity = @('Hour', 'Day', 'Week'), [object]$TimeZone, [string]$Path, [string]$OutputPath, [ValidateSet('de', 'en')] [string]$Language, [ValidateSet('de', 'en')] [string]$CsvCulture, [ValidateSet('health', 'pine', 'dark')] [string]$Theme = 'health', [string]$ModulePath, [switch]$NoExport, [switch]$Csv ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ([string]::IsNullOrWhiteSpace($ModulePath)) { $candidate = Join-Path (Split-Path -Parent $PSScriptRoot) 'SMBeat.psd1' if (Test-Path -LiteralPath $candidate) { $ModulePath = $candidate } else { $ModulePath = 'SMBeat' } } $totalWatch = [System.Diagnostics.Stopwatch]::StartNew() $importWatch = [System.Diagnostics.Stopwatch]::StartNew() Import-Module $ModulePath -Force -ErrorAction Stop $importWatch.Stop() $module = Get-Module -Name SMBeat if ($null -eq $module) { throw 'Import-Module SMBeat failed.' } $payload = [PSCustomObject]@{ HasStart = $PSBoundParameters.ContainsKey('Start') HasEnd = $PSBoundParameters.ContainsKey('End') Start = $Start End = $End LastHours = $LastHours LastDays = $LastDays LastWeeks = $LastWeeks Granularity = @($Granularity) TimeZone = $TimeZone Path = $Path OutputPath = $OutputPath Language = $Language CsvCulture = $CsvCulture Theme = $Theme NoExport = [bool]$NoExport Csv = [bool]$Csv } $measurement = & $module { param($P, [double]$ImportMs) $phases = New-Object System.Collections.Generic.List[object] $outputFiles = New-Object System.Collections.Generic.List[string] function Add-SMBeatMeasurePhase { param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [System.Diagnostics.Stopwatch]$Watch, [string]$Detail = '' ) $Watch.Stop() [void]$phases.Add([PSCustomObject]@{ Phase = $Name Seconds = [math]::Round($Watch.Elapsed.TotalSeconds, 3) Detail = $Detail }) } [void]$phases.Add([PSCustomObject]@{ Phase = 'module-import' Seconds = [math]::Round(($ImportMs / 1000.0), 3) Detail = '' }) $watch = [System.Diagnostics.Stopwatch]::StartNew() $nativeJson = Initialize-SMBeatSampleJsonNative Add-SMBeatMeasurePhase -Name 'native-json-init' -Watch $watch -Detail ('native={0}' -f $nativeJson) $watch = [System.Diagnostics.Stopwatch]::StartNew() $root = Get-SMBeatDataRoot -Path $P.Path $tz = Resolve-SMBeatTimeZone -TimeZone $P.TimeZone $config = Read-SMBeatConfig -DataRoot $root $interval = [int](Get-SMBeatPropertyValue -Object $config -Name 'IntervalSec' -Default 60) if ($interval -le 0) { $interval = 60 } $useAvailable = -not $P.HasStart -and $P.LastHours -le 0 -and $P.LastDays -le 0 -and $P.LastWeeks -le 0 if ($useAvailable) { $windowArgs = @{ DataRoot = $root } if ($P.HasEnd) { $windowArgs['End'] = $P.End } } else { $windowArgs = @{ LastHours = $P.LastHours LastDays = $P.LastDays LastWeeks = $P.LastWeeks } if ($P.HasStart) { $windowArgs['Start'] = $P.Start } if ($P.HasEnd) { $windowArgs['End'] = $P.End } } $window = Resolve-SMBeatReportWindow @windowArgs $windowStart = $window.StartUtc $windowEnd = $window.EndUtc $duration = $windowEnd - $windowStart $prevEnd = $windowStart $prevStart = $windowStart - $duration Add-SMBeatMeasurePhase -Name 'resolve-window' -Watch $watch -Detail ('days={0:0.###}' -f $duration.TotalDays) $generatedUtc = Get-SMBeatUtcNow $spanStart = $generatedUtc.AddDays(-7) $loadStart = $windowStart if ($spanStart -lt $loadStart) { $loadStart = $spanStart } $loadEnd = $windowEnd if ($generatedUtc -gt $loadEnd) { $loadEnd = $generatedUtc } $watch = [System.Diagnostics.Stopwatch]::StartNew() $sampleDir = Get-SMBeatSampleDirectory -DataRoot $root $inputFiles = @() $inputBytes = [int64]0 if (Test-Path -LiteralPath $sampleDir) { $readStart = $prevStart if ($loadStart -lt $readStart) { $readStart = $loadStart } $readEnd = $prevEnd if ($loadEnd -gt $readEnd) { $readEnd = $loadEnd } $startDay = $readStart.Date.AddDays(-1) $endDay = $readEnd.Date.AddDays(1) $fileList = New-Object System.Collections.Generic.List[object] foreach ($file in @(Get-ChildItem -LiteralPath $sampleDir -Filter '*.jsonl' -ErrorAction SilentlyContinue)) { $dayPart = [System.IO.Path]::GetFileNameWithoutExtension($file.Name) $fileDay = [datetime]::MinValue if (-not [datetime]::TryParseExact($dayPart, 'yyyy-MM-dd', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None, [ref]$fileDay)) { continue } if ($fileDay -lt $startDay -or $fileDay -gt $endDay) { continue } [void]$fileList.Add($file) $inputBytes += [int64]$file.Length } $inputFiles = @($fileList.ToArray()) } Add-SMBeatMeasurePhase -Name 'scan-input' -Watch $watch -Detail ('files={0} bytes={1}' -f $inputFiles.Count, $inputBytes) $watch = [System.Diagnostics.Stopwatch]::StartNew() $windows = Read-SMBeatSampleWindows -DataRoot $root -PrevStart $prevStart -PrevEnd $prevEnd -WindowStart $loadStart -WindowEnd $loadEnd $loaded = @($windows.Current) $previousRecords = @($windows.Previous) Add-SMBeatMeasurePhase -Name 'read-samples' -Watch $watch -Detail ('current={0} previous={1}' -f $loaded.Count, $previousRecords.Count) $watch = [System.Diagnostics.Stopwatch]::StartNew() $records = @(Get-SMBeatRecordsInWindow -Records $loaded -StartUtc $windowStart -EndUtc $windowEnd) Add-SMBeatMeasurePhase -Name 'filter-current' -Watch $watch -Detail ('records={0}' -f $records.Count) $watch = [System.Diagnostics.Stopwatch]::StartNew() $previousTotals = $null if ($previousRecords.Count -gt 0) { $previousReport = New-SMBeatReportFromSamples -Records $previousRecords -StartUtc $prevStart -EndUtc $prevEnd -TimeZone $tz -Granularity @() -IntervalSec $interval -TotalsOnly $previousTotals = $previousReport.Totals } Add-SMBeatMeasurePhase -Name 'aggregate-previous' -Watch $watch -Detail ('records={0}' -f $previousRecords.Count) $watch = [System.Diagnostics.Stopwatch]::StartNew() $report = New-SMBeatReportFromSamples -Records $records -StartUtc $windowStart -EndUtc $windowEnd -TimeZone $tz -Granularity $P.Granularity -IntervalSec $interval -PreviousTotals $previousTotals -GeneratedUtc $generatedUtc -SkipSpans Add-SMBeatMeasurePhase -Name 'aggregate-current' -Watch $watch -Detail ('records={0}' -f $records.Count) $watch = [System.Diagnostics.Stopwatch]::StartNew() Add-SMBeatReportSpans -Report $report -Records $loaded -TimeZone $tz -IntervalSec $interval | Out-Null Add-SMBeatMeasurePhase -Name 'aggregate-spans' -Watch $watch -Detail ('records={0}' -f $loaded.Count) $watch = [System.Diagnostics.Stopwatch]::StartNew() $diskSince = Get-SMBeatCollectionStartUtc -DataRoot $root if ($null -ne $diskSince) { $report | Add-Member -NotePropertyName CollectedSinceUtc -NotePropertyValue $diskSince -Force } $eventStart = $windowStart.AddHours(-2) $events = @(Read-SMBeatCollectorEvents -DataRoot $root -StartUtc $eventStart -EndUtc $windowEnd) $report | Add-Member -NotePropertyName Events -NotePropertyValue $events -Force Merge-SMBeatTechFromDataRoot -Report $report -DataRoot $root -Config $config Add-SMBeatMeasurePhase -Name 'events-and-tech' -Watch $watch -Detail ('events={0}' -f $events.Count) $language = Get-SMBeatLanguage -Language $P.Language $csvCulture = $P.CsvCulture if ([string]::IsNullOrWhiteSpace($csvCulture)) { $csvCulture = $language } $watch = [System.Diagnostics.Stopwatch]::StartNew() $html = ConvertTo-SMBeatHtmlReport -Report $report -Language $language -Theme $P.Theme Add-SMBeatMeasurePhase -Name 'render-html' -Watch $watch -Detail ('chars={0}' -f $html.Length) $reportOutputPath = $P.OutputPath if ([string]::IsNullOrWhiteSpace($reportOutputPath)) { $reportOutputPath = Get-SMBeatDefaultReportRoot } if (-not $P.NoExport) { $stamp = Get-SMBeatExportStamp $watch = [System.Diagnostics.Stopwatch]::StartNew() if (-not (Test-Path -LiteralPath $reportOutputPath)) { New-Item -ItemType Directory -Path $reportOutputPath -Force | Out-Null } $htmlFile = Join-Path $reportOutputPath ('SMBeat-Measure-Report-{0}.html' -f $stamp) $utf8Bom = New-Object System.Text.UTF8Encoding $true [System.IO.File]::WriteAllText($htmlFile, $html, $utf8Bom) [void]$outputFiles.Add($htmlFile) Add-SMBeatMeasurePhase -Name 'write-html' -Watch $watch -Detail $htmlFile if ($P.Csv) { $watch = [System.Diagnostics.Stopwatch]::StartNew() $csvSets = @( [PSCustomObject]@{ Name = 'shares'; Rows = @($report.ByShare) } [PSCustomObject]@{ Name = 'clients'; Rows = @($report.ByClient) } [PSCustomObject]@{ Name = 'users'; Rows = @($report.ByUser) } [PSCustomObject]@{ Name = 'servers'; Rows = @($report.ByServer) } ) if ($report.PSObject.Properties['BySmbPerf']) { $csvSets += [PSCustomObject]@{ Name = 'smbperf'; Rows = @($report.BySmbPerf) } } if ($report.Hourly -and $report.Hourly.Count -gt 0) { $csvSets += [PSCustomObject]@{ Name = 'hourly'; Rows = @($report.Hourly) } } if ($report.Daily -and $report.Daily.Count -gt 0) { $csvSets += [PSCustomObject]@{ Name = 'daily'; Rows = @($report.Daily) } } if ($report.Weekly -and $report.Weekly.Count -gt 0) { $csvSets += [PSCustomObject]@{ Name = 'weekly'; Rows = @($report.Weekly) } } foreach ($set in $csvSets) { $csvFile = Join-Path $reportOutputPath ('SMBeat-Measure-{0}-{1}.csv' -f $set.Name, $stamp) Write-SMBeatCsvFile -Path $csvFile -InputObject $set.Rows -CsvCulture $csvCulture [void]$outputFiles.Add($csvFile) } Add-SMBeatMeasurePhase -Name 'write-csv' -Watch $watch -Detail ('files={0}' -f $csvSets.Count) } } [PSCustomObject]@{ ModuleVersion = Get-SMBeatModuleVersion ComputerName = [string]$env:COMPUTERNAME DataRoot = $root WindowStart = $windowStart WindowEnd = $windowEnd WindowDays = [math]::Round($duration.TotalDays, 3) NativeJson = [bool]$nativeJson InputFiles = $inputFiles.Count InputBytes = $inputBytes LoadedRecords = $loaded.Count ReportRecords = $records.Count SampleCount = [int]$report.SampleCount OutputFiles = $outputFiles.ToArray() Phases = $phases.ToArray() } } $payload $importWatch.Elapsed.TotalMilliseconds $totalWatch.Stop() $measurement | Add-Member -NotePropertyName TotalSeconds -NotePropertyValue ([math]::Round($totalWatch.Elapsed.TotalSeconds, 3)) -Force foreach ($phase in @($measurement.Phases)) { $pct = 0.0 if ($measurement.TotalSeconds -gt 0) { $pct = [math]::Round((100.0 * [double]$phase.Seconds / [double]$measurement.TotalSeconds), 1) } $phase | Add-Member -NotePropertyName Percent -NotePropertyValue $pct -Force } Write-Host '' Write-Host ('SMBeat {0} report measurement' -f $measurement.ModuleVersion) Write-Host ('Data root : {0}' -f $measurement.DataRoot) Write-Host ('Window : {0:u} - {1:u} ({2} days)' -f $measurement.WindowStart, $measurement.WindowEnd, $measurement.WindowDays) Write-Host ('Input : {0} files, {1:N0} bytes' -f $measurement.InputFiles, $measurement.InputBytes) Write-Host ('Records : loaded={0}, report={1}, samples={2}' -f $measurement.LoadedRecords, $measurement.ReportRecords, $measurement.SampleCount) Write-Host ('Native JSON : {0}' -f $measurement.NativeJson) Write-Host ('Total : {0:N3} s' -f $measurement.TotalSeconds) Write-Host '' Write-Host (($measurement.Phases | Select-Object Phase, Seconds, Percent, Detail | Format-Table -AutoSize | Out-String).TrimEnd()) if ($measurement.OutputFiles.Count -gt 0) { Write-Host '' Write-Host 'Output files:' foreach ($file in $measurement.OutputFiles) { Write-Host (' {0}' -f $file) } } $measurement |