Trim Process Working Sets

Version: 1.6.9
Creator Name: Guy Leech
Date Created: 2019-07-25
Date Modified: 2019-08-04
Scripting language: PS
Download Count: 338

Completely empty or trim process working sets to a specific size in order to make more memory available for other processes/users. The memory is paged oiut and pages of this will be paged back in if the process needs it. WARNING. If too many processes are trimmed too frequently, performance can suffer due to hard page faults so use with caution

The Script

<#
.SYNOPSIS

Empty working sets or trim them down to a specific value for a given process or all processes in a specified session

.PARAMETER id

Process id or session id to act on

.PARAMETER trimToMB

The size of the working set to trim down to. If 0 or negative then the entire working set will be emptied. The WS can grow above this as long as hard limit is not in place.

.CONTEXT

Computer, Session

.LINK

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

.NOTES

If too many processes are trimmed too frequently, performance can suffer due to hard page faults so use with caution

.MODIFICATION_HISTORY:

@guyrleech 25/07/19

#>

[CmdletBinding()]

Param
(
    [Parameter(Mandatory=$true,HelpMessage='Process or session id to act on')]
    [int]$id ,
    [Parameter(Mandatory=$true,HelpMessage='Working set size to trim down to')]
    [int]$trimToMB,
    [Parameter(Mandatory=$false,HelpMessage='Processes to exclude from memory trim (eg, cmd/powershell/winlogon)')]
    [string]$excludedProcesses
)

[bool]$sessionContext = $true ## works on either a single process all all processes in a session

$VerbosePreference = 'SilentlyContinue'
$DebugPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'

[int]$thePid = -1
[int]$theSessionId = -1
[array]$processes = @()

if( $sessionContext )
{
    $theSessionId = $id
    $processes = @( Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $theSessionId } )

    if( ! $processes -or ! $processes.Count )
    {
        Throw "Failed to get any processes for session $theSessionId"
    }
}
else
{
    $thePid = $id
    $processes = @( Get-Process -Id $thePid -ErrorAction SilentlyContinue )

    if( ! $processes -or ! $processes.Count )
    {
        Throw "Failed to get a process for pid $thePid"
    }
}

[int]$maxWorkingSet = $trimToMB * 1MB

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 );
    }
}
'@

[int]$thisMinimumWorkingSet = -1 
[int]$thisMaximumWorkingSet = -1 
[int]$thisFlags = 0
[int]$counter = 0

$listOfExcludedProcesses = $excludedProcesses -split "/"
ForEach( $process in $processes )
{
    if ($listOfExcludedProcesses -like "$($process.ProcessName)") {
        Write-Output "Excluded process from memory trim: $($Process.ProcessName) pid $($Process.Id)"
        continue
    }
    else
    {
        Write-Verbose "Trimming memory for $($process.ProcessName)"
        if( ! $process.Handle )
        {
            Write-Warning -Message "No process handle for process $($process.ProcessName) pid $($process.Id)"
        }
        else
        {
            ## 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 )
            {                
                Write-Warning ( "Failed to get working set info for {0} pid {1} - {2}" -f $process.ProcessName , $process.Id , $LastError)
                $thisMinimumWorkingSet = 1KB ## will be set to the minimum by the call
            }
            else
            {
                [int]$originalMaxWorkingSet = 0
                if( $maxWorkingSet -le 0 )
                {
                    $thisMaximumWorkingSet = $thisMinimumWorkingSet = -1 ## emptying the working set
                }
                else
                {
                    $originalMaxWorkingSet = $thisMaximumWorkingSet
                    $thisMaximumWorkingSet = $maxWorkingSet ## not completely emptying & can grow above this , assuming it doesn't have a hard WS limit flag set
                }
   
                ## see https://msdn.microsoft.com/en-us/library/windows/desktop/ms686237(v=vs.85).aspx
                $result = [PInvoke.Win32.Memory]::SetProcessWorkingSetSizeEx( $process.Handle , $thisMinimumWorkingSet , $thisMaximumWorkingSet , $thisFlags );$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()

                if( ! $result )
                {                   
                    Write-Error ( "Failed to set working set size for {0} pid {1} to {2}MB - {3}" -f $process.ProcessName , $process.Id , $maxWorkingSet , $LastError)
                }
                else
                {
                    $counter++
                }
            }
        }
    }
}

Write-Output -InputObject ( "{0} working sets of {1} process{2}" -f $(if( $maxWorkingSet -le 0 ) { 'Emptied' } else { 'Trimmed' }) , $counter , $(if( $counter -ne 1 ) { 'es' } ) )