From 9ebe40233717bdb30b20c895cf4b3bb968b0fc74 Mon Sep 17 00:00:00 2001 From: Justin Lebar Date: Thu, 25 Oct 2012 11:36:24 -0400 Subject: [PATCH] Bug 799595 - Add nsIMessageLoop::PostIdleTask and use it to take screenshots. r=cjones, a=blocking-basecamp --- dom/browser-element/BrowserElementChild.js | 62 +++++--- modules/libpref/src/init/all.js | 4 + xpcom/base/Makefile.in | 2 + xpcom/base/nsIMessageLoop.idl | 36 +++++ xpcom/base/nsMessageLoop.cpp | 159 +++++++++++++++++++++ xpcom/base/nsMessageLoop.h | 27 ++++ xpcom/build/XPCOMModule.inc | 1 + xpcom/build/nsXPCOMCID.h | 5 + xpcom/build/nsXPComInit.cpp | 1 + 9 files changed, 279 insertions(+), 18 deletions(-) create mode 100644 xpcom/base/nsIMessageLoop.idl create mode 100644 xpcom/base/nsMessageLoop.cpp create mode 100644 xpcom/base/nsMessageLoop.h diff --git a/dom/browser-element/BrowserElementChild.js b/dom/browser-element/BrowserElementChild.js index ded67ea44c29..403cfdb8e91f 100644 --- a/dom/browser-element/BrowserElementChild.js +++ b/dom/browser-element/BrowserElementChild.js @@ -453,29 +453,54 @@ BrowserElementChild.prototype = { _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); @@ -497,10 +522,11 @@ BrowserElementChild.prototype = { "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") }); }, diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index bfb968eb77f4..afb66b3ca626 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -3802,3 +3802,7 @@ pref("dom.mozApps.maxLocalId", 1000); // 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); diff --git a/xpcom/base/Makefile.in b/xpcom/base/Makefile.in index bd4879eb71cb..9200e5fc09cf 100644 --- a/xpcom/base/Makefile.in +++ b/xpcom/base/Makefile.in @@ -42,6 +42,7 @@ CPPSRCS = \ nsErrorAsserts.cpp \ nsGZFileWriter.cpp \ MemoryInfoDumper.cpp \ + nsMessageLoop.cpp \ $(NULL) ifeq ($(OS_ARCH),Linux) @@ -135,6 +136,7 @@ XPIDLSRCS = \ nsIMutable.idl \ nsIMemoryReporter.idl \ nsIGZFileWriter.idl \ + nsIMessageLoop.idl \ $(NULL) ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) diff --git a/xpcom/base/nsIMessageLoop.idl b/xpcom/base/nsIMessageLoop.idl new file mode 100644 index 000000000000..724da72d5547 --- /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); +}; diff --git a/xpcom/base/nsMessageLoop.cpp b/xpcom/base/nsMessageLoop.cpp new file mode 100644 index 000000000000..0f1bd536e4e2 --- /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 +{ +public: + MessageLoopIdleTask(nsIRunnable* aTask, uint32_t aEnsureRunsAfterMS); + virtual ~MessageLoopIdleTask() {} + virtual void Run(); + +private: + nsresult Init(uint32_t aEnsureRunsAfterMS); + + nsCOMPtr mTask; + nsCOMPtr 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 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 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); +} diff --git a/xpcom/base/nsMessageLoop.h b/xpcom/base/nsMessageLoop.h new file mode 100644 index 000000000000..a26a4b4bb0aa --- /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); diff --git a/xpcom/build/XPCOMModule.inc b/xpcom/build/XPCOMModule.inc index 8b51963675b1..00180300ea05 100644 --- a/xpcom/build/XPCOMModule.inc +++ b/xpcom/build/XPCOMModule.inc @@ -78,3 +78,4 @@ COMPONENT(MEMORY_REPORTER_MANAGER, nsMemoryReporterManagerConstructor) COMPONENT(IOUTIL, nsIOUtilConstructor) COMPONENT(CYCLE_COLLECTOR_LOGGER, nsCycleCollectorLoggerConstructor) + COMPONENT(MESSAGE_LOOP, nsMessageLoopConstructor) diff --git a/xpcom/build/nsXPCOMCID.h b/xpcom/build/nsXPCOMCID.h index ee641596c086..2dc040da8090 100644 --- a/xpcom/build/nsXPCOMCID.h +++ b/xpcom/build/nsXPCOMCID.h @@ -75,6 +75,11 @@ */ #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. diff --git a/xpcom/build/nsXPComInit.cpp b/xpcom/build/nsXPComInit.cpp index 074d2b7e6817..4343ace6d7e7 100644 --- a/xpcom/build/nsXPComInit.cpp +++ b/xpcom/build/nsXPComInit.cpp @@ -96,6 +96,7 @@ extern nsresult nsStringInputStreamConstructor(nsISupports *, REFNSIID, void **) #include "nsSystemInfo.h" #include "nsMemoryReporterManager.h" +#include "nsMessageLoop.h" #include #include "mozilla/Services.h"