diff --git a/.gitattributes b/.gitattributes index 96c2e0d3..f9336d18 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,9 @@ # Needed for publishing of examples, build worker defaults to core.autocrlf=input. * text eol=autocrlf +# Preserve the published guidance scripts' upstream LF representation. +guidance/* text eol=lf + *.mof text eol=crlf *.sh text eol=lf *.svg eol=lf diff --git a/docs/content/index.md b/docs/content/index.md index 8e126e11..2eb0b96b 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -57,6 +57,7 @@ Look up the framework's exact contracts and the module-development standards it | [Scenario matrix](reference/scenario-matrix.md) | Which jobs run for each trigger scenario. | | [Framework test IDs](reference/framework-test-ids.md) | The framework tests enforced on source code and on the built module. | | [Dependencies](reference/dependencies.md) | The actions, modules, and services the workflow composes. | +| [PowerShell guidance scripts](reference/guidance-scripts.md) | Runnable reference scripts for common PowerShell implementation patterns. | ## Specification diff --git a/docs/content/reference/guidance-scripts.md b/docs/content/reference/guidance-scripts.md new file mode 100644 index 00000000..745a7ce0 --- /dev/null +++ b/docs/content/reference/guidance-scripts.md @@ -0,0 +1,31 @@ +--- +title: PowerShell guidance scripts +description: Reference PowerShell scripts that demonstrate implementation patterns used by the PSModule framework. +--- + +# PowerShell guidance scripts + +The [guidance directory](https://github.com/PSModule/Process-PSModule/tree/main/guidance) contains runnable reference scripts for common PowerShell implementation patterns. They are framework learning assets, not Process-PSModule module source, so they are intentionally kept outside `src/` and are not included in a module manifest. + +Run an individual script only when its scenario is suitable for the local environment. Several scripts create temporary files, make web requests, or measure execution time. + +| Script | Focus | +| --- | --- | +| `Add-Array.ps1` | Array and generic list population | +| `Add-HashTable.ps1` | Hashtable population styles | +| `Add-String.ps1` | String construction approaches | +| `Caller.ps1` | Caller discovery through the PowerShell call stack | +| `ClassExtension.ps1` | Class inheritance and base-method invocation | +| `Loops.ps1` | Loop and function-call overhead | +| `Out-Null.ps1` | Discarding command output | +| `PipelineExecution.ps1` | Pipeline parameter evaluation and lifecycle blocks | +| `PSCallStack.ps1` | Nested-call stack inspection | +| `PSCmdlet.ps1` | The `$PSCmdlet` variable at nested call levels | +| `PSModuleTest.psm1` | Module-component import and exports | +| `Read-File.ps1` | File-reading approaches | +| `root.ps1` | Nested PSScriptAnalyzer binary-module loading | +| `WebCalls.ps1` | Web-request protocol comparisons | + +## Maintenance + +The files were imported byte-for-byte from [`PSModule/docs/guidance`](https://github.com/PSModule/docs/tree/main/guidance). When that published set changes, import the complete current file from its Git blob into this directory and preserve its contents. Keep this index synchronized with the directory so users can discover every available script. diff --git a/docs/zensical.toml b/docs/zensical.toml index cdd222bf..38152659 100644 --- a/docs/zensical.toml +++ b/docs/zensical.toml @@ -47,6 +47,7 @@ nav = [ {"Scenario matrix" = "reference/scenario-matrix.md"}, {"Framework test IDs" = "reference/framework-test-ids.md"}, {"Dependencies" = "reference/dependencies.md"}, + {"PowerShell guidance scripts" = "reference/guidance-scripts.md"}, ]}, {"Specification" = [ "specification/index.md", diff --git a/guidance/Add-Array.ps1 b/guidance/Add-Array.ps1 new file mode 100644 index 00000000..83f57f8e --- /dev/null +++ b/guidance/Add-Array.ps1 @@ -0,0 +1,50 @@ +$tests = @{ + 'PowerShell Explicit Assignment' = { + param($count) + + $result = foreach ($i in 1..$count) { + $i + } + $null = $result # just added for linter issues + } + '.Add(..) to List' = { + param($count) + + $result = [Collections.Generic.List[int]]::new() + foreach ($i in 1..$count) { + $result.Add($i) + } + } + '+= Operator to Array' = { + param($count) + + $result = @() + foreach ($i in 1..$count) { + $result += $i + } + } +} + +5kb, 10kb, 100kb | ForEach-Object { + $groupResult = foreach ($test in $tests.GetEnumerator()) { + $ms = (Measure-Command { & $test.Value -Count $_ }).TotalMilliseconds + + [pscustomobject]@{ + CollectionSize = $_ + Test = $test.Key + TotalMilliseconds = [math]::Round($ms, 2) + } + + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + } + + $groupResult = $groupResult | Sort-Object TotalMilliseconds + $groupResult | Select-Object *, @{ + Name = 'RelativeSpeed' + Expression = { + $relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds + [math]::Round($relativeSpeed, 2).ToString() + 'x' + } + } +} diff --git a/guidance/Add-HashTable.ps1 b/guidance/Add-HashTable.ps1 new file mode 100644 index 00000000..36ed6aa4 --- /dev/null +++ b/guidance/Add-HashTable.ps1 @@ -0,0 +1,17 @@ +# Adding 10000 items property-style +Measure-Command { + $HashProp = @{} + 1..10000 | ForEach-Object { $HashProp.$_ = $_ } +} + +# Adding 10000 items using the Add method +Measure-Command { + $HashMethod = @{} + 1..10000 | ForEach-Object { $HashMethod.Add($_, $_) } +} + +# Adding 10000 items dictionary-style +Measure-Command { + $HashDict = @{} + 1..10000 | ForEach-Object { $HashDict[$_] = $_ } +} diff --git a/guidance/Add-String.ps1 b/guidance/Add-String.ps1 new file mode 100644 index 00000000..c27dac10 --- /dev/null +++ b/guidance/Add-String.ps1 @@ -0,0 +1,48 @@ +$tests = @{ + 'StringBuilder' = { + $sb = [System.Text.StringBuilder]::new() + foreach ($i in 0..$args[0]) { + $sb = $sb.AppendLine("Iteration $i") + } + $sb.ToString() + } + 'Join operator' = { + $string = @( + foreach ($i in 0..$args[0]) { + "Iteration $i" + } + ) -join "`n" + $string + } + 'Addition Assignment +=' = { + $string = '' + foreach ($i in 0..$args[0]) { + $string += "Iteration $i`n" + } + $string + } +} + +10kb, 50kb, 100kb | ForEach-Object { + $groupResult = foreach ($test in $tests.GetEnumerator()) { + $ms = (Measure-Command { & $test.Value $_ }).TotalMilliseconds + + [pscustomobject]@{ + Iterations = $_ + Test = $test.Key + TotalMilliseconds = [math]::Round($ms, 2) + } + + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + } + + $groupResult = $groupResult | Sort-Object TotalMilliseconds + $groupResult | Select-Object *, @{ + Name = 'RelativeSpeed' + Expression = { + $relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds + [math]::Round($relativeSpeed, 2).ToString() + 'x' + } + } +} diff --git a/guidance/Caller.ps1 b/guidance/Caller.ps1 new file mode 100644 index 00000000..e05e89e2 --- /dev/null +++ b/guidance/Caller.ps1 @@ -0,0 +1,54 @@ +function Invoke-Function4 { + <# + .SYNOPSIS + Demonstrates caller detection using Get-PSCallStack. + #> + [CmdletBinding()] + param() + 'In: ' + $MyInvocation.InvocationName + $caller = (Get-PSCallStack)[1].Command + 'Caller: ' + $caller +} + +function Invoke-Function3 { + <# + .SYNOPSIS + Demonstrates nested caller detection at depth 3. + #> + [CmdletBinding()] + param() + 'In: ' + $MyInvocation.InvocationName + $caller = (Get-PSCallStack)[1].Command + 'Caller: ' + $caller + Invoke-Function4 +} + +function Invoke-Function2 { + <# + .SYNOPSIS + Demonstrates nested caller detection at depth 2. + #> + [CmdletBinding()] + param() + 'In: ' + $MyInvocation.InvocationName + $caller = (Get-PSCallStack)[1].Command + 'Caller: ' + $caller + Invoke-Function3 +} + +function Invoke-Function1 { + <# + .SYNOPSIS + Entry point demonstrating caller detection through the call stack. + #> + [CmdletBinding()] + param() + 'In: ' + $MyInvocation.InvocationName + Get-PSCallStack + $caller = (Get-PSCallStack)[1].Command + 'Caller: ' + $caller + Invoke-Function2 +} + +# Test the functions +Invoke-Function1 diff --git a/guidance/ClassExtension.ps1 b/guidance/ClassExtension.ps1 new file mode 100644 index 00000000..661315e2 --- /dev/null +++ b/guidance/ClassExtension.ps1 @@ -0,0 +1,41 @@ +class C { + [string]$Name + + C([string]$Name) { + $this.Name = $Name + } + + [string]GetInfo() { + return "Name: $($this.Name)" + } +} + +class B : C { + [int]$Age + + B([string]$Name, [int]$Age) : base($Name) { + $this.Age = $Age + } + + [string]GetInfo() { + # Cast $this to the parent class (C) to call its GetInfo() + return "$(([C]$this).GetInfo()), Age: $($this.Age)" + } +} + +class A : B { + [string]$Role + + A([string]$Name, [int]$Age, [string]$Role) : base($Name, $Age) { + $this.Role = $Role + } + + [string]GetInfo() { + # Cast $this to B to call B’s GetInfo(), which itself calls C’s GetInfo() + return "$(([B]$this).GetInfo()), Role: $($this.Role)" + } +} + +# Creating and testing an instance +$person = [A]::new('John Doe', 30, 'Manager') +$person.GetInfo() diff --git a/guidance/Loops.ps1 b/guidance/Loops.ps1 new file mode 100644 index 00000000..4f885f00 --- /dev/null +++ b/guidance/Loops.ps1 @@ -0,0 +1,38 @@ +$ranGen = New-Object System.Random +$RepeatCount = 10000 + +'Basic for-loop = {0}ms' -f (Measure-Command -Expression { + for ($i = 0; $i -lt $RepeatCount; $i++) { + $Null = $ranGen.Next() + } + }).TotalMilliseconds + +'Wrapped in a function = {0}ms' -f (Measure-Command -Expression { + function Get-RandNum_Core { + <# + .SYNOPSIS + Gets a random number using a shared Random instance. + #> + param ($ranGen) + $ranGen.Next() + } + + for ($i = 0; $i -lt $RepeatCount; $i++) { + $Null = Get-RandNum_Core $ranGen + } + }).TotalMilliseconds + +'For-loop in a function = {0}ms' -f (Measure-Command -Expression { + function Get-RandNum_All { + <# + .SYNOPSIS + Gets random numbers in a loop using a shared Random instance. + #> + param ($ranGen) + for ($i = 0; $i -lt $RepeatCount; $i++) { + $Null = $ranGen.Next() + } + } + + Get-RandNum_All $ranGen + }).TotalMilliseconds diff --git a/guidance/Out-Null.ps1 b/guidance/Out-Null.ps1 new file mode 100644 index 00000000..b9342828 --- /dev/null +++ b/guidance/Out-Null.ps1 @@ -0,0 +1,50 @@ +$tests = @{ + 'Assign to $null' = { + $arrayList = [System.Collections.ArrayList]::new() + foreach ($i in 0..$args[0]) { + $null = $arraylist.Add($i) + } + } + 'Cast to [void]' = { + $arrayList = [System.Collections.ArrayList]::new() + foreach ($i in 0..$args[0]) { + [void]$arraylist.Add($i) + } + } + 'Redirect to $null' = { + $arrayList = [System.Collections.ArrayList]::new() + foreach ($i in 0..$args[0]) { + $arraylist.Add($i) > $null + } + } + 'Pipe to Out-Null' = { + $arrayList = [System.Collections.ArrayList]::new() + foreach ($i in 0..$args[0]) { + $arraylist.Add($i) | Out-Null + } + } +} + +10kb, 50kb, 100kb | ForEach-Object { + $groupResult = foreach ($test in $tests.GetEnumerator()) { + $ms = (Measure-Command { & $test.Value $_ }).TotalMilliseconds + + [pscustomobject]@{ + Iterations = $_ + Test = $test.Key + TotalMilliseconds = [math]::Round($ms, 2) + } + + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + } + + $groupResult = $groupResult | Sort-Object TotalMilliseconds + $groupResult | Select-Object *, @{ + Name = 'RelativeSpeed' + Expression = { + $relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds + [math]::Round($relativeSpeed, 2).ToString() + 'x' + } + } +} diff --git a/guidance/PSCallStack.ps1 b/guidance/PSCallStack.ps1 new file mode 100644 index 00000000..9ac5c977 --- /dev/null +++ b/guidance/PSCallStack.ps1 @@ -0,0 +1,49 @@ +function Invoke-Function4 { + <# + .SYNOPSIS + Demonstrates Get-PSCallStack at the deepest call level. + #> + [CmdletBinding()] + param() + 'In: ' + $MyInvocation.InvocationName + Get-PSCallStack +} + +function Invoke-Function3 { + <# + .SYNOPSIS + Demonstrates Get-PSCallStack at call depth 3. + #> + [CmdletBinding()] + param() + 'In: ' + $MyInvocation.InvocationName + Get-PSCallStack + Invoke-Function4 +} + +function Invoke-Function2 { + <# + .SYNOPSIS + Demonstrates Get-PSCallStack at call depth 2. + #> + [CmdletBinding()] + param() + 'In: ' + $MyInvocation.InvocationName + Get-PSCallStack + Invoke-Function3 +} + +function Invoke-Function1 { + <# + .SYNOPSIS + Entry point demonstrating Get-PSCallStack through nested calls. + #> + [CmdletBinding()] + param() + "In: " + $MyInvocation.InvocationName + Get-PSCallStack + Invoke-Function2 +} + +# Test the functions +Invoke-Function1 diff --git a/guidance/PSCmdlet.ps1 b/guidance/PSCmdlet.ps1 new file mode 100644 index 00000000..082f93b5 --- /dev/null +++ b/guidance/PSCmdlet.ps1 @@ -0,0 +1,49 @@ +function Invoke-Function4 { + <# + .SYNOPSIS + Demonstrates PSCmdlet variable at the deepest call level. + #> + [CmdletBinding()] + param() + '4: ' + $MyInvocation.InvocationName + $PSCmdlet | ConvertTo-Json +} + +function Invoke-Function3 { + <# + .SYNOPSIS + Demonstrates PSCmdlet variable at call depth 3. + #> + [CmdletBinding()] + param() + '3: ' + $MyInvocation.InvocationName + $PSCmdlet | ConvertTo-Json + Invoke-Function4 +} + +function Invoke-Function2 { + <# + .SYNOPSIS + Demonstrates PSCmdlet variable at call depth 2. + #> + [CmdletBinding()] + param() + '2: ' + $MyInvocation.InvocationName + $PSCmdlet | ConvertTo-Json + Invoke-Function3 +} + +function Invoke-Function1 { + <# + .SYNOPSIS + Entry point demonstrating PSCmdlet variable through nested calls. + #> + [CmdletBinding()] + param() + '1: ' + $MyInvocation.InvocationName + $PSCmdlet | ConvertTo-Json + Invoke-Function2 +} + +# Test the functions +Invoke-Function1 diff --git a/guidance/PSModuleTest.psm1 b/guidance/PSModuleTest.psm1 new file mode 100644 index 00000000..ab67c5ef --- /dev/null +++ b/guidance/PSModuleTest.psm1 @@ -0,0 +1,73 @@ +[Cmdletbinding()] +param() + +Write-Verbose 'Importing subcomponents' +$Folders = 'init', 'classes', 'private', 'public' +# Import everything in these folders +Foreach ($Folder in $Folders) { + $Root = Join-Path -Path $PSScriptRoot -ChildPath $Folder + Write-Verbose "Processing folder: $Root" + if (Test-Path -Path $Root) { + Write-Verbose "Getting all files in $Root" + $Files = $null + $Files = Get-ChildItem -Path $Root -Include '*.ps1', '*.psm1' -Recurse + # dot source each file + foreach ($File in $Files) { + Write-Verbose "Importing $($File)" + Import-Module $File + Write-Verbose "Importing $($File): Done" + } + } +} + +. "$PSScriptRoot\finally.ps1" + +# Define the types to export with type accelerators. +$ExportableTypes = @( + [Book] + [BookList] +) + +# Get the internal TypeAccelerators class to use its static methods. +$TypeAcceleratorsClass = [psobject].Assembly.GetType( + 'System.Management.Automation.TypeAccelerators' +) +# Ensure none of the types would clobber an existing type accelerator. +# If a type accelerator with the same name exists, throw an exception. +$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get +foreach ($Type in $ExportableTypes) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + $Message = @( + "Unable to register type accelerator '$($Type.FullName)'" + 'Accelerator already exists.' + ) -join ' - ' + + throw [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($Message), + 'TypeAcceleratorAlreadyExists', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $Type.FullName + ) + } +} +# Add type accelerators for every exportable type. +foreach ($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Add($Type.FullName, $Type) +} +# Remove type accelerators when the module is removed. +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + foreach ($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Remove($Type.FullName) + } +}.GetNewClosure() + +$Param = @{ + Function = (Get-ChildItem -Path "$PSScriptRoot\public" -Include '*.ps1' -Recurse).BaseName + Variable = '*' + Cmdlet = '*' + Alias = '*' +} + +Write-Verbose 'Exporting module members' + +Export-ModuleMember @Param diff --git a/guidance/PipelineExecution.ps1 b/guidance/PipelineExecution.ps1 new file mode 100644 index 00000000..e8d264f8 --- /dev/null +++ b/guidance/PipelineExecution.ps1 @@ -0,0 +1,137 @@ +function Set-DefaultParameterValue { + <# + .SYNOPSIS + Sets a default parameter value with a simulated delay. + + .PARAMETER Parameter + The parameter value to set. + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([string])] + param ( + [string]$Parameter = 'Default-Parameter' + ) + if ($PSCmdlet.ShouldProcess($Parameter, 'Set default parameter value')) { + Write-Verbose "Set-DefaultParameterValue: Setting Parameter to '$Parameter'" + Start-Sleep -Seconds 1 + return $Parameter + } +} + +function Step-One { + <# + .SYNOPSIS + First pipeline step that multiplies input by 10. + + .PARAMETER InputNumber + The number to process. + + .PARAMETER Parameter + Optional parameter with a default value. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [int]$InputNumber, + + [string]$Parameter = (Set-DefaultParameterValue -Parameter 'Step-One-Default') + ) + begin { + Write-Verbose "Step-One: BEGIN block. Parameter=$Parameter" + Start-Sleep -Seconds 1 + } + process { + Write-Verbose "Step-One: PROCESS block. InputNumber=$InputNumber, Parameter=$Parameter" + $Output = $InputNumber * 10 + Write-Output $Output + Start-Sleep -Seconds 1 + } + end { + Write-Verbose "Step-One: END block. Parameter=$Parameter" + Start-Sleep -Seconds 1 + } + + clean { + Write-Verbose "Step-One: CLEANUP block. Parameter=$Parameter" + Start-Sleep -Seconds 1 + } +} + +function Step-Two { + <# + .SYNOPSIS + Second pipeline step that adds 5 to input. + + .PARAMETER InputNumber + The number to process. + + .PARAMETER Parameter + Optional parameter with a default value. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [int]$InputNumber, + + [string]$Parameter = (Set-DefaultParameterValue -Parameter 'Step-Two-Default') + ) + begin { + Write-Verbose "Step-Two: BEGIN block. Parameter=$Parameter" + Start-Sleep -Seconds 1 + } + process { + Write-Verbose "Step-Two: PROCESS block. InputNumber=$InputNumber, Parameter=$Parameter" + $Output = $InputNumber + 5 + Write-Output $Output + Start-Sleep -Seconds 1 + } + end { + Write-Verbose "Step-Two: END block. Parameter=$Parameter" + Start-Sleep -Seconds 1 + } + clean { + Write-Verbose "Step-One: CLEANUP block. Parameter=$Parameter" + Start-Sleep -Seconds 1 + } +} + +function Step-Three { + <# + .SYNOPSIS + Third pipeline step that subtracts 2 from input. + + .PARAMETER InputNumber + The number to process. + + .PARAMETER Parameter + Optional parameter with a default value. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [int]$InputNumber, + + [string]$Parameter = (Set-DefaultParameterValue -Parameter 'Step-Three-Default') + ) + begin { + Write-Verbose "Step-Three: BEGIN block. Parameter=$Parameter" + Start-Sleep -Seconds 1 + } + process { + Write-Verbose "Step-Three: PROCESS block. InputNumber=$InputNumber, Parameter=$Parameter" + $Output = $InputNumber - 2 + Write-Output $Output + Start-Sleep -Seconds 1 + } + end { + Write-Verbose "Step-Three: END block. Parameter=$Parameter" + Start-Sleep -Seconds 1 + } + clean { + Write-Verbose "Step-One: CLEANUP block. Parameter=$Parameter" + Start-Sleep -Seconds 1 + } +} + +$VerbosePreference = 'Continue' +1, 2, 3 | Step-One | Step-Two | Step-Three diff --git a/guidance/Read-File.ps1 b/guidance/Read-File.ps1 new file mode 100644 index 00000000..1f842e58 --- /dev/null +++ b/guidance/Read-File.ps1 @@ -0,0 +1,97 @@ +# Function to simulate processing each line +function Test-LineProcessing { + <# + .SYNOPSIS + Simulates processing a single line of text. + + .DESCRIPTION + Returns whether the input text has a length greater than zero. + + .PARAMETER InputText + The text line to process. + #> + param([string]$InputText) + # Simulate some work by creating a simple string operation + $InputText.Length > 0 +} + +$tests = @{ + 'Get-Content + foreach' = { + param($filePath) + $content = Get-Content -Path $filePath + foreach ($line in $content) { + Test-LineProcessing -InputText $line + } + } + 'Get-Content | ForEach-Object' = { + param($filePath) + Get-Content -Path $filePath | + ForEach-Object -Process { + Test-LineProcessing -InputText $_ + } + } + 'StreamReader' = { + param($filePath) + $sr = New-Object -TypeName System.IO.StreamReader -ArgumentList $filePath + try { + while ($sr.Peek() -ge 0) { + $line = $sr.ReadLine() + Test-LineProcessing -InputText $line + } + } finally { + $sr.Dispose() + } + } + 'Get-Content -ReadCount 1' = { + param($filePath) + Get-Content -Path $filePath -ReadCount 1 | + ForEach-Object -Process { + Test-LineProcessing -InputText $_ + } + } +} + +# Create test files +$testFiles = @{ + 'test-small.txt' = (1..100 | ForEach-Object { "This is line $_ with some additional text to make it realistic." }) + 'test-medium.txt' = (1..5000 | ForEach-Object { + "This is line $_ with some additional text to make it realistic and longer for testing purposes." + }) + 'test-large.txt' = (1..50000 | ForEach-Object { + "This is line $_ with some additional text to make it realistic and longer for testing purposes with even more content." + }) +} + +# Generate test files +foreach ($file in $testFiles.GetEnumerator()) { + $file.Value | Out-File -FilePath $file.Key -Encoding UTF8 +} + +'test-small.txt', 'test-medium.txt', 'test-large.txt' | ForEach-Object { + $groupResult = foreach ($test in $tests.GetEnumerator()) { + $ms = (Measure-Command { & $test.Value $_ }).TotalMilliseconds + + [pscustomobject]@{ + TestFile = $_ + Test = $test.Key + TotalMilliseconds = [math]::Round($ms, 2) + } + + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + } + + $groupResult = $groupResult | Sort-Object TotalMilliseconds + $groupResult | Select-Object *, @{ + Name = 'RelativeSpeed' + Expression = { + $relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds + [math]::Round($relativeSpeed, 2).ToString() + 'x' + } + } +} + +# Cleanup test files +'test-small.txt', 'test-medium.txt', 'test-large.txt' | ForEach-Object { + Remove-Item $_ -Force -ErrorAction SilentlyContinue +} diff --git a/guidance/WebCalls.ps1 b/guidance/WebCalls.ps1 new file mode 100644 index 00000000..6e941cb7 --- /dev/null +++ b/guidance/WebCalls.ps1 @@ -0,0 +1,80 @@ +# Define the list of URLs (images or other files) to test. +$urls = @( + 'https://github.com' + 'https://microsoft.com' + 'https://google.com' + 'https://bing.com' + 'https://yahoo.com' + 'https://duckduckgo.com' + 'https://wikipedia.org' + 'https://youtube.com' +) + +# Number of times to repeat each test per URL. +$iterations = 50 + +$results = @() + +foreach ($url in $urls) { + Write-Host "Testing URL: $url" + + for ($i = 1; $i -le $iterations; $i++) { + # 1) Invoke-RestMethod - default + $timeIR = Measure-Command { + Invoke-RestMethod -Uri $url | Out-Null + } + $results += [PSCustomObject]@{ + URL = $url + Method = 'Invoke-RestMethod (HTTP/1.1)' + ElapsedMilliseconds = $timeIR.TotalMilliseconds + } + + # 2) Invoke-RestMethod - HTTP/2 + $timeIR2 = Measure-Command { + Invoke-RestMethod -Uri $url -HttpVersion 2.0 | Out-Null + } + $results += [PSCustomObject]@{ + URL = $url + Method = 'Invoke-RestMethod (HTTP/2.0)' + ElapsedMilliseconds = $timeIR2.TotalMilliseconds + } + + # 3) Invoke-WebRequest - default + $timeIW = Measure-Command { + Invoke-WebRequest -Uri $url | Out-Null + } + $results += [PSCustomObject]@{ + URL = $url + Method = 'Invoke-WebRequest (HTTP/1.1)' + ElapsedMilliseconds = $timeIW.TotalMilliseconds + } + + # 4) Invoke-WebRequest - HTTP/2 + $timeIW2 = Measure-Command { + Invoke-WebRequest -Uri $url -HttpVersion 2.0 | Out-Null + } + $results += [PSCustomObject]@{ + URL = $url + Method = 'Invoke-WebRequest (HTTP/2.0)' + ElapsedMilliseconds = $timeIW2.TotalMilliseconds + } + } +} + +# Summarize: average, min, and max of each combination +$summary = $results | + Group-Object URL, Method | + ForEach-Object { + $elapsed = $_.Group | Select-Object -ExpandProperty ElapsedMilliseconds + [PSCustomObject]@{ + URL = $_.Group[0].URL + Method = $_.Group[0].Method + AverageTime_ms = [Math]::Round(($elapsed | Measure-Object -Average).Average, 2) + MinTime_ms = [Math]::Round(($elapsed | Measure-Object -Minimum).Minimum, 2) + MaxTime_ms = [Math]::Round(($elapsed | Measure-Object -Maximum).Maximum, 2) + } + } | + Sort-Object URL, Method + +# Display in a table +$summary | Format-Table -AutoSize diff --git a/guidance/root.ps1 b/guidance/root.ps1 new file mode 100644 index 00000000..4998bc2b --- /dev/null +++ b/guidance/root.ps1 @@ -0,0 +1,28 @@ +# +# Script module for module 'PSScriptAnalyzer' +# +Set-StrictMode -Version Latest + +# Set up some helper variables to make it easier to work with the module +$PSModule = $ExecutionContext.SessionState.Module +$PSModuleRoot = $PSModule.ModuleBase + +# Import the appropriate nested binary module based on the current PowerShell version +$binaryModuleRoot = $PSModuleRoot + + +if (($PSVersionTable.Keys -contains 'PSEdition') -and ($PSVersionTable.PSEdition -ne 'Desktop')) { + $binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath 'coreclr' +} else { + if ($PSVersionTable.PSVersion -lt [Version]'5.0') { + $binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath 'PSv3' + } +} + +$binaryModulePath = Join-Path -Path $binaryModuleRoot -ChildPath 'Microsoft.Windows.PowerShell.ScriptAnalyzer.dll' +$binaryModule = Import-Module -Name $binaryModulePath -PassThru + +# When the module is unloaded, remove the nested binary module that was loaded with it +$PSModule.OnRemove = { + Remove-Module -ModuleInfo $binaryModule +}