Leaky PowerShell

When writing a Powershell script that will be running for an extended period of time, you should be aware of how to scope your variables to be garbage collection friendly. PowerShell itself is written on the .Net framework butits GC is a bit different.

Update: I've come to find out, that's not entirely true. Lots of PowerShell is in fact written in .Net, hense the System.Management.Automation.PSObject class at the heart of everything PowerShell, but there are also portions of it written in C++. Look up some of the older PowerShell videos on Channel9 it's some facinating stuff but be warned, there is some Haskel talk.

The following code is intended to run for an extended period of time so we put the logic inside a 'while' loop with a sleep state at the end of each itteration, this should continue running the opperation indefinately.

while ($true) {
    $events = Get-WinEvent -ComputerName localhost -LogName System -MaxEvents 1000
    $events | ConvertTo-Json -Compress | Out-File -Append .\System-Events.json
    Start-Sleep -Secconds 2
}

This code gets the last 1000 events from the System event log and serializes them into a JSON format (very common alternative to XML for data serialization) and writes it to a file. You would think that since the $events variable is declared inside of the while loop and is reused on each itteration that the memory it consumes would be reused every cycle, that's not really the case. PowerShell appears to perform stack frame allocation only inside of function calls. In english that means that it only allocates a new chunk of memory (a stack) from the heap (total memory reserved by the process) when a function is used and this stack is not marked for deletion until the function is out of scope. Since the interations of the while loop use the same stack every time, the $events variable never goes out of scope even though it's reference gets reassined. The chunk of memory storing the old events doesn't go away, it just loses the $events name which gets assigned to some other chunk of memory. This simple script can leak GBs of memory in just a matter of days.

A simple solution is to move the logic inside of the loop into a function, forcing the $events variable to be declared and used inside it's own memory space each time:

function Dump-SystemEvents {
    $events = Get-WinEvent -ComputerName localhost -LogName System -MaxEvents 1000
    $events | ConvertTo-Json -Compress | Out-File -Append .\System-Events.json
}

while ($true) { Dump-SystemEvents Start-Sleep -Seconds 2 }

This code works the same way, behaves the same but the memory footprint is much much better. The next time you write a script that may be running for a while, watch it for a few days to see how it behaves in production.