Created
June 30, 2024 14:20
-
-
Save aollivierre/8706734de92749cde9ba27ef72d0c1c8 to your computer and use it in GitHub Desktop.
Benchmark-Download-PS7
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
param ( | |
[switch]$VerboseOutput | |
) | |
# Define the URL for the latest PowerShell 7 release | |
$url = (Invoke-RestMethod https://api.github.com/repos/PowerShell/PowerShell/releases/latest).assets | | |
Where-Object { $_.name -like '*win-x64.msi' } | | |
Select-Object -ExpandProperty browser_download_url | |
Write-Host "Downloading PowerShell from $url" | |
$baseFolder = "C:\tmp" | |
$minFileSizeMB = 100 | |
$maxRetries = 3 | |
$retryDelaySeconds = 2 | |
# Function to log the time taken for each method | |
function Log-Time { | |
param ( | |
[string]$MethodName, | |
[int]$Time | |
) | |
Write-Host "$MethodName Time: $Time ms" | |
return @{MethodName=$MethodName; Time=$Time} | |
} | |
# Function to check if the file size is greater than or equal to $minFileSizeMB | |
function Verify-FileSize { | |
param ( | |
[string]$filePath, | |
[int]$minSizeMB | |
) | |
$fileInfo = Get-Item $filePath | |
return $fileInfo.Length -ge ($minSizeMB * 1MB) | |
} | |
# Function to create a folder for a method and return the destination path | |
function Get-DestinationPath { | |
param ( | |
[string]$MethodName, | |
[string]$Extension | |
) | |
$methodFolder = "$baseFolder\$MethodName" | |
if (-not (Test-Path -Path $methodFolder)) { | |
New-Item -Path $methodFolder -ItemType Directory | Out-Null | |
} | |
$timestamp = Get-Date -Format "yyyyMMddHHmmss" | |
return "$methodFolder\pwsh-$timestamp.$Extension" | |
} | |
# Function to download the file using TcpClient and NetworkStream | |
function Download-FileUsingSocket { | |
param ( | |
[string]$url, | |
[string]$destination | |
) | |
$uri = [System.Uri]::new($url) | |
$client = [System.Net.Sockets.TcpClient]::new($uri.Host, 443) | |
$sslStream = [System.Net.Security.SslStream]::new($client.GetStream()) | |
$sslStream.AuthenticateAsClient($uri.Host) | |
$request = "GET $($uri.AbsolutePath)$($uri.Query) HTTP/1.1`r`nHost: $($uri.Host)`r`nUser-Agent: PowerShell/7.4.3`r`nConnection: close`r`n`r`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 + "`r`n" | |
} | |
if ($VerboseOutput) { | |
Write-Host "Response Headers:`r`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() | |
} | |
# Function to download file using HttpClient with streaming and retry logic | |
function Download-FileWithHttpClient { | |
param ( | |
[string]$url, | |
[string]$destination | |
) | |
Add-Type -TypeDefinition @" | |
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)); | |
} | |
} | |
} | |
} | |
"@ | |
[FileDownloader]::DownloadFileAsync($url, $destination, $maxRetries, $retryDelaySeconds, { | |
param($message) | |
if ($VerboseOutput) { | |
Write-Host $message | |
} | |
}).GetAwaiter().GetResult() | |
} | |
# Create the C:\tmp folder if it doesn't exist | |
if (-not (Test-Path -Path $baseFolder)) { | |
New-Item -Path $baseFolder -ItemType Directory | Out-Null | |
} | |
# Initialize stopwatch | |
$stopwatch = [System.Diagnostics.Stopwatch]::new() | |
# Collect results | |
$results = [System.Collections.Generic.List[PSObject]]::new() | |
# Method 1: Using Invoke-WebRequest | |
$destination = Get-DestinationPath -MethodName "Invoke-WebRequest" -Extension "msi" | |
$stopwatch.Restart() | |
Invoke-WebRequest -Uri $url -OutFile $destination | |
$stopwatch.Stop() | |
if (Verify-FileSize -filePath $destination -minSizeMB $minFileSizeMB) { | |
$results.Add((Log-Time -MethodName "Invoke-WebRequest" -Time $stopwatch.ElapsedMilliseconds)) | |
} else { | |
Write-Host "Invoke-WebRequest: Download failed. File size check failed." | |
} | |
# Method 2: Using Invoke-RestMethod | |
$destination = Get-DestinationPath -MethodName "Invoke-RestMethod" -Extension "msi" | |
$stopwatch.Restart() | |
Invoke-RestMethod -Uri $url -OutFile $destination | |
$stopwatch.Stop() | |
if (Verify-FileSize -filePath $destination -minSizeMB $minFileSizeMB) { | |
$results.Add((Log-Time -MethodName "Invoke-RestMethod" -Time $stopwatch.ElapsedMilliseconds)) | |
} else { | |
Write-Host "Invoke-RestMethod: Download failed. File size check failed." | |
} | |
# Method 3: Using System.Net.WebClient | |
$destination = Get-DestinationPath -MethodName "WebClient" -Extension "msi" | |
$webclient = [System.Net.WebClient]::new() | |
$stopwatch.Restart() | |
$webclient.DownloadFile($url, $destination) | |
$stopwatch.Stop() | |
$webclient.Dispose() | |
if (Verify-FileSize -filePath $destination -minSizeMB $minFileSizeMB) { | |
$results.Add((Log-Time -MethodName "WebClient" -Time $stopwatch.ElapsedMilliseconds)) | |
} else { | |
Write-Host "WebClient: Download failed. File size check failed." | |
} | |
# Method 4: Using System.Net.Http.HttpClient | |
$destination = Get-DestinationPath -MethodName "HttpClient" -Extension "msi" | |
$httpclient = [System.Net.Http.HttpClient]::new() | |
$stopwatch.Restart() | |
$response = $httpclient.GetAsync($url).Result | |
[System.IO.File]::WriteAllBytes($destination, $response.Content.ReadAsByteArrayAsync().Result) | |
$stopwatch.Stop() | |
$httpclient.Dispose() | |
if (Verify-FileSize -filePath $destination -minSizeMB $minFileSizeMB) { | |
$results.Add((Log-Time -MethodName "HttpClient" -Time $stopwatch.ElapsedMilliseconds)) | |
} else { | |
Write-Host "HttpClient: Download failed. File size check failed." | |
} | |
# Method 5: Using TcpClient and SslStream (Socket) | |
$destination = Get-DestinationPath -MethodName "Socket" -Extension "msi" | |
$stopwatch.Restart() | |
try { | |
Download-FileUsingSocket -url $url -destination $destination | |
} | |
catch { | |
Write-Host "Socket: Error downloading file - $_" | |
} | |
finally { | |
$stopwatch.Stop() | |
if (Verify-FileSize -filePath $destination -minSizeMB $minFileSizeMB) { | |
$results.Add((Log-Time -MethodName "Socket" -Time $stopwatch.ElapsedMilliseconds)) | |
} else { | |
Write-Host "Socket: Download failed. File size check failed." | |
} | |
} | |
# Method 6: Using Start-BitsTransfer | |
$destination = Get-DestinationPath -MethodName "Start-BitsTransfer" -Extension "msi" | |
$stopwatch.Restart() | |
$job = Start-BitsTransfer -Source $url -Destination $destination | |
while ($job.JobState -eq 'Transferring') { | |
Start-Sleep -Seconds 1 | |
} | |
$stopwatch.Stop() | |
if (Verify-FileSize -filePath $destination -minSizeMB $minFileSizeMB) { | |
$results.Add((Log-Time -MethodName "Start-BitsTransfer" -Time $stopwatch.ElapsedMilliseconds)) | |
} else { | |
Write-Host "Start-BitsTransfer: Download failed. File size check failed." | |
} | |
# Method 7: High-Performance Method using HttpClient with streaming and retry logic | |
$destination = Get-DestinationPath -MethodName "HttpClient-HighPerf" -Extension "msi" | |
$stopwatch.Restart() | |
try { | |
Download-FileWithHttpClient -url $url -destination $destination | |
} | |
catch { | |
Write-Host "HttpClient-HighPerf: Error downloading file - $_" | |
} | |
finally { | |
$stopwatch.Stop() | |
if (Verify-FileSize -filePath $destination -minSizeMB $minFileSizeMB) { | |
$results.Add((Log-Time -MethodName "HttpClient-HighPerf" -Time $stopwatch.ElapsedMilliseconds)) | |
} else { | |
Write-Host "HttpClient-HighPerf: Download failed. File size check failed." | |
} | |
} | |
# Summary of all methods | |
Write-Host "Summary:" | |
foreach ($result in $results) { | |
Write-Host "$($result.MethodName) Time: $($result.Time) ms" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment