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.