diff --git a/Chronometer/Classes/Chronometer.ps1 b/Chronometer/Classes/Chronometer.ps1 index fe867df..37eed40 100644 --- a/Chronometer/Classes/Chronometer.ps1 +++ b/Chronometer/Classes/Chronometer.ps1 @@ -3,18 +3,26 @@ class Chronometer [hashtable]$FileMap = @{} $Breakpoint = @() - [void]AddBreakpoint([string[]]$Path) + [void]AddBreakpoint([string[]]$Path, [int[]]$LineNumber) { foreach($file in (Resolve-Path $Path -ea 0)) { $script = [MonitoredScript]@{Path=$file.Path} $lines = $script.SetScript($file) + if($LineNumber -ne $null) + { + $bpLine = $LineNumber + } + else + { + $bpLine = (1..$lines) + } $this.fileMap[$file.Path] = $script $breakpointParam = @{ Script = $file - Line = (1..$lines) + Line = $bpLine Action = {[ScriptProfiler]::RecordExecution( $_) } } $this.breakPoint += Set-PSBreakpoint @breakpointParam @@ -42,6 +50,10 @@ class Chronometer [MonitoredScript[]] GetResults() { + foreach($node in $this.FileMap.Values) + { + $node.PostProcessing() + } return $this.FileMap.Values } } diff --git a/Chronometer/Classes/MonitoredScript.ps1 b/Chronometer/Classes/MonitoredScript.ps1 index c2c04d7..e85acf6 100644 --- a/Chronometer/Classes/MonitoredScript.ps1 +++ b/Chronometer/Classes/MonitoredScript.ps1 @@ -46,4 +46,36 @@ class MonitoredScript $this.lastRecord = $record $this.lastNode = $node } + + [void] PostProcessing() + { + $this.lastNode = $null + $this.ExecutionTime = 0 + foreach($node in $this.line) + { + $command = $node.text -replace '\s','' + + switch -Regex ($command) + { + '^}$|^}#|^$' { + if($node.HitCount -eq 0) + { + $node.HitCount = $this.lastNode.HitCount + } + $node.Milliseconds = 0 + $node.Average = 0 + $this.lastNode = $node + } + '^{$|^{#}' { + $node.Milliseconds = 0 + $node.Average = 0 + $this.lastNode = $node + } + default { + $this.lastNode = $node + } + } + $this.ExecutionTime += $node.Milliseconds + } + } } \ No newline at end of file diff --git a/Chronometer/Private/Write-ScriptLine.ps1 b/Chronometer/Private/Write-ScriptLine.ps1 new file mode 100644 index 0000000..217728c --- /dev/null +++ b/Chronometer/Private/Write-ScriptLine.ps1 @@ -0,0 +1,29 @@ + +function Write-ScriptLine +{ + param( + [scriptline] + $line, + $WarningAt = [int]::MaxValue, + $ErrorAt = [int]::MaxValue + ) + + if($line) + { + $Color = 'Green' + if($line.HitCount -eq 0) + { + $Color = 'Gray' + } + elseif($line.Average -ge $ErrorAt) + { + $Color = 'Red' + } + elseif($line.Average -ge $WarningAt) + { + $Color = 'Yellow' + } + + Write-Host $line.toString() -ForegroundColor $Color + } +} \ No newline at end of file diff --git a/Chronometer/Public/Format-Chronometer.ps1 b/Chronometer/Public/Format-Chronometer.ps1 new file mode 100644 index 0000000..f12be7e --- /dev/null +++ b/Chronometer/Public/Format-Chronometer.ps1 @@ -0,0 +1,50 @@ +function Format-Chronometer +{ + <# + .Description + Generates a report from a Chronometer + + .Example + $script = ls C:\workspace\PSGraph\PSGraph -Recurse -Filter *.ps1 + $resultes = Get-Chronometer -Path $script.fullname -ScriptBlock {Invoke-Pester C:\workspace\PSGraph} + $results | Format-Chronometer -WarnAt 20 -ErrorAt 200 + #> + [cmdletbinding()] + param( + # This is a MonitoredScript object from Get-Chronometer + [Parameter( + ValueFromPipeline=$true + )] + [MonitoredScript[]] + $InputObject, + + # If the average time of a command is more than this, the output is yellow + [int] + $WarningAt = 20, + + #If the average time of a comamand is more than this, the output is red + [int] + $ErrorAt = 200 + ) + + begin { + $green = @{ForgroundColor='green'} + $grey = @{ForgroundColor='grey'} + $yellow = @{ForgroundColor='grey'} + $yellow = @{ForgroundColor='grey'} + $yellow = @{ForgroundColor='grey'} + } + process + { + foreach($script in $InputObject) + { + Write-Host '' + Write-Host "Script: $($script.Path)" -ForegroundColor Green + Write-Host "Execution Time: $($script.ExecutionTime)" -ForegroundColor Green + foreach($line in $script.line) + { + Write-ScriptLine $line -WarningAt $WarningAt -ErrorAt $ErrorAt + } + } + } +} diff --git a/Chronometer/Public/Get-Chronometer.ps1 b/Chronometer/Public/Get-Chronometer.ps1 index bc95fa2..c13c51f 100644 --- a/Chronometer/Public/Get-Chronometer.ps1 +++ b/Chronometer/Public/Get-Chronometer.ps1 @@ -15,6 +15,10 @@ function Get-Chronometer [string[]] $Path, + # Line numbers within the script file to measure + [int[]] + $LineNumber = $null, + # The script to start the scrupt or execute other commands [alias('Script','CommandScript')] [scriptblock] @@ -24,7 +28,7 @@ function Get-Chronometer $Chronometer = [Chronometer]::New() Write-Verbose "Setting breapoints" - $Chronometer.AddBreakpoint($Path) + $Chronometer.AddBreakpoint($Path,$LineNumber) if($Chronometer.breakPoint -ne $null) { diff --git a/Chronometer/chronometer.psd1 b/Chronometer/chronometer.psd1 index 73e6e5c..066b559 100644 Binary files a/Chronometer/chronometer.psd1 and b/Chronometer/chronometer.psd1 differ diff --git a/README.md b/README.md index 5360d69..540fa0f 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,35 @@ A module for measuring performance of Powershell scripts, one line at a time ## Project status -Experimental. Just a working idea at the moment. Functions and argument names are still up in the air. Also don't consider it stable or tested. Use at your own risk. +Preview release. The core logic is fleshed out but more testing is needed. # Getting started ## Prerequirements You need to have Powershell 5.0 or newer. This module uses classes. ## Installing Chronometer -Place the Chronometer folder into your `$PSModulePath`. I will publish to the Powershell Gallery once the project is more stable. +This is published in the Powershell Gallery + + Install-Module Chronometer ## Basic usage Provide a script file and a command to execute. $path = myscript.ps1 - Get-Chronometer -Path $path -Script {. .\myscript.ps1} -OutVariable report - $report.line | % tostring + $Chronometer = Get-Chronometer -Path $path -Script {. .\myscript.ps1} + $Chronometer | Format-Chronometer -The user experience is important to me but I am working on the core logic right now. I will loop back to make it more intuitive and simple to use. ## Things to know The `Path` can be any ps1 and the script can run any command. Ideally, you would either execute the script or load the script and execute a command inside it. +Here is a more complex example: + + $script = ls C:\workspace\PSGraph\PSGraph -Recurse -Filter *.ps1 + $Chronometer = @{ + Path = $script.fullname + Script = {Invoke-Pester C:\workspace\PSGraph} + } + $results = Get-Chronometer @Chronometer + $results | Format-Chronometer + diff --git a/ScratchFiles/example.ps1 b/ScratchFiles/example.ps1 index fab56a3..47b3502 100644 --- a/ScratchFiles/example.ps1 +++ b/ScratchFiles/example.ps1 @@ -10,7 +10,8 @@ foreach($n in 1..10) "test string" sleep -Milliseconds 3 } + sleep -Milliseconds 12 -sleep -Milliseconds 5 +sleep -Milliseconds 120 +$test = 1+1 $test = 1+1 -$test = 1+1 \ No newline at end of file diff --git a/Tests/Unit.Tests.ps1 b/Tests/Unit.Tests.ps1 index ece0505..71f832d 100644 --- a/Tests/Unit.Tests.ps1 +++ b/Tests/Unit.Tests.ps1 @@ -17,6 +17,29 @@ Describe "Basic unit tests" -Tags Build { $results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"} $results | Should Not BeNullOrEmpty } + + it "Executes a script with linenumbers and gives results" { + # Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"} + $params = @{ + Path = "$PSScriptRoot\..\ScratchFiles\example.ps1" + Script = {. "$PSScriptRoot\..\ScratchFiles\example.ps1"} + LineNumber = 2,3,5,6 + } + $results = Get-Chronometer @params + $results | Should Not BeNullOrEmpty + } + } + + Context "Function: Format-Chronometer" { + + it "Does not throw" { + {$null | Format-Chronometer } | Should Not Throw + } + + it "Can process a result object without throwing" { + $results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"} + $results | Format-Chronometer *>&1 | Should Not BeNullOrEmpty + } } InModuleScope $moduleName { @@ -44,11 +67,17 @@ Describe "Basic unit tests" -Tags Build { } Context "Class: MonitoredScript" { - {[MonitoredScript]::New()} | Should Not Throw + it "Creates an object" { + {[MonitoredScript]::New()} | Should Not Throw + } + + it "SetScript()" { + pushd $projectRoot + $monitor = [MonitoredScript]::New() + {$monitor.SetScript(".\scratchfiles\example.ps1")} | Should Not Throw + popd + } } - Context "Class: MonitoredScript" { - {[MonitoredScript]::SetScript("$projectRoot\scratchfiles\example.ps1")} | Should Not Throw - } } } \ No newline at end of file diff --git a/psake.ps1 b/psake.ps1 index b51af94..cf732c6 100644 --- a/psake.ps1 +++ b/psake.ps1 @@ -33,7 +33,7 @@ Task Init { Task UnitTests -Depends Init { $lines 'Running quick unit tests to fail early if there is an error' - $TestResults = Invoke-Pester -Path $ProjectRoot\Tests\*unit* -PassThru -Tag Build -Show Failed + $TestResults = Invoke-Pester -Path $ProjectRoot\Tests\*unit* -PassThru -Tag Build if($TestResults.FailedCount -gt 0) { @@ -47,7 +47,7 @@ Task Test -Depends UnitTests { "`n`tSTATUS: Testing with PowerShell $PSVersion" # Gather test results. Store them in a variable and file - $TestResults = Invoke-Pester -Path $ProjectRoot\Tests -PassThru -OutputFormat NUnitXml -OutputFile "$ProjectRoot\$TestFile" -Tag Build -Show Failed + $TestResults = Invoke-Pester -Path $ProjectRoot\Tests -PassThru -OutputFormat NUnitXml -OutputFile "$ProjectRoot\$TestFile" -Tag Build # In Appveyor? Upload our tests! #Abstract this into a function? If($ENV:BHBuildSystem -eq 'AppVeyor')