Bug 405711 - Memory Leak / COM Leak in OleEventSink.java and IE.java
Summary: Memory Leak / COM Leak in OleEventSink.java and IE.java
Status: RESOLVED FIXED
Alias: None
Product: Platform
Classification: Eclipse Project
Component: SWT (show other bugs)
Version: 3.7   Edit
Hardware: PC Windows 7
: P3 normal (vote)
Target Milestone: 4.3 M7   Edit
Assignee: Grant Gayed CLA
QA Contact:
URL:
Whiteboard:
Keywords:
: 402453 (view as bug list)
Depends on:
Blocks:
 
Reported: 2013-04-15 07:12 EDT by Martin Kolb CLA
Modified: 2013-06-11 05:57 EDT (History)
3 users (show)

See Also:


Attachments
Patch with fix proposal (2.11 KB, patch)
2013-04-15 07:12 EDT, Martin Kolb CLA
no flags Details | Diff
Workaround sample code (11.30 KB, application/octet-stream)
2013-06-11 05:57 EDT, Christof Engel CLA
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Martin Kolb CLA 2013-04-15 07:12:23 EDT
Created attachment 229717 [details]
Patch with fix proposal

We found bugs in IE.java and OleEventSInk.java which have the effect that in both cases the complete IE DOM is kept alive because of a missing Release() call on the COM object. 
Both problems arise as soon as you subscribe to events. 
 
The first is in OleEventSink.Invoke in this callstack: 
 
Variant.setData(int) line: 979 
OleEventSink.Invoke(int, int, int, int, int, int, int, int) line: 113     
OleEventSink.access$1(OleEventSink, int, int, int, int, int, int, int, int) line: 97   
OleEventSink$1.method6(int[]) line: 71     
COMObject.callback6(int[]) line: 119 
OS.DispatchMessageW(MSG) line: not available [native method]   
OS.DispatchMessage(MSG) line: 2546   
Display.readAndDispatch() line: 3756 
 
The setData call will lead to an AddRef call on every IDispatch parameter; then the subscribers are notified but nobody is releasing the refCount of the IDispatches anymore: 

           for (int j = 0; j < dispParams.cArgs; j++){ 
                 eventInfo[j] = new Variant(); 
                 eventInfo[j].setData(dispParams.rgvarg + offset); 
                 offset = offset - size; 
           } 
     } 
           
     OleEvent event = new OleEvent(); 
     event.arguments = eventInfo; 
     notifyListener(dispIdMember,event); 
     return COM.S_OK; 
} 
 
A correction would be to do the release after the notification call or even better put this code into a finally clause to avoid that someone puts in a return statement skipping the Release: 
 
finally { 
     // undo the AddRef command done in setData call above, otherwise the complete browser DOM will be kept alive 
     for (int j = 0; j < eventInfo.length; j++){ 
           if (eventInfo[j].getType() == COM.VT_DISPATCH) { 
                 eventInfo[j].dispose(); 
           } 
     }           
} 
       
      return COM.S_OK; 
} 
 
Someone tried already to fix this bug at the wrong place inside this handleEvent function. This will lead to crashes in case of multiple event listeners therefore Release must be called after all listeners have been notified (s. above ): 
 
           OleListener oleListener = new OleListener() { 
                 public void handleEvent(OleEvent event) { 
… 
                       /* 
                       * Dispose all arguments passed in the OleEvent.  This must be 
                       * done to properly release any IDispatch reference that was 
                       * automatically addRef'ed when constructing the OleEvent.   
                       */ 
                       Variant[] arguments = event.arguments; 
                       for (int i = 0; i < arguments.length; i++) { 
                            arguments[i].dispose(); 
                       } 
 
 

----
 
The second problem is similar in IE.handleDOMEvent: 
 
     } else if (eventType.equals(EVENT_MOUSEMOVE)) { 
           /* 
           * Feature in IE.  Spurious and redundant mousemove events are often received.  The workaround 
           * is to not fire MouseMove events whose x and y values match the last MouseMove.   
           */ 
           if (newEvent.x == lastMouseMoveX && newEvent.y == lastMouseMoveY) return; 
           newEvent.type = SWT.MouseMove; 
           lastMouseMoveX = newEvent.x; lastMouseMoveY = newEvent.y; 
 
The above return will skip the absolutely necessary Release call 
 
     event.dispose(); 
     browser.notifyListeners(newEvent.type, newEvent); 
 
at the end of handleDOMEvent. 
 
There are two simple solutions either add the missing event.dispose() before the return or the better one in our opinion to put a try / finally around the very long code in handleDOMEvent and to call event.dispose in the finally clause. 
 
 
Reproduction: 
To reproduce we used an SWT IE wrapper navigating once to http://www.bing.com/maps/ and then used the opened the context menu of IE with the mouse (to trigger some mousemove events) and chose the Refresh menu item. This caused about 10-20 MB with each Refresh. The bing maps example fits well as its DOM is pretty big because of all the images. Using Windows task manager memory statistics shows the increasing memory usage. 

--------------


We attached a patch containing our proposed fixes.

We assert that we authored 100% of the content of this contribution and have the rights to donate the content to Eclipse under the EPL. 

Martin Kolb (SAP AG, martin.kolb@sap.com)
Christof Engel (SAP AG, christof.engel@sap.com)
Comment 1 Grant Gayed CLA 2013-04-15 10:43:24 EDT
The patch makes perfect sense and works well, thanks for your contribution!  Fixed > 20130415, commit: http://git.eclipse.org/c/platform/eclipse.platform.swt.git/commit/?id=61aaa5d345230600c799f6ea9d7e339f868270d4 .
Comment 2 Grant Gayed CLA 2013-04-15 11:54:28 EDT
Note to self: This change will cause problems for clients doing the same wrong thing as IE.java was (disposing the OleEvent's argument Variants).  Document in porting guide.
Comment 3 Martin Kolb CLA 2013-04-19 07:17:31 EDT
*** Bug 402453 has been marked as a duplicate of this bug. ***
Comment 4 Christof Engel CLA 2013-06-11 05:55:42 EDT
This code snippet shows a WORKAROUND for the bugs mentioned above. To make use of the fixes you have to instantiate instances of BugFixedBrowser instead of the SWT Browser class.

///////////////// BEGIN snippet /////////////////////
package com.mycomp.mycontrol;

import java.lang.reflect.Field;

import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.internal.ole.win32.IDispatch;
import org.eclipse.swt.internal.ole.win32.ITypeInfo;
import org.eclipse.swt.internal.ole.win32.IUnknown;
import org.eclipse.swt.internal.win32.OS;
import org.eclipse.swt.ole.win32.OleAutomation;
import org.eclipse.swt.ole.win32.OleControlSite;
import org.eclipse.swt.ole.win32.OleEvent;
import org.eclipse.swt.ole.win32.OleListener;
import org.eclipse.swt.ole.win32.Variant;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;

/**
 * Own Browser subclass to work around some huge memory leaks in SWT, and to avoid storing the navigation history on setUrl().
 * A patch for the memory leaks (occuring in the COM area) has been provided to the SWT team and is shipped with Eclipse 4.3.
 * For older releases (3.x, 4.2 as used by Design Studio), we can fix most of the issues reflectively in this class.
 */
@SuppressWarnings("restriction")
public class BugFixedBrowser extends Browser {

	private static final boolean DISABLE_SWT_PATCH = Boolean.getBoolean("com.mycomp.disableSWTPatch"); //$NON-NLS-1$

	private static final String PROPERTY_TYPE = "type"; //$NON-NLS-1$

	private static final String EVENT_MOUSEMOVE = "mousemove"; //$NON-NLS-1$

	private static final String PROPERTY_SCREENX = "screenX"; //$NON-NLS-1$

	private static final String PROPERTY_SCREENY = "screenY"; //$NON-NLS-1$

	static final String ABOUT_BLANK = "about:blank"; //$NON-NLS-1$

	static final int DocumentComplete = 0x103;

	/**
	 * Comment for <code>originalDomListener</code>
	 */
	protected OleListener originalDomListener;

	@SuppressWarnings("javadoc")
	protected final Browser browser;

	@SuppressWarnings("javadoc")
	protected int lastMouseMoveX, lastMouseMoveY;

	private BugFixingDocumentCompleteOleListener bugFixingListenerDocComplete;

	private class BugFixingDocumentCompleteOleListener implements OleListener {
		public BugFixingDocumentCompleteOleListener() {
		}

		@Override
		public void handleEvent(OleEvent e) {
			try {
				fixHandleEventDocumentCompleteWhenBrowserDisposed(e);

			} catch (Exception ex) {
				// nothing to do
			}
		}

		/*
		   IE.handleEvent => case DocumentComplete
		   if (browser.isDisposed()) return; << cleanup code at end of handleEvent is not run
		   
		   This only occurs when SWT widget is closed faster than DocumentComplete
		   is fired and should hopefully occur rarely.
		*/
		private void fixHandleEventDocumentCompleteWhenBrowserDisposed(OleEvent event) {
			if (event.type != DocumentComplete) {
				return; // should not occur because of registration
			}

			Object webBrowser;
			try {
				webBrowser = getWebBrowserField();

				OleAutomation auto = (OleAutomation) getFieldOfWebBrowser(webBrowser, "auto"); //$NON-NLS-1$ 
				if (auto != null) { // auto
					switch (event.type) {
						case DocumentComplete: {
							boolean performingInitialNavigate = ((Boolean) getFieldOfWebBrowser(webBrowser, "performingInitialNavigate")).booleanValue(); //$NON-NLS-1$
							if (performingInitialNavigate) {
								break;
							}

							Variant varResult = event.arguments[1];
							String url = varResult.getString();

							String html = (String) getFieldOfWebBrowser(webBrowser, "html"); //$NON-NLS-1$
							if (html != null && url.equals(ABOUT_BLANK)) {
								// nothing to do
							} else {
								if (BugFixedBrowser.this.browser.isDisposed()) {
									// in the SWT code there is a return statement which leads to a skipping of the following cleanup code
									// at the end of the handleEvent method:
									// --------------------------------------
									/*
									* Dispose all arguments passed in the OleEvent.  This must be
									* done to properly release any IDispatch reference that was
									* automatically addRef'ed when constructing the OleEvent.  
									*/
									Variant[] arguments = event.arguments;
									for (Variant argument : arguments) {
										argument.dispose();
									}
									// --------------------------------------
									return;
								}
							}
						}
							break;
					}
				}
			} catch (Exception e) {
				// Logger.logError("Problem fixing documentComplete bug", e); //$NON-NLS-1$
			}

		}

	}

	// https://bugs.eclipse.org/bugs/show_bug.cgi?id=405711
	final class BugFixingOleListener implements OleListener {
		@Override
		public void handleEvent(OleEvent e) {
			BugFixedBrowser.this.originalDomListener.handleEvent(e);
			Variant arg = e.arguments[0];
			OleAutomation event = arg.getAutomation();
			try {
				fixHandleDomEventReturnBug(event);

			} finally {
				fixOleEventSinkInvokeBug(e);

				event.dispose();
			}
		}

		// Fix for return statement in IE.handleDOMEvent skipping event.dispose at end of function
		private void fixHandleDomEventReturnBug(OleAutomation event) {
			int[] rgdispid = event.getIDsOfNames(new String[] {PROPERTY_TYPE});
			int dispIdMember = rgdispid[0];
			Variant pVarResult = event.getProperty(dispIdMember);
			String eventType = pVarResult.getString();
			pVarResult.dispose();
			if (eventType.equals(EVENT_MOUSEMOVE)) {
				Event newEvent = new Event();
				/*
				* The position of mouse events is received in screen-relative coordinates
				* in order to handle pages with frames, since frames express their event
				* coordinates relative to themselves rather than relative to their top-
				* level page.  Convert screen-relative coordinates to be browser-relative.
				*/
				rgdispid = event.getIDsOfNames(new String[] {PROPERTY_SCREENX});
				dispIdMember = rgdispid[0];
				pVarResult = event.getProperty(dispIdMember);
				int screenX = pVarResult.getInt();
				pVarResult.dispose();

				rgdispid = event.getIDsOfNames(new String[] {PROPERTY_SCREENY});
				dispIdMember = rgdispid[0];
				pVarResult = event.getProperty(dispIdMember);
				int screenY = pVarResult.getInt();
				pVarResult.dispose();

				Point position = new Point(screenX, screenY);
				position = BugFixedBrowser.this.browser.getDisplay().map(null, BugFixedBrowser.this.browser, position);
				newEvent.x = position.x;
				newEvent.y = position.y;
				/*
				* Feature in IE. Spurious and redundant mousemove
				* events are often received. The workaround is to not
				* fire MouseMove events whose x and y values match the
				* last MouseMove.
				*/
				if (newEvent.x == BugFixedBrowser.this.lastMouseMoveX && newEvent.y == BugFixedBrowser.this.lastMouseMoveY) {
					immutableDispose(event); // 1 extra dispose which was skipped in IE.handleDOMEvent 
				} else {
					BugFixedBrowser.this.lastMouseMoveX = newEvent.x;
					BugFixedBrowser.this.lastMouseMoveY = newEvent.y;
				}
			}
		}

		// fix for bug in OleEventSink.Invoke
		private void fixOleEventSinkInvokeBug(OleEvent e) {
			if (e != null && e.arguments != null) {
				for (Variant argument : e.arguments) {
					argument.dispose();
				}
			}
		}
	}

	// dispose event object without resetting its inner interface pointers; code is from OleAutomation.dispose
	static void immutableDispose(OleAutomation event) {
		Class<?> theClass;
		try {
			theClass = Class.forName("org.eclipse.swt.ole.win32.OleAutomation"); //$NON-NLS-1$
			Field field = theClass.getDeclaredField("objIDispatch"); //$NON-NLS-1$
			field.setAccessible(true);
			IDispatch objIDispatch = (IDispatch) field.get(event);
			if (objIDispatch != null) {
				objIDispatch.Release();
			}
			field = theClass.getDeclaredField("objITypeInfo"); //$NON-NLS-1$
			field.setAccessible(true);
			ITypeInfo objITypeInfo = (ITypeInfo) field.get(event);
			if (objITypeInfo != null) {
				objITypeInfo.Release();
			}
			field = theClass.getDeclaredField("objIUnknown"); //$NON-NLS-1$
			field.setAccessible(true);
			IUnknown objIUnknown = (IUnknown) field.get(event);
			if (objIUnknown != null) {
				objIUnknown.Release();
				OS.OleUninitialize();
			}
		} catch (Exception e) {
			// Logger.logError("Failed to dispose event object", e); //$NON-NLS-1$
		}
	}

	/**
	 * @param parent
	 */
	public BugFixedBrowser(Composite parent) {
		super(parent, SWT.NONE);
		this.browser = this;
		if (!DISABLE_SWT_PATCH) {
			try {
				// 1. DOM listener
				Field field = Class.forName("org.eclipse.swt.browser.IE").getDeclaredField("domListener"); //$NON-NLS-1$ //$NON-NLS-2$
				field.setAccessible(true);
				Object ie = getWebBrowserField();
				this.originalDomListener = (OleListener) field.get(ie);
				OleListener bugFixingListener = new BugFixingOleListener();
				field.set(ie, bugFixingListener);
				// ------------------------

				// 2. DocumentComplete listener
				field = Class.forName("org.eclipse.swt.browser.IE").getDeclaredField("site"); //$NON-NLS-1$ //$NON-NLS-2$
				field.setAccessible(true);
				OleControlSite site = (OleControlSite) field.get(ie);
				this.bugFixingListenerDocComplete = new BugFixingDocumentCompleteOleListener();
				site.addEventListener(DocumentComplete, this.bugFixingListenerDocComplete);

				Listener disposeListener = new Listener() {
					@Override
					public void handleEvent(Event e) {
						switch (e.type) {
							case SWT.Dispose: {
								removeEventListener();
								break;
							}
						}
					}
				};
				addListener(SWT.Dispose, disposeListener);
				// ------------------------

			} catch (Exception e) {
				// Logger.logError("Failed to apply SWT patch", e); //$NON-NLS-1$
			}
		}
	}

	void removeEventListener() {
		if (!DISABLE_SWT_PATCH) {
			try {
				// 1. DOM listener
				// Do NOT change value of org.eclipse.swt.browser.IE.domListener as this is necessary for removeEventListener calls. 
				// And better keep value of this.originalDomListener untouched as there might be some DOM event coming in after this code here has run.

				// 2. DocumentComplete listener
				Field field = Class.forName("org.eclipse.swt.browser.IE").getDeclaredField("site"); //$NON-NLS-1$ //$NON-NLS-2$
				Object ie = getWebBrowserField();
				field.setAccessible(true);
				OleControlSite site = (OleControlSite) field.get(ie);
				if (site != null) {
					site.removeEventListener(DocumentComplete, this.bugFixingListenerDocComplete);
				}
				this.bugFixingListenerDocComplete = null;

			} catch (Exception e) {
				// Logger.logError("Failed to remove event listener for SWT patch", e); //$NON-NLS-1$
			}
		}
	}

	@Override
	protected void checkSubclass() {
		// comment out check (otherwise SWT won't let us subclass)
	}


	///////////////////////////////////////////////////////////////////////////////////////////////

	Object getFieldOfWebBrowser(Object webBrowser, String fieldName) {
		try {
			Field field = webBrowser.getClass().getDeclaredField(fieldName);
			field.setAccessible(true);
			return field.get(webBrowser);
		} catch (Exception e) {
			// Logger.logError("Failed to access " + fieldName, e); //$NON-NLS-1$
			return null;
		}
	}

	Object getWebBrowserField() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
		Field field = Browser.class.getDeclaredField("webBrowser"); //$NON-NLS-1$
		field.setAccessible(true);
		return field.get(this);
	}

}
///////////////// EOF snippet /////////////////////
Comment 5 Christof Engel CLA 2013-06-11 05:57:16 EDT
Created attachment 232224 [details]
Workaround sample code