Bug 799595 - Add nsIMessageLoop::PostIdleTask and use it to take screenshots. r=cjones, a=blocking-basecamp
authorJustin Lebar <justin.lebar@gmail.com>
Thu, 25 Oct 2012 11:36:24 -0400
changeset 116375 66da0cb5ec0bdb7090f6eb4518ec6ea3606694f1
parent 116374 eae4cdc7f382d9c0f774bb9bff045eb29fcd2a54
child 116376 15d6d97a9b5d5be885579458d4d6569a6f319ef6
push id1708
push userakeybl@mozilla.com
push dateMon, 19 Nov 2012 21:10:21 +0000
treeherdermozilla-beta@27b14fe50103 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerscjones, blocking-basecamp
bugs799595
milestone18.0a2
Bug 799595 - Add nsIMessageLoop::PostIdleTask and use it to take screenshots. r=cjones, a=blocking-basecamp
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
@@ -3797,8 +3797,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
@@ -130,16 +131,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"