{"id":1007,"date":"2023-05-15T08:56:56","date_gmt":"2023-05-15T15:56:56","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/powershell-community\/?p=1007"},"modified":"2023-05-15T08:56:56","modified_gmt":"2023-05-15T15:56:56","slug":"measuring-script-execution-time","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/powershell-community\/measuring-script-execution-time\/","title":{"rendered":"Measuring script execution time"},"content":{"rendered":"<p>Most of the time while developing PowerShell scripts we don&#8217;t need to worry about performance, or execution time. After all, scripts were made to run automation in the background. However, as your scripts become more sophisticated, and you need to work with complex data or big data sizes, performance becomes something to keep in mind. Measuring a script execution time is the first step towards script optimization.<\/p>\n<h2>Measure-Command<\/h2>\n<p>PowerShell has a built-in cmdlet called <code>Measure-Command<\/code>, which measures the execution time of other cmdlets, or script blocks. It has two parameters:<\/p>\n<ul>\n<li><strong>Expression<\/strong>: The script block to be measured.<\/li>\n<li><strong>InputObject<\/strong>: Optional input to be passed to the script block. You can use <code>$_<\/code> or <code>$PSItem<\/code> to access them.<\/li>\n<\/ul>\n<p>Besides the two parameters, objects in the pipeline are also passed to the script block. <code>Measure-Command<\/code> returns an object of type <code>System.TimeSpan<\/code>, giving us more flexibility on how to work with the result.<\/p>\n<pre><code class=\"language-powershell\">Measure-Command { foreach ($number in 1..1000) { &lt;# Do work #&gt; } }<\/code><\/pre>\n<pre><code class=\"language-powershell-console\">Days              : 0\nHours             : 0\nMinutes           : 0\nSeconds           : 0\nMilliseconds      : 8\nTicks             : 85034\nTotalDays         : 9.84189814814815E-08\nTotalHours        : 2.36205555555556E-06\nTotalMinutes      : 0.000141723333333333\nTotalSeconds      : 0.0085034\nTotalMilliseconds : 8.5034<\/code><\/pre>\n<p>Using the pipeline or the <strong>InputObject<\/strong> parameter.<\/p>\n<pre><code class=\"language-powershell\">1..1000 |\n    Measure-Command -Expression { foreach ($number in $_) { &lt;# Do work #&gt; } } |\n    Select-Object TotalMilliseconds<\/code><\/pre>\n<pre><code class=\"language-powershell-console\">TotalMilliseconds\n-----------------\n            10.60<\/code><\/pre>\n<pre><code class=\"language-powershell\">Measure-Command -InputObject (1..1000) -Expression { $_ | % { &lt;# Do work #&gt; } } |\n    Select-Object TotalMilliseconds<\/code><\/pre>\n<pre><code class=\"language-powershell-console\">TotalMilliseconds\n-----------------\n            19.98<\/code><\/pre>\n<h2>Scope and Object Modification<\/h2>\n<p><code>Measure-Command<\/code> runs the script block in the current scope, meaning variables in the current scope gets modified if referenced in the script block.<\/p>\n<pre><code class=\"language-powershell\">$studyVariable = 0\nMeasure-Command { 1..10 | % { $studyVariable += 1 } }\nWrite-Host \"Current variable value: $studyVariable.\"<\/code><\/pre>\n<pre><code class=\"language-powershell-console\">Days              : 0\nHours             : 0\nMinutes           : 0\nSeconds           : 0\nMilliseconds      : 15\nTicks             : 155838\nTotalDays         : 1.80368055555556E-07\nTotalHours        : 4.32883333333333E-06\nTotalMinutes      : 0.00025973\nTotalSeconds      : 0.0155838\nTotalMilliseconds : 15.5838\n\nCurrent variable value: 10.<\/code><\/pre>\n<p>To overcome this, you can use the invocation operator <code>&amp;<\/code> and enclose the script block in <code>{}<\/code>, to execute in a separate context.<\/p>\n<pre><code class=\"language-powershell\">$studyVariable = 0\nMeasure-Command { & { 1..10 | % { $studyVariable += 1 } } }\nWrite-Host \"Current variable value: $studyVariable.\"<\/code><\/pre>\n<pre><code class=\"language-powershell-console\">Days              : 0\nHours             : 0\nMinutes           : 0\nSeconds           : 0\nMilliseconds      : 8\nTicks             : 86542\nTotalDays         : 1.00164351851852E-07\nTotalHours        : 2.40394444444444E-06\nTotalMinutes      : 0.000144236666666667\nTotalSeconds      : 0.0086542\nTotalMilliseconds : 8.6542\n\nCurrent variable value: 0.<\/code><\/pre>\n<p>It&#8217;s also worth remember that if your script block modifies system resources, files, databases or any other static data, the object gets modified.<\/p>\n<pre><code class=\"language-powershell\">$scriptBlock = {\n    if (!(Test-Path -Path C:\\SuperCoolFolder)) {\n        New-Item -Path C:\\ -Name SuperCoolFolder -ItemType Directory\n    }\n}\n\nMeasure-Command -Expression { & $scriptBlock }\nGet-ChildItem C:\\ -Filter SuperCoolFolder | Select-Object FullName<\/code><\/pre>\n<pre><code class=\"language-powershell-console\">Days              : 0\nHours             : 0\nMinutes           : 0\nSeconds           : 0\nMilliseconds      : 11\nTicks             : 118978\nTotalDays         : 1.37706018518519E-07\nTotalHours        : 3.30494444444444E-06\nTotalMinutes      : 0.000198296666666667\nTotalSeconds      : 0.0118978\nTotalMilliseconds : 11.8978\n\nFullName : C:\\SuperCoolFolder<\/code><\/pre>\n<p>As a cool exercise, try figuring out why the output from <code>New-Item<\/code> didn&#8217;t show up.<\/p>\n<h2>Output and Alternatives<\/h2>\n<p><code>Measure-Command<\/code> returns a <code>System.TimeSpan<\/code> object, but not the result from the script. If your study also includes the result, there are two ways you can go about it.<\/p>\n<h3>Saving the output in a variable<\/h3>\n<p>We know that scripts executed with <code>Measure-Object<\/code> runs in the current scope. So we could assign the result to a variable, and work with it.<\/p>\n<pre><code class=\"language-powershell\">$range = 1..100\n$evenCount = 0\n$scriptBlock = {\n    foreach ($number in $range) {\n        if ($number % 2 -eq 0) {\n            $evenCount++\n        }\n    }\n}\n\nMeasure-Command -InputObject (1..100) -Expression $scriptBlock |\n    Format-List TotalMilliseconds\nWrite-Host \"The count of even numbers in 1..100 is $evenCount.\"<\/code><\/pre>\n<pre><code class=\"language-powershell-console\">TotalMilliseconds : 1.3838\n\nThe count of even numbers in 1..100 is 50.<\/code><\/pre>\n<h3>Custom Function<\/h3>\n<p>If you are serious about the performance variable, and want to keep the script block as clean as possible, we could elaborate our own function, and shape the output as we want.<\/p>\n<p>The <code>Measure-Command<\/code> Cmdlet uses an object called <code>System.Diagnostics.Stopwatch<\/code>. It works like a real stopwatch, and we control it using its methods, like <code>Start()<\/code>, <code>Stop()<\/code>, etc. All we need to do is start it before executing our script block, stop it after execution finishes, and collect the result from the <strong>Elapsed<\/strong> property.<\/p>\n<pre><code class=\"language-powershell\">function Measure-CommandEx {\n\n    [CmdletBinding()]\n    param (\n        [Parameter(Mandatory, Position = 0)]\n        [scriptblock]$Expression,\n\n        [Parameter(ValueFromPipeline)]\n        [psobject[]]$InputObject\n    )\n\n    Begin {\n        $stopWatch = New-Object -TypeName 'System.Diagnostics.Stopwatch'\n\n        &lt;#\n            We need to define result as a list because the way objects\n            are passed to the pipeline. If you pass a collection of objects,\n            the pipeline sends them one by one, and the result\n            is always overridden by the last item.\n        #&gt;\n        [System.Collections.Generic.List[PSObject]]$result = @()\n    }\n\n    Process {\n        if ($InputObject) {\n\n            # Starting the stopwatch.\n            $stopWatch.Start()\n\n            # Creating the '$_' variable.\n            $dollarUn = New-Object -TypeName psvariable -ArgumentList @('_', $InputObject)\n\n            &lt;#\n                Overload is:\n                    InvokeWithContext(\n                        Dictionary&lt;string, scriptblock&gt; functionsToDefine,\n                        List&lt;psvariable&gt; variablesToDefine,\n                        object[] args\n                    )\n            #&gt;\n            $result.AddRange($Expression.InvokeWithContext($null, $dollarUn, $null))\n\n            $stopWatch.Stop()\n        }\n        else {\n            $stopWatch.Start()\n            $result.AddRange($Expression.InvokeReturnAsIs())\n            $stopWatch.Stop()\n        }\n    }\n\n    End {\n        return [PSCustomObject]@{\n            ElapsedTimespan = $stopWatch.Elapsed\n            Result = $result\n        }\n    }\n}<\/code><\/pre>\n<p>Note that there is overhead when using the <strong>InputObject<\/strong> parameter, meaning there is a difference in the overall execution time.<\/p>\n<h2>Conclusion<\/h2>\n<p>I hope you, like me, learned something new today, and had fun along the way.<\/p>\n<p>Until a next time, happy scripting!<\/p>\n<h2>Links<\/h2>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/powershell\/module\/microsoft.powershell.utility\/measure-command\">Measure-Command<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.management.automation.scriptblock.invokewithcontext\">InvokeWithContext Method<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.management.automation.scriptblock.invokereturnasis\">InvokeReturnAsIs Method<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/FranciscoNabas\/WindowsUtils\">Test our WindowsUtils module!<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/FranciscoNabas\">See what I&#8217;m up to<\/a><\/li>\n<\/ul>\n<p><!-- link references --><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post shows how to measure script execution time in PowerShell<\/p>\n","protected":false},"author":62334,"featured_media":77,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[13,2],"tags":[83,91,90,3],"class_list":["post-1007","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-powershell","category-powershell-community","tag-automation","tag-measure-command","tag-performance","tag-powershell"],"acf":[],"blog_post_summary":"<p>This post shows how to measure script execution time in PowerShell<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/posts\/1007","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/users\/62334"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/comments?post=1007"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/posts\/1007\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/media\/77"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/media?parent=1007"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/categories?post=1007"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/tags?post=1007"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}