Bug 799595 - Add nsIMessageLoop::PostIdleTask and use it to take screenshots. r=cjones
authorJustin Lebar <justin.lebar@gmail.com>
Thu, 25 Oct 2012 11:36:24 -0400
changeset 111406 15c00f89393920d668b1038a4f59c753cd33061a
parent 111405 9a9ae8ce3b8dfed7e013155cab96fe7a02e7b7c1
child 111407 df865cb16fb049544033a50b2ca61d9aff486fe8
push id23747
push userryanvm@gmail.com
push dateFri, 26 Oct 2012 01:15:16 +0000
treeherdermozilla-central@5ecff3e46ed5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerscjones
bugs799595
milestone19.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 799595 - Add nsIMessageLoop::PostIdleTask and use it to take screenshots. r=cjones
dom/browser-element/BrowserElementChild.js
modules/libpref/src/init/all.js
xpcom/base/Makefile.in
xpcom/base/nsIMessageLoop.idl
xpcom/base/nsMessageLoop.cpp
xpcom/base/nsMessageLoop.h
xpcom/build/XPCOMModule.inc
xpcom/build/nsXPCOMCID.h
xpcom/build/nsXPComInit.cpp
--- a/dom/browser-element/BrowserElementChild.js
+++ b/dom/browser-element/BrowserElementChild.js
@@ -448,39 +448,64 @@ BrowserElementChild.prototype = {
 
     debug("scroll event " + win);
     sendAsyncMsg("scroll", { top: win.scrollY, left: win.scrollX });
   },
 
   _recvGetScreenshot: function(data) {
     debug("Received getScreenshot message: (" + data.json.id + ")");
 
+    let self = this;
+    let maxWidth = data.json.args.width;
+    let maxHeight = data.json.args.height;
+    let domRequestID = data.json.id;
+
+    let takeScreenshotClosure = function() {
+      self._takeScreenshot(maxWidth, maxHeight, domRequestID);
+    };
+
+    let maxDelayMS = 2000;
+    try {
+      maxDelayMS = Services.prefs.getIntPref('dom.browserElement.maxScreenshotDelayMS');
+    }
+    catch(e) {}
+
+    // Try to wait for the event loop to go idle before we take the screenshot,
+    // but once we've waited maxDelayMS milliseconds, go ahead and take it
+    // anyway.
+    Cc['@mozilla.org/message-loop;1'].getService(Ci.nsIMessageLoop).postIdleTask(
+      takeScreenshotClosure, maxDelayMS);
+  },
+
+  /**
+   * Actually take a screenshot and foward the result up to our parent, given
+   * the desired maxWidth and maxHeight, and given the DOMRequest ID associated
+   * with the request from the parent.
+   */
+  _takeScreenshot: function(maxWidth, maxHeight, domRequestID) {
     // You can think of the screenshotting algorithm as carrying out the
     // following steps:
     //
-    // - Let max-width be data.json.args.width, and let max-height be
-    //   data.json.args.height.
-    //
-    // - Let scale-width be the factor by which we'd need to downscale the
-    //   viewport so it would fit within max-width.  (If the viewport's width
-    //   is less than max-width, let scale-width be 1.) Compute scale-height
+    // - Let scaleWidth be the factor by which we'd need to downscale the
+    //   viewport so it would fit within maxWidth.  (If the viewport's width
+    //   is less than maxWidth, let scaleWidth be 1.) Compute scaleHeight
     //   the same way.
     //
-    // - Scale the viewport by max(scale-width, scale-height).  Now either the
-    //   viewport's width is no larger than max-width, the viewport's height is
-    //   no larger than max-height, or both.
+    // - Scale the viewport by max(scaleWidth, scaleHeight).  Now either the
+    //   viewport's width is no larger than maxWidth, the viewport's height is
+    //   no larger than maxHeight, or both.
     //
-    // - Crop the viewport so its width is no larger than max-width and its
-    //   height is no larger than max-height.
+    // - Crop the viewport so its width is no larger than maxWidth and its
+    //   height is no larger than maxHeight.
     //
     // - Return a screenshot of the page's viewport scaled and cropped per
     //   above.
-
-    let maxWidth = data.json.args.width;
-    let maxHeight = data.json.args.height;
+    debug("Taking a screenshot: maxWidth=" + maxWidth +
+          ", maxHeight=" + maxHeight +
+          ", domRequestID=" + domRequestID + ".");
 
     let scaleWidth = Math.min(1, maxWidth / content.innerWidth);
     let scaleHeight = Math.min(1, maxHeight / content.innerHeight);
 
     let scale = Math.max(scaleWidth, scaleHeight);
 
     let canvasWidth = Math.min(maxWidth, Math.round(content.innerWidth * scale));
     let canvasHeight = Math.min(maxHeight, Math.round(content.innerHeight * scale));
@@ -492,20 +517,21 @@ BrowserElementChild.prototype = {
     canvas.height = canvasHeight;
 
     var ctx = canvas.getContext("2d");
     ctx.scale(scale, scale);
     ctx.drawWindow(content, 0, 0, content.innerWidth, content.innerHeight,
                    "rgb(255,255,255)");
 
     sendAsyncMsg('got-screenshot', {
-      id: data.json.id,
-      // Hack around the fact that we can't specify opaque PNG, this requires
-      // us to unpremultiply the alpha channel which is expensive on ARM
-      // processors because they lack a hardware integer division instruction.
+      id: domRequestID,
+      // Use JPEG to hack around the fact that we can't specify opaque PNG.
+      // This requires us to unpremultiply the alpha channel, which is
+      // expensive on ARM processors because they lack a hardware integer
+      // division instruction.
       successRv: canvas.toDataURL("image/jpeg")
     });
   },
 
   _recvFireCtxCallback: function(data) {
     debug("Received fireCtxCallback message: (" + data.json.menuitem + ")");
     // We silently ignore if the embedder uses an incorrect id in the callback
     if (data.json.menuitem in this._ctxHandlers) {
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -3806,8 +3806,12 @@ pref("dom.mozApps.dev_mode", false);
 // Lowest localId for apps.
 pref("dom.mozApps.maxLocalId", 1000);
 
 // Minimum delay in milliseconds between network activity notifications (0 means
 // no notifications). The delay is the same for both download and upload, though
 // they are handled separately. This pref is only read once at startup:
 // a restart is required to enable a new value.
 pref("network.activity.blipIntervalMilliseconds", 0);
+
+// When we're asked to take a screenshot, don't wait more than 2000ms for the
+// event loop to become idle before actually taking the screenshot.
+pref("dom.browserElement.maxScreenshotDelayMS", 2000);
--- a/xpcom/base/Makefile.in
+++ b/xpcom/base/Makefile.in
@@ -37,16 +37,17 @@ CPPSRCS		= \
 		nsCycleCollector.cpp \
 		nsStackWalk.cpp \
 		nsMemoryReporterManager.cpp \
 		ClearOnShutdown.cpp \
 		VisualEventTracer.cpp \
 		nsErrorAsserts.cpp \
 		nsGZFileWriter.cpp \
 		MemoryInfoDumper.cpp \
+		nsMessageLoop.cpp \
 		$(NULL)
 
 ifeq ($(OS_ARCH),Linux)
 CPPSRCS += MapsMemoryReporter.cpp
 endif
 
 ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 CPPSRCS		+= nsMacUtilsImpl.cpp
@@ -132,16 +133,17 @@ XPIDLSRCS	= \
 		nsIErrorService.idl \
 		nsIException.idl \
 		nsIExceptionService.idl \
 		nsIVersionComparator.idl \
 		nsIUUIDGenerator.idl \
 		nsIMutable.idl \
 		nsIMemoryReporter.idl \
 		nsIGZFileWriter.idl \
+		nsIMessageLoop.idl \
 		$(NULL)
 
 ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 XPIDLSRCS	+= nsIMacUtils.idl
 endif
 
 # we don't want the shared lib, but we want to force the creation of a static lib.
 FORCE_STATIC_LIB = 1
new file mode 100644
--- /dev/null
+++ b/xpcom/base/nsIMessageLoop.idl
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIRunnable;
+
+/**
+ * This service allows access to the current thread's Chromium MessageLoop
+ * instance, with some extra sugar added.  If you're calling from C++, it may
+ * or may not make sense for you to use this interface.  If you're calling from
+ * JS, you don't have a choice!
+ *
+ * Right now, you can only call PostIdleTask(), but nothing is stopping you
+ * from adding other methods.
+ *
+ * nsIMessageLoop's contractid is "@mozilla.org/message-loop;1".
+ */
+[scriptable, uuid(3E8C58E8-E52C-43E0-8E66-669CA788FF5F)]
+interface nsIMessageLoop : nsISupports
+{
+  /**
+   * Posts a task to be run when this thread's message loop is idle, or after
+   * ensureRunsAfterMS milliseconds have elapsed.  (That is, the task is
+   * guaranteed to run /eventually/.)
+   *
+   * Note that if the event loop is busy, we will hold a reference to the task
+   * until ensureRunsAfterMS milliseconds have elapsed.  Be careful when
+   * specifying long timeouts and tasks which hold references to windows or
+   * other large objects, because you can leak memory in a difficult-to-detect
+   * way!
+   */
+  void postIdleTask(in nsIRunnable task, in uint32_t ensureRunsAfterMS);
+};
new file mode 100644
--- /dev/null
+++ b/xpcom/base/nsMessageLoop.cpp
@@ -0,0 +1,159 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMessageLoop.h"
+#include "mozilla/WeakPtr.h"
+#include "base/message_loop.h"
+#include "base/task.h"
+#include "nsIRunnable.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsAutoPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+
+namespace {
+
+/**
+ * This Task runs its nsIRunnable when Run() is called, or after
+ * aEnsureRunsAfterMS milliseconds have elapsed since the object was
+ * constructed.
+ *
+ * Note that the MessageLoop owns this object and will delete it after it calls
+ * Run().  Tread lightly.
+ */
+class MessageLoopIdleTask
+  : public Task
+  , public SupportsWeakPtr<MessageLoopIdleTask>
+{
+public:
+  MessageLoopIdleTask(nsIRunnable* aTask, uint32_t aEnsureRunsAfterMS);
+  virtual ~MessageLoopIdleTask() {}
+  virtual void Run();
+
+private:
+  nsresult Init(uint32_t aEnsureRunsAfterMS);
+
+  nsCOMPtr<nsIRunnable> mTask;
+  nsCOMPtr<nsITimer> mTimer;
+};
+
+/**
+ * This timer callback calls MessageLoopIdleTask::Run() when its timer fires.
+ * (The timer can't call back into MessageLoopIdleTask directly since that's
+ * not a refcounted object; it's owned by the MessageLoop.)
+ *
+ * We keep a weak reference to the MessageLoopIdleTask, although a raw pointer
+ * should in theory suffice: When the MessageLoopIdleTask runs (right before
+ * the MessageLoop deletes it), it cancels its timer.  But the weak pointer
+ * saves us from worrying about an edge case somehow messing us up here.
+ */
+class MessageLoopTimerCallback
+  : public nsITimerCallback
+{
+public:
+  MessageLoopTimerCallback(MessageLoopIdleTask* aTask);
+  virtual ~MessageLoopTimerCallback() {};
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSITIMERCALLBACK
+
+private:
+  WeakPtr<MessageLoopIdleTask> mTask;
+};
+
+MessageLoopIdleTask::MessageLoopIdleTask(nsIRunnable* aTask,
+                                         uint32_t aEnsureRunsAfterMS)
+  : mTask(aTask)
+{
+  // Init() really shouldn't fail, but if it does, we schedule our runnable
+  // immediately, because it's more important to guarantee that we run the task
+  // eventually than it is to run the task when we're idle.
+  nsresult rv = Init(aEnsureRunsAfterMS);
+  if (NS_FAILED(rv)) {
+    NS_WARNING("Running idle task early because we couldn't initialize our timer.");
+    NS_DispatchToCurrentThread(mTask);
+
+    mTask = nullptr;
+    mTimer = nullptr;
+  }
+}
+
+nsresult
+MessageLoopIdleTask::Init(uint32_t aEnsureRunsAfterMS)
+{
+  mTimer = do_CreateInstance("@mozilla.org/timer;1");
+  NS_ENSURE_STATE(mTimer);
+
+  nsRefPtr<MessageLoopTimerCallback> callback =
+    new MessageLoopTimerCallback(this);
+
+  return mTimer->InitWithCallback(callback, aEnsureRunsAfterMS,
+                                  nsITimer::TYPE_ONE_SHOT);
+}
+
+/* virtual */ void
+MessageLoopIdleTask::Run()
+{
+  // Null out our pointers because if Run() was called by the timer, this
+  // object will be kept alive by the MessageLoop until the MessageLoop calls
+  // Run().
+
+  if (mTimer) {
+    mTimer->Cancel();
+    mTimer = nullptr;
+  }
+
+  if (mTask) {
+    mTask->Run();
+    mTask = nullptr;
+  }
+}
+
+MessageLoopTimerCallback::MessageLoopTimerCallback(MessageLoopIdleTask* aTask)
+  : mTask(aTask->asWeakPtr())
+{}
+
+NS_IMETHODIMP
+MessageLoopTimerCallback::Notify(nsITimer* aTimer)
+{
+  // We don't expect to hit the case when the timer fires but mTask has been
+  // deleted, because mTask should cancel the timer before the mTask is
+  // deleted.  But you never know...
+  NS_WARN_IF_FALSE(mTask, "This timer shouldn't have fired.");
+
+  if (mTask) {
+    mTask->Run();
+  }
+  return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS1(MessageLoopTimerCallback, nsITimerCallback)
+
+} // anonymous namespace
+
+NS_IMPL_ISUPPORTS1(nsMessageLoop, nsIMessageLoop)
+
+NS_IMETHODIMP
+nsMessageLoop::PostIdleTask(nsIRunnable* aTask, uint32_t aEnsureRunsAfterMS)
+{
+  // The message loop owns MessageLoopIdleTask and deletes it after calling
+  // Run().  Be careful...
+  MessageLoop::current()->PostIdleTask(FROM_HERE,
+    new MessageLoopIdleTask(aTask, aEnsureRunsAfterMS));
+  return NS_OK;
+}
+
+nsresult
+nsMessageLoopConstructor(nsISupports* aOuter,
+                         const nsIID& aIID,
+                         void** aInstancePtr)
+{
+  NS_ENSURE_FALSE(aOuter, NS_ERROR_NO_AGGREGATION);
+  nsISupports* messageLoop = new nsMessageLoop();
+  return messageLoop->QueryInterface(aIID, aInstancePtr);
+}
new file mode 100644
--- /dev/null
+++ b/xpcom/base/nsMessageLoop.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIMessageLoop.h"
+
+/*
+ * nsMessageLoop implements nsIMessageLoop, which wraps Chromium's MessageLoop
+ * class and adds a bit of sugar.
+ */
+class nsMessageLoop : public nsIMessageLoop
+{
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIMESSAGELOOP
+
+  virtual ~nsMessageLoop() {}
+};
+
+#define NS_MESSAGE_LOOP_CID \
+{0x67b3ac0c, 0xd806, 0x4d48, \
+{0x93, 0x9e, 0x6a, 0x81, 0x9e, 0x6c, 0x24, 0x8f}}
+
+extern nsresult
+nsMessageLoopConstructor(nsISupports* outer,
+                         const nsIID& aIID,
+                         void* *aInstancePtr);
--- a/xpcom/build/XPCOMModule.inc
+++ b/xpcom/build/XPCOMModule.inc
@@ -73,8 +73,9 @@
 #if defined(MOZ_WIDGET_COCOA)
     COMPONENT(MACUTILSIMPL, nsMacUtilsImplConstructor)
 #endif
 
     COMPONENT(SYSTEMINFO, nsSystemInfoConstructor)
     COMPONENT(MEMORY_REPORTER_MANAGER, nsMemoryReporterManagerConstructor)
     COMPONENT(IOUTIL, nsIOUtilConstructor)
     COMPONENT(CYCLE_COLLECTOR_LOGGER, nsCycleCollectorLoggerConstructor)
+    COMPONENT(MESSAGE_LOOP, nsMessageLoopConstructor)
--- a/xpcom/build/nsXPCOMCID.h
+++ b/xpcom/build/nsXPCOMCID.h
@@ -71,16 +71,21 @@
 #define NS_MEMORY_REPORTER_MANAGER_CONTRACTID "@mozilla.org/memory-reporter-manager;1"
 
 /**
  * Cycle collector logger contract id
  */
 #define NS_CYCLE_COLLECTOR_LOGGER_CONTRACTID "@mozilla.org/cycle-collector-logger;1"
 
 /**
+ * nsMessageLoop contract id
+ */
+#define NS_MESSAGE_LOOP_CONTRACTID "@mozilla.org/message-loop;1"
+
+/**
  * The following are the CIDs and Contract IDs of the nsISupports wrappers for 
  * primative types.  
  */
 #define NS_SUPPORTS_ID_CID \
 { 0xacf8dc40, 0x4a25, 0x11d3, \
 { 0x98, 0x90, 0x0, 0x60, 0x8, 0x96, 0x24, 0x22 } }
 #define NS_SUPPORTS_ID_CONTRACTID "@mozilla.org/supports-id;1"
 
--- a/xpcom/build/nsXPComInit.cpp
+++ b/xpcom/build/nsXPComInit.cpp
@@ -91,16 +91,17 @@ extern nsresult nsStringInputStreamConst
 #endif
 
 #ifdef MOZ_WIDGET_COCOA
 #include "nsMacUtilsImpl.h"
 #endif
 
 #include "nsSystemInfo.h"
 #include "nsMemoryReporterManager.h"
+#include "nsMessageLoop.h"
 
 #include <locale.h>
 #include "mozilla/Services.h"
 #include "mozilla/Omnijar.h"
 #include "mozilla/HangMonitor.h"
 #include "mozilla/Telemetry.h"
 
 #include "nsChromeRegistry.h"