3

I am in Windows 7, .NET 4.5, IE10. I have a simple windows form application with a WebBrowser control. Every time the page navigates, I want to add a click handler. I notice that memory is leaking when I do this. I am in fact detaching the event handlers and so am confused as to why memory is leaking.

I noticed that I can attach to the HtmlDocument.Click event and do not see the leaks associated with HtmlElement.Click. This is demonstrated in the program below that periodically navigates using a timer.

Also of interest is that the URL does seem to matter... e.g. I see the problem with http://www.google.com but not with http://thin.npr.org (or about:blank). This would lead me to believe that at the core this is a javascript problem, but then the "fix" I am currently employing (see below) doesn't really have anything to do with javascript...

So what is causing the problem and how do I work around the issues so my program does not leak memory/handles?

My current "fix" is to call Marshal.ReleaseComObject on the HtmlElement.DomElement property. Is this a correct thing to do? Shouldn't HtmlElement clean up after itself, like HtmlDocument apparently does? Should I expect problems with other classes in addition to HtmlElement?

I'm tempted to sprinkle Marshal.ReleaseComObject generously anywhere that I see a com object (and not assume the containing class will properly do so). Is that a safe thing to do?

My complete program follows:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }

    public class Form1 : Form
    {
        Timer t;
        WebBrowser webBrowser;

        public Form1()
        {
            webBrowser = new WebBrowser 
            { 
               Dock = DockStyle.Fill, 
               Url = new Uri("http://www.google.com/")
            };
            webBrowser.DocumentCompleted += webBrowser_DocumentCompleted;
            this.Controls.Add(webBrowser);

            t = new Timer { Interval = 2000 };
            t.Tick += t_Tick;
        }

        void t_Tick(object sender, EventArgs e)
        {
            t.Stop();
            webBrowser.Navigate("http://www.google.com/");
        }

        void webBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
        {
            if (webBrowser.ReadyState == WebBrowserReadyState.Complete)
            {
                var doc = webBrowser.Document;
                if (doc == null) //no page loaded
                    return;

                //the following two lines do not cause a leak
                doc.Click += click_handler;
                doc.Click -= click_handler;

                var body = doc.Body;

                if (body != null)
                {
                    //the following 2 lines cause memory and handle leaks
                    body.Click += click_handler;
                    body.Click -= click_handler;

                    //uncommenting the following line appears to fix the leak
                    //Marshal.ReleaseComObject(body.DomElement);

                    body = null;
                }

                GC.Collect(2, GCCollectionMode.Forced);
                t.Start();
            }
        }

        void click_handler(object sender, HtmlElementEventArgs e)
        {
        }
    }
}

For anyone curious, I did a test using a WeakReference to the body object in my program and verified that it IS being garbage collected.

After running the "Fixed" version for some time (maybe an hour), it is steady at ~40 megs of private bytes, and 640 handles. The "leaky" version gets up to hundreds of megs and thousands of handles, and does not appear to be slowing down. In production a user had a process using over 900 megs when things stopped functioning (images wouldn't load). Closing the application took several seconds to tear everything down.

TCC
  • 2,546
  • 1
  • 24
  • 35
  • 1
    Try implementing `FEATURE_BROWSER_EMULATION` and `FEATURE_MANAGE_SCRIPT_CIRCULAR_REFS` [WebBrowser features](http://stackoverflow.com/a/18333982/1768303) and see if it makes any difference (just an idea). – noseratio Oct 15 '13 at 23:58
  • 1
    I implemented every feature I could think of and this doesn't appear to have an effect! Still glad for the information, it fixed a completely unrelated problem :) – TCC Oct 16 '13 at 16:19
  • 1
    The `Marshal.ReleaseComObject` "fix" mentioned in the question has been working for several users in production for a couple of days now. I'm tempted to say it's a solved issue ... still seems wrong. – TCC Oct 16 '13 at 22:50

1 Answers1

0

THIS WORKS! Handles will be released immediately after closing the webbrowser. USE CODE:

using system.windows.forms;
[DllImport("KERNEL32.DLL", EntryPoint = "SetProcessWorkingSetSize", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
internal static extern bool SetProcessWorkingSetSize(IntPtr pProcess, int dwMinimumWorkingSetSize, int dwMaximumWorkingSetSize);

[DllImport("KERNEL32.DLL", EntryPoint = "GetCurrentProcess", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr GetCurrentProcess();

// **Release handles invoking the above**
IntPtr pHandle = GetCurrentProcess();
SetProcessWorkingSetSize(pHandle, -1, -1);

Happy coding!

sanrnsam7
  • 131
  • 1
  • 2
  • 11