-
Notifications
You must be signed in to change notification settings - Fork 45
/
Change CPU priorities.ps1
318 lines (263 loc) · 12.5 KB
/
Change CPU priorities.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#Requires -version 3.0
<#
Change priorities of processes which over consume CPU
Use this script at your own risk.
Guy Leech, 2018
Modification history:
23/04/18 GL Added process id capability so can be launched via ControlUp Script Based Action.
Added -forceIt as equivalent to -confirm:$false for use via scheduled tasks
#>
<#
.SYNOPSIS
Change the priority class of processes which consume over a specified amount of CPU during the monitoring period to aid fair sharing of CPU
.DESCRIPTION
When one or more threads are ready to run, the OS will place the higher priority thread on a CPU first.
Therefore reducing the priority class of processes which are deemed to be consuming too much CPU means
that other processes will get preferential access to the CPU.
When a process stops over-consuming then its priority will be returned to what it was before it was reduced by the script.
.PARAMETER frequency
How often, in seconds, to check the CPU consumption of processes
.PARAMETER threshold
The percentage of time, in the time period defined by -frequency, which the process was consuming CPU, over which its CPU priority class is lowered
.PARAMETER includeNames
A comma separated list of process names that will have their processes instances and potentially punished
.PARAMETER excludeNames
A comma separated list of process names that will not have their instances examined or punished
.PARAMETER includeUsers
A comma separated list of usernames that will have their processes examined and potentially punished. The script must be run elevated
.PARAMETER excludeUsers
A comma separated list of usernames that will not have their processes examined or punished. The script must be run elevated
.PARAMETER excludeSessions
A comma separated list of session IDs which will not have their processes examined or punished
.PARAMETER selfOnly
Only examine processes in the current session
.PARAMETER forceIt
DO not prompt for confirmation before adjusting CPU priority
.EXAMPLE
& '\Change CPU priorities.ps1' -confirm:$false
Monitor all accessible processes, except those in session 0 like services, and change their priority as needed without prompting
.EXAMPLE
& '\Change CPU priorities.ps1' -confirm:$false -includeNames msiexec -confirm:$false -frequency 30 -threshold 33 -verbose
Monitor all msiexec.exe processes every 30 seconds and if any of those have consumed over 33% CPU in those 30 seconds then
their CPU priority will be decreased by one class. When any affected processes reduce their consumption to below 33% then
their priority will be returned to what it was before it was changed by this script. Verbose output will be shown.
.NOTES
The PowerShell process running the script will have its priority class set to High to give it the best chance of being able to run on a busy system.
This priority will be reset when the script exits, e.g. via ctrl-C
There is a very small risk that a process could terminate and a new one be created with the same process id when the script is sleeping but this is
vyer unlikely and the script may cope with it anyway.
#>
[CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='High')]
Param
(
[ValidateScript({$_ -gt 0 })]
[int]$frequency = 60 ,
[ValidateScript({$_ -gt 0 -and $_ -le 100 })]
[decimal]$threshold = 50 ,
[string[]]$includeNames = @() ,
[string[]]$excludeNames = @(),
[string[]]$includeUsers = @() ,
[string[]]$excludeUsers = @() ,
[int[]]$excludeSessions = @( 0 ) ,
[int[]]$pids = @() ,
[switch]$forceIt ,
[switch]$selfOnly
)
[hashtable]$getProcessParams = @{}
if( $includeUsers.Count -or $excludeUsers.Count )
{
if( $PSVersionTable.PSVersion.Major -ge 3 )
{
$myWindowsPrincipal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())
# Get the security principal for the Administrator role
$adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator
# Check to see if we are currently running "as Administrator"
if ( $myWindowsPrincipal.IsInRole($adminRole))
{
$getProcessParams.Add( 'IncludeUserName' , $true )
}
else
{
Write-Warning "Unable to get user names since not running elevated"
return
}
}
else
{
Write-Warning "Unable to get user names as requires PowerShell 3.0 or higher and this is $($PSVersionTable.psversion.ToString())"
return
}
}
if( $pids -and $pids.Count )
{
$getProcessParams.Add( 'Id' , $pids )
}
[hashtable]$processes = @{}
[int]$pulse = 0
[decimal]$excessive = 100 / $frequency
## Priority reducing table
[hashtable]$lowerPriority =
@{
'Idle' = 'Idle'
'BelowNormal' = 'Idle'
'Normal' = 'BelowNormal'
'AboveNormal' = 'Normal'
'High' = 'AboveNormal'
'Realtime' = 'High'
}
[int]$ownSession = -1
if( $selfOnly )
{
$ownSession = (Get-Process -Id $pid).SessionId
}
Function Revert-Processes( [hashtable]$processes )
{
$processes.GetEnumerator() | ForEach-Object `
{
if( $_.Value.AdjustedPriority )
{
$priority = $_.Value.OriginalPriority
Get-Process -Id $_.Value.Id -ErrorAction SilentlyContinue | ForEach-Object { $_.PriorityClass = $priority }
$_.Value.AdjustedPriority = $false
}
}
}
[string]$originalPriority = $null
Get-Process -Id $pid | Select -First 1 | ForEach-Object { $originalPriority = $_.PriorityClass ; $_.PriorityClass = 'High' }
## workaround for scheduled task not liking -confirm:$false being passed
if( $forceIt )
{
$ConfirmPreference = 'None'
}
## Put main loop in a try block so can revert process priorties back at exit via finally block, e.g. if ctrl-c pressed
try
{
While( $true )
{
[int]$excluded = 0
[int]$inspected = 0
Get-Process @getProcessParams -ErrorAction SilentlyContinue | ForEach-Object `
{
$thisProcess = $_
if( $thisProcess.HasExited )
{
$processes.Remove( $thisProcess.Id )
}
elseif( $thisProcess.Id -ne $PID ) ## don't adjust ourself
{
[bool]$included = $true
if( $includeUsers.Count )
{
$included = $includeUsers -contains $thisProcess.UserName
}
elseif( $excludeUsers.Count )
{
$included = $excludeUsers -notcontains $thisProcess.UserName
}
if( $included -and $includeNames.Count )
{
$included = $includeNames -contains $thisProcess.Name
}
elseif( $included -and $excludeNames.Count )
{
$included = $excludeNames -notcontains $thisProcess.Name
}
if( $included -and $excludeSessions.Count )
{
$included = $excludeSessions -notcontains $thisProcess.SessionId
}
if( $included -and $ownSession -ge 0 )
{
$included = ( $thisProcess.SessionId -eq $ownSession )
}
if( $included )
{
$inspected++
## If we have the process already then look how much CPU it has used since last time sampled
$existingProcess = $processes[ $thisProcess.Id ]
if( $existingProcess )
{
[int]$cpuConsumptionPercent = ( $thisProcess.TotalProcessorTime.TotalSeconds - $existingProcess.TotalCPUSeconds ) * $excessive
if( $cpuConsumptionPercent -gt $threshold )
{
Write-Verbose "$($thisProcess.Id) : $($thisProcess.Name) : Consumed $cpuConsumptionPercent % CPU (had $($thisProcess.TotalProcessorTime.TotalSeconds - $existingProcess.TotalCPUSeconds) secs), priority class $($thisProcess.PriorityClass)"
if( $existingProcess.OriginalPriority )
{
$newPriority = $lowerPriority[ $existingProcess.OriginalPriority.ToString() ] ## set to same reduced priority rather than keep decreasing it
if( $PSCmdlet.ShouldProcess( "$($thisProcess.Name) ($($thisProcess.id))" , "Change priority to $newPriority" ))
{
$thisProcess.PriorityClass = $newPriority
$existingProcess.AdjustedPriority = $true
}
}
else
{
Write-Warning "Unable to get current priority for $($thisProcess.Name) ($($thisProcess.Id)) so unable to reduce it - probably a permissions issue"
}
}
elseif( $existingProcess.AdjustedPriority )
{
Write-Verbose "$($thisProcess.Id) : $($thisProcess.Name) : Consumed $cpuConsumptionPercent % CPU, priority class $($thisProcess.PriorityClass) but now below threshold of $threshold so setting back to $($existingProcess.OriginalPriority)"
$existingProcess.AdjustedPriority = $false
$thisProcess.PriorityClass = $existingProcess.OriginalPriority
}
## update main table with changed stats
$existingProcess.TotalCPUSeconds = $thisProcess.TotalProcessorTime.TotalSeconds
## mark this process as alive so we can make a pass to remove dead processes
$existingProcess.Pulse = $pulse
}
else ## process did not exist in our hash table so add it with extra properties
{
try
{
Add-Member -InputObject $thisProcess -NotePropertyMembers `
@{
## Record CPU consumption as TotalProcessTime is read-only so we can't update the existing hash table entry on next pass
TotalCPUSeconds = $thisProcess.TotalProcessorTime.TotalSeconds;
Pulse = $pulse ;
OriginalPriority = $thisProcess.PriorityClass
AdjustedPriority = $false
}
$processes.Add( $thisProcess.Id , $thisProcess )
}
catch
{
Write-Verbose "Failed to add process id $($thisProcess.Id) $($thisprocess.Name): $($_.Exception.Message)"
}
}
}
else
{
$excluded++
}
}
}
## see if no pids found/left so that we exit
if( $pids -and $pids.Count -and ! $inspected )
{
Write-Warning "None of the specified pids $($pids -join ', ') were found or were not included or were excluded so exiting"
break
}
## remove processes which are no longer alive so have missed the change in the pulse value - have to use a clone of the hashtable since we can't change it when enumerating over it
if( $processes.Count )
{
[hashtable]$clonedProcesses = $processes.Clone()
$clonedProcesses.GetEnumerator() | ForEach-Object `
{
if( $_.Value.HasExited -or $_.Value.Pulse -ne $pulse )
{
$processes.Remove( $_.Key )
}
}
Clear-Variable clonedProcesses
}
Write-Verbose "$(Get-Date -Format u) : sleeping for $frequency seconds with $($processes.Count) processes monitored & $excluded excluded"
Start-Sleep -Seconds $frequency
$pulse = ! $pulse
}
}
Finally
{
Revert-Processes $processes
(Get-Process -Id $pid).PriorityClass = $originalPriority
}