Measure-MemoryUsage.ps1

<#PSScriptInfo
.Version 0.0.1
.Guid 19631007-c2aa-4441-9061-f131fff11b19
.Author Ronald Bode (iRon)
.CompanyName PowerSnippets.com
.Copyright Ronald Bode (iRon)
.Tags Measure Memory Usage Command ScriptBlock
.LicenseUri https://github.com/iRon7/Measure-MemoryUsage/LICENSE
.ProjectUri https://github.com/iRon7/Measure-MemoryUsage
.IconUri https://raw.githubusercontent.com/iRon7/Measure-MemoryUsage/master/Measure-MemoryUsage.png
.ExternalModuleDependencies
.RequiredScripts
.ExternalScriptDependencies
.ReleaseNotes
.PrivateData
#>


<#
.SYNOPSIS
Measure memory usage of a command or script block.
 
.DESCRIPTION
This cmdlet tries to determine the memory usage of a command or script block by executing it a new session and
comparing the memory usage before and the peak usage after the execution of the command or script block.
 
.INPUTS
ScriptBlock or Expression
 
.OUTPUTS
PSCustomObject
 
.EXAMPLE
# Measure the memory usage of the `Get-Service` command
 
    Measure-MemoryUsage Get-Service
 
    WorkingSet PageMemory VirtualMemory Duration
    ---------- ---------- ------------- --------
    48.0 KB 64.0 KB 192 KB 00:00:01.2212212
 
.EXAMPLE
# Measure the memory usage of ScriptBlock for Windows PowerShell 5.1 and PowerShell 7
 
Memory used under PowerShell 7 (launched from PowerShell 7):
 
    Measure-MemoryUsage { Get-ChildItem -Recurse *.ps1 }
 
    WorkingSet PageMemory VirtualMemory Duration
    ---------- ---------- ------------- --------
    32.0 KB 36.0 KB 0 bytes 00:00:00.0692499
 
Memory used under Windows PowerShell 5.1:
 
    Measure-MemoryUsage -PSVersion 5.1 { Get-ChildItem -Recurse *.ps1 }
 
    WorkingSet PageMemory VirtualMemory Duration
    ---------- ---------- ------------- --------
    572 KB 1.16 MB 188 KB 00:00:00.2387508
 
.EXAMPLE
# Measure the memory usage of different ways to output a list of objects
 
    $Syntaxes = [Ordered]@{
        PlusIs = {
            $a = @()
            0..50000 | ForEach-Object { $a += [PSCustomObject]@{ Index = $_; Name = "Name$_" } }
            $a | Export-Csv .\Test.csv
        }
        List = {
            $a = [System.Collections.Generic.List[Object]]::new()
            0..50000 | ForEach-Object { $a.Add([PSCustomObject]@{ Index = $_; Name = "Name$_" }) }
            $a | Export-Csv .\Test.csv
        }
        Assign = {
            $a = 0..50000 | ForEach-Object { [PSCustomObject]@{ Index = $_; Name = "Name$_" } }
            $a | Export-Csv .\Test.csv
        }
        Pipeline = {
            0..50000 | ForEach-Object { [PSCustomObject]@{ Index = $_; Name = "Name$_" } } | Export-Csv .\Test.csv
        }
    }
 
    Foreach ($SyntaxName in $Syntaxes.Keys) {
        $Syntax = $Syntaxes[$SyntaxName]
        $Properties = [Ordered]@{ Syntax = $SyntaxName }
        Foreach ($PSVersion in '5.1', '7') {
            $Params = @{ Command = $Syntax }
            if ($PSVersion -eq '5.1') { $Params.PSVersion = $PSVersion }
            $MemoryUsage = Measure-MemoryUsage @Params
            $Properties["PowerShell $PSVersion"] = $MemoryUsage.WorkingSet
        }
        [PSCustomObject]$Properties
    }
 
Yields:
 
    Syntax PowerShell 5.1 PowerShell 7
    ------ -------------- ------------
    PlusIs 11.3 MB 48.8 MB
    List 1.42 MB 28.0 KB
    Assign 1.44 MB 28.0 KB
    Pipeline 39.0 MB 24.0 KB
 
> [!WARNING]
> The results of measured memory usage is an indication of the memory used by a specific command
> but might be erratic and even negative due to the (automatic) garbage collector and other factors.
> Besides, the results might be inconclusive or misleading due to the actual memory definitions for
> [private bytes, virtual bytes and working sets][1]
 
.PARAMETER Command
The command or script block to measure the memory usage of.
 
.PARAMETER PSVersion
The version of PowerShell to use. Default is the current version./
This parameter is only available in PowerShell 7 and later.
 
.LINK
[1]: https://stackoverflow.com/a/1986486/1701026 "private bytes, virtual bytes and working sets"
#>


Using namespace System.Management.Automation

[CmdletBinding()][OutputType([PSCustomObject])] param(
    [Parameter(Mandatory = $true, ValueFromPipeline = $true)]$Command,
    [Version]$PSVersion
)

begin {
    # https://stackoverflow.com/a/57535522/1701026

    Class ByteSize {
        hidden Static $Shlwapi
        Static ByteSize() {
            $TypeParams = @{
                Name = 'ShlwapiFunctions'
                Namespace = 'ShlwapiFunctions'
                MemberDefinition = '[DllImport("Shlwapi.dll", CharSet=CharSet.Auto)]public static extern int StrFormatByteSize(long fileSize, System.Text.StringBuilder pwszBuff, int cchBuff);'
                PassThru = $true
            }
            [ByteSize]::Shlwapi = Add-Type @TypeParams
        }
        hidden $_Value
        ByteSize($Value) { $this._Value = $Value }
        [String]ToString() {
            $Bytes = New-Object Text.StringBuilder 20
            $Return = [ByteSize]::Shlwapi::StrFormatByteSize($this._Value, $Bytes, $Bytes.Capacity)
            if (-not $Return) { throw "Failed to convert $($this._Value) format byte size." }
            return $Bytes.ToString()
        }
    }
}

process {
    # https://stackoverflow.com/questions/1984186/what-is-private-bytes-virtual-bytes-working-set
    $MemoryUsage = try {
        $Params = @{
            ScriptBlock = {
                $Process0 = Get-Process -Id $PID
                $Process0.Refresh()
                $DateTime0 = [DateTime]::Now
                $null = Invoke-Expression $Args[0]
                $DateTime1 = [DateTime]::Now
                $Process1 = Get-Process -Id $PID
                [PSCustomObject]@{
                    StartPageMemory            = $Process0.PagedMemorySize64
                    StartPrivateMemorySize     = $Process0.PrivateMemorySize64
                    StartVirtualMemorySize     = $Process0.VirtualMemorySize64
                    StartWorkingSet            = $Process0.WorkingSet64
                    FinalPageMemory            = $Process1.PagedMemorySize64
                    FinalPrivateMemorySize     = $Process1.PrivateMemorySize64
                    FinalVirtualMemorySize     = $Process1.VirtualMemorySize64
                    FinalWorkingSet            = $Process1.WorkingSet64
                    FinalPeakPageMemory        = $Process1.PeakPagedMemorySize64
                    FinalPeakVirtualMemorySize = $Process1.PeakVirtualMemorySize64
                    FinalPeakWorkingSet        = $Process1.PeakWorkingSet64
                    DeltaWorkingSet            = $Process1.PeakWorkingSet64        - $Process0.WorkingSet64
                    DeltaPagedMemory           = $Process1.PeakPagedMemorySize64   - $Process0.PagedMemorySize64
                    DeltaVirtualMemory         = $Process1.PeakVirtualMemorySize64 - $Process0.VirtualMemorySize64
                    DeltaDateTime              = $DateTime1 - $DateTime0
                }
            }
            ArgumentList = [String]$Command
        }
        if ($PSBoundParameters.ContainsKey('PSVersion')) { $Params['PSVersion'] = $PSVersion }
        Start-Job @Params | Receive-Job -AutoRemoveJob -Wait
    }
    catch [ParameterBindingException] { $PSCmdlet.ThrowTerminatingError($_) }

    $MemoryUsage.PSObject.Properties.Add([PSScriptProperty]::new('WorkingSet',    { [String][ByteSize]$this.DeltaWorkingSet }))
    $MemoryUsage.PSObject.Properties.Add([PSScriptProperty]::new('PageMemory',    { [String][ByteSize]$this.DeltaPagedMemory }))
    $MemoryUsage.PSObject.Properties.Add([PSScriptProperty]::new('VirtualMemory', { [String][ByteSize]$this.DeltaVirtualMemory }))
    $MemoryUsage.PSObject.Properties.Add([PSScriptProperty]::new('Duration',      { [String]$this.DeltaDateTime }))

    $DefaultDisplaySet = 'WorkingSet', 'PageMemory', 'VirtualMemory', 'Duration'
    $DefaultDisplayPropertySet = [PSPropertySet]::new('DefaultDisplayPropertySet', [string[]]$DefaultDisplaySet)
    $PSStandardMembers = [PSMemberInfo[]]@($DefaultDisplayPropertySet)
    $MemoryUsage | Add-Member MemberSet PSStandardMembers $PSStandardMembers
    $MemoryUsage
}