Bug 480819. Explicitly track all elements that need to be frozen when a document enters bfcache. r+sr=jst
authorRobert O'Callahan <robert@ocallahan.org>
Fri, 08 May 2009 13:32:32 +1200
changeset 28108 fdd555ade242c6b18890333feab0a018cbaccc0f
parent 28106 7ede9cad7a672ca0df32d89aab8499276985fff1
child 28109 3802713fbe2e6daa55c6606f9a67efcb8f566f0c
push id6896
push userrocallahan@mozilla.com
push dateFri, 08 May 2009 03:22:56 +0000
treeherdermozilla-central@c97e93f23f89 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs480819
milestone1.9.2a1pre
Bug 480819. Explicitly track all elements that need to be frozen when a document enters bfcache. r+sr=jst
content/base/public/nsIDocument.h
content/base/src/nsDocument.cpp
content/base/src/nsGenericElement.h
content/base/src/nsNodeUtils.cpp
content/base/test/Makefile.in
content/base/test/test_plugin_freezing.html
content/html/content/src/nsHTMLMediaElement.cpp
content/html/content/src/nsHTMLObjectElement.cpp
content/html/content/src/nsHTMLSharedObjectElement.cpp
layout/base/nsPresShell.cpp
modules/plugin/test/testplugin/README
modules/plugin/test/testplugin/nptest.cpp
modules/plugin/test/testplugin/nptest.h
--- a/content/base/public/nsIDocument.h
+++ b/content/base/public/nsIDocument.h
@@ -46,19 +46,22 @@
 #include "nsWeakPtr.h"
 #include "nsIWeakReferenceUtils.h"
 #include "nsILoadGroup.h"
 #include "nsCRT.h"
 #include "mozFlushType.h"
 #include "nsIAtom.h"
 #include "nsCompatibility.h"
 #include "nsTObserverArray.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
 #include "nsNodeInfoManager.h"
 #include "nsIStreamListener.h"
 #include "nsIObserver.h"
+#include "nsAutoPtr.h"
 #ifdef MOZ_SMIL
 class nsSMILAnimationController;
 #endif // MOZ_SMIL
 
 class nsIContent;
 class nsPresContext;
 class nsIPresShell;
 class nsIDocShell;
@@ -97,18 +100,18 @@ class nsBindingManager;
 class nsIDOMNodeList;
 class mozAutoSubtreeModified;
 struct JSObject;
 class nsFrameLoader;
 class nsIBoxObject;
 
 // IID for the nsIDocument interface
 #define NS_IDOCUMENT_IID      \
-{ 0x62579239, 0xb619, 0x4bf2, \
-  { 0x8d, 0x39, 0x0b, 0x73, 0xe8, 0x66, 0x3a, 0x85 } }
+  { 0x9abf0b96, 0xc9e2, 0x4d49, \
+    { 0x9c, 0x0a, 0x37, 0xc1, 0x22, 0x39, 0x83, 0x50 } }
 
 // Flag for AddStyleSheet().
 #define NS_STYLESHEET_FROM_CATALOG                (1 << 0)
 
 //----------------------------------------------------------------------
 
 // Document interface.  This is implemented by all document objects in
 // Gecko.
@@ -1114,16 +1117,22 @@ public:
 
   /**
    * Return whether the document is currently showing (in the sense of
    * OnPageShow() having been called already and OnPageHide() not having been
    * called yet.
    */
   PRBool IsShowing() { return mIsShowing; }
 
+  void RegisterFreezableElement(nsIContent* aContent);
+  PRBool UnregisterFreezableElement(nsIContent* aContent);
+  typedef void (* FreezableElementEnumerator)(nsIContent*, void*);
+  void EnumerateFreezableElements(FreezableElementEnumerator aEnumerator,
+                                  void* aData);
+
 #ifdef MOZ_SMIL
   // Getter for this document's SMIL Animation Controller
   virtual nsSMILAnimationController* GetAnimationController() = 0;
 #endif // MOZ_SMIL
 
   /**
    * Prevents user initiated events from being dispatched to the document and
    * subdocuments.
@@ -1176,16 +1185,22 @@ protected:
   nsCOMPtr<nsINode> mCachedRootContent;
 
   // We'd like these to be nsRefPtrs, but that'd require us to include
   // additional headers that we don't want to expose.
   // The cleanup is handled by the nsDocument destructor.
   nsNodeInfoManager* mNodeInfoManager; // [STRONG]
   nsICSSLoader* mCSSLoader; // [STRONG]
 
+  // The set of all object, embed, applet, video and audio elements for
+  // which this is the owner document. (They might not be in the document.)
+  // These are non-owning pointers, the elements are responsible for removing
+  // themselves when they go away.
+  nsAutoPtr<nsTHashtable<nsPtrHashKey<nsIContent> > > mFreezableElements;
+
   // Table of element properties for this document.
   nsPropertyTable mPropertyTable;
 
   // Compatibility mode
   nsCompatibility mCompatMode;
 
   // True if BIDI is enabled.
   PRPackedBool mBidiEnabled;
--- a/content/base/src/nsDocument.cpp
+++ b/content/base/src/nsDocument.cpp
@@ -7525,8 +7525,53 @@ nsDocument::UnsuppressEventHandlingAndFi
 
   if (aFireEvents) {
     NS_DispatchToCurrentThread(new nsDelayedEventDispatcher(documents));
   } else {
     FireOrClearDelayedEvents(documents, PR_FALSE);
   }
 }
 
+void
+nsIDocument::RegisterFreezableElement(nsIContent* aContent)
+{
+  if (!mFreezableElements) {
+    mFreezableElements = new nsTHashtable<nsPtrHashKey<nsIContent> >();
+    if (!mFreezableElements)
+      return;
+    mFreezableElements->Init();
+  }
+  mFreezableElements->PutEntry(aContent);
+}
+
+PRBool
+nsIDocument::UnregisterFreezableElement(nsIContent* aContent)
+{
+  if (!mFreezableElements)
+    return PR_FALSE;
+  if (!mFreezableElements->GetEntry(aContent))
+    return PR_FALSE;
+  mFreezableElements->RemoveEntry(aContent);
+  return PR_TRUE;
+}
+
+struct EnumerateFreezablesData {
+  nsIDocument::FreezableElementEnumerator mEnumerator;
+  void* mData;
+};
+
+static PLDHashOperator
+EnumerateFreezables(nsPtrHashKey<nsIContent>* aEntry, void* aData)
+{
+  EnumerateFreezablesData* data = static_cast<EnumerateFreezablesData*>(aData);
+  data->mEnumerator(aEntry->GetKey(), data->mData);
+  return PL_DHASH_NEXT;
+}
+
+void
+nsIDocument::EnumerateFreezableElements(FreezableElementEnumerator aEnumerator,
+                                        void* aData)
+{
+  if (!mFreezableElements)
+    return;
+  EnumerateFreezablesData data = { aEnumerator, aData };
+  mFreezableElements->EnumerateEntries(EnumerateFreezables, &data);
+}
--- a/content/base/src/nsGenericElement.h
+++ b/content/base/src/nsGenericElement.h
@@ -964,16 +964,29 @@ protected:
     return static_cast<nsDOMSlots*>(GetSlots());
   }
 
   nsDOMSlots *GetExistingDOMSlots() const
   {
     return static_cast<nsDOMSlots*>(GetExistingSlots());
   }
 
+  void RegisterFreezableElement() {
+    nsIDocument* doc = GetOwnerDoc();
+    if (doc) {
+      doc->RegisterFreezableElement(this);
+    }
+  }
+  void UnregisterFreezableElement() {
+    nsIDocument* doc = GetOwnerDoc();
+    if (doc) {
+      doc->UnregisterFreezableElement(this);
+    }
+  }
+
   /**
    * GetContentsAsText will take all the textnodes that are children
    * of |this| and concatenate the text in them into aText.  It
    * completely ignores any non-text-node children of |this|; in
    * particular it does not descend into any children of |this| that
    * happen to be container elements.
    *
    * @param aText the resulting text [OUT]
--- a/content/base/src/nsNodeUtils.cpp
+++ b/content/base/src/nsNodeUtils.cpp
@@ -561,24 +561,31 @@ nsNodeUtils::CloneAndAdopt(nsINode *aNod
       // After cloning the document itself, we want to clone the children into
       // the cloned document (somewhat like cloning and importing them into the
       // cloned document).
       nodeInfoManager = clone->mNodeInfo->NodeInfoManager();
     }
   }
   else if (nodeInfoManager) {
     nsIDocument* oldDoc = aNode->GetOwnerDoc();
+    PRBool wasRegistered = PR_FALSE;
     if (oldDoc && aNode->IsNodeOfType(nsINode::eELEMENT)) {
-      oldDoc->ClearBoxObjectFor(static_cast<nsIContent*>(aNode));
+      nsIContent* content = static_cast<nsIContent*>(aNode);
+      oldDoc->ClearBoxObjectFor(content);
+      wasRegistered = oldDoc->UnregisterFreezableElement(content);
     }
 
     aNode->mNodeInfo.swap(newNodeInfo);
 
     nsIDocument* newDoc = aNode->GetOwnerDoc();
     if (newDoc) {
+      if (wasRegistered) {
+        newDoc->RegisterFreezableElement(static_cast<nsIContent*>(aNode));
+      }
+
       nsPIDOMWindow* window = newDoc->GetInnerWindow();
       if (window) {
         nsCOMPtr<nsIEventListenerManager> elm;
         aNode->GetListenerManager(PR_FALSE, getter_AddRefs(elm));
         if (elm) {
           window->SetMutationListeners(elm->MutationListenerBits());
           if (elm->MayHavePaintEventListener()) {
             window->SetHasPaintEventListeners();
--- a/content/base/test/Makefile.in
+++ b/content/base/test/Makefile.in
@@ -304,16 +304,17 @@ include $(topsrcdir)/config/rules.mk
 		test_bug473162-2.html \
 		test_XHRSendData.html \
 		file_XHRSendData.sjs \
 		file_XHRSendData_doc.xml \
 		file_XHRSendData_doc.xml^headers^ \
 		test_bug466751.xhtml \
 		test_bug461555.html \
 		test_sync_xhr_timer.xhtml \
+		test_plugin_freezing.html \
 		$(NULL)
 
 # Disabled for now. Mochitest isn't reliable enough for these.
 # test_bug444546.html \
 # bug444546.sjs \
 
 libs:: $(_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_plugin_freezing.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for plugin freezing and thawing</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="content" style="display: none">
+</div>
+<embed id='e1' type='application/x-test'></embed>
+<script>
+var e1 = document.getElementById('e1');
+var w;
+
+var testIndex = 0;
+var tests;
+
+window.addEventListener("unload", function() {
+  e1.stopWatchingInstanceCount();
+  if (w) {
+    w.close();
+  }
+}, false);
+
+function nextTest() {
+  if (testIndex == tests.length) {
+    SimpleTest.finish();
+    return;
+  }
+
+  var test = tests[testIndex];
+  ++testIndex;
+  test();
+}
+
+function waitForInstanceCount(n) {
+  if (e1.getInstanceCount() == n) {
+    ok(true, "reached instance count " + n);
+    nextTest();
+    return;
+  }
+  setTimeout(function() { waitForInstanceCount(n); }, 100);
+}
+
+tests = [
+  function() { waitForInstanceCount(1); },
+  function() { w.location.href = "about:blank";
+               waitForInstanceCount(0); },
+];
+
+try {
+  e1.startWatchingInstanceCount();
+  var w = window.open("data:text/html,<embed id='e2' type='application/x-test'></embed>");
+  w.onload = nextTest;
+  SimpleTest.waitForExplicitFinish();
+} catch (err) {
+  todo(false, "Instances already being watched?");
+}
+
+</script>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/content/html/content/src/nsHTMLMediaElement.cpp
+++ b/content/html/content/src/nsHTMLMediaElement.cpp
@@ -734,20 +734,22 @@ nsHTMLMediaElement::nsHTMLMediaElement(n
     mPausedBeforeFreeze(PR_FALSE),
     mWaitingFired(PR_FALSE),
     mIsBindingToTree(PR_FALSE),
     mIsRunningLoadMethod(PR_FALSE),
     mIsLoadingFromSrcAttribute(PR_FALSE),
     mDelayingLoadEvent(PR_FALSE),
     mIsRunningSelectResource(PR_FALSE)
 {
+  RegisterFreezableElement();
 }
 
 nsHTMLMediaElement::~nsHTMLMediaElement()
 {
+  UnregisterFreezableElement();
   if (mDecoder) {
     mDecoder->Shutdown();
     mDecoder = nsnull;
   }
   if (mChannel) {
     mChannel->Cancel(NS_BINDING_ABORTED);
     mChannel = nsnull;
   }
--- a/content/html/content/src/nsHTMLObjectElement.cpp
+++ b/content/html/content/src/nsHTMLObjectElement.cpp
@@ -140,20 +140,22 @@ private:
 NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Object)
 
 
 nsHTMLObjectElement::nsHTMLObjectElement(nsINodeInfo *aNodeInfo,
                                          PRBool aFromParser)
   : nsGenericHTMLFormElement(aNodeInfo),
     mIsDoneAddingChildren(!aFromParser)
 {
+  RegisterFreezableElement();
 }
 
 nsHTMLObjectElement::~nsHTMLObjectElement()
 {
+  UnregisterFreezableElement();
   DestroyImageLoadingContent();
 }
 
 PRBool
 nsHTMLObjectElement::IsDoneAddingChildren()
 {
   return mIsDoneAddingChildren;
 }
--- a/content/html/content/src/nsHTMLSharedObjectElement.cpp
+++ b/content/html/content/src/nsHTMLSharedObjectElement.cpp
@@ -167,20 +167,22 @@ private:
 NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(SharedObject)
 
 
 nsHTMLSharedObjectElement::nsHTMLSharedObjectElement(nsINodeInfo *aNodeInfo,
                                                      PRBool aFromParser)
   : nsGenericHTMLElement(aNodeInfo),
     mIsDoneAddingChildren(aNodeInfo->Equals(nsGkAtoms::embed) || !aFromParser)
 {
+  RegisterFreezableElement();
 }
 
 nsHTMLSharedObjectElement::~nsHTMLSharedObjectElement()
 {
+  UnregisterFreezableElement();
   DestroyImageLoadingContent();
 }
 
 PRBool
 nsHTMLSharedObjectElement::IsDoneAddingChildren()
 {
   return mIsDoneAddingChildren;
 }
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -6653,63 +6653,49 @@ PresShell::AddOverrideStyleSheet(nsIStyl
 
 nsresult
 PresShell::RemoveOverrideStyleSheet(nsIStyleSheet *aSheet)
 {
   return mStyleSet->RemoveStyleSheet(nsStyleSet::eOverrideSheet, aSheet);
 }
 
 static void
-StopPluginInstance(PresShell *aShell, nsIContent *aContent)
-{
-  nsIFrame *frame = aShell->FrameManager()->GetPrimaryFrameFor(aContent, -1);
-
-  nsIObjectFrame *objectFrame = do_QueryFrame(frame);
-  if (!objectFrame)
-    return;
-
-  objectFrame->StopPlugin();
-}
-
+FreezeElement(nsIContent *aContent, void *aShell)
+{
 #ifdef MOZ_MEDIA
-static void
-StopMediaInstance(PresShell *aShell, nsIContent *aContent)
-{
   nsCOMPtr<nsIDOMHTMLMediaElement> domMediaElem(do_QueryInterface(aContent));
-  if (!domMediaElem)
+  if (domMediaElem) {
+    nsHTMLMediaElement* mediaElem = static_cast<nsHTMLMediaElement*>(aContent);
+    mediaElem->Freeze();
     return;
-
-  nsHTMLMediaElement* mediaElem = static_cast<nsHTMLMediaElement*>(aContent);
-  mediaElem->Freeze();
-}
+  }
 #endif
 
+  nsIPresShell* shell = static_cast<nsIPresShell*>(aShell);
+  nsIFrame *frame = shell->FrameManager()->GetPrimaryFrameFor(aContent, -1);
+  nsIObjectFrame *objectFrame = do_QueryFrame(frame);
+  if (objectFrame) {
+    objectFrame->StopPlugin();
+  }
+}
+
 static PRBool
 FreezeSubDocument(nsIDocument *aDocument, void *aData)
 {
   nsIPresShell *shell = aDocument->GetPrimaryShell();
   if (shell)
     shell->Freeze();
 
   return PR_TRUE;
 }
 
 void
 PresShell::Freeze()
 {
-  nsCOMPtr<nsIDOMDocument> domDoc = do_QueryInterface(mDocument);
-  if (domDoc) {
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("object"), StopPluginInstance);
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("applet"), StopPluginInstance);
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("embed"), StopPluginInstance);
-#ifdef MOZ_MEDIA
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("video"), StopMediaInstance);
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("audio"), StopMediaInstance);
-#endif
-  }
+  mDocument->EnumerateFreezableElements(FreezeElement, this);
 
   if (mCaret)
     mCaret->SetCaretVisible(PR_FALSE);
 
   mPaintingSuppressed = PR_TRUE;
 
   if (mDocument)
     mDocument->EnumerateSubDocuments(FreezeSubDocument, nsnull);
@@ -6750,62 +6736,48 @@ PresShell::NeedsFocusOrBlurAfterSuppress
       }
     }
 
     mDelayedBlurFocusTargets.AppendElement(nsBlurOrFocusTarget(aTarget, aEventType));
   }
 }
 
 static void
-StartPluginInstance(PresShell *aShell, nsIContent *aContent)
-{
-  nsCOMPtr<nsIObjectLoadingContent> objlc(do_QueryInterface(aContent));
-  if (!objlc)
-    return;
-
-  nsCOMPtr<nsIPluginInstance> inst;
-  objlc->EnsureInstantiation(getter_AddRefs(inst));
-}
-
+ThawElement(nsIContent *aContent, void *aShell)
+{
 #ifdef MOZ_MEDIA
-static void
-StartMediaInstance(PresShell *aShell, nsIContent *aContent)
-{
   nsCOMPtr<nsIDOMHTMLMediaElement> domMediaElem(do_QueryInterface(aContent));
-  if (!domMediaElem)
+  if (domMediaElem) {
+    nsHTMLMediaElement* mediaElem = static_cast<nsHTMLMediaElement*>(aContent);
+    mediaElem->Thaw();
     return;
-
-  nsHTMLMediaElement* mediaElem = static_cast<nsHTMLMediaElement*>(aContent);
-  mediaElem->Thaw();
-}
+  }
 #endif
 
+  nsCOMPtr<nsIObjectLoadingContent> objlc(do_QueryInterface(aContent));
+  if (objlc) {
+    nsCOMPtr<nsIPluginInstance> inst;
+    objlc->EnsureInstantiation(getter_AddRefs(inst));
+  }
+}
+
 static PRBool
 ThawSubDocument(nsIDocument *aDocument, void *aData)
 {
   nsIPresShell *shell = aDocument->GetPrimaryShell();
   if (shell)
     shell->Thaw();
 
   return PR_TRUE;
 }
 
 void
 PresShell::Thaw()
 {
-  nsCOMPtr<nsIDOMDocument> domDoc = do_QueryInterface(mDocument);
-  if (domDoc) {
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("object"), StartPluginInstance);
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("applet"), StartPluginInstance);
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("embed"), StartPluginInstance);
-#ifdef MOZ_MEDIA
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("video"), StartMediaInstance);
-    EnumeratePlugins(domDoc, NS_LITERAL_STRING("audio"), StartMediaInstance);
-#endif
-  }
+  mDocument->EnumerateFreezableElements(ThawElement, this);
 
   if (mDocument)
     mDocument->EnumerateSubDocuments(ThawSubDocument, nsnull);
 
   UnsuppressPainting();
 }
 
 //--------------------------------------------------------
--- a/modules/plugin/test/testplugin/README
+++ b/modules/plugin/test/testplugin/README
@@ -96,8 +96,24 @@ rectangle only. So if you request wmode=
 !hasWidget, you can assume that complex clip regions are not supported.
 
 * getClipRegionRectEdge(i, edge)
 Returns the integer screen pixel coordinate of an edge of a rectangle from the
 plugin's clip region. If i is less than zero or greater than or equal to
 getClipRegionRectCount(), this will throw an error. The coordinates are
 the same as for getEdge. See getClipRegionRectCount() above for
 notes on platform plugin limitations.
+
+== Instance lifecycle ==
+
+The test plugin supports the following scriptable methods:
+
+* startWatchingInstanceCount()
+Marks all currently running instances as "ignored". Throws an exception if
+there is already a watch (startWatchingInstanceCount has already been
+called on some instance without a corresponding stopWatchingInstanceCount).
+
+* getInstanceCount()
+Returns the count of currently running instances that are not ignored.
+Throws an exception if there is no watch.
+
+* stopWatchingInstanceCount()
+Stops watching. Throws an exception if there is no watch.
--- a/modules/plugin/test/testplugin/nptest.cpp
+++ b/modules/plugin/test/testplugin/nptest.cpp
@@ -63,41 +63,65 @@ typedef bool (* ScriptableFunction)
 static bool setUndefinedValueTest(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool identifierToStringTest(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool queryPrivateModeState(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool lastReportedPrivateModeState(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool hasWidget(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool getEdge(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool getClipRegionRectCount(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool getClipRegionRectEdge(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
+static bool startWatchingInstanceCount(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
+static bool getInstanceCount(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
+static bool stopWatchingInstanceCount(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 
 static const NPUTF8* sPluginMethodIdentifierNames[] = {
   "setUndefinedValueTest",
   "identifierToStringTest",
   "queryPrivateModeState",
   "lastReportedPrivateModeState",
   "hasWidget",
   "getEdge",
   "getClipRegionRectCount",
   "getClipRegionRectEdge",
+  "startWatchingInstanceCount",
+  "getInstanceCount",
+  "stopWatchingInstanceCount",
 };
 static NPIdentifier sPluginMethodIdentifiers[ARRAY_LENGTH(sPluginMethodIdentifierNames)];
 static const ScriptableFunction sPluginMethodFunctions[ARRAY_LENGTH(sPluginMethodIdentifierNames)] = {
   setUndefinedValueTest,
   identifierToStringTest,
   queryPrivateModeState,
   lastReportedPrivateModeState,
   hasWidget,
   getEdge,
   getClipRegionRectCount,
   getClipRegionRectEdge,
+  startWatchingInstanceCount,
+  getInstanceCount,
+  stopWatchingInstanceCount,
 };
 
 static bool sIdentifiersInitialized = false;
 
+/**
+ * Incremented for every startWatchingInstanceCount.
+ */
+static int32_t sCurrentInstanceCountWatchGeneration = 0;
+/**
+ * Tracks the number of instances created or destroyed since the last
+ * startWatchingInstanceCount.
+ */
+static int32_t sInstanceCount = 0;
+/**
+ * True when we've had a startWatchingInstanceCount with no corresponding
+ * stopWatchingInstanceCount.
+ */
+static bool sWatchingInstanceCount = false;
+
 static void initializeIdentifiers()
 {
   if (!sIdentifiersInitialized) {
     NPN_GetStringIdentifiers(sPluginMethodIdentifierNames,
         ARRAY_LENGTH(sPluginMethodIdentifierNames), sPluginMethodIdentifiers);
     sIdentifiersInitialized = true;    
   }
 }
@@ -267,16 +291,18 @@ NPP_New(NPMIMEType pluginType, NPP insta
     return NPERR_GENERIC_ERROR;
   }
   NPN_RetainObject(scriptableObject);
   scriptableObject->npp = instance;
   scriptableObject->drawMode = DM_DEFAULT;
   scriptableObject->drawColor = 0;
   instanceData->scriptableObject = scriptableObject;
 
+  instanceData->instanceCountWatchGeneration = sCurrentInstanceCountWatchGeneration;
+  
   bool requestWindow = false;
   // handle extra params
   for (int i = 0; i < argc; i++) {
     if (strcmp(argn[i], "drawmode") == 0) {
       if (strcmp(argv[i], "solid") == 0)
         scriptableObject->drawMode = DM_SOLID_COLOR;    
     }
     else if (strcmp(argn[i], "color") == 0) {
@@ -312,26 +338,32 @@ NPP_New(NPMIMEType pluginType, NPP insta
   // do platform-specific initialization
   NPError err = pluginInstanceInit(instanceData);
   if (err != NPERR_NO_ERROR) {
     NPN_ReleaseObject(scriptableObject);
     free(instanceData);
     return err;
   }
 
+  ++sInstanceCount;
   return NPERR_NO_ERROR;
 }
 
 NPError
 NPP_Destroy(NPP instance, NPSavedData** save)
 {
   InstanceData* instanceData = (InstanceData*)(instance->pdata);
   pluginInstanceShutdown(instanceData);
   NPN_ReleaseObject(instanceData->scriptableObject);
   free(instanceData);
+
+  if (sCurrentInstanceCountWatchGeneration == instanceData->instanceCountWatchGeneration) {
+    --sInstanceCount;
+  }
+
   return NPERR_NO_ERROR;
 }
 
 NPError
 NPP_SetWindow(NPP instance, NPWindow* window)
 {
   InstanceData* instanceData = (InstanceData*)(instance->pdata);
   void* oldWindow = instanceData->window.window;
@@ -714,8 +746,46 @@ getClipRegionRectEdge(NPObject* npobj, c
 
   InstanceData* id = static_cast<InstanceData*>(static_cast<TestNPObject*>(npobj)->npp->pdata);
   int32_t r = pluginGetClipRegionRectEdge(id, rectIndex, RectEdge(edge));
   if (r == NPTEST_INT32_ERROR)
     return false;
   INT32_TO_NPVARIANT(r, *result);
   return true;
 }
+
+static bool
+startWatchingInstanceCount(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result)
+{
+  if (argCount != 0)
+    return false;
+  if (sWatchingInstanceCount)
+    return false;
+
+  sWatchingInstanceCount = true;
+  sInstanceCount = 0;
+  ++sCurrentInstanceCountWatchGeneration;
+  return true;
+}
+
+static bool
+getInstanceCount(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result)
+{
+  if (argCount != 0)
+    return false;
+  if (!sWatchingInstanceCount)
+    return false;
+
+  INT32_TO_NPVARIANT(sInstanceCount, *result);
+  return true;
+}
+
+static bool
+stopWatchingInstanceCount(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result)
+{
+  if (argCount != 0)
+    return false;
+  if (!sWatchingInstanceCount)
+    return false;
+
+  sWatchingInstanceCount = false;
+  return true;
+}
--- a/modules/plugin/test/testplugin/nptest.h
+++ b/modules/plugin/test/testplugin/nptest.h
@@ -52,13 +52,14 @@ typedef struct TestNPObject : NPObject {
   PRUint32 drawColor; // 0xAARRGGBB
 } TestNPObject;
 
 typedef struct InstanceData {
   NPP npp;
   NPWindow window;
   TestNPObject* scriptableObject;
   void* platformData;
+  uint32_t instanceCountWatchGeneration;
   bool lastReportedPrivateModeState;
   bool hasWidget;
 } InstanceData;
 
 #endif // nptest_h_