Skip to content

Speed up the legacy Should assertion pipeline#2914

Open
nohwnd wants to merge 1 commit into
mainfrom
nohwnd-perf-should-pipeline
Open

Speed up the legacy Should assertion pipeline#2914
nohwnd wants to merge 1 commit into
mainfrom
nohwnd-perf-should-pipeline

Conversation

@nohwnd

@nohwnd nohwnd commented Jul 19, 2026

Copy link
Copy Markdown
Member

Profiling 1 | Should -Be 1 showed that most of the time is not in the comparison, it is in the plumbing around it. Two changes:

  • Invoke-Assertion is a 12-parameter function with Mandatory and Validate attributes, and its body is 2 lines. Binding those parameters plus building the splat hashtable costs tens of microseconds on every assertion. I inlined the body into the three call sites in Should's end block. Test-AssertionResult keeps reading $file/$lineNumber/$lineText/$shouldThrow/$addErrorCallback through dynamic scoping exactly as it read the equally named Invoke-Assertion parameters before, the locals are typed [string]/[int] to mirror the old parameter coercion (a $null ScriptName still becomes ''). Invoke-Assertion itself stays defined.
  • ArraysAreEqual built four arrays and made four helper calls to compare two single values. I added a fast path for the common case where each side is a single non-null value that PowerShell does not enumerate, bare or wrapped in a one-element object[], which is what the pipeline always hands to Be. Anything enumerable still takes the full path, including a one-element array that wraps another collection.

1 | Should -Be 1 goes from 243us to 194us per call, measured in the same process on the inline build. On an assertion-heavy workload (400 tests, 2 assertions each) the whole run drops about 15%.

Verification: full suite green on macOS with both the inline and the dot-sourced build, all *.ts.ps1 files pass in fresh processes, PSScriptAnalyzer finding count is unchanged. The risky part is the dynamic scoping into Test-AssertionResult, that is covered by tst/functions/assertions/*.Tests.ps1 and by custom-operator tests in Pester.CustomAssertion.ts.ps1.

Related perf PRs: #2915, #2916, #2917, #2918. Measured together they cut ~21% from four representative workloads (inline build, macOS).

🤖

Invoke-Assertion is a 12-parameter function with Mandatory and Validate
attributes, and its body is 2 lines. Binding those parameters plus building
the splat hashtable costs tens of microseconds on every assertion, so its
body is inlined into the three call sites in Should's end block.
Test-AssertionResult keeps reading file/lineNumber/lineText/shouldThrow/
addErrorCallback through dynamic scoping exactly as it read the equally
named parameters before. Invoke-Assertion itself stays defined.

ArraysAreEqual built four arrays and made four helper calls to compare two
single values. Added a fast path for the case where each side is a single
non-null value that PowerShell does not enumerate, bare or wrapped in a
one-element object[], which is what the pipeline always hands to Be.
Anything enumerable still takes the full path.

1 | Should -Be 1 goes from 243us to 194us per call, measured in the same
process on the inline build.

🤖
# checks cannot match (one non-null element), the counts are both 1, and IsArray is false for
# both elements, ending in $Second -eq $First (-ceq when case sensitive) - which is what we do
# here without the @() allocations and four helper-function calls.
$singleFirst = if ($First -is [object[]] -and 1 -eq $First.Length) { $First[0] } else { $First }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can $First be [object[]] when the parameter declaration has [object]?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants