Skip to content

Commit 5a76792

Browse files
🩹 [Patch]: Module startup is faster and performance checks are reproducible (#69)
Module startup is now faster and performance regressions are easier to catch because the import path was optimized and repeatable benchmark tooling is included in the repository. ## Changed: Faster module startup Cold module import is significantly faster in fresh PowerShell sessions, and parallel session startup is also reduced, so common CLI and automation entry points spend less time on initialization. | Metric | Before | After | Ī” | | ------ | ------ | ----- | - | | Cold import (fresh `pwsh`) | 66.7 ms | **42.3 ms** | **āˆ’37%** | | Parallel runspaces Ɨ4 (import + roundtrip) | 43.5 ms | **28.1 ms** | **āˆ’35%** | | Per-op (seal / open / keypair) | 104 / 104–121 / 49 µs | 99 / 100–112 / 46 µs | āˆ’4–7% | ## Changed: Reproducible performance verification The repository now includes benchmark scripts for cold import timing, per-operation stopwatch measurements, and Profiler traces, so performance changes can be validated the same way in future PRs. ## Technical Details - Kept the `Import-Module` assembly-load approach after evaluating alternatives because `Add-Type -Path` and `Assembly.LoadFrom` were slower on measured runs. - Optimized import-time initialization by using a direct runtime path combine operation and avoiding redundant interop calls for fixed `crypto_box` size constants. - Added `tools/benchmark` scripts for local module assembly, stopwatch benchmarks, import benchmarks, and profiler traces, then cleaned them to satisfy linter rules. - Public interfaces remain unchanged, full test suite remains green, and multi-session behavior (parallel runspaces/processes) is preserved. - Implementation progress for the linked issue is complete: benchmarked load strategies, selected the fastest safe path, and documented the decision in this PR. <details> <summary>Related issues</summary> - Fixes #58 </details> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 0044cd6 commit 5a76792

18 files changed

Lines changed: 673 additions & 142 deletions

ā€Ž.github/mkdocs.ymlā€Ž

Lines changed: 0 additions & 75 deletions
This file was deleted.

ā€Ž.github/workflows/Process-PSModule.ymlā€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ permissions:
2727

2828
jobs:
2929
Process-PSModule:
30-
uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@da180bac16b13bfbcdf08b2e4e221b5b49e5ff28 # v6.1.4
30+
uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@fb1bdb8fefd243292f779d2a856a38db6fe6daf4 # v6.1.13
3131
secrets: inherit

ā€Ž.github/zensical.tomlā€Ž

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
[project]
2+
site_name = "Sodium"
3+
repo_name = "PSModule/Sodium"
4+
repo_url = "https://github.com/PSModule/Sodium"
5+
6+
[project.theme]
7+
variant = "classic"
8+
language = "en"
9+
logo = "Assets/icon.png"
10+
favicon = "Assets/icon.png"
11+
features = [
12+
"navigation.instant",
13+
"navigation.instant.progress",
14+
"navigation.indexes",
15+
"navigation.top",
16+
"navigation.tracking",
17+
"navigation.expand",
18+
"search.suggest",
19+
"search.highlight",
20+
"content.code.copy"
21+
]
22+
23+
[[project.theme.palette]]
24+
media = "(prefers-color-scheme)"
25+
toggle.icon = "lucide/sun-moon"
26+
toggle.name = "Switch to dark mode"
27+
28+
[[project.theme.palette]]
29+
media = "(prefers-color-scheme: dark)"
30+
scheme = "slate"
31+
primary = "black"
32+
accent = "green"
33+
toggle.icon = "lucide/moon"
34+
toggle.name = "Switch to light mode"
35+
36+
[[project.theme.palette]]
37+
media = "(prefers-color-scheme: light)"
38+
scheme = "default"
39+
primary = "indigo"
40+
accent = "green"
41+
toggle.icon = "lucide/sun"
42+
toggle.name = "Switch to system preference"
43+
44+
[project.theme.icon]
45+
repo = "fontawesome/brands/github"
46+
47+
[project.markdown_extensions.toc]
48+
permalink = true
49+
50+
[project.markdown_extensions.attr_list]
51+
[project.markdown_extensions.admonition]
52+
[project.markdown_extensions.md_in_html]
53+
[project.markdown_extensions.pymdownx.details]
54+
[project.markdown_extensions.pymdownx.superfences]
55+
56+
[[project.extra.social]]
57+
icon = "fontawesome/brands/discord"
58+
link = "https://discord.gg/jedJWCPAhD"
59+
name = "PSModule on Discord"
60+
61+
[[project.extra.social]]
62+
icon = "fontawesome/brands/github"
63+
link = "https://github.com/PSModule/"
64+
name = "PSModule on GitHub"
65+
66+
[project.extra.consent]
67+
title = "Cookie consent"
68+
description = "We use cookies to recognize your repeated visits and preferences, as well as to measure the effectiveness of our documentation and whether users find what they're searching for. With your consent, you're helping us to make our documentation better."
69+
actions = ["accept", "reject"]

ā€ŽPSModule/Sodium/Sodium.csā€Ž

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@ public static KeyPairBase64 GenerateKeyPairBase64(string seedText)
203203
}
204204

205205
public static string DerivePublicKeyBase64(string privateKeyBase64)
206+
{
207+
return Convert.ToBase64String(DerivePublicKey(privateKeyBase64));
208+
}
209+
210+
public static byte[] DerivePublicKey(string privateKeyBase64)
206211
{
207212
ArgumentNullException.ThrowIfNull(privateKeyBase64);
208213
var privateKey = DecodeBase64Exact(privateKeyBase64, SecretKeyBytes, "private key");
@@ -213,7 +218,7 @@ public static string DerivePublicKeyBase64(string privateKeyBase64)
213218
{
214219
throw new InvalidOperationException("Unable to derive public key from private key.");
215220
}
216-
return Convert.ToBase64String(publicKey);
221+
return publicKey;
217222
}
218223
finally
219224
{
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
function Assert-SodiumNativeRuntime {
2+
<#
3+
.SYNOPSIS
4+
Provides platform-specific diagnostics after Sodium native initialization fails.
5+
6+
.DESCRIPTION
7+
Checks the Windows Visual C++ runtime after a native initialization exception and throws a targeted message when the required
8+
runtime is unavailable.
9+
10+
.NOTES
11+
This function only runs after native library loading fails, which cannot be reproduced safely after the library is loaded in the test process.
12+
#>
13+
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()]
14+
[OutputType([void])]
15+
[CmdletBinding()]
16+
param()
17+
18+
process {
19+
if ($IsWindows -and $script:ProcessArchitecture -in @('X64', 'X86')) {
20+
$hasRuntime = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture $script:ProcessArchitecture
21+
if (-not $hasRuntime) {
22+
$message = "Sodium native initialization failed; the Visual C++ Redistributable for " +
23+
"$($script:ProcessArchitecture) appears to be missing or below the required version."
24+
throw $message
25+
}
26+
}
27+
}
28+
}

ā€Žsrc/functions/private/Initialize-Sodium.ps1ā€Ž

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,19 @@
2323
$initializationResult = [PSModule.Sodium]::sodium_init()
2424
} catch {
2525
$script:Supported = $false
26-
if ($IsWindows) {
27-
if ($script:ProcessArchitecture -in @('X64', 'X86')) {
28-
$hasRuntime = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture $script:ProcessArchitecture
29-
if (-not $hasRuntime) {
30-
$message = "Sodium native initialization failed; the Visual C++ Redistributable for " +
31-
"$($script:ProcessArchitecture) appears to be missing or below the required version."
32-
throw $message
33-
}
34-
}
35-
}
26+
Assert-SodiumNativeRuntime
3627
throw
3728
}
3829
if ($initializationResult -lt 0) {
3930
throw 'Sodium initialization failed.'
4031
}
4132

42-
$script:SodiumPublicKeyBytes = [PSModule.Sodium]::crypto_box_publickeybytes().ToUInt32()
43-
$script:SodiumPrivateKeyBytes = [PSModule.Sodium]::crypto_box_secretkeybytes().ToUInt32()
44-
$script:SodiumSealBytes = [PSModule.Sodium]::crypto_box_sealbytes().ToUInt32()
45-
$script:SodiumSeedBytes = [PSModule.Sodium]::crypto_box_seedbytes().ToUInt32()
33+
# Fixed crypto_box constants (curve25519xsalsa20poly1305). The C# layer queries and validates the real
34+
# native values at type initialization; hardcoding here avoids four extra interop call sites at import.
35+
$script:SodiumPublicKeyBytes = 32u
36+
$script:SodiumPrivateKeyBytes = 32u
37+
$script:SodiumSealBytes = 48u
38+
$script:SodiumSeedBytes = 32u
4639
$script:SodiumInitialized = $true
4740
}
4841

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
function Resolve-SodiumRuntimeIdentifier {
2+
<#
3+
.SYNOPSIS
4+
Resolves the native Sodium runtime identifier.
5+
6+
.DESCRIPTION
7+
Maps the active operating system and process architecture to the runtime identifier used by the bundled native library.
8+
#>
9+
[OutputType([string])]
10+
[CmdletBinding()]
11+
param(
12+
[Parameter(Mandatory)]
13+
[System.Runtime.InteropServices.Architecture] $ProcessArchitecture,
14+
15+
[Parameter()]
16+
[switch] $Linux,
17+
18+
[Parameter()]
19+
[switch] $MacOS,
20+
21+
[Parameter()]
22+
[switch] $Windows
23+
)
24+
25+
process {
26+
switch ($true) {
27+
$Linux {
28+
switch ($ProcessArchitecture) {
29+
'Arm64' { return 'linux-arm64' }
30+
'X64' { return 'linux-x64' }
31+
default {
32+
$message = "Unsupported Linux process architecture: $ProcessArchitecture. " +
33+
'Please refer to the documentation for supported architectures.'
34+
throw $message
35+
}
36+
}
37+
}
38+
$MacOS {
39+
switch ($ProcessArchitecture) {
40+
'Arm64' { return 'osx-arm64' }
41+
'X64' { return 'osx-x64' }
42+
default {
43+
$message = "Unsupported macOS process architecture: $ProcessArchitecture. " +
44+
'Please refer to the documentation for supported architectures.'
45+
throw $message
46+
}
47+
}
48+
}
49+
$Windows {
50+
switch ($ProcessArchitecture) {
51+
'X64' { return 'win-x64' }
52+
'X86' { return 'win-x86' }
53+
default {
54+
$message = "Unsupported Windows process architecture: $ProcessArchitecture. " +
55+
'Please refer to the documentation for supported architectures.'
56+
throw $message
57+
}
58+
}
59+
}
60+
default {
61+
throw 'Unsupported platform. Please refer to the documentation for more information.'
62+
}
63+
}
64+
}
65+
}

ā€Žsrc/functions/public/ConvertFrom-SodiumSealedBox.ps1ā€Ž

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,6 @@
6868
[string] $PrivateKey
6969
)
7070

71-
begin {
72-
Initialize-Sodium
73-
}
74-
7571
process {
7672
try {
7773
if (-not [string]::IsNullOrWhiteSpace($PublicKey)) {

ā€Žsrc/functions/public/ConvertTo-SodiumSealedBox.ps1ā€Ž

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@
5555
[ValidateNotNullOrEmpty()]
5656
[string] $PublicKey
5757
)
58-
begin {
59-
Initialize-Sodium
60-
}
61-
6258
process {
6359
try {
6460
return [PSModule.Sodium]::SealBase64($Message, $PublicKey)

ā€Žsrc/functions/public/Get-SodiumPublicKey.ps1ā€Ž

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@
6767
https://psmodule.io/Sodium/Functions/Get-SodiumPublicKey/
6868
#>
6969

70+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
71+
'PSUseOutputTypeCorrectly', '',
72+
Justification = 'The unary comma preserves the byte array as one pipeline object.'
73+
)]
7074
[OutputType([string], ParameterSetName = 'Base64')]
7175
[OutputType([byte[]], ParameterSetName = 'AsByteArray')]
7276
[CmdletBinding(DefaultParameterSetName = 'Base64')]
@@ -81,14 +85,10 @@
8185
[switch] $AsByteArray
8286
)
8387

84-
begin {
85-
Initialize-Sodium
86-
}
87-
8888
process {
8989
if ($AsByteArray) {
9090
try {
91-
return [System.Convert]::FromBase64String([PSModule.Sodium]::DerivePublicKeyBase64($PrivateKey))
91+
return , ([PSModule.Sodium]::DerivePublicKey($PrivateKey))
9292
} catch [System.Management.Automation.MethodInvocationException] {
9393
throw $_.Exception.InnerException
9494
}
@@ -100,5 +100,4 @@
100100
}
101101
}
102102

103-
end {}
104103
}

0 commit comments

Comments
Ā (0)