Skip to content

Instantly share code, notes, and snippets.

@Stewie410
Created July 2, 2024 15:33
Show Gist options
  • Save Stewie410/888b395415960564fbdda1d625e8f84a to your computer and use it in GitHub Desktop.
Save Stewie410/888b395415960564fbdda1d625e8f84a to your computer and use it in GitHub Desktop.
ps7 download benchmark, with Hyperfine
<#
.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