I have a windows service that is doing a lot of exchange remote calls to get some server information. I noticed that as longs as the time passes the memory used by the service starts growing until a memory exception is thrown. I have searched and it looks like there is a known memory leak in the System.Management.Automation
that does not dispose all the memory of the Runspace
created while calling the close and/or dispose method. I reviewed a post that suggest using the CreateOutOfProcessRunspace
of the RunspaceFactory
but not sure how to use it.
Here is how the issue can be reproduced: (System.Management.Automation
dll referenced)
for (int i = 0; i < 1000; i++)
{
var runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
runspace.Close();
runspace.Dispose();
}
If you run this code, you will see how the memory is incremented. Due to the requirements, keeping a connection open as much as possible is not a good solution.
Do you know how I can fix this issue, even using the CreateOutOfProcessRunspace
method of the RunspaceFactory
or how to propery dispose the memory?
Thanks in advance
EDIT
I was using the V3 and change the runspace creation to use the CreateRunspacePool method and it looks like the leak is gone. Thanks so much for your help!
I can see the problem in PS v3.0 but not in PS v2.0. Here is the code I use to
see this (all examples are in PowerShell):
for() {
$runspace = [runspacefactory]::CreateRunspace()
$runspace.Open()
$runspace.Close()
$p = Get-Process -Id $PID
'{0} {1}' -f $p.Handles, ($p.PrivateMemorySize / 1mb)
}
It looks like handles and memory are leaking in v3.0 in the code above.
As far as v2.0 does not have this problem, one possible workaround may be to
start the service using PS v2.0, i.e. PowerShell.exe -Version 2.0
.
If this is not possible I can think of two more workarounds. One of them is not
to create runspaces directly but use [powershell]
instead. For example, this
code does not show the leak in v3.0:
for() {
$ps = [powershell]::Create()
$p = $ps.AddCommand('Get-Process').AddParameter('Id', $PID).Invoke()
'{0} {1}' -f $p.Handles, ($p.PrivateMemorySize / 1mb)
$ps.Dispose()
}
Another workaround, if it is applicable, may be use of
[runspacefactory]::CreateRunspacePool()
. This way also does not show the
leak:
$rs = [runspacefactory]::CreateRunspacePool()
$rs.Open()
for() {
$ps = [powershell]::Create()
$ps.RunspacePool = $rs
$p = $ps.AddCommand('Get-Process').AddParameter('Id', $PID).Invoke()
'{0} {1}' -f $p.Handles, ($p.PrivateMemorySize / 1mb)
$ps.Dispose()
}
#$rs.Close() # just a reminder, it's not called here due to the infinite loop
The last one also works much faster because the runspace is kind of reused.
I was also facing the same issue when I was using v1 of System.Management.Automation. But the issue was solved with v3 of System.Management.Automation and changing the code to use the CreateOutOfProcessRunspace method
Here is the code
using (PowerShellProcessInstance instance = new PowerShellProcessInstance(new Version(4, 0), null, null, false))
{
using (var runspace = RunspaceFactory.CreateOutOfProcessRunspace(new TypeTable(new string[0]), instance))
{
runspace.Open();
using (PowerShell powerShellInstance = PowerShell.Create())
{
powerShellInstance.Runspace = runspace;
var filePath = GetScriptFullName(powerShellScriptType);
powerShellInstance.Commands.AddScript(File.ReadAllText(filePath));
var includeScript = GetIncludeScript();
powerShellInstance.AddParameters(new List<string>
{
userName,
plainPassword,
includeScript
});
Collection<PSObject> psOutput = powerShellInstance.Invoke();
// check the other output streams (for example, the error stream)
if (powerShellInstance.Streams.Error.Count > 0)
{
// error records were written to the error stream.
// do something with the items found.
var exceptions = "";
foreach (var error in powerShellInstance.Streams.Error)
{
exceptions += error.Exception + "\n";
}
throw new InvalidPowerShellStateException(exceptions);
}
return psOutput;
}
}
}