Bug 738011 - Separate "detached" from "ghost" windows in about:memory. r=njn,mccr8
authorJustin Lebar <justin.lebar@gmail.com>
Sat, 31 Mar 2012 15:39:31 -0700
changeset 90810 11033861846565439c317767b72906743878e6cf
parent 90809 ee28936ed0abaf13cee119995c76842972bbb8d9
child 90811 bd73daadcfe99bf35df61d03ec0d997511520b66
push id22393
push usermbrubeck@mozilla.com
push dateMon, 02 Apr 2012 17:52:25 +0000
treeherdermozilla-central@95df15895e02 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnjn, mccr8
bugs738011
milestone14.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 738011 - Separate "detached" from "ghost" windows in about:memory. r=njn,mccr8
dom/base/nsWindowMemoryReporter.cpp
dom/base/nsWindowMemoryReporter.h
extensions/universalchardet/tests/bug631751be_text.html
js/xpconnect/tests/mochitest/test_bug462428.html
mobile/android/components/UpdatePrompt.js
modules/libpref/src/init/all.js
toolkit/mozapps/update/updater/progressui_null.cpp
--- a/dom/base/nsWindowMemoryReporter.cpp
+++ b/dom/base/nsWindowMemoryReporter.cpp
@@ -32,132 +32,131 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsWindowMemoryReporter.h"
 #include "nsGlobalWindow.h"
+#include "nsIEffectiveTLDService.h"
+#include "mozilla/Services.h"
+#include "mozilla/Preferences.h"
+#include "nsNetCID.h"
+#include "nsPrintfCString.h"
 
+using namespace mozilla;
 
 nsWindowMemoryReporter::nsWindowMemoryReporter()
+  : mCheckForGhostWindowsCallbackPending(false)
 {
+  mDetachedWindows.Init();
 }
 
-NS_IMPL_ISUPPORTS1(nsWindowMemoryReporter, nsIMemoryMultiReporter)
+NS_IMPL_ISUPPORTS3(nsWindowMemoryReporter, nsIMemoryMultiReporter, nsIObserver,
+                   nsSupportsWeakReference)
 
 /* static */
 void
 nsWindowMemoryReporter::Init()
 {
   // The memory reporter manager is going to own this object.
-  NS_RegisterMemoryMultiReporter(new nsWindowMemoryReporter());
+  nsWindowMemoryReporter *reporter = new nsWindowMemoryReporter();
+  NS_RegisterMemoryMultiReporter(reporter);
+
+  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+  if (os) {
+    // DOM_WINDOW_DESTROYED_TOPIC announces what we call window "detachment",
+    // when a window's docshell is set to NULL.
+    os->AddObserver(reporter, DOM_WINDOW_DESTROYED_TOPIC, /* weakRef = */ true);
+  }
 }
 
-static void
-AppendWindowURI(nsGlobalWindow *aWindow, nsACString& aStr)
+static already_AddRefed<nsIURI>
+GetWindowURI(nsIDOMWindow *aWindow)
 {
-  nsCOMPtr<nsIDocument> doc = do_QueryInterface(aWindow->GetExtantDocument());
+  nsCOMPtr<nsPIDOMWindow> pWindow = do_QueryInterface(aWindow);
+  NS_ENSURE_TRUE(pWindow, NULL);
+
+  nsCOMPtr<nsIDocument> doc = do_QueryInterface(pWindow->GetExtantDocument());
   nsCOMPtr<nsIURI> uri;
 
   if (doc) {
     uri = doc->GetDocumentURI();
   }
 
   if (!uri) {
-    nsIPrincipal *principal = aWindow->GetPrincipal();
+    nsCOMPtr<nsIScriptObjectPrincipal> scriptObjPrincipal =
+      do_QueryInterface(aWindow);
+    NS_ENSURE_TRUE(scriptObjPrincipal, NULL);
+
+    nsIPrincipal *principal = scriptObjPrincipal->GetPrincipal();
 
     if (principal) {
       principal->GetURI(getter_AddRefs(uri));
     }
   }
 
+  return uri.forget();
+}
+
+static void
+AppendWindowURI(nsGlobalWindow *aWindow, nsACString& aStr)
+{
+  nsCOMPtr<nsIURI> uri = GetWindowURI(aWindow);
+
   if (uri) {
     nsCString spec;
     uri->GetSpec(spec);
 
     // A hack: replace forward slashes with '\\' so they aren't
     // treated as path separators.  Users of the reporters
     // (such as about:memory) have to undo this change.
     spec.ReplaceChar('/', '\\');
 
     aStr += spec;
   } else {
+    // If we're unable to find a URI, we're dealing with a chrome window with
+    // no document in it (or somesuch), so we call this a "system window".
     aStr += NS_LITERAL_CSTRING("[system]");
   }
 }
 
 NS_MEMORY_REPORTER_MALLOC_SIZEOF_FUN(DOMStyleMallocSizeOf, "windows")
 
 static nsresult
 CollectWindowReports(nsGlobalWindow *aWindow,
                      nsWindowSizes *aWindowTotalSizes,
+                     nsTHashtable<nsUint64HashKey> *aGhostWindowIDs,
                      nsIMemoryMultiReporterCallback *aCb,
                      nsISupports *aClosure)
 {
-  // DOM window objects fall into one of three categories:
-  // - "active" windows are currently either displayed in an active
-  //   tab, or a child of such a window.
-  // - "cached" windows are in the fastback cache.
-  // - "other" windows are closed (or navigated away from w/o being
-  //   cached) yet held alive by either a website or our code. The
-  //   latter case may be a memory leak, but not necessarily.
-  //
-  // For each window we show how much memory the window and its
-  // document, etc, use, and we report those per URI, where the URI is
-  // the document URI, if available, or the codebase of the principal in
-  // the window. In the case where we're unable to find a URI we're
-  // dealing with a chrome window with no document in it (or somesuch),
-  // and for that we make the URI be the string "[system]".
-  //
-  // Outer windows are lumped in with inner windows, because the amount
-  // of memory used by outer windows is small.
-  //
-  // The path we give to the reporter callback for "active" and "cached"
-  // windows (both inner and outer) is as follows:
-  //
-  //   explicit/window-objects/top(<top-outer-uri>, id=<top-outer-id>)/<category>/window(<window-uri>)/...
-  //
-  // The path we give for "other" windows is as follows:
-  //
-  //   explicit/window-objects/top(none)/window(<window-uri>)/...
-  //
-  // Where:
-  // - <category> is "active" or "cached", as described above.
-  // - <top-outer-id> is the window id (nsPIDOMWindow::WindowID()) of
-  //   the top outer window (i.e. tab, or top level chrome window).
-  // - <top-inner-uri> is the URI of the top outer window.  Excepting
-  //   special windows (such as browser.xul or hiddenWindow.html) it's
-  //   what the address bar shows for the tab.
-  // - <window-uri> is the URI of aWindow.
-  //
-  // Exposing the top-outer-id ensures that each tab gets its own
-  // sub-tree, even if multiple tabs are showing the same URI.
-
   nsCAutoString windowPath("explicit/window-objects/");
 
+  // Our window should have a null top iff it has a null docshell.
+  MOZ_ASSERT(!!aWindow->GetTop() == !!aWindow->GetDocShell());
+
   nsGlobalWindow *top = aWindow->GetTop();
-  windowPath += NS_LITERAL_CSTRING("top(");
   if (top) {
+    windowPath += NS_LITERAL_CSTRING("top(");
     AppendWindowURI(top, windowPath);
     windowPath += NS_LITERAL_CSTRING(", id=");
     windowPath.AppendInt(top->WindowID());
-  } else {
-    windowPath += NS_LITERAL_CSTRING("none");
-  }
-  windowPath += NS_LITERAL_CSTRING(")/");
+    windowPath += NS_LITERAL_CSTRING(")/");
 
-  nsIDocShell *docShell = aWindow->GetDocShell();
-  if (docShell) {
-    MOZ_ASSERT(top, "'cached' or 'active' window lacks a top window");
     windowPath += aWindow->IsFrozen() ? NS_LITERAL_CSTRING("cached/")
                                       : NS_LITERAL_CSTRING("active/");
   } else {
-    MOZ_ASSERT(!top, "'other' window has a top window");
+    nsCOMPtr<nsIURI> uri = GetWindowURI(aWindow);
+
+    if (aGhostWindowIDs->Contains(aWindow->WindowID())) {
+      windowPath += NS_LITERAL_CSTRING("top(none)/ghost/");
+    } else {
+      windowPath += NS_LITERAL_CSTRING("top(none)/detached/");
+    }
   }
 
   windowPath += NS_LITERAL_CSTRING("window(");
   AppendWindowURI(aWindow, windowPath);
   windowPath += NS_LITERAL_CSTRING(")");
 
 #define REPORT(_pathTail, _amount, _desc)                                     \
   do {                                                                        \
@@ -228,22 +227,30 @@ nsWindowMemoryReporter::CollectReports(n
     nsGlobalWindow::GetWindowsTable();
   NS_ENSURE_TRUE(windowsById, NS_OK);
 
   // Hold on to every window in memory so that window objects can't be
   // destroyed while we're calling the memory reporter callback.
   WindowArray windows;
   windowsById->Enumerate(GetWindows, &windows);
 
+  // Get the IDs of all the "ghost" windows.
+  nsTHashtable<nsUint64HashKey> ghostWindows;
+  ghostWindows.Init();
+  CheckForGhostWindows(&ghostWindows);
+
+  nsCOMPtr<nsIEffectiveTLDService> tldService = do_GetService(
+    NS_EFFECTIVETLDSERVICE_CONTRACTID);
+  NS_ENSURE_STATE(tldService);
+
   // Collect window memory usage.
-  nsRefPtr<nsGlobalWindow> *w = windows.Elements();
-  nsRefPtr<nsGlobalWindow> *end = w + windows.Length();
   nsWindowSizes windowTotalSizes(NULL);
-  for (; w != end; ++w) {
-    nsresult rv = CollectWindowReports(*w, &windowTotalSizes, aCb, aClosure);
+  for (PRUint32 i = 0; i < windows.Length(); i++) {
+    nsresult rv = CollectWindowReports(windows[i], &windowTotalSizes,
+                                       &ghostWindows, aCb, aClosure);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
 #define REPORT(_path, _amount, _desc)                                         \
   do {                                                                        \
     nsresult rv;                                                              \
     rv = aCb->Callback(EmptyCString(), NS_LITERAL_CSTRING(_path),             \
                        nsIMemoryReporter::KIND_OTHER,                         \
@@ -281,9 +288,189 @@ nsWindowMemoryReporter::CollectReports(n
 NS_IMETHODIMP
 nsWindowMemoryReporter::GetExplicitNonHeap(PRInt64* aAmount)
 {
   // This reporter only measures heap memory.
   *aAmount = 0;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsWindowMemoryReporter::Observe(nsISupports *aSubject, const char *aTopic,
+                                const PRUnichar *aData)
+{
+  // A window was detached.  Insert it into mDetachedWindows and run
+  // CheckForGhostWindows sometime soon.
 
+  MOZ_ASSERT(!strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC));
+
+void
+nsWindowMemoryReporter::ObserveDOMWindowDetached(nsISupports *aWindow)
+{
+  nsWeakPtr weakWindow = do_GetWeakReference(aWindow);
+  if (!weakWindow) {
+    NS_WARNING("Couldn't take weak reference to a window?");
+    return;
+  }
+
+  mDetachedWindows.Put(weakWindow, TimeStamp());
+
+  if (!mCheckForGhostWindowsCallbackPending) {
+    nsCOMPtr<nsIRunnable> runnable =
+      NS_NewRunnableMethod(this,
+                           &nsWindowMemoryReporter::CheckForGhostWindowsCallback);
+    NS_DispatchToCurrentThread(runnable);
+    mCheckForGhostWindowsCallbackPending = true;
+  }
+
+  return NS_OK;
+}
+
+void
+nsWindowMemoryReporter::CheckForGhostWindowsCallback()
+{
+  mCheckForGhostWindowsCallbackPending = false;
+  CheckForGhostWindows();
+}
+
+struct CheckForGhostWindowsEnumeratorData
+{
+  nsTHashtable<nsCStringHashKey> *nonDetachedDomains;
+  nsTHashtable<nsUint64HashKey> *ghostWindowIDs;
+  nsIEffectiveTLDService *tldService;
+  PRUint32 ghostTimeout;
+  TimeStamp now;
+};
+
+static PLDHashOperator
+CheckForGhostWindowsEnumerator(nsISupports *aKey, TimeStamp& aTimeStamp,
+                               void* aClosure)
+{
+  CheckForGhostWindowsEnumeratorData *data =
+    static_cast<CheckForGhostWindowsEnumeratorData*>(aClosure);
+
+  nsWeakPtr weakKey = do_QueryInterface(aKey);
+  nsCOMPtr<nsIDOMWindow> window = do_QueryReferent(weakKey);
+  if (!window) {
+    // The window object has been destroyed.  Stop tracking its weak ref in our
+    // hashtable.
+    return PL_DHASH_REMOVE;
+  }
+
+  nsCOMPtr<nsIDOMWindow> top;
+  window->GetTop(getter_AddRefs(top));
+  if (top) {
+    // The window is no longer detached, so we no longer want to track it.
+    return PL_DHASH_REMOVE;
+  }
+
+  nsCOMPtr<nsIURI> uri = GetWindowURI(window);
+
+  nsCAutoString domain;
+  data->tldService->GetBaseDomain(uri, 0, domain);
+
+  if (data->nonDetachedDomains->Contains(domain)) {
+    // This window shares a domain with a non-detached window, so reset its
+    // clock.
+    aTimeStamp = TimeStamp();
+  } else {
+    // This window does not share a domain with a non-detached window, so it
+    // meets ghost criterion (2).
+    if (aTimeStamp.IsNull()) {
+      // This may become a ghost window later; start its clock.
+      aTimeStamp = data->now;
+    } else if ((data->now - aTimeStamp).ToSeconds() > data->ghostTimeout) {
+      // This definitely is a ghost window, so add it to ghostWindowIDs, if
+      // that is not null.
+      if (data->ghostWindowIDs) {
+        nsCOMPtr<nsPIDOMWindow> pWindow = do_QueryInterface(window);
+        if (pWindow) {
+          data->ghostWindowIDs->PutEntry(pWindow->WindowID());
+        }
+      }
+    }
+  }
+
+  return PL_DHASH_NEXT;
+}
+
+struct GetNonDetachedWindowDomainsEnumeratorData
+{
+  nsTHashtable<nsCStringHashKey> *nonDetachedDomains;
+  nsIEffectiveTLDService *tldService;
+};
+
+static PLDHashOperator
+GetNonDetachedWindowDomainsEnumerator(const PRUint64& aId, nsGlobalWindow* aWindow,
+                                      void* aClosure)
+{
+  GetNonDetachedWindowDomainsEnumeratorData *data =
+    static_cast<GetNonDetachedWindowDomainsEnumeratorData*>(aClosure);
+
+  if (!aWindow->GetTop()) {
+    // This window is detached, so we don't care about its domain.
+    return PL_DHASH_NEXT;
+  }
+
+  nsCOMPtr<nsIURI> uri = GetWindowURI(aWindow);
+
+  nsCAutoString domain;
+  data->tldService->GetBaseDomain(uri, 0, domain);
+
+  data->nonDetachedDomains->PutEntry(domain);
+  return PL_DHASH_NEXT;
+}
+
+/**
+ * Iterate over mDetachedWindows and update it to reflect the current state of
+ * the world.  In particular:
+ *
+ *   - Remove weak refs to windows which no longer exist.
+ *
+ *   - Remove references to windows which are no longer detached.
+ *
+ *   - Reset the timestamp on detached windows which share a domain with a
+ *     non-detached window (they no longer meet ghost criterion (2)).
+ *
+ *   - If a window now meets ghost criterion (2) but didn't before, set its
+ *     timestamp to now.
+ *
+ * Additionally, if aOutGhostIDs is not null, fill it with the window IDs of
+ * all ghost windows we found.
+ */
+void
+nsWindowMemoryReporter::CheckForGhostWindows(
+  nsTHashtable<nsUint64HashKey> *aOutGhostIDs /* = NULL */)
+{
+  nsCOMPtr<nsIEffectiveTLDService> tldService = do_GetService(
+    NS_EFFECTIVETLDSERVICE_CONTRACTID);
+  if (!tldService) {
+    NS_WARNING("Couldn't get TLDService.");
+    return;
+  }
+
+  nsGlobalWindow::WindowByIdTable *windowsById =
+    nsGlobalWindow::GetWindowsTable();
+  if (!windowsById) {
+    NS_WARNING("GetWindowsTable returned null");
+    return;
+  }
+
+  nsTHashtable<nsCStringHashKey> nonDetachedWindowDomains;
+  nonDetachedWindowDomains.Init();
+
+  // Populate nonDetachedWindowDomains.
+  GetNonDetachedWindowDomainsEnumeratorData nonDetachedEnumData =
+    { &nonDetachedWindowDomains, tldService };
+  windowsById->EnumerateRead(GetNonDetachedWindowDomainsEnumerator,
+                             &nonDetachedEnumData);
+
+  PRUint32 ghostTimeout =
+    Preferences::GetUint("memory.ghost_window_timeout_seconds", 60);
+
+  // Update mDetachedWindows and write the ghost window IDs into aOutGhostIDs,
+  // if it's not null.
+  CheckForGhostWindowsEnumeratorData ghostEnumData =
+    { &nonDetachedWindowDomains, aOutGhostIDs, tldService,
+      ghostTimeout, TimeStamp::Now() };
+  mDetachedWindows.Enumerate(CheckForGhostWindowsEnumerator,
+                             &ghostEnumData);
+}
--- a/dom/base/nsWindowMemoryReporter.h
+++ b/dom/base/nsWindowMemoryReporter.h
@@ -34,45 +34,162 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef nsWindowMemoryReporter_h__
 #define nsWindowMemoryReporter_h__
 
 #include "nsIMemoryReporter.h"
+#include "nsIObserver.h"
+#include "nsDataHashtable.h"
+#include "nsWeakReference.h"
+#include "mozilla/TimeStamp.h"
 
 // This should be used for any nsINode sub-class that has fields of its own
 // that it needs to measure;  any sub-class that doesn't use it will inherit
 // SizeOfExcludingThis from its super-class.  SizeOfIncludingThis() need not be
 // defined, it is inherited from nsINode.
 #define NS_DECL_SIZEOF_EXCLUDING_THIS \
   virtual size_t SizeOfExcludingThis(nsMallocSizeOfFun aMallocSizeOf) const;
 
 class nsWindowSizes {
 public:
-    nsWindowSizes(nsMallocSizeOfFun aMallocSizeOf) {
-      memset(this, 0, sizeof(nsWindowSizes));
-      mMallocSizeOf = aMallocSizeOf;
-    }
-    nsMallocSizeOfFun mMallocSizeOf;
-    size_t mDOM;
-    size_t mStyleSheets;
-    size_t mLayoutArenas;
-    size_t mLayoutStyleSets;
-    size_t mLayoutTextRuns;
+  nsWindowSizes(nsMallocSizeOfFun aMallocSizeOf) {
+    memset(this, 0, sizeof(nsWindowSizes));
+    mMallocSizeOf = aMallocSizeOf;
+  }
+  nsMallocSizeOfFun mMallocSizeOf;
+  size_t mDOM;
+  size_t mStyleSheets;
+  size_t mLayoutArenas;
+  size_t mLayoutStyleSets;
+  size_t mLayoutTextRuns;
 };
 
-class nsWindowMemoryReporter: public nsIMemoryMultiReporter
+/**
+ * nsWindowMemoryReporter is responsible for the 'explicit/window-objects'
+ * memory reporter.
+ *
+ * We classify DOM window objects into one of three categories:
+ *
+ *   - "active" windows, which are displayed in a tab (as the top-level window
+ *     or an iframe),
+ *
+ *   - "cached" windows, which are in the fastback cache (aka the bfcache), and
+ *
+ *   - "detached" windows, which have a null docshell.  A window becomes
+ *     detached when its <iframe> or tab containing the window is destroyed --
+ *     i.e., when the window is no longer active or cached.
+ *
+ * Additionally, we classify a subset of detached windows as "ghost" windows.
+ * Although ghost windows can happen legitimately (a page can hold a reference
+ * to a cross-domain window and then close its container), the presence of
+ * ghost windows is often indicative of a memory leak.
+ *
+ * A window is a ghost if it meets the following three criteria:
+ *
+ *   1) The window is detached.
+ *
+ *   2) There exist no non-detached windows with the same base domain as
+ *      the window's principal.  (For example, the base domain of
+ *      "wiki.mozilla.co.uk" is "mozilla.co.uk".)  This criterion makes us less
+ *      likely to flag a legitimately held-alive detached window as a ghost.
+ *
+ *   3) The window has met criteria (1) and (2) above for at least
+ *      memory.ghost_window_timeout_seconds.  This criterion is in place so we
+ *      don't immediately declare a window a ghost before the GC/CC has had a
+ *      chance to run.
+ *
+ * nsWindowMemoryReporter observes window detachment and uses mDetachedWindows
+ * to remember when a window first met criteria (1) and (2).  When we generate
+ * a memory report, we use this accounting to determine which windows are
+ * ghosts.
+ *
+ *
+ * We use the following memory reporter path for active and cached windows:
+ *
+ *   explicit/window-objects/top(<top-outer-uri>, id=<top-outer-id>)/<category>/window(<window-uri>)/...
+ *
+ * For detached and ghost windows, we use
+ *
+ *   explicit/window-objects/top(none)/<category>/window(<window-uri>)/...
+ *
+ * Where
+ *
+ * - <category> is "active", "cached", "detached", or "ghost", as described
+ *   above.
+ *
+ * - <top-outer-id> is the window id of the top outer window (i.e. the tab, or
+ *   the top level chrome window).  Exposing this ensures that each tab gets
+ *   its own sub-tree, even if multiple tabs are showing the same URI.
+ *
+ * - <top-uri> is the URI of the top window.  Excepting special windows (such
+ *   as browser.xul or hiddenWindow.html) it's what the address bar shows for
+ *   the tab.
+ *
+ */
+class nsWindowMemoryReporter: public nsIMemoryMultiReporter,
+                              public nsIObserver,
+                              public nsSupportsWeakReference
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIMEMORYMULTIREPORTER
+  NS_DECL_NSIOBSERVER
 
   static void Init();
 
 private:
   // Protect ctor, use Init() instead.
   nsWindowMemoryReporter();
+
+  /**
+   * Get the number of seconds for which a window must satisfy ghost criteria
+   * (1) and (2) before we deem that it satisfies criterion (3).
+   */
+  PRUint32 GetGhostTimeout();
+
+  void ObserveDOMWindowDetached(nsISupports* aWindow);
+  void ObserveAfterMinimizeMemoryUsage();
+
+  /**
+   * When we observe a DOM window being detached, we enqueue an asynchronous
+   * event which calls this method.  This method then calls
+   * CheckForGhostWindows.
+   */
+  void CheckForGhostWindowsCallback();
+
+  /**
+   * Iterate over all weak window pointers in mDetachedWindows and update our
+   * accounting of which windows meet ghost criterion (2).
+   *
+   * This method also cleans up mDetachedWindows, removing entries for windows
+   * which have been destroyed or are no longer detached.
+   *
+   * If aOutGhostIDs is non-null, we populate it with the Window IDs of the
+   * ghost windows.
+   *
+   * This is called asynchronously after we observe a DOM window being detached
+   * from its docshell, and also right before we generate a memory report.
+   */
+  void CheckForGhostWindows(nsTHashtable<nsUint64HashKey> *aOutGhostIDs = NULL);
+
+  /**
+   * Maps a weak reference to a detached window (nsIWeakReference) to the time
+   * when we observed that the window met ghost criterion (2) above.
+   *
+   * If the window has not yet met criterion (2) it maps to the null timestamp.
+   *
+   * (Although windows are not added to this table until they're detached, it's
+   * possible for a detached window to become non-detached, and we won't
+   * remove it from the table until CheckForGhostWindows runs.)
+   */
+  nsDataHashtable<nsISupportsHashKey, mozilla::TimeStamp> mDetachedWindows;
+
+  /**
+   * True if we have an asynchronous call to CheckForGhostWindows pending.
+   */
+  bool mCheckForGhostWindowsCallbackPending;
 };
 
 #endif // nsWindowMemoryReporter_h__
 
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -3519,16 +3519,17 @@ pref("browser.dom.window.dump.enabled", 
 // SPS Profiler
 pref("profiler.enabled", false);
 pref("profiler.interval", 10);
 pref("profiler.entries", 100000);
 
 // Network API
 pref("dom.network.enabled", true);
 pref("dom.network.metered", false);
+
 #ifdef XP_WIN
 // On 32-bit Windows, fire a low-memory notification if we have less than this
 // many mb of virtual address space available.
 pref("memory.low_virtual_memory_threshold_mb", 128);
 
 // On Windows 32- or 64-bit, fire a low-memory notification if we have less
 // than this many mb of commit space (physical memory plus page file) left.
 pref("memory.low_commit_space_threshold_mb", 128);
@@ -3537,8 +3538,13 @@ pref("memory.low_commit_space_threshold_
 // than this many mb of physical memory available on the whole machine.
 pref("memory.low_physical_memory_threshold_mb", 0);
 
 // On Windows 32- or 64-bit, don't fire a low-memory notification because of
 // low available physical memory or low commit space more than once every
 // low_memory_notification_interval_ms.
 pref("memory.low_memory_notification_interval_ms", 10000);
 #endif
+
+// How long must we wait before declaring that a window is a "ghost" (i.e., a
+// likely leak)?  This should be longer than it usually takes for an eligible
+// window to be collected via the GC/CC.
+pref("memory.ghost_window_timeout_seconds", 60);