Cap Memory of Process

Version: 1.1.2
Creator Name: Guy Leech
Date Created: 2019-06-10
Date Modified: 2019-12-12
Scripting language: PS
Download Count: 48

Set the maximum working set size for a process such that it cannot consume more than that amount of memory. Gives the ability to set a memory limit on a process, such as one with a known memory leak, so that it cannot consume more than the maximum memory specified via the parameter. Additional memory allocations will be allowed by the OS paging out some of the existing working set, generally the least recently used.
Tags: memory, working set

The Script

<#
    Set a hard working set limit for the process

    @guyrleech 2019

    Based on code from https://github.com/guyrleech/Microsoft/blob/master/Trimmer.ps1
#>

if( $args.Count -ne 2 -or ! $args[0] -or ! $args[1] )
{
    Throw 'Must pass the pid of the process to act on and working set size in MB as the only arguments'
}

[int]$thePid = $args[0] -as [int]
[int]$maxWorkingSet = ($args[1] -as [int]) * 1MB

$process = Get-Process -Id $thePid -ErrorAction SilentlyContinue

if( ! $process )
{
    Throw "Failed to get a process for pid $thePid"
}

if( ! $process.Handle )
{
    Throw "No process handle for pid $thePid"
}

Add-Type -Debug:$false @'
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace PInvoke.Win32
{
  
    public static class Memory
    {
        [DllImport("kernel32.dll", SetLastError=true)]
        public static extern bool SetProcessWorkingSetSizeEx( IntPtr hProcess, int min, int max , int flags );
        [DllImport("kernel32.dll", SetLastError=true)]
        public static extern bool GetProcessWorkingSetSizeEx( IntPtr hProcess, ref int min, ref int max , ref int flags );
    }
}
'@

$statsBefore = $null
$statsAfter  = $null

[int]$flags = 4 ## QUOTA_LIMITS_HARDWS_MAX_ENABLE
[int]$thisMinimumWorkingSet = -1 
[int]$thisMaximumWorkingSet = -1 
[int]$thisFlags = -1 ## Grammar alert! :-)

## https://msdn.microsoft.com/en-us/library/windows/desktop/ms683227(v=vs.85).aspx
[bool]$result = [PInvoke.Win32.Memory]::GetProcessWorkingSetSizeEx( $process.Handle, [ref]$thisMinimumWorkingSet , [ref]$thisMaximumWorkingSet , [ref]$thisFlags );$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()

if( $result )
{
    ## convert flags value - if not hard then will be soft so no point reporting that separately IMHO
    [bool]$hardMinimumWorkingSet = $thisFlags -band 1 ## QUOTA_LIMITS_HARDWS_MIN_ENABLE
    [bool]$hardMaximumWorkingSet = $thisFlags -band 4 ## QUOTA_LIMITS_HARDWS_MAX_ENABLE
    $statsBefore = New-Object -TypeName pscustomobject -ArgumentList @{ 
            'Hard Minimum Working Set Limit' = $hardMinimumWorkingSet ; 'Hard Maximum Working Set Limit' = $hardMaximumWorkingSet ;
            'Working Set (MB)' = [math]::Round( $process.WorkingSet64 / 1MB , 1 ) ; 'Peak Working Set (MB)' = [math]::Round( $process.PeakWorkingSet64 / 1MB , 1 );
            'Commit Size (MB)' = [math]::Round( $process.PagedMemorySize / 1MB , 1 ); 
            'Paged Pool Memory Size (KB)' = [math]::Round( $process.PagedSystemMemorySize64 / 1KB , 1 ); 'Non-paged Pool Memory Size (KB)' = [math]::Round( $process.NonpagedSystemMemorySize64 / 1KB , 1 );
            'Minimum Working Set (KB)' = $thisMinimumWorkingSet / 1KB ; 'Maximum Working Set (KB)' = $thisMaximumWorkingSet / 1KB }
}
else
{                   
    Write-Warning ( "Failed to get working set info for {0} pid {1} - {2}" -f $process.Name , $process.Id , $LastError)
    $thisMinimumWorkingSet = 1KB ## will be set to the minimum by the call
}
                                     
## see https://msdn.microsoft.com/en-us/library/windows/desktop/ms686237(v=vs.85).aspx
$result = [PInvoke.Win32.Memory]::SetProcessWorkingSetSizeEx( $process.Handle , $thisMinimumWorkingSet , $maxWorkingSet , $flags );$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()

if( ! $result )
{                   
    Write-Error ( "Failed to set working set info for {0} pid {1} to {2}MB - {3}" -f $process.Name , $process.Id , $maxWorkingSet / 1MB , $LastError)
}

$result = [PInvoke.Win32.Memory]::GetProcessWorkingSetSizeEx( $process.Handle, [ref]$thisMinimumWorkingSet , [ref]$thisMaximumWorkingSet , [ref]$thisFlags );$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()

if( $result )
{
    $process = Get-Process -Id $process.Id -ErrorAction Continue
    ## convert flags value - if not hard then will be soft so no point reporting that separately IMHO
    [bool]$hardMinimumWorkingSet = $thisFlags -band 1 ## QUOTA_LIMITS_HARDWS_MIN_ENABLE
    [bool]$hardMaximumWorkingSet = $thisFlags -band 4 ## QUOTA_LIMITS_HARDWS_MAX_ENABLE
    $statsAfter = New-Object -TypeName pscustomobject -ArgumentList @{ 
            'Hard Minimum Working Set Limit' = $hardMinimumWorkingSet ; 'Hard Maximum Working Set Limit' = $hardMaximumWorkingSet ;
            'Working Set (MB)' = [math]::Round( $process.WorkingSet64 / 1MB , 1 );'Peak Working Set (MB)' = [math]::Round( $process.PeakWorkingSet64 / 1MB , 1 ) ;
            'Commit Size (MB)' = [math]::Round( $process.PagedMemorySize / 1MB , 1 ); 
            'Paged Pool Memory Size (KB)' = [math]::Round( $process.PagedSystemMemorySize64 / 1KB , 1 ); 'Non-paged Pool Memory Size (KB)' = [math]::Round( $process.NonpagedSystemMemorySize64 / 1KB , 1 );
            'Minimum Working Set (KB)' = $thisMinimumWorkingSet / 1KB ; 'Maximum Working Set (KB)' = $thisMaximumWorkingSet / 1KB }
}

"Memory statistics for process '$($process.Name)' ($thePid) started at $(Get-Date -Date $process.StartTime -Format G):"

@( ForEach( $statBefore in $statsBefore.GetEnumerator() )
{
    New-Object -TypeName psobject -Property @{ 'Value' = $statBefore.Name ; 'Before' = $statBefore.Value ; 'After' = $statsAfter[ $statBefore.Name ] }
} ) | Sort-Object -Property Value | Format-Table -AutoSize -Property 'Value','Before','After'