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 CSV writer as Show-SMBeat. The report is never opened. By default, diagnostic report files with an SMBeat-Measure prefix are written to the report directory. .PARAMETER NoExport Render HTML in memory but do not write HTML or CSV files. .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 ) 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 } $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 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 $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 |