none
Memory leaks while using powershell runspace in .NET RRS feed

  • General discussion

  • I found a huge memory leak in project which appears after execution PS command. Here I create example that represent my problem:

    public class TestCase
    {
        public void Test()
        {
            GetCommandsInfo(GetModules());
        }

        private void GetCommandsInfo(string[] modules)
        {
            InitialSessionState initialState = InitialSessionState.CreateDefault();
            initialState.ImportPSModule(modules);

            using (Runspace runspace = RunspaceFactory.CreateRunspace(initialState))
            {
                runspace.Open();

                using (Pipeline pipeline = runspace.CreatePipeline())
                {
                    string script = "Get-Command -ListImported -CommandType Cmdlet,Alias";
                    pipeline.Commands.AddScript(script);

                    IEnumerable psObjects = pipeline.Invoke();
                    if (psObjects == null)
                    {
                        Debug.Fail("Pipeline.Invoke failed!");
                        throw new InvalidOperationException();
                    }
                }

                runspace.Close();
            }
        }

        private static string[] GetModules()
        {
            string[] result;
            RunspaceConfiguration rsConfig = RunspaceConfiguration.Create();
            using (Runspace runspace = RunspaceFactory.CreateRunspace(rsConfig))
            {
                runspace.Open();

                using (Pipeline pipeline = runspace.CreatePipeline())
                {
                    pipeline.Commands.AddScript("Get-Module -ListAvailable");
                    IEnumerable psObjects = pipeline.Invoke();
                    if (psObjects == null)
                    {
                        Debug.Fail("Pipeline.Invoke failed!");
                        throw new InvalidOperationException();
                    }

                    result = GetModules(psObjects);
                }

                runspace.Close();
            }

            return result;
        }

        private static string[] GetModules(IEnumerable psObjects)
        {
            if (psObjects == null)
            {
                Debug.Fail("Get-Module: collection of PS Objects is null!");
                throw new ArgumentNullException("psObjects");
            }

            List<string> result = new List<string>();
            foreach (PSObject psObject in psObjects)
            {
                if (psObject == null || psObject.ImmediateBaseObject == null)
                {
                    Debug.Fail("Get-Module: an item is null!");
                    continue;
                }

                PSModuleInfo moduleInfo = psObject.ImmediateBaseObject as PSModuleInfo;
                if (moduleInfo == null)
                {
                    Debug.Fail("Get-Module: an item is not PSModuleInfo!");
                }
                result.Add(moduleInfo.Name);
            }

            return result.ToArray();
        }
    }

    After memory usage investigation I found that most of memory occupied by inner structures from System.Management.Automation. So I start to search the way to release resources and found that the same behavior can be reproduced in PS:

    Get-Module -ListAvailable | ForEach { Import-Module $_.Name }
    Get-Command -ListImported -CommandType Cmdlet,Alias

    I already have tried to initiated garbage collecting and to remove all new variables that appears after script execution. I even have tried to start new Runspace into PowerShell ISE to run script, but resources still didn't release.

    During my research I found that this code still eat a bunch of memory:

    public static void TestLeak()
    {
        using (Runspace runspace = RunspaceFactory.CreateRunspace())
        {
            runspace.Open();

            using (Pipeline pipeline = runspace.CreatePipeline())
            {
                StringBuilder builder = new StringBuilder();
                builder.Append(@"for ($i = 0; $i -lt 1000; $i++)
                {
                    Get-Process *
                }");
                pipeline.Commands.AddScript(builder.ToString());

                Collection<PSObject> psObjects = pipeline.Invoke();
                Console.WriteLine("Test count: " + psObjects.Count);
            }

            runspace.Close();
        }
    }

    But if we change this:
    Get-Process *
    to this:
    $res = Get-Process *
    then memory doesn't leak. Variable goes out of scope and then freed up by garbage collector. But in first case it looks like PowerShell stores results somewhere in inner structures. I have tried to add variable to my script to store cmdlet's results but nothing changed like my variable never goes out of scope!

    I am stuck and can't find any resolutions for this problem. Maybe someone met such a problem and can help me with that.


    • Edited by v.bohatikov Thursday, October 24, 2019 9:28 AM
    • Changed type Bill_Stewart Tuesday, April 14, 2020 5:02 PM
    • Moved by Bill_Stewart Tuesday, April 14, 2020 5:02 PM Off-topic
    Thursday, October 24, 2019 9:23 AM

All replies

  • Sorry but we are not Microsoft employees and we don't have access to the code.

    I would recommend looking on the PowerShell Github project and checking to see if there are any reported issues.


    -- Bill Stewart [Bill_Stewart]

    Thursday, October 24, 2019 1:37 PM
  • Am I rightly understand you that that is am issue? I think maybe I do something wrong?

    Thursday, October 24, 2019 4:21 PM
  • If you don't remove the runspaces after using them they will continue to consume memory.

    For assistance writing and debugging C# code post in the C# developers forum.

    To see unremoved runspaces in the current pool use "Get-RunSpace"

    To remove a runspace from memory use "Dispose()"

    A runspace factory will persist runspaces until it is disposed.

    Again - post to C# developers forum to get assistance with managing Net code and object removal.


    \_(ツ)_/

    Thursday, October 24, 2019 4:58 PM
  • Yes, I know about disposing runspace and using construction will call Dispose method but it doesn't helps. There isn't a problem with C# code! I am a .NET developer but not a PowerShell specialist. Have not deep knowledge of things that happens under the hood and I ask for help with PowerShell things.

    As I wrote earlier you can create situation when memory doesn't freed in PowerShell script by two rows of code:

    Get-Module -ListAvailable | ForEach { Import-Module $_.Name }
    Get-Command -ListImported -CommandType Cmdlet,Alias

    After that I have try to free the memory but nothing helps me with that. And I went here to ask for advice, what I need to do to release occupied memory? Does it possible? Because now memory fees only after closing process!

    Friday, October 25, 2019 10:09 AM
  • What PowerShell things. You are working with Net classes. They just happen to be the same classes used by PowerShell. It is still C# code and needs to be asked in a C# forum.

    The only way to free the memory in a script that is executing is to wait for the garbage collector.  Also, as aa C3 developer on Windows you would also know that memory is allocated but not freed from the working set until it is needed elsewhere.  This is not a leak because it will be freed or reused,  If you are constantly re-allocating chunks of memory - which happens every time you create a thread - then the memory will be retained in anticipation of its being needed.  This is more efficient then constantly releasing and re-allocating thread memory.  When all runspaces are closed and the PowerShell host is ended or does garbage collection the memory will slowly disappear.

    So first find what memory is "leaking". What memory numbers are we talking about? How do you know this is real memory or just the current value of the workingset?

    Since this is C# code you will need to post in a C# or Net forum unless you can demonstrate this running a script with the PowerShell engine.

    If you really think this is a bug then post in the UserVoice site with a bug report.

    Also note you first example of a fix.  When the variable is not used the output accumulates to the runspace output buffer.  That will only be freed when the runspace is disposed and any variables that you have extracted into from the runspace output.

    To be clearer: the following line will retain al output in "psObjects " until the variable goes out of scope.  The app/process will not see this as freed until its garbage collection chooses to release the memory.

      IEnumerable psObjects = pipeline.Invoke();


    \_(ツ)_/

    Friday, October 25, 2019 12:10 PM