Skip to content

Commit

Permalink
Performance improvements
Browse files Browse the repository at this point in the history
I did some profiling of the module today, and identified several hotspots that have been improved in this commit.  The main three are:

ScriptProperties on the PesterState object (FailedCount, etc) have been replaced with normal properties that are incremented in the AddTestResult function.

Parsing performed for the BeforeEach / AfterEach commands is run a lot and was expensive; this code has been moved into some compiled C# for more speed.

Calls to Get-IgnoreErrorPreference were also adding up quite a bit.  These have been replaced with a lookup of a script-scope variable instead.  This results in a tiny bit of duplication in the Mock code, but it's well worth it.

There are a few lesser changes as well.  On my WMF 5.0 system, the time it takes to execute Pester's own test suite has gone down by around one third.
  • Loading branch information
dlwyatt committed Jan 17, 2015
1 parent 4b3231a commit 63dfab7
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 70 deletions.
2 changes: 1 addition & 1 deletion Functions/Assertions/Should.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function Parse-ShouldArgs([array] $shouldArgs) {

function Get-TestResult($shouldArgs, $value) {
$assertionMethod = $shouldArgs.AssertionMethod
$command = Get-Command $assertionMethod -ErrorAction (Get-IgnoreErrorPreference)
$command = Get-Command $assertionMethod -ErrorAction $script:IgnoreErrorPreference

if ($null -eq $command)
{
Expand Down
2 changes: 1 addition & 1 deletion Functions/Describe.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ about_TestDrive
[ScriptBlock] $Fixture = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)")
)

if ($null -eq (Get-Variable -Name Pester -ValueOnly -ErrorAction (Get-IgnoreErrorPreference)))
if ($null -eq (Get-Variable -Name Pester -ValueOnly -ErrorAction $script:IgnoreErrorPreference))
{
# User has executed a test script directly instead of calling Invoke-Pester
$Pester = New-PesterState -Path (Resolve-Path .) -TestNameFilter $null -TagFilter @() -SessionState $PSCmdlet.SessionState
Expand Down
2 changes: 1 addition & 1 deletion Functions/InModuleScope.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function InModuleScope
$ScriptBlock
)

if ($null -eq (Get-Variable -Name Pester -ValueOnly -ErrorAction (Get-IgnoreErrorPreference)))
if ($null -eq (Get-Variable -Name Pester -ValueOnly -ErrorAction $script:IgnoreErrorPreference))
{
# User has executed a test script directly instead of calling Invoke-Pester
$Pester = New-PesterState -Path (Resolve-Path .) -TestNameFilter $null -TagFilter @() -ExcludeTagFilter @() -SessionState $PSCmdlet.SessionState
Expand Down
11 changes: 10 additions & 1 deletion Functions/Mock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,16 @@ function MockPrototype {
$moduleName = $ExecutionContext.SessionState.Module.Name
}

[object] $ArgumentList = Get-Variable -Name args -ValueOnly -Scope Local -ErrorAction (Get-IgnoreErrorPreference)
if ($PSVersionTable.PSVersion.Major -ge 3)
{
[string] $IgnoreErrorPreference = 'Ignore'
}
else
{
[string] $IgnoreErrorPreference = 'SilentlyContinue'
}

[object] $ArgumentList = Get-Variable -Name args -ValueOnly -Scope Local -ErrorAction $IgnoreErrorPreference
if ($null -eq $ArgumentList) { $ArgumentList = @() }

Invoke-Mock -CommandName $functionName -ModuleName $moduleName -BoundParameters $PSBoundParameters -ArgumentList $ArgumentList
Expand Down
49 changes: 28 additions & 21 deletions Functions/PesterState.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@ function New-PesterState

$script:TestResult = @()

function EnterDescribe ($Name)
$script:TotalCount = 0
$script:Time = [timespan]0
$script:PassedCount = 0
$script:FailedCount = 0
$script:SkippedCount = 0
$script:PendingCount = 0

function EnterDescribe([string]$Name)
{
if ($CurrentDescribe)
{
Expand All @@ -64,7 +71,7 @@ function New-PesterState
$script:CurrentDescribe = $null
}

function EnterContext ($Name)
function EnterContext([string]$Name)
{
if ( -not $CurrentDescribe )
{
Expand Down Expand Up @@ -126,6 +133,7 @@ function New-PesterState
[string] $ParameterizedSuiteName,
[System.Collections.IDictionary] $Parameters
)

$previousTime = $script:MostRecentTimestamp
$script:MostRecentTimestamp = $script:Stopwatch.Elapsed

Expand All @@ -149,6 +157,17 @@ function New-PesterState

}

$script:TotalCount++
$script:Time += $Time

switch ($Result)
{
Passed { $script:PassedCount++; break; }
Failed { $script:FailedCount++; break; }
Skipped { $script:SkippedCount++; break; }
Pending { $script:PendingCount++; break; }
}

$Script:TestResult += Microsoft.PowerShell.Utility\New-Object -TypeName PsObject -Property @{
Describe = $CurrentDescribe
Context = $CurrentContext
Expand Down Expand Up @@ -178,7 +197,13 @@ function New-PesterState
"BeforeAll",
"AfterAll",
"Strict",
"Quiet"
"Quiet",
"Time",
"TotalCount",
"PassedCount",
"FailedCount",
"SkippedCount",
"PendingCount"

$ExportedFunctions = "EnterContext",
"LeaveContext",
Expand All @@ -190,24 +215,6 @@ function New-PesterState

Export-ModuleMember -Variable $ExportedVariables -function $ExportedFunctions
} -ArgumentList $Path, $TagFilter, $ExcludeTagFilter, $TestNameFilter, $SessionState, $Strict, $Quiet |
Add-Member -MemberType ScriptProperty -Name TotalCount -Value {
@( $this.TestResult ).Count
} -PassThru |
Add-Member -MemberType ScriptProperty -Name PassedCount -Value {
@( $this.TestResult | where { $_.Result -eq "Passed" } ).count
} -PassThru |
Add-Member -MemberType ScriptProperty -Name FailedCount -Value {
@( $this.TestResult | where { $_.Result -eq "Failed" } ).count
} -PassThru |
Add-Member -MemberType ScriptProperty -Name SkippedCount -Value {
@( $this.TestResult | where { $_.Result -eq "Skipped" } ).count
} -PassThru |
Add-Member -MemberType ScriptProperty -Name PendingCount -Value {
@( $this.TestResult | where { $_.Result -eq "Pending" } ).count
} -PassThru |
Add-Member -MemberType ScriptProperty -Name Time -Value {
$this.TestResult | foreach { [timespan]$total=0 } { $total = $total + ( $_.time ) } { [timespan]$total }
} -PassThru |
Add-Member -MemberType ScriptProperty -Name Scope -Value {
if ($this.CurrentTest) { 'It' }
elseif ($this.CurrentContext) { 'Context' }
Expand Down
69 changes: 43 additions & 26 deletions Functions/SetupTeardown.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,16 @@ function Add-SetupAndTeardown

for ($i = 0; $i -lt $tokens.Count; $i++)
{
if ($tokens[$i].Type -eq [System.Management.Automation.PSTokenType]::Command -and
(IsSetupOrTeardownCommand -CommandName $tokens[$i].Content))
$token = $tokens[$i]
$type = $token.Type
if ($type -eq [System.Management.Automation.PSTokenType]::Command -and
(IsSetupOrTeardownCommand -CommandName $token.Content))
{
$openBraceIndex, $closeBraceIndex = Get-BraceIndecesForCommand -Tokens $tokens -CommandIndex $i
Add-SetupTeardownFromTokens -Tokens $tokens -CommandIndex $i -OpenBraceIndex $openBraceIndex -CloseBraceIndex $closeBraceIndex -CodeText $codeText
$i = $closeBraceIndex
}
elseif ($tokens[$i].Type -eq [System.Management.Automation.PSTokenType]::GroupStart)
elseif ($type -eq [System.Management.Automation.PSTokenType]::GroupStart)
{
# We don't want to parse Setup or Teardown commands in child scopes here, so anything
# bounded by a GroupStart / GroupEnd token pair which is not immediately preceded by
Expand Down Expand Up @@ -246,40 +248,55 @@ function Get-GroupStartTokenForCommand
return $CommandIndex + 1
}

function Get-GroupCloseTokenIndex
{
param (
[System.Management.Automation.PSToken[]] $Tokens,
[int] $GroupStartTokenIndex
)

$groupLevel = 1

for ($i = $GroupStartTokenIndex + 1; $i -lt $Tokens.Count; $i++)
Add-Type -TypeDefinition @'
namespace Pester
{
switch ($Tokens[$i].Type)
{
([System.Management.Automation.PSTokenType]::GroupStart)
{
$groupLevel++
break
}
using System;
using System.Management.Automation;
([System.Management.Automation.PSTokenType]::GroupEnd)
public static class ClosingBraceFinder
{
public static int GetClosingBraceIndex(PSToken[] tokens, int startIndex)
{
$groupLevel--
int groupLevel = 1;
int len = tokens.Length;
if ($groupLevel -le 0)
for (int i = startIndex + 1; i < len; i++)
{
return $i
PSTokenType type = tokens[i].Type;
if (type == PSTokenType.GroupStart)
{
groupLevel++;
}
else if (type == PSTokenType.GroupEnd)
{
groupLevel--;
if (groupLevel <= 0) { return i; }
}
}
break
return -1;
}
}
}
'@

function Get-GroupCloseTokenIndex
{
param (
[System.Management.Automation.PSToken[]] $Tokens,
[int] $GroupStartTokenIndex
)

$closeIndex = [Pester.ClosingBraceFinder]::GetClosingBraceIndex($Tokens, $GroupStartTokenIndex)

if ($closeIndex -lt 0)
{
throw 'No corresponding GroupEnd token was found.'
}

throw 'No corresponding GroupEnd token was found.'
return $closeIndex
}

function Add-SetupTeardownFromTokens
Expand Down
6 changes: 3 additions & 3 deletions Functions/TestDrive.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function Get-TestDriveChildItem {
function Remove-TestDrive {

$DriveName = "TestDrive"
$Drive = Get-PSDrive -Name $DriveName -ErrorAction (Get-IgnoreErrorPreference)
$Drive = Get-PSDrive -Name $DriveName -ErrorAction $script:IgnoreErrorPreference
$Path = ($Drive).Root


Expand All @@ -76,15 +76,15 @@ function Remove-TestDrive {

if ( $Drive )
{
$Drive | Remove-PSDrive -Force -ErrorAction (Get-IgnoreErrorPreference)
$Drive | Remove-PSDrive -Force -ErrorAction $script:IgnoreErrorPreference
}

if (Microsoft.PowerShell.Management\Test-Path -Path $Path)
{
Microsoft.PowerShell.Management\Remove-Item -Path $Path -Force -Recurse
}

if (Get-Variable -Name $DriveName -Scope Global -ErrorAction (Get-IgnoreErrorPreference)) {
if (Get-Variable -Name $DriveName -Scope Global -ErrorAction $script:IgnoreErrorPreference) {
Remove-Variable -Scope Global -Name $DriveName -Force
}
}
Expand Down
3 changes: 1 addition & 2 deletions Pester.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ FunctionsToExport = @(
'BeforeAll',
'AfterAll'
'Get-MockDynamicParameters',
'Set-DynamicParameterVariables',
'Get-IgnoreErrorPreference'
'Set-DynamicParameterVariables'
)

# # Cmdlets to export from this module
Expand Down
23 changes: 9 additions & 14 deletions Pester.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
# Version: $version$
# Changeset: $sha$

if ($PSVersionTable.PSVersion.Major -ge 3)
{
$script:IgnoreErrorPreference = 'Ignore'
}
else
{
$script:IgnoreErrorPreference = 'SilentlyContinue'
}

$moduleRoot = Split-Path -Path $MyInvocation.MyCommand.Path

"$moduleRoot\Functions\*.ps1", "$moduleRoot\Functions\Assertions\*.ps1" |
Expand Down Expand Up @@ -251,19 +260,6 @@ function Get-ScriptBlockScope
[scriptblock].GetProperty('SessionStateInternal', $flags).GetValue($ScriptBlock, $null)
}

function Get-IgnoreErrorPreference
{
if ($PSVersionTable.PSVersion.Major -ge 3)
{
return 'Ignore'
}
else
{
return 'SilentlyContinue'
}

}

if (($null -ne $psISE) -and ($PSVersionTable.PSVersion.Major -ge 3))
{
Import-IseSnippet -Path $PSScriptRoot\Snippets
Expand All @@ -273,4 +269,3 @@ Export-ModuleMember Describe, Context, It, In, Mock, Assert-VerifiableMocks, Ass
Export-ModuleMember New-Fixture, Get-TestDriveItem, Should, Invoke-Pester, Setup, InModuleScope, Invoke-Mock
Export-ModuleMember BeforeEach, AfterEach, BeforeAll, AfterAll
Export-ModuleMember Get-MockDynamicParameters, Set-DynamicParameterVariables
Export-ModuleMember Get-IgnoreErrorPreference

0 comments on commit 63dfab7

Please sign in to comment.