-
-
Save Stewie410/888b395415960564fbdda1d625e8f84a to your computer and use it in GitHub Desktop.
ps7 download benchmark, with Hyperfine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Benchmark PowerShell download method(s) | |
.DESCRIPTION | |
Benchmark PowerShell download method(s), with hyperfine | |
.PARAMETER Uri | |
Specify URI to download (default: latest PS7 release) | |
.PARAMETER Warmup | |
Amount of warmup operations to perform (default: 3) | |
.PARAMETER Runs | |
Number of runs for each benchmark (default: 5) | |
.PARAMETER All | |
Run benchmarks for all download methods (default) | |
.PARAMETER WebRequest | |
Only run benchmark for Invoke-WebRequest | |
.PARAMETER RestMethod | |
Only run benchmark for Invoke-RestMethod | |
.PARAMETER WebClient | |
Only run benchmark for WebClient | |
.PARAMETER HttpClient | |
Only run benchmark for HttpClient | |
.PARAMETER Socket | |
Only run benchmark for Socket | |
.PARAMETER BitsTransfer | |
Only run benchmark for Start-BitsTransfer | |
.PARAMETER HttpClientAsync | |
Only run benchmark for Async HttpClient | |
.PARAMETER MaxRetries | |
Maximum number of download attempts for HighPerf benchmark | |
.PARAMETER RetryDelay | |
Time to wait between download attempts in Seconds, for HighPerf benchmark | |
#> | |
[CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'all')] | |
param( | |
[Parameter(Position = 0)] | |
[string] | |
$Uri = ( | |
(Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest').asserts | | |
Where-Object { $_.name -like '*win-x64.msi' } | | |
Select-Object -ExpandProperty 'browser_download_url' -First 1 | |
), | |
[Parameter()] | |
[int] | |
$Warmup = 3, | |
[Parameter()] | |
[int] | |
$Runs = 5, | |
[Parameter(ParameterSetName = 'all')] | |
[switch] | |
$All, | |
[Parameter(Mandatory, ParameterSetname = 'WebRequest')] | |
[switch] | |
$WebRequest, | |
[Parameter(Mandatory, ParameterSetName = 'RestMethod')] | |
[switch] | |
$RestMethod, | |
[Parameter(Mandatory, ParameterSetName = 'WebClient')] | |
[switch] | |
$WebClient, | |
[Parameter(Mandatory, ParameterSetName = 'HttpClient')] | |
[switch] | |
$HttpClient, | |
[Parameter(Mandatory, ParameterSetName = 'Socket')] | |
[switch] | |
$Socket, | |
[Parameter(Mandatory, ParameterSetName = 'BitsTransfer')] | |
[switch] | |
$BitsTransfer, | |
[Parameter(Mandatory, ParameterSetName = 'Async')] | |
[switch] | |
$HttpClientAsync, | |
[Parameter(ParameterSetName = 'Async')] | |
[Parameter(ParameterSetName = 'all')] | |
[int] | |
$MaxRetries = 3, | |
[Parameter(ParameterSetName = 'Async')] | |
[Parameter(ParameterSetName = 'all')] | |
[int] | |
$RetryDelay = 2 | |
) | |
function Invoke-Benchmark { | |
param([System.Collections.HashTable] $table) | |
$stage = (New-TemporaryFile).FullName | |
Remove-Item -Path $stage -Force | |
$static = @( | |
"--warmup", $Warmup, | |
"--runs", $Runs, | |
# "--show-output", | |
"--shell", "pwsh", | |
"--prepare", """New-Item -Path $stage -ItemType 'File' -Force""", | |
"--cleanup", """Remove-Item -Path $stage -Force""" | |
) | |
$names = @( | |
foreach ($name in $table.Keys) { | |
"--command-name" | |
$name | |
} | |
) | |
$actions = @( | |
foreach ($k in $table.Keys) { | |
"'Invoke-Command -ScriptBlock {" + $table[$k] + "} -ArgumentList @(""$stage"")'" | |
} | |
) | |
$opts = $static + $names + $actions | |
hyperfine @opts | |
} | |
function Get-SocketAction { | |
return { | |
param([string] $destination) | |
$url = [System.Uri]::new($Uri) | |
$client = [System.Net.Sockets.TcpClient]::new($url.Host, 443) | |
$sslStream = [System.Net.Security.SslStream]::new($client.GetStream()) | |
$sslStream.AuthenticateAsClient($url.Host) | |
$request = "GET $($url.AbsolutePath)$($url.Query) HTTP/1.1`nHost: $($url.Host)`nUser-Agent: PowerShell/7.4.3`nConnection: close`n`n" | |
$requestBytes = [System.Text.Encoding]::ASCII.GetBytes($request) | |
$sslStream.Write($requestBytes) | |
$sslStream.Flush() | |
$buffer = [byte[]]::new(8192) | |
$response = [System.IO.MemoryStream]::new() | |
while ($true) { | |
$received = $sslStream.Read($buffer, 0, $buffer.Length) | |
if ($received -eq 0) { break } | |
$response.Write($buffer, 0, $received) | |
} | |
$response.Position = 0 | |
$reader = [System.IO.StreamReader]::new($response) | |
$headers = "" | |
while (($line = $reader.ReadLine()) -ne "") { | |
$headers += $line + "`n" | |
} | |
if ($VerboseOutput) { | |
Write-Host "Response Headers:`n$headers" | |
} | |
if ($headers -match "Location: (.+)") { | |
$redirectUrl = $matches[1].Trim() | |
if ($VerboseOutput) { | |
Write-Host "Following redirect to $redirectUrl" | |
} | |
Download-FileUsingSocket -url $redirectUrl -destination $destination | |
return | |
} | |
if ($headers -match "Content-Length: (\d+)") { | |
$contentLength = [int]$matches[1] | |
$contentBytes = [byte[]]::new($contentLength) | |
$response.Read($contentBytes, 0, $contentLength) | |
[System.IO.File]::WriteAllBytes($destination, $contentBytes) | |
} else { | |
Write-Host "Socket: Content-Length not found in headers." | |
} | |
$sslStream.Close() | |
$client.Close() | |
} | |
} | |
# TODO: reimplement in powershell directly (oof) | |
function Get-HttpClientAsyncAction { | |
return { | |
param([string] $destination) | |
$cs = @" | |
using System; | |
using System.IO; | |
using System.Net.Http; | |
using System.Threading.Tasks; | |
public class FileDownloader | |
{ | |
public static async Task DownloadFileAsync(string url, string destination, int maxRetries, int retryDelaySeconds, Action<string> logAction) | |
{ | |
var handler = new SocketsHttpHandler | |
{ | |
PooledConnectionLifetime = TimeSpan.FromMinutes(15) | |
}; | |
var httpClient = new HttpClient(handler); | |
int attempt = 0; | |
while (attempt < maxRetries) | |
{ | |
try | |
{ | |
using (var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)) | |
using (var streamToReadFrom = await response.Content.ReadAsStreamAsync()) | |
using (var fileStream = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 81920, useAsync: true)) | |
{ | |
await streamToReadFrom.CopyToAsync(fileStream); | |
} | |
break; | |
} | |
catch (Exception ex) | |
{ | |
logAction?.Invoke($"Attempt {attempt + 1} failed: {ex.Message}"); | |
attempt++; | |
if (attempt >= maxRetries) | |
{ | |
throw; | |
} | |
await Task.Delay(TimeSpan.FromSeconds(retryDelaySeconds)); | |
} | |
} | |
} | |
} | |
"@ | |
Add-Type -TypeDefinition $cs | |
[FileDownloader]::DownloadFileAsync($Uri, $destination, $maxRetries, 3, 2, { | |
param($message) | |
Write-Verbose -Message $message | |
}).GetAwaiter().GetResult() | |
} | |
} | |
function Get-WebRequestAction { | |
return { | |
param([string] $destination) | |
Invoke-WebRequest -Uri $Uri -OutFile $destination | |
} | |
} | |
function Get-RestMethodAction { | |
return { | |
param([string] $destination) | |
Invoke-RestMethod -Uri $Uri -OutFile $destination | |
} | |
} | |
function Get-WebClientAction { | |
return { | |
param([string] $destination) | |
$client = [System.Net.WebClient]::new() | |
$client.DownloadFile($Uri, $destination) | |
$client.Dispose() | |
} | |
} | |
function Get-HttpClientAction { | |
return { | |
param([string] $destination) | |
$client = [System.Net.Http.HttpClient]::new() | |
$response = $client.GetAsync($Uri).Result | |
[System.IO.File]::WriteAllBytes($destination, $response.COntent.ReadAsByteArrayAsync().Result) | |
$client.Dispose() | |
} | |
} | |
function Get-BitsTransferAction { | |
return { | |
param([string] $destination) | |
$job = Start-BitsTransfer -Source $Uri -Destination $destination | |
while ($job.JobState -eq 'Transferring') { | |
Start-Sleep -Milliseconds 10 | |
} | |
} | |
} | |
# Require hyperfine in path | |
Get-Command -Name 'hyperfine' -ErrorAction 'Stop' | Out-Null | |
# Determine benchmarks to run | |
$blocks = @{} | |
$VerboseOutput = $PSBoundParameters["Verbose"] | |
switch ($PSCmdlet.ParameterSetName) { | |
'WebRequest' { | |
$blocks["Invoke-WebRequest"] = Get-WebRequestAction | |
} | |
'RestMethod' { | |
$blocks["Invoke-RestMethod"] = Get-RestMethodAction | |
} | |
'WebClient' { | |
$blocks["System.Net.WebClient"] = Get-WebClientAction | |
} | |
'HttpClient' { | |
$blocks["System.Net.Http.HttpClient"] = Get-HttpClientAction | |
} | |
'Socket' { | |
$blocks["Socket"] = Get-SocketAction | |
} | |
'BitsTransfer' { | |
$blocks["Start-BitsTransfer"] = Get-BitsTransferAction | |
} | |
'HighPerf' { | |
$blocks["System.Net.Http.Client (Async)"] = Get-HttpClientAsyncAction | |
} | |
default { | |
$blocks = @{ | |
"Invoke-WebRequest" = Get-WebRequestAction | |
"Invoke-RestMethod" = Get-RestMethodAction | |
"System.Net.WebClient" = Get-WebClientAction | |
"System.Net.Http.HttpClient" = Get-HttpClientAction | |
"System.Net.Http.HttpClient (Async)" = Get-HttpClientAsyncAction | |
"Socket" = Get-SocketAction | |
"Start-BitsTransfer" = Get-BitsTransferAction | |
} | |
} | |
} | |
# Run benchmark(s) | |
Invoke-Benchmark $blocks |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment