1042 lines
40 KiB
PowerShell
1042 lines
40 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$DataRoot = (Join-Path (Split-Path -Parent $PSScriptRoot) 'target\mhash-benchmark\data'),
|
|
[string]$OutputRoot = (Join-Path (Split-Path -Parent $PSScriptRoot) 'target\mhash-benchmark\results'),
|
|
[string[]]$Sizes = @('1MiB', '64MiB', '256MiB'),
|
|
[ValidateSet(
|
|
'crc32', 'crc-32', 'crc64-xz', 'crc-64-xz', 'crc64',
|
|
'xxh32', 'xxhash32', 'xxhash-32', 'xxh64', 'xxhash64', 'xxhash-64',
|
|
'xxh3', 'xxh3-64', 'xxhash3-64', 'xxh3-128', 'xxhash3-128',
|
|
'md4', 'md5', 'ripemd160', 'ripemd-160', 'blake2sp', 'blake2-sp',
|
|
'sha1', 'sha-1', 'sha224', 'sha-224', 'sha256', 'sha-256',
|
|
'sha384', 'sha-384', 'sha512', 'sha-512',
|
|
'sha3-224', 'sha3_224', 'sha3-256', 'sha3_256',
|
|
'sha3-384', 'sha3_384', 'sha3-512', 'sha3_512',
|
|
'blake3', 'blake3-256', 'blake3-512',
|
|
'kangarootwelve-264', 'k12-264', 'kangarootwelve-256',
|
|
'k12-256', 'kangarootwelve', 'kangarootwelve-512', 'k12-512',
|
|
'parallelhash128-264', 'parallelhash-128-264',
|
|
'parallelhash256-528', 'parallelhash-256-528',
|
|
'streebog-256', 'streebog256', 'gost-256',
|
|
'streebog-512', 'streebog512', 'gost-512'
|
|
)]
|
|
[string[]]$Algorithms = @('md5', 'sha1', 'sha256', 'sha512'),
|
|
[ValidateSet('text', 'sum', 'json', 'jsonl')]
|
|
[string[]]$DigestFormats = @('sum'),
|
|
[int]$Repeat = 3,
|
|
[int]$Warmup = 1,
|
|
[string]$DigestPath,
|
|
[switch]$SkipBuild,
|
|
[switch]$NoExternal,
|
|
[switch]$Extended,
|
|
[switch]$SizeOnly,
|
|
[switch]$Hyperfine,
|
|
[int]$HyperfineRuns = 0,
|
|
[int]$HyperfineWarmup = -1
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Set-StrictMode -Version Latest
|
|
|
|
if ($Extended) {
|
|
if (-not $PSBoundParameters.ContainsKey('Sizes')) {
|
|
$Sizes = @('1MiB', '64MiB', '256MiB', '1GiB')
|
|
}
|
|
if (-not $PSBoundParameters.ContainsKey('Repeat')) {
|
|
$Repeat = 5
|
|
}
|
|
if (-not $PSBoundParameters.ContainsKey('Warmup')) {
|
|
$Warmup = 2
|
|
}
|
|
if (-not $PSBoundParameters.ContainsKey('DigestFormats')) {
|
|
$DigestFormats = @('sum', 'jsonl', 'json')
|
|
}
|
|
}
|
|
|
|
if ($Repeat -lt 0) {
|
|
throw 'Repeat must be zero or greater.'
|
|
}
|
|
if (($Repeat -eq 0) -and (-not $SizeOnly)) {
|
|
throw 'Repeat must be at least 1 unless -SizeOnly is set.'
|
|
}
|
|
if ($Warmup -lt 0) {
|
|
throw 'Warmup must be zero or greater.'
|
|
}
|
|
if ($HyperfineRuns -lt 0) {
|
|
throw 'HyperfineRuns must be zero or greater. Use 0 to mirror -Repeat.'
|
|
}
|
|
if ($HyperfineWarmup -lt -1) {
|
|
throw 'HyperfineWarmup must be -1 or greater. Use -1 to mirror -Warmup.'
|
|
}
|
|
if ($HyperfineRuns -eq 0) {
|
|
$HyperfineRuns = [Math]::Max(1, $Repeat)
|
|
}
|
|
if ($HyperfineWarmup -eq -1) {
|
|
$HyperfineWarmup = $Warmup
|
|
}
|
|
|
|
function Invoke-StrictNative {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$FilePath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$ArgumentList
|
|
)
|
|
|
|
& $FilePath @ArgumentList
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $($ArgumentList -join ' ')"
|
|
}
|
|
}
|
|
|
|
function ConvertTo-ByteCount {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Value
|
|
)
|
|
|
|
if ($Value -notmatch '^(?<number>\d+)(?<unit>B|KiB|MiB|GiB|K|M|G)?$') {
|
|
throw "Invalid size '$Value'. Use values like 1MiB, 64MiB, or 1GiB."
|
|
}
|
|
|
|
$number = [int64]$Matches.number
|
|
$unit = if ($Matches.unit) { $Matches.unit } else { 'B' }
|
|
switch ($unit) {
|
|
'B' { return $number }
|
|
{ $_ -in @('K', 'KiB') } { return $number * 1024L }
|
|
{ $_ -in @('M', 'MiB') } { return $number * 1024L * 1024L }
|
|
{ $_ -in @('G', 'GiB') } { return $number * 1024L * 1024L * 1024L }
|
|
default { throw "Unsupported size unit '$unit'." }
|
|
}
|
|
}
|
|
|
|
function Format-ByteSize {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[int64]$Bytes
|
|
)
|
|
|
|
if ($Bytes -ge 1GB) {
|
|
return ('{0:N0}GiB' -f ($Bytes / 1GB))
|
|
}
|
|
if ($Bytes -ge 1MB) {
|
|
return ('{0:N0}MiB' -f ($Bytes / 1MB))
|
|
}
|
|
if ($Bytes -ge 1KB) {
|
|
return ('{0:N0}KiB' -f ($Bytes / 1KB))
|
|
}
|
|
return "${Bytes}B"
|
|
}
|
|
|
|
function ConvertTo-FileSha256 {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path
|
|
)
|
|
|
|
try {
|
|
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function ConvertTo-JsonLines {
|
|
param(
|
|
[object[]]$InputObject = @(),
|
|
[int]$Depth = 8
|
|
)
|
|
|
|
foreach ($item in $InputObject) {
|
|
$item | ConvertTo-Json -Depth $Depth -Compress
|
|
}
|
|
}
|
|
|
|
function Write-JsonLines {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[object[]]$InputObject = @(),
|
|
[int]$Depth = 8
|
|
)
|
|
|
|
$lines = @(ConvertTo-JsonLines -InputObject $InputObject -Depth $Depth)
|
|
if ($lines.Count -eq 0) {
|
|
[System.IO.File]::WriteAllText($Path, '', [System.Text.UTF8Encoding]::new($false))
|
|
return
|
|
}
|
|
Set-Content -LiteralPath $Path -Value $lines -Encoding utf8NoBOM
|
|
}
|
|
|
|
function ConvertTo-QuotedArgument {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Argument
|
|
)
|
|
|
|
if ($Argument -match '^[A-Za-z0-9_./:\\-]+$') {
|
|
return $Argument
|
|
}
|
|
|
|
return '"' + ($Argument -replace '"', '\"') + '"'
|
|
}
|
|
|
|
function ConvertTo-NativeCommandLine {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$FilePath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$ArgumentList
|
|
)
|
|
|
|
$parts = @($FilePath) + $ArgumentList
|
|
return (($parts | ForEach-Object { ConvertTo-QuotedArgument -Argument $_ }) -join ' ')
|
|
}
|
|
|
|
function New-DeterministicFile {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[Parameter(Mandatory = $true)]
|
|
[int64]$Bytes
|
|
)
|
|
|
|
if ((Test-Path -LiteralPath $Path -PathType Leaf) -and ((Get-Item -LiteralPath $Path).Length -eq $Bytes)) {
|
|
return
|
|
}
|
|
|
|
$directory = Split-Path -Parent $Path
|
|
if (-not [string]::IsNullOrWhiteSpace($directory)) {
|
|
New-Item -ItemType Directory -Force -Path $directory | Out-Null
|
|
}
|
|
|
|
$buffer = [byte[]]::new(1MB)
|
|
for ($index = 0; $index -lt $buffer.Length; $index++) {
|
|
$buffer[$index] = [byte](($index * 31 + 17) -band 0xff)
|
|
}
|
|
|
|
$stream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read)
|
|
try {
|
|
$remaining = $Bytes
|
|
while ($remaining -gt 0) {
|
|
$write = [int][Math]::Min($buffer.Length, $remaining)
|
|
$stream.Write($buffer, 0, $write)
|
|
$remaining -= $write
|
|
}
|
|
}
|
|
finally {
|
|
$stream.Dispose()
|
|
}
|
|
}
|
|
|
|
function Resolve-CommandPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Name
|
|
)
|
|
|
|
$command = Get-Command -Name $Name -ErrorAction SilentlyContinue
|
|
if ($null -eq $command) {
|
|
return $null
|
|
}
|
|
return $command.Source
|
|
}
|
|
|
|
function Resolve-CoreutilsCommandPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Name
|
|
)
|
|
|
|
$source = Resolve-CommandPath -Name $Name
|
|
if ([string]::IsNullOrWhiteSpace($source)) {
|
|
return $null
|
|
}
|
|
|
|
$candidate = Join-Path $env:USERPROFILE "scoop\apps\uutils-coreutils\current\$Name.exe"
|
|
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
|
return $candidate
|
|
}
|
|
|
|
return $source
|
|
}
|
|
|
|
function Get-ToolVersion {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Name,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[string[]]$VersionArguments = @('--version')
|
|
)
|
|
|
|
try {
|
|
$output = @(& $Path @VersionArguments 2>&1)
|
|
$firstLine = @($output | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | Select-Object -First 1)
|
|
if ($firstLine.Count -gt 0) {
|
|
$candidate = [string]$firstLine[0]
|
|
if ($candidate.IndexOf([char]0xfffd) -lt 0) {
|
|
return $candidate
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
try {
|
|
$item = Get-Item -LiteralPath $Path -ErrorAction Stop
|
|
if (-not [string]::IsNullOrWhiteSpace($item.VersionInfo.ProductVersion)) {
|
|
return "$Name $($item.VersionInfo.ProductVersion)"
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($item.VersionInfo.FileVersion)) {
|
|
return "$Name $($item.VersionInfo.FileVersion)"
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
return $Name
|
|
}
|
|
|
|
function Get-BinaryArtifactInfo {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Tool,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[string]$Version = ''
|
|
)
|
|
|
|
$fullPath = [System.IO.Path]::GetFullPath($Path)
|
|
if (-not (Test-Path -LiteralPath $fullPath -PathType Leaf)) {
|
|
return [pscustomobject]@{
|
|
tool = $Tool
|
|
path = $fullPath
|
|
exists = $false
|
|
size_bytes = $null
|
|
size_label = ''
|
|
sha256 = ''
|
|
last_write_utc = ''
|
|
product_version = ''
|
|
file_version = ''
|
|
version = $Version
|
|
}
|
|
}
|
|
|
|
$item = Get-Item -LiteralPath $fullPath
|
|
[pscustomobject]@{
|
|
tool = $Tool
|
|
path = $fullPath
|
|
exists = $true
|
|
size_bytes = [int64]$item.Length
|
|
size_label = Format-ByteSize -Bytes ([int64]$item.Length)
|
|
sha256 = ConvertTo-FileSha256 -Path $fullPath
|
|
last_write_utc = $item.LastWriteTimeUtc.ToString('o')
|
|
product_version = if ($item.VersionInfo) { [string]$item.VersionInfo.ProductVersion } else { '' }
|
|
file_version = if ($item.VersionInfo) { [string]$item.VersionInfo.FileVersion } else { '' }
|
|
version = $Version
|
|
}
|
|
}
|
|
|
|
function Get-GitMetadata {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Root
|
|
)
|
|
|
|
$git = Resolve-CommandPath -Name 'git'
|
|
if ([string]::IsNullOrWhiteSpace($git)) {
|
|
return [pscustomobject]@{
|
|
available = $false
|
|
commit = ''
|
|
branch = ''
|
|
status_short = @()
|
|
}
|
|
}
|
|
|
|
Push-Location -LiteralPath $Root
|
|
try {
|
|
$commit = (& $git rev-parse --short HEAD 2>$null)
|
|
$branch = (& $git branch --show-current 2>$null)
|
|
$status = @(& $git status --short 2>$null)
|
|
return [pscustomobject]@{
|
|
available = $true
|
|
commit = if ($LASTEXITCODE -eq 0) { [string]$commit } else { '' }
|
|
branch = [string]$branch
|
|
status_short = $status
|
|
}
|
|
}
|
|
catch {
|
|
return [pscustomobject]@{
|
|
available = $true
|
|
commit = ''
|
|
branch = ''
|
|
status_short = @("git metadata failed: $($_.Exception.Message)")
|
|
}
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
function New-Candidate {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Tool,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Algorithm,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$FilePath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$Arguments,
|
|
[string]$OutputFormat = 'native',
|
|
[string]$Version = ''
|
|
)
|
|
|
|
[pscustomobject]@{
|
|
Tool = $Tool
|
|
Algorithm = $Algorithm
|
|
FilePath = $FilePath
|
|
Arguments = $Arguments
|
|
OutputFormat = $OutputFormat
|
|
Version = $Version
|
|
}
|
|
}
|
|
|
|
function New-CandidatesForAlgorithm {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Algorithm,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$InputPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$DigestExe,
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable]$Tools,
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$DigestFormats,
|
|
[switch]$NoExternal
|
|
)
|
|
|
|
$candidates = [System.Collections.Generic.List[object]]::new()
|
|
foreach ($format in $DigestFormats) {
|
|
$candidates.Add((New-Candidate -Tool 'mercury-mhash:auto' -Algorithm $Algorithm -FilePath $DigestExe -Arguments @('--algorithm', $Algorithm, '--format', $format, $InputPath) -OutputFormat $format -Version $Tools.mhash_version))
|
|
$candidates.Add((New-Candidate -Tool 'mercury-mhash:read' -Algorithm $Algorithm -FilePath $DigestExe -Arguments @('--algorithm', $Algorithm, '--format', $format, '--io', 'read', $InputPath) -OutputFormat $format -Version $Tools.mhash_version))
|
|
}
|
|
|
|
if (-not $NoExternal) {
|
|
if ($Tools.pwsh) {
|
|
$psAlgorithm = @{
|
|
md5 = 'MD5'
|
|
sha1 = 'SHA1'
|
|
sha256 = 'SHA256'
|
|
sha512 = 'SHA512'
|
|
}[$Algorithm]
|
|
if ($psAlgorithm) {
|
|
$escapedInputPath = $InputPath.Replace("'", "''")
|
|
$script = "Get-FileHash -LiteralPath '$escapedInputPath' -Algorithm $psAlgorithm | Select-Object -ExpandProperty Hash"
|
|
$candidates.Add((New-Candidate -Tool 'powershell:Get-FileHash' -Algorithm $Algorithm -FilePath $Tools.pwsh -Arguments @('-NoProfile', '-Command', $script) -Version $Tools.pwsh_version))
|
|
}
|
|
}
|
|
|
|
if ($Tools.certutil) {
|
|
$certAlgorithm = @{
|
|
md5 = 'MD5'
|
|
sha1 = 'SHA1'
|
|
sha256 = 'SHA256'
|
|
sha512 = 'SHA512'
|
|
}[$Algorithm]
|
|
if ($certAlgorithm) {
|
|
$candidates.Add((New-Candidate -Tool 'windows:certutil' -Algorithm $Algorithm -FilePath $Tools.certutil -Arguments @('-hashfile', $InputPath, $certAlgorithm) -Version $Tools.certutil_version))
|
|
}
|
|
}
|
|
|
|
$sumPath = $Tools["${Algorithm}sum"]
|
|
if ($sumPath) {
|
|
$candidates.Add((New-Candidate -Tool "coreutils:${Algorithm}sum" -Algorithm $Algorithm -FilePath $sumPath -Arguments @($InputPath) -Version $Tools["${Algorithm}sum_version"]))
|
|
}
|
|
|
|
if ($Tools.openssl) {
|
|
$opensslAlgorithm = @{
|
|
md5 = 'md5'
|
|
sha1 = 'sha1'
|
|
sha256 = 'sha256'
|
|
sha512 = 'sha512'
|
|
}[$Algorithm]
|
|
if ($opensslAlgorithm) {
|
|
$candidates.Add((New-Candidate -Tool 'openssl:dgst' -Algorithm $Algorithm -FilePath $Tools.openssl -Arguments @('dgst', "-$opensslAlgorithm", $InputPath) -Version $Tools.openssl_version))
|
|
}
|
|
}
|
|
}
|
|
|
|
return $candidates
|
|
}
|
|
|
|
function Invoke-MeasuredProcess {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[object]$Candidate
|
|
)
|
|
|
|
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
|
$startInfo.FileName = $Candidate.FilePath
|
|
foreach ($argument in $Candidate.Arguments) {
|
|
[void]$startInfo.ArgumentList.Add($argument)
|
|
}
|
|
$startInfo.UseShellExecute = $false
|
|
$startInfo.RedirectStandardOutput = $true
|
|
$startInfo.RedirectStandardError = $true
|
|
$startInfo.CreateNoWindow = $true
|
|
|
|
$process = [System.Diagnostics.Process]::new()
|
|
$process.StartInfo = $startInfo
|
|
$timer = [System.Diagnostics.Stopwatch]::StartNew()
|
|
$started = $process.Start()
|
|
if (-not $started) {
|
|
throw "Failed to start $($Candidate.Tool)."
|
|
}
|
|
|
|
$peakWorkingSet = 0L
|
|
try {
|
|
$process.Refresh()
|
|
$peakWorkingSet = [Math]::Max($peakWorkingSet, [int64]$process.WorkingSet64)
|
|
}
|
|
catch {
|
|
}
|
|
while (-not $process.WaitForExit(5)) {
|
|
try {
|
|
$process.Refresh()
|
|
$peakWorkingSet = [Math]::Max($peakWorkingSet, [int64]$process.WorkingSet64)
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
$process.WaitForExit()
|
|
$timer.Stop()
|
|
|
|
$stdout = $process.StandardOutput.ReadToEnd()
|
|
$stderr = $process.StandardError.ReadToEnd()
|
|
try {
|
|
$process.Refresh()
|
|
$peakWorkingSet = [Math]::Max($peakWorkingSet, [int64]$process.PeakWorkingSet64)
|
|
}
|
|
catch {
|
|
}
|
|
|
|
$elapsedMs = $timer.Elapsed.TotalMilliseconds
|
|
$cpuMs = $process.TotalProcessorTime.TotalMilliseconds
|
|
$exitCode = $process.ExitCode
|
|
$process.Dispose()
|
|
|
|
$cpuOneCorePercent = if ($elapsedMs -gt 0) { ($cpuMs / $elapsedMs) * 100.0 } else { 0.0 }
|
|
$cpuMachinePercent = if ([Environment]::ProcessorCount -gt 0) { $cpuOneCorePercent / [Environment]::ProcessorCount } else { 0.0 }
|
|
|
|
[pscustomobject]@{
|
|
elapsed_ms = $elapsedMs
|
|
cpu_ms = $cpuMs
|
|
cpu_one_core_percent = $cpuOneCorePercent
|
|
cpu_machine_percent = $cpuMachinePercent
|
|
peak_working_set_bytes = $peakWorkingSet
|
|
exit_code = $exitCode
|
|
stdout = $stdout.Trim()
|
|
stderr = $stderr.Trim()
|
|
}
|
|
}
|
|
|
|
function Invoke-HyperfineSuite {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$HyperfinePath,
|
|
[Parameter(Mandatory = $true)]
|
|
[object]$File,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Algorithm,
|
|
[Parameter(Mandatory = $true)]
|
|
[object[]]$Candidates,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$OutputPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$Runs,
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$Warmup
|
|
)
|
|
|
|
$arguments = @(
|
|
'--style', 'basic',
|
|
'--shell', 'none',
|
|
'--output', 'pipe',
|
|
'--warmup', [string]$Warmup,
|
|
'--runs', [string]$Runs,
|
|
'--export-json', $OutputPath
|
|
)
|
|
foreach ($candidate in $Candidates) {
|
|
$name = '{0}/{1}/{2}/{3}' -f $candidate.Tool, $candidate.OutputFormat, $Algorithm, $File.label
|
|
$commandLine = ConvertTo-NativeCommandLine -FilePath $candidate.FilePath -ArgumentList $candidate.Arguments
|
|
$arguments += @('--command-name', $name, $commandLine)
|
|
}
|
|
|
|
$started = (Get-Date).ToUniversalTime().ToString('o')
|
|
$output = @(& $HyperfinePath @arguments 2>&1)
|
|
$exitCode = $LASTEXITCODE
|
|
$finished = (Get-Date).ToUniversalTime().ToString('o')
|
|
$parsed = $null
|
|
if (Test-Path -LiteralPath $OutputPath -PathType Leaf) {
|
|
try {
|
|
$parsed = Get-Content -LiteralPath $OutputPath -Raw | ConvertFrom-Json
|
|
}
|
|
catch {
|
|
Write-Warning "Failed to parse hyperfine JSON ${OutputPath}: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
timestamp_utc = $started
|
|
finished_utc = $finished
|
|
size_label = $File.label
|
|
bytes = $File.bytes
|
|
algorithm = $Algorithm
|
|
runs = $Runs
|
|
warmup = $Warmup
|
|
export_path = $OutputPath
|
|
exit_code = $exitCode
|
|
stdout_sample = (($output | Select-Object -First 20) -join [Environment]::NewLine)
|
|
stderr_sample = ''
|
|
results = if ($null -ne $parsed) { $parsed.results } else { @() }
|
|
}
|
|
}
|
|
|
|
function ConvertTo-Summary {
|
|
param(
|
|
[object[]]$Records = @()
|
|
)
|
|
|
|
$successful = @($Records | Where-Object exit_code -eq 0)
|
|
$successful |
|
|
Group-Object size_label, algorithm, tool, output_format |
|
|
ForEach-Object {
|
|
$rows = @($_.Group)
|
|
$first = $rows[0]
|
|
$elapsed = @($rows | ForEach-Object { [double]$_.elapsed_ms })
|
|
$cpuMs = @($rows | ForEach-Object { [double]$_.cpu_ms })
|
|
$cpu = @($rows | ForEach-Object { [double]$_.cpu_one_core_percent })
|
|
$memory = @($rows | ForEach-Object { [double]$_.peak_working_set_bytes })
|
|
$bytes = [double]$first.bytes
|
|
$meanElapsed = ($elapsed | Measure-Object -Average).Average
|
|
$minElapsed = ($elapsed | Measure-Object -Minimum).Minimum
|
|
$meanCpuMs = ($cpuMs | Measure-Object -Average).Average
|
|
$maxMemory = ($memory | Measure-Object -Maximum).Maximum
|
|
$meanCpu = ($cpu | Measure-Object -Average).Average
|
|
$throughput = if ($meanElapsed -gt 0) { ($bytes / 1MB) / ($meanElapsed / 1000.0) } else { 0.0 }
|
|
$binarySizeBytes = if ($null -ne $first.binary_size_bytes) { [int64]$first.binary_size_bytes } else { 0L }
|
|
[pscustomobject]@{
|
|
size_label = $first.size_label
|
|
bytes = [int64]$first.bytes
|
|
algorithm = $first.algorithm
|
|
tool = $first.tool
|
|
output_format = $first.output_format
|
|
runs = $rows.Count
|
|
mean_ms = [Math]::Round($meanElapsed, 3)
|
|
min_ms = [Math]::Round($minElapsed, 3)
|
|
mean_cpu_ms = [Math]::Round($meanCpuMs, 3)
|
|
throughput_mib_s = [Math]::Round($throughput, 2)
|
|
peak_working_set_mib = [Math]::Round($maxMemory / 1MB, 2)
|
|
mean_cpu_one_core_percent = [Math]::Round($meanCpu, 1)
|
|
binary_size_bytes = $binarySizeBytes
|
|
binary_size_label = if ($binarySizeBytes -gt 0) { Format-ByteSize -Bytes $binarySizeBytes } else { '' }
|
|
version = $first.version
|
|
}
|
|
} |
|
|
Sort-Object algorithm, bytes, output_format, mean_ms, tool
|
|
}
|
|
|
|
function Write-MarkdownReport {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[object[]]$Summary = @(),
|
|
[Parameter(Mandatory = $true)]
|
|
[object]$Environment,
|
|
[object[]]$Artifacts = @(),
|
|
[object[]]$HyperfineRows = @()
|
|
)
|
|
|
|
$builder = [System.Text.StringBuilder]::new()
|
|
[void]$builder.AppendLine('# Digest Benchmark Report')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('This report is produced by `scripts/benchmark-mhash.ps1`. It measures warm-cache CLI hashing throughput and process telemetry. Windows does not provide a safe non-admin cache-drop primitive, so disk-cold numbers are intentionally out of scope.')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## Environment')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine(('- Mode: `{0}`' -f $Environment.mode))
|
|
[void]$builder.AppendLine(('- Timestamp: `{0}`' -f $Environment.timestamp_utc))
|
|
[void]$builder.AppendLine(('- Machine: `{0}`' -f $Environment.machine_name))
|
|
[void]$builder.AppendLine(('- OS: `{0}`' -f $Environment.os))
|
|
[void]$builder.AppendLine(('- Process architecture: `{0}`' -f $Environment.process_architecture))
|
|
[void]$builder.AppendLine(('- Processor count: `{0}`' -f $Environment.processor_count))
|
|
[void]$builder.AppendLine(('- PowerShell: `{0}`' -f $Environment.powershell_version))
|
|
[void]$builder.AppendLine(('- Git: `{0}` `{1}`' -f $Environment.git.branch, $Environment.git.commit))
|
|
[void]$builder.AppendLine(('- Git status entries: `{0}`' -f @($Environment.git.status_short).Count))
|
|
[void]$builder.AppendLine(('- Sizes: `{0}`' -f ($Environment.sizes -join ', ')))
|
|
[void]$builder.AppendLine(('- Algorithms: `{0}`' -f ($Environment.algorithms -join ', ')))
|
|
[void]$builder.AppendLine(('- Digest formats: `{0}`' -f ($Environment.mhash_formats -join ', ')))
|
|
[void]$builder.AppendLine(('- Repeat: `{0}`' -f $Environment.repeat))
|
|
[void]$builder.AppendLine(('- Warmup: `{0}`' -f $Environment.warmup))
|
|
[void]$builder.AppendLine(('- External tools: `{0}`' -f (-not $Environment.no_external)))
|
|
[void]$builder.AppendLine(('- Hyperfine requested: `{0}`' -f $Environment.hyperfine_requested))
|
|
[void]$builder.AppendLine(('- Data root: `{0}`' -f $Environment.data_root))
|
|
[void]$builder.AppendLine(('- Output root: `{0}`' -f $Environment.output_root))
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## Tool Artifacts')
|
|
[void]$builder.AppendLine()
|
|
if ($Artifacts.Count -eq 0) {
|
|
[void]$builder.AppendLine('No tool artifacts were discovered.')
|
|
}
|
|
else {
|
|
[void]$builder.AppendLine('| Tool | Size | SHA-256 | Path | Version |')
|
|
[void]$builder.AppendLine('|---|---:|---|---|---|')
|
|
foreach ($artifact in ($Artifacts | Sort-Object tool, path)) {
|
|
$sha = if (-not [string]::IsNullOrWhiteSpace($artifact.sha256)) { $artifact.sha256.Substring(0, [Math]::Min(16, $artifact.sha256.Length)) } else { '' }
|
|
[void]$builder.AppendLine(('| {0} | {1} | `{2}` | `{3}` | {4} |' -f $artifact.tool, $artifact.size_label, $sha, $artifact.path, $artifact.version))
|
|
}
|
|
}
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## Summary')
|
|
[void]$builder.AppendLine()
|
|
if ($Summary.Count -eq 0) {
|
|
[void]$builder.AppendLine('No process benchmark records were emitted. This is expected for `-SizeOnly` runs.')
|
|
}
|
|
else {
|
|
[void]$builder.AppendLine('| Algorithm | Size | Tool | Format | Mean ms | Min ms | CPU ms | MiB/s | Peak WS MiB | CPU % of one core | Binary |')
|
|
[void]$builder.AppendLine('|---|---:|---|---|---:|---:|---:|---:|---:|---:|---:|')
|
|
foreach ($row in $Summary) {
|
|
[void]$builder.AppendLine(('| {0} | {1} | {2} | {3} | {4:N3} | {5:N3} | {6:N3} | {7:N2} | {8:N2} | {9:N1} | {10} |' -f $row.algorithm, $row.size_label, $row.tool, $row.output_format, $row.mean_ms, $row.min_ms, $row.mean_cpu_ms, $row.throughput_mib_s, $row.peak_working_set_mib, $row.mean_cpu_one_core_percent, $row.binary_size_label))
|
|
}
|
|
}
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## Hyperfine')
|
|
[void]$builder.AppendLine()
|
|
if ($HyperfineRows.Count -eq 0) {
|
|
if ($Environment.hyperfine_requested) {
|
|
[void]$builder.AppendLine('Hyperfine was requested, but no hyperfine summary rows were produced. Check the raw hyperfine output for command failures.')
|
|
}
|
|
else {
|
|
[void]$builder.AppendLine('Hyperfine was not requested. Pass `-Hyperfine` to add hyperfine timing exports when `hyperfine` is installed.')
|
|
}
|
|
}
|
|
else {
|
|
[void]$builder.AppendLine('| Algorithm | Size | Command | Mean ms | Stddev ms | Min ms | Max ms | Runs |')
|
|
[void]$builder.AppendLine('|---|---:|---|---:|---:|---:|---:|---:|')
|
|
foreach ($row in $HyperfineRows) {
|
|
[void]$builder.AppendLine(('| {0} | {1} | {2} | {3:N3} | {4:N3} | {5:N3} | {6:N3} | {7} |' -f $row.algorithm, $row.size_label, $row.command, $row.mean_ms, $row.stddev_ms, $row.min_ms, $row.max_ms, $row.runs))
|
|
}
|
|
}
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('Raw JSON, JSONL, CSV, artifact, and optional hyperfine files are emitted beside this report for deeper analysis.')
|
|
|
|
Set-Content -LiteralPath $Path -Value $builder.ToString() -Encoding utf8NoBOM
|
|
}
|
|
|
|
$workspaceRoot = Split-Path -Parent $PSScriptRoot
|
|
$outputRootFull = [System.IO.Path]::GetFullPath($OutputRoot)
|
|
$dataRootFull = [System.IO.Path]::GetFullPath($DataRoot)
|
|
New-Item -ItemType Directory -Force -Path $outputRootFull, $dataRootFull | Out-Null
|
|
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$mode = if ($SizeOnly) { 'size-only' } elseif ($Extended) { 'extended' } else { 'default' }
|
|
|
|
if ([string]::IsNullOrWhiteSpace($DigestPath)) {
|
|
$DigestPath = Join-Path $workspaceRoot 'target\release-fast\mhash.exe'
|
|
}
|
|
$DigestPath = [System.IO.Path]::GetFullPath($DigestPath)
|
|
|
|
if (-not $SkipBuild) {
|
|
Invoke-StrictNative -FilePath 'cargo' -ArgumentList @('build', '--profile', 'release-fast', '-p', 'mercury-mhash', '--bin', 'mhash')
|
|
}
|
|
if (-not (Test-Path -LiteralPath $DigestPath -PathType Leaf)) {
|
|
throw "Digest binary not found: $DigestPath"
|
|
}
|
|
|
|
$tools = @{
|
|
mhash = $DigestPath
|
|
mhash_version = Get-ToolVersion -Name 'mhash' -Path $DigestPath
|
|
pwsh = Resolve-CommandPath -Name 'pwsh'
|
|
certutil = Resolve-CommandPath -Name 'certutil'
|
|
openssl = Resolve-CommandPath -Name 'openssl'
|
|
hyperfine = Resolve-CommandPath -Name 'hyperfine'
|
|
cargo = Resolve-CommandPath -Name 'cargo'
|
|
rustc = Resolve-CommandPath -Name 'rustc'
|
|
}
|
|
if ($tools.pwsh) {
|
|
$tools.pwsh_version = Get-ToolVersion -Name 'pwsh' -Path $tools.pwsh -VersionArguments @('-NoProfile', '-Command', '$PSVersionTable.PSVersion.ToString()')
|
|
}
|
|
if ($tools.certutil) {
|
|
$tools.certutil_version = Get-ToolVersion -Name 'certutil' -Path $tools.certutil -VersionArguments @('-?')
|
|
}
|
|
if ($tools.openssl) {
|
|
$tools.openssl_version = Get-ToolVersion -Name 'openssl' -Path $tools.openssl -VersionArguments @('version')
|
|
}
|
|
if ($tools.hyperfine) {
|
|
$tools.hyperfine_version = Get-ToolVersion -Name 'hyperfine' -Path $tools.hyperfine
|
|
}
|
|
if ($tools.cargo) {
|
|
$tools.cargo_version = Get-ToolVersion -Name 'cargo' -Path $tools.cargo
|
|
}
|
|
if ($tools.rustc) {
|
|
$tools.rustc_version = Get-ToolVersion -Name 'rustc' -Path $tools.rustc
|
|
}
|
|
foreach ($algorithm in @('md5', 'sha1', 'sha256', 'sha512')) {
|
|
$name = "${algorithm}sum"
|
|
$path = Resolve-CoreutilsCommandPath -Name $name
|
|
$tools[$name] = $path
|
|
if ($path) {
|
|
$tools["${name}_version"] = Get-ToolVersion -Name $name -Path $path
|
|
}
|
|
}
|
|
|
|
$artifactSpecs = [System.Collections.Generic.List[object]]::new()
|
|
$artifactSpecs.Add([pscustomobject]@{ tool = 'mercury-mhash'; path = $tools.mhash; version = $tools.mhash_version })
|
|
foreach ($spec in @(
|
|
[pscustomobject]@{ tool = 'powershell:pwsh'; path = $tools.pwsh; version = if ($tools.ContainsKey('pwsh_version')) { $tools.pwsh_version } else { '' } },
|
|
[pscustomobject]@{ tool = 'windows:certutil'; path = $tools.certutil; version = if ($tools.ContainsKey('certutil_version')) { $tools.certutil_version } else { '' } },
|
|
[pscustomobject]@{ tool = 'openssl:dgst'; path = $tools.openssl; version = if ($tools.ContainsKey('openssl_version')) { $tools.openssl_version } else { '' } },
|
|
[pscustomobject]@{ tool = 'hyperfine'; path = $tools.hyperfine; version = if ($tools.ContainsKey('hyperfine_version')) { $tools.hyperfine_version } else { '' } }
|
|
)) {
|
|
if (-not [string]::IsNullOrWhiteSpace($spec.path)) {
|
|
$artifactSpecs.Add($spec)
|
|
}
|
|
}
|
|
foreach ($algorithm in @('md5', 'sha1', 'sha256', 'sha512')) {
|
|
$name = "${algorithm}sum"
|
|
if (-not [string]::IsNullOrWhiteSpace($tools[$name])) {
|
|
$artifactSpecs.Add([pscustomobject]@{
|
|
tool = "coreutils:$name"
|
|
path = $tools[$name]
|
|
version = if ($tools.ContainsKey("${name}_version")) { $tools["${name}_version"] } else { '' }
|
|
})
|
|
}
|
|
}
|
|
|
|
$toolArtifacts = @($artifactSpecs | ForEach-Object {
|
|
Get-BinaryArtifactInfo -Tool $_.tool -Path $_.path -Version $_.version
|
|
})
|
|
$artifactByPath = @{}
|
|
foreach ($artifact in $toolArtifacts) {
|
|
if ($artifact.exists) {
|
|
$artifactByPath[$artifact.path] = $artifact
|
|
}
|
|
}
|
|
|
|
$files = @()
|
|
if (-not $SizeOnly) {
|
|
$files = @(foreach ($size in $Sizes) {
|
|
$bytes = ConvertTo-ByteCount -Value $size
|
|
$label = Format-ByteSize -Bytes $bytes
|
|
$path = Join-Path $dataRootFull "mhash-$label.bin"
|
|
Write-Host "Preparing $label fixture: $path"
|
|
New-DeterministicFile -Path $path -Bytes $bytes
|
|
[pscustomobject]@{
|
|
label = $label
|
|
bytes = $bytes
|
|
path = $path
|
|
sha256 = ConvertTo-FileSha256 -Path $path
|
|
}
|
|
})
|
|
}
|
|
|
|
$records = [System.Collections.Generic.List[object]]::new()
|
|
$matrix = [System.Collections.Generic.List[object]]::new()
|
|
if (-not $SizeOnly) {
|
|
foreach ($file in $files) {
|
|
foreach ($algorithm in $Algorithms) {
|
|
$candidates = @(New-CandidatesForAlgorithm -Algorithm $algorithm -InputPath $file.path -DigestExe $DigestPath -Tools $tools -DigestFormats $DigestFormats -NoExternal:$NoExternal)
|
|
foreach ($candidate in $candidates) {
|
|
$matrix.Add([pscustomobject]@{
|
|
file = $file
|
|
algorithm = $algorithm
|
|
candidate = $candidate
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($SizeOnly) {
|
|
Write-Host 'Size-only mode: skipping fixture generation and process benchmarks.'
|
|
}
|
|
else {
|
|
$totalRuns = ($Repeat + $Warmup) * $matrix.Count
|
|
Write-Host "Benchmark matrix: sizes=$($files.Count), algorithms=$($Algorithms.Count), mhash_formats=$($DigestFormats.Count), cases=$($matrix.Count), process_runs=$totalRuns, repeat=$Repeat, warmup=$Warmup"
|
|
foreach ($case in $matrix) {
|
|
$file = $case.file
|
|
$algorithm = $case.algorithm
|
|
$candidate = $case.candidate
|
|
$candidatePath = [System.IO.Path]::GetFullPath($candidate.FilePath)
|
|
$artifact = if ($artifactByPath.ContainsKey($candidatePath)) { $artifactByPath[$candidatePath] } else { $null }
|
|
$binarySizeBytes = if (($null -ne $artifact) -and ($artifact.exists)) { [int64]$artifact.size_bytes } else { 0L }
|
|
for ($iteration = -$Warmup; $iteration -lt $Repeat; $iteration++) {
|
|
$isWarmup = $iteration -lt 0
|
|
$label = if ($isWarmup) { 'warmup' } else { "run $($iteration + 1)" }
|
|
Write-Host ("[{0} {1} {2}/{3}] {4}" -f $file.label, $algorithm, $candidate.Tool, $candidate.OutputFormat, $label)
|
|
$measurement = Invoke-MeasuredProcess -Candidate $candidate
|
|
if ($measurement.exit_code -ne 0) {
|
|
Write-Warning "$($candidate.Tool) $algorithm failed with exit code $($measurement.exit_code): $($measurement.stderr)"
|
|
}
|
|
if (-not $isWarmup) {
|
|
$records.Add([pscustomobject]@{
|
|
timestamp_utc = (Get-Date).ToUniversalTime().ToString('o')
|
|
size_label = $file.label
|
|
bytes = $file.bytes
|
|
fixture_sha256 = $file.sha256
|
|
algorithm = $algorithm
|
|
tool = $candidate.Tool
|
|
output_format = $candidate.OutputFormat
|
|
version = $candidate.Version
|
|
binary_path = $candidatePath
|
|
binary_size_bytes = $binarySizeBytes
|
|
binary_size_label = if ($binarySizeBytes -gt 0) { Format-ByteSize -Bytes $binarySizeBytes } else { '' }
|
|
repeat = $iteration + 1
|
|
elapsed_ms = [Math]::Round($measurement.elapsed_ms, 3)
|
|
cpu_ms = [Math]::Round($measurement.cpu_ms, 3)
|
|
cpu_one_core_percent = [Math]::Round($measurement.cpu_one_core_percent, 3)
|
|
cpu_machine_percent = [Math]::Round($measurement.cpu_machine_percent, 3)
|
|
peak_working_set_bytes = $measurement.peak_working_set_bytes
|
|
exit_code = $measurement.exit_code
|
|
stdout_sample = if ($measurement.stdout.Length -gt 160) { $measurement.stdout.Substring(0, 160) } else { $measurement.stdout }
|
|
stderr_sample = if ($measurement.stderr.Length -gt 160) { $measurement.stderr.Substring(0, 160) } else { $measurement.stderr }
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$hyperfineSuites = [System.Collections.Generic.List[object]]::new()
|
|
if ($Hyperfine -and (-not $SizeOnly)) {
|
|
if ([string]::IsNullOrWhiteSpace($tools.hyperfine)) {
|
|
Write-Warning 'Hyperfine was requested but hyperfine was not found on PATH. Skipping hyperfine integration.'
|
|
}
|
|
else {
|
|
foreach ($file in $files) {
|
|
foreach ($algorithm in $Algorithms) {
|
|
$suiteCandidates = @($matrix |
|
|
Where-Object { ($_.file.path -eq $file.path) -and ($_.algorithm -eq $algorithm) } |
|
|
ForEach-Object { $_.candidate })
|
|
if ($suiteCandidates.Count -eq 0) {
|
|
continue
|
|
}
|
|
$hyperfineSuitePath = Join-Path $outputRootFull ("mhash-benchmark-{0}.hyperfine.{1}.{2}.json" -f $timestamp, $algorithm, $file.label)
|
|
Write-Host ("[hyperfine {0} {1}] commands={2}, runs={3}, warmup={4}" -f $file.label, $algorithm, $suiteCandidates.Count, $HyperfineRuns, $HyperfineWarmup)
|
|
$suite = Invoke-HyperfineSuite -HyperfinePath $tools.hyperfine -File $file -Algorithm $algorithm -Candidates $suiteCandidates -OutputPath $hyperfineSuitePath -Runs $HyperfineRuns -Warmup $HyperfineWarmup
|
|
if ($suite.exit_code -ne 0) {
|
|
Write-Warning "hyperfine $algorithm $($file.label) failed with exit code $($suite.exit_code)."
|
|
}
|
|
$hyperfineSuites.Add($suite)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$hyperfineRows = @(foreach ($suite in $hyperfineSuites) {
|
|
foreach ($result in @($suite.results)) {
|
|
$times = @($result.times | ForEach-Object { [double]$_ })
|
|
$min = if ($null -ne $result.min) { [double]$result.min } elseif ($times.Count -gt 0) { ($times | Measure-Object -Minimum).Minimum } else { 0.0 }
|
|
$max = if ($null -ne $result.max) { [double]$result.max } elseif ($times.Count -gt 0) { ($times | Measure-Object -Maximum).Maximum } else { 0.0 }
|
|
[pscustomobject]@{
|
|
size_label = $suite.size_label
|
|
bytes = $suite.bytes
|
|
algorithm = $suite.algorithm
|
|
command = $result.command
|
|
runs = $times.Count
|
|
mean_ms = [Math]::Round(([double]$result.mean) * 1000.0, 3)
|
|
stddev_ms = [Math]::Round(([double]$result.stddev) * 1000.0, 3)
|
|
min_ms = [Math]::Round($min * 1000.0, 3)
|
|
max_ms = [Math]::Round($max * 1000.0, 3)
|
|
}
|
|
}
|
|
})
|
|
|
|
$rawPath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.raw.json"
|
|
$recordsJsonlPath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.records.jsonl"
|
|
$summaryPath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.summary.json"
|
|
$summaryJsonlPath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.summary.jsonl"
|
|
$csvPath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.summary.csv"
|
|
$artifactPath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.artifacts.json"
|
|
$artifactCsvPath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.artifacts.csv"
|
|
$hyperfinePath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.hyperfine.json"
|
|
$hyperfineCsvPath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.hyperfine.csv"
|
|
$markdownPath = Join-Path $outputRootFull "mhash-benchmark-$timestamp.md"
|
|
|
|
$environment = [pscustomobject]@{
|
|
mode = $mode
|
|
timestamp_utc = (Get-Date).ToUniversalTime().ToString('o')
|
|
invocation = $MyInvocation.Line
|
|
script_path = $PSCommandPath
|
|
workspace_root = [System.IO.Path]::GetFullPath($workspaceRoot)
|
|
machine_name = $env:COMPUTERNAME
|
|
os = [System.Runtime.InteropServices.RuntimeInformation]::OSDescription
|
|
os_architecture = [string][System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
|
process_architecture = [string][System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture
|
|
processor_count = [Environment]::ProcessorCount
|
|
powershell_version = $PSVersionTable.PSVersion.ToString()
|
|
git = Get-GitMetadata -Root $workspaceRoot
|
|
repeat = $Repeat
|
|
warmup = $Warmup
|
|
sizes = $Sizes
|
|
algorithms = $Algorithms
|
|
mhash_formats = $DigestFormats
|
|
no_external = [bool]$NoExternal
|
|
extended = [bool]$Extended
|
|
size_only = [bool]$SizeOnly
|
|
hyperfine_requested = [bool]$Hyperfine
|
|
hyperfine_available = -not [string]::IsNullOrWhiteSpace($tools.hyperfine)
|
|
hyperfine_runs = $HyperfineRuns
|
|
hyperfine_warmup = $HyperfineWarmup
|
|
data_root = $dataRootFull
|
|
output_root = $outputRootFull
|
|
tools = $tools
|
|
tool_artifacts = $toolArtifacts
|
|
fixtures = $files
|
|
}
|
|
|
|
$summary = @(ConvertTo-Summary -Records @($records))
|
|
[pscustomobject]@{
|
|
environment = $environment
|
|
records = $records
|
|
summary = $summary
|
|
tool_artifacts = $toolArtifacts
|
|
hyperfine_suites = $hyperfineSuites
|
|
hyperfine_summary = $hyperfineRows
|
|
} | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $rawPath -Encoding utf8NoBOM
|
|
|
|
Write-JsonLines -Path $recordsJsonlPath -InputObject @($records) -Depth 8
|
|
ConvertTo-Json -InputObject $summary -Depth 7 | Set-Content -LiteralPath $summaryPath -Encoding utf8NoBOM
|
|
Write-JsonLines -Path $summaryJsonlPath -InputObject $summary -Depth 7
|
|
if ($summary.Count -gt 0) {
|
|
$summary | Export-Csv -LiteralPath $csvPath -NoTypeInformation -Encoding utf8NoBOM
|
|
}
|
|
else {
|
|
[System.IO.File]::WriteAllText($csvPath, '', [System.Text.UTF8Encoding]::new($false))
|
|
}
|
|
ConvertTo-Json -InputObject $toolArtifacts -Depth 7 | Set-Content -LiteralPath $artifactPath -Encoding utf8NoBOM
|
|
$toolArtifacts | Export-Csv -LiteralPath $artifactCsvPath -NoTypeInformation -Encoding utf8NoBOM
|
|
ConvertTo-Json -InputObject @($hyperfineSuites) -Depth 12 | Set-Content -LiteralPath $hyperfinePath -Encoding utf8NoBOM
|
|
if ($hyperfineRows.Count -gt 0) {
|
|
$hyperfineRows | Export-Csv -LiteralPath $hyperfineCsvPath -NoTypeInformation -Encoding utf8NoBOM
|
|
}
|
|
else {
|
|
[System.IO.File]::WriteAllText($hyperfineCsvPath, '', [System.Text.UTF8Encoding]::new($false))
|
|
}
|
|
Write-MarkdownReport -Path $markdownPath -Summary $summary -Environment $environment -Artifacts $toolArtifacts -HyperfineRows $hyperfineRows
|
|
|
|
Write-Host "Raw results: $rawPath"
|
|
Write-Host "Records JSONL: $recordsJsonlPath"
|
|
Write-Host "Summary JSON: $summaryPath"
|
|
Write-Host "Summary JSONL: $summaryJsonlPath"
|
|
Write-Host "Summary CSV: $csvPath"
|
|
Write-Host "Artifact JSON: $artifactPath"
|
|
Write-Host "Artifact CSV: $artifactCsvPath"
|
|
if ($Hyperfine) {
|
|
Write-Host "Hyperfine JSON: $hyperfinePath"
|
|
Write-Host "Hyperfine CSV: $hyperfineCsvPath"
|
|
}
|
|
Write-Host "Markdown report: $markdownPath"
|