views:

782

answers:

3

EDIT - nulled thread before measuring finishing memory

This is all running .NET Compact Framework 2.0 under Windows CE 5.0.

I've come across some interesting behaviour in developing my App. Whenever I try to create a form and have it run a separate thread it seems to leak 392 bytes when it's closed and I'm unsure why.

My approach so far has been create a new thread and have it a) Create the form and continuously call Application.DoEvents until it shuts or b) Create the form pass it to Application.Run(Form).

The following is a sample form that illustrates the problem.

public partial class TestForm : Form
{
    public TestForm()
    {
        InitializeComponent();
    }

    private void DoMemoryTest(bool useApplicationRun)
    {
        GC.WaitForPendingFinalizers();
        GC.Collect();
        long originalMem = GC.GetTotalMemory(true);

        Thread t;
        if (useApplicationRun)
            t = new Thread(new ThreadStart(AppRunThread));
        else
            t = new Thread(new ThreadStart(DoEventThread));
        t.Start();

        Thread.Sleep(3000);//Dodgey hack
        t.Join();
        t = null;

        GC.WaitForPendingFinalizers();
        GC.Collect();
        long terminatingMem = GC.GetTotalMemory(true);

        MessageBox.Show(String.Format("An increase of {0} bytes was measured from {1} bytes", 
                        terminatingMem - originalMem, originalMem));
    }

    private void button1_Click(object sender, EventArgs e)
    {
        DoMemoryTest(false);
    }

    private void button2_Click(object sender, EventArgs e)
    {
        DoMemoryTest(true);
    }

    private void AppRunThread()
    {
        Application.Run(new OpenCloseForm());
    }

    private void DoEventThread()
    {
        using (OpenCloseForm frm = new OpenCloseForm())
        {
            frm.Show();
            do
            {
                Application.DoEvents();
            } while (frm.Showing);
        }
    }

    /// <summary>
    /// Basic form that opens for a short period before shutting itself
    /// </summary>
    class OpenCloseForm : Form
    {
        public OpenCloseForm()
        {
            this.Text = "Closing Soon";
            this.Size = new Size(100, 100);
            this.TopMost = true;
        }

        public volatile bool Showing = false; //dodgy hack for DoEventThread

        System.Threading.Timer timer;
        protected override void OnLoad(EventArgs e)
        {
            Showing = true;
            base.OnLoad(e);
            timer = new System.Threading.Timer(new TimerCallback(TimerTick), null, 1000, 1000);
        }

        delegate void CloseDelegate();
        private void TimerTick(object obj)
        {
            this.Invoke(new CloseDelegate(this.Close));
        }

        protected override void OnClosed(EventArgs e)
        {
            base.OnClosed(e);
            Showing = false;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (timer != null)
                {
                    timer.Dispose();
                    timer = null;
                }
            }
            base.Dispose(disposing);
        }
    }

    //Designer code to follow....

    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.timer1 = new System.Windows.Forms.Timer();
        this.button1 = new System.Windows.Forms.Button();
        this.button2 = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // button1
        // 
        this.button1.Location = new System.Drawing.Point(32, 47);
        this.button1.Name = "button1";
        this.button1.Size = new System.Drawing.Size(116, 39);
        this.button1.TabIndex = 1;
        this.button1.Text = "DoEvents Loop";
        this.button1.Click += new System.EventHandler(this.button1_Click);
        // 
        // button2
        // 
        this.button2.Location = new System.Drawing.Point(32, 115);
        this.button2.Name = "button2";
        this.button2.Size = new System.Drawing.Size(116, 39);
        this.button2.TabIndex = 2;
        this.button2.Text = "Application.Run";
        this.button2.Click += new System.EventHandler(this.button2_Click);
        // 
        // TestForm
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
        this.AutoScroll = true;
        this.ClientSize = new System.Drawing.Size(177, 180);
        this.Controls.Add(this.button2);
        this.Controls.Add(this.button1);
        this.Name = "TestForm";
        this.Text = "TestForm";
        this.TopMost = true;
        this.ResumeLayout(false);

    }

    #endregion

    private System.Windows.Forms.Timer timer1;
    private System.Windows.Forms.Button button1;
    private System.Windows.Forms.Button button2;
}

Am I missing something in regards to the disposal of controls? Does anyone have any ideas on where to go from this?

Thanks in advance.

A: 

I might be wrong as I'm not an expert in CF, but .net won't neccessarily release the memory that it has taken unless the system is under memory pressure.

My understanding was that if the app needed x bytes of memory once, it will probably need at least x bytes again at some point. It wont release that memory unless something else in the OS needs it.

Spence
I know you're right about GC behavior on the desktop CLR (giving memory back to the OS), but I believe GC.GetTotalMemory returns only the data .NET is actually using, not the amount it has allocated from the OS.
Jonathan
could it be an issue with JIT of the type? Ie when you check it isn't compiled but then when you call it is? does it leak every time you click?
Spence
I agree with Jonathan, all my testing seems to be consistent with the Garbage Collector's idea of how much memory the framework has allocated, not the actual physical memory footprint as reported by GlobalMemoryStatus.
Josh LOL
And yes, every time I click it leaks (Occasionally it leaks a little more but I think thats related to me clicking too quickly).
Josh LOL
A: 

First, I'd ask why you're creating enough forms on other threads that you care about 392 bytes, but that's beside the point I guess.

The first place I'd start at is that managed thread instance that you don't get rid of in the DoMemoryTest method. Call t.Join() after your Thread.Sleep(3000) call, then set it to null (so it can be GCed even in debug mode).

Jonathan
oops, that was a silly oversight. Unfortunately after clearing it up the leak has only reduced to 336 bytes now.I mainly care because I'm afraid I've somehow missed some fundamental concept with the .NET CF and I don't want it to bite me later on. Thanks for the response.
Josh LOL
+3  A: 

Alrighty, the first thing I have to say about this code is WTF?! You need to do some research into how Windows messages work. I'll cover a little bit here, but you really need to understand it before diving in and trying to crazy stuff like what I'm seeing here.

  1. When a Window is created, all "actions" for that Window are delivered via Windows Messages. When you call Refresh, or Click or whatever, that is a Windows Message.
  2. These messages are dispatched from a "message pump" which is simply a loop that calls PeekMessage, GetMEsssage, TranslateMessage and DispatchMessage APIs.
  3. Application.Run does this in managed code.
  4. Windows Messages for a Form must be delivered in the same thread context as the window was created. That means the pump must also be on the same thread. This is why Control.Invoke exists.
  5. A pump on one thread will no even see messages to a window on another thread

So there are certainly some issues with what you're doing. I'm not certain what the "big picture" of what you're trying to achieve is and how you may have noticed this bug, but this code tells me you've got some fundamental problems in your architecture.

But what about this leak you think you've found anyway? Well, there is no leak. You're not understanding CF (and managed) memory management. Again, research is recommended, but MSDN has a really good webcast that covers it well.

The short of it for this scenario is that you have created some objects on separate threads. Those threads create a bunch of things, some of which are IDisposable, some not. When the thread tears down, those items no longer have roots are therefore available for collection. When Collect is called, the GC then walks all the roots and notes every object that has a root (mark). Those that do not are then "freed"(sweep). The region in the GC heap that they were is in simply no longer marked as in use - see the webcast for details. If the item implementred IDisposabe, it then gets a new root so the finalizer can still exist and run on the next GC cycle. And lastly the finalizer thread runs (non-deterministically).

You code does not account for this behavior. You've not run Collect twice. You've not waited for finalizers after the Collect call (and simply calling WaitForPendingFinalizers may not be enough). Since your threads themselves are not marked as background threads, who knows what their position in their life cycle and state of GC usage might be.

So when we really get down to it, the question is this: what exactly are you trying to solve? You are running in a managed memory environment. Unless you're seeing OOM's you almost always shouldn't be worrying about memory levels - that's the whole point of having a GC in the first place. Don't try building a convoluted, where's Waldo academic exercise and have us try to find the leak.

If you are actually having a problem, then you should first make sure your application design is something in line with the way Windows applications should be written and then use tools like RPM to profile what roots are holding the memory and fix the leaks you have created (yes, leaks still can and do happen in managed code). Of course you can always ask reasonable questions about the real world problem here as well.

ctacke
Thanks for the response. The problem I was trying to solve was to have a "System is busy" animated form that I can show whilst the program is doing something intensive. Whilst I know I can accomplish this through callbacks to a loading form, I was hoping to separate this form from the actual computation code, hence having a form animating on a separate thread. In trying to accomplish this I noticed the memory disappearing and was curious to what was happening. Ill have a look at that webcast too.
Josh LOL