Bug 620935 - Make console object available in Web Workers, r=ehsan, r=bz
authorAndrea Marchesini <amarchesini@mozilla.com>
Sun, 26 Jan 2014 12:35:17 +0000
changeset 181278 99c61f40109b72615ac80d44f98a7933870cd46a
parent 181277 faca83ad9b80a37932601bd484bbe7edfebc19bb
child 181279 b182f29bb7b1ebb63554dbfb5155548190fe3648
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, bz
bugs620935
milestone29.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 620935 - Make console object available in Web Workers, r=ehsan, r=bz
browser/extensions/pdfjs/content/build/pdf.worker.js
dom/base/ConsoleAPI.js
dom/bindings/Bindings.conf
dom/bindings/Exceptions.cpp
dom/bindings/Exceptions.h
dom/webidl/WorkerConsole.webidl
dom/webidl/WorkerGlobalScope.webidl
dom/webidl/moz.build
dom/workers/Console.cpp
dom/workers/Console.h
dom/workers/WorkerScope.cpp
dom/workers/WorkerScope.h
dom/workers/moz.build
dom/workers/test/console_worker.js
dom/workers/test/mochitest.ini
dom/workers/test/test_console.html
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -34755,53 +34755,18 @@ var WorkerMessageHandler = PDFJS.WorkerM
       pdfManager.terminate();
       deferred.resolve();
     });
   }
 };
 
 var consoleTimer = {};
 
-var workerConsole = {
-  log: function log() {
-    var args = Array.prototype.slice.call(arguments);
-    globalScope.postMessage({
-      action: 'console_log',
-      data: args
-    });
-  },
-
-  error: function error() {
-    var args = Array.prototype.slice.call(arguments);
-    globalScope.postMessage({
-      action: 'console_error',
-      data: args
-    });
-    throw 'pdf.js execution error';
-  },
-
-  time: function time(name) {
-    consoleTimer[name] = Date.now();
-  },
-
-  timeEnd: function timeEnd(name) {
-    var time = consoleTimer[name];
-    if (!time) {
-      error('Unknown timer name ' + name);
-    }
-    this.log('Timer:', name, Date.now() - time);
-  }
-};
-
-
 // Worker thread?
 if (typeof window === 'undefined') {
-  if (!('console' in globalScope)) {
-    globalScope.console = workerConsole;
-  }
 
   // Listen for unsupported features so we can pass them on to the main thread.
   PDFJS.UnsupportedManager.listen(function (msg) {
     globalScope.postMessage({
       action: '_unsupported_feature',
       data: msg
     });
   });
--- a/dom/base/ConsoleAPI.js
+++ b/dom/base/ConsoleAPI.js
@@ -209,29 +209,32 @@ ConsoleAPI.prototype = {
           this.timerRegistry.clear();
         }
       }
     }
   },
 
   /**
    * Queue a call to a console method. See the CALL_DELAY constant.
+   * This method is the entry point for the console.* for workers.
    *
    * @param string aMethod
    *        The console method the code has invoked.
    * @param object aArguments
    *        The arguments passed to the console method.
+   * @param array aStack
+   *        The stack of the console method. Used by console.* for workers.
    */
-  queueCall: function CA_queueCall(aMethod, aArguments)
+  queueCall: function CA_queueCall(aMethod, aArguments, aStack = null)
   {
     let window = this._window.get();
     let metaForCall = {
       private: PrivateBrowsingUtils.isWindowPrivate(window),
       timeStamp: Date.now(),
-      stack: this.getStackTrace(aMethod != "trace" ? 1 : null),
+      stack: (aStack ? aStack : this.getStackTrace(aMethod != "trace" ? 1 : null)),
     };
 
     if (aMethod == "time" || aMethod == "timeEnd") {
       metaForCall.monotonicTimer = window.performance.now();
     }
 
     this._queuedCalls.push([aMethod, aArguments, metaForCall]);
 
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -1523,16 +1523,22 @@ DOMInterfaces = {
     'headerFile': 'mozilla/dom/WorkerScope.h',
     'workers': True,
     'concrete': False,
     'implicitJSContext': [
         'close', 'importScripts',
     ],
 },
 
+'WorkerConsole': {
+    'headerFile': 'mozilla/dom/workers/bindings/Console.h',
+    'workers': True,
+    'implicitJSContext': [ 'trace', 'time', 'timeEnd' ],
+},
+
 'WorkerLocation': {
     'headerFile': 'mozilla/dom/workers/bindings/Location.h',
     'workers': True,
 },
 
 'WorkerNavigator': {
     'headerFile': 'mozilla/dom/workers/bindings/Navigator.h',
     'workers': True,
--- a/dom/bindings/Exceptions.cpp
+++ b/dom/bindings/Exceptions.cpp
@@ -276,17 +276,17 @@ public:
   // A null aStackDescription or an aIndex that's out of range for the
   // number of frames aStackDescription has will mean that the
   // JSStackFrame will never look at the stack description.  Instead,
   // it is expected to be initialized by the caller as needed.
   JSStackFrame(StackDescriptionOwner* aStackDescription, size_t aIndex);
   virtual ~JSStackFrame();
 
   static already_AddRefed<nsIStackFrame>
-  CreateStack(JSContext* cx);
+  CreateStack(JSContext* aCx, int32_t aMaxDepth = -1);
   static already_AddRefed<nsIStackFrame>
   CreateStackFrameLocation(uint32_t aLanguage,
                            const char* aFilename,
                            const char* aFunctionName,
                            int32_t aLineNumber,
                            nsIStackFrame* aCaller);
 
 private:
@@ -490,21 +490,24 @@ NS_IMETHODIMP JSStackFrame::ToString(nsA
   }
   static const char format[] = "%s frame :: %s :: %s :: line %d";
   _retval.AppendPrintf(format, frametype, filename.get(),
                        funname.get(), GetLineno());
   return NS_OK;
 }
 
 /* static */ already_AddRefed<nsIStackFrame>
-JSStackFrame::CreateStack(JSContext* cx)
+JSStackFrame::CreateStack(JSContext* aCx, int32_t aMaxDepth)
 {
   static const unsigned MAX_FRAMES = 100;
+  if (aMaxDepth < 0) {
+    aMaxDepth = MAX_FRAMES;
+  }
 
-  JS::StackDescription* desc = JS::DescribeStack(cx, MAX_FRAMES);
+  JS::StackDescription* desc = JS::DescribeStack(aCx, aMaxDepth);
   if (!desc) {
     return nullptr;
   }
 
   nsRefPtr<StackDescriptionOwner> descOwner = new StackDescriptionOwner(desc);
 
   nsRefPtr<JSStackFrame> first = new JSStackFrame(descOwner, 0);
   return first.forget();
@@ -525,19 +528,19 @@ JSStackFrame::CreateStackFrameLocation(u
   self->mFunname = aFunctionName;
 
   self->mCaller = aCaller;
 
   return self.forget();
 }
 
 already_AddRefed<nsIStackFrame>
-CreateStack(JSContext* cx)
+CreateStack(JSContext* aCx, int32_t aMaxDepth)
 {
-  return JSStackFrame::CreateStack(cx);
+  return JSStackFrame::CreateStack(aCx, aMaxDepth);
 }
 
 already_AddRefed<nsIStackFrame>
 CreateStackFrameLocation(uint32_t aLanguage,
                          const char* aFilename,
                          const char* aFunctionName,
                          int32_t aLineNumber,
                          nsIStackFrame* aCaller)
--- a/dom/bindings/Exceptions.h
+++ b/dom/bindings/Exceptions.h
@@ -31,18 +31,20 @@ bool
 ThrowExceptionObject(JSContext* aCx, nsIException* aException);
 
 already_AddRefed<nsIStackFrame>
 GetCurrentJSStack();
 
 // Internal stuff not intended to be widely used.
 namespace exceptions {
 
+// aMaxDepth can be used to define a maximal depth for the stack trace. If the
+// value is -1, a default maximal depth will be selected.
 already_AddRefed<nsIStackFrame>
-CreateStack(JSContext* cx);
+CreateStack(JSContext* aCx, int32_t aMaxDepth = -1);
 
 already_AddRefed<nsIStackFrame>
 CreateStackFrameLocation(uint32_t aLanguage,
                          const char* aFilename,
                          const char* aFunctionName,
                          int32_t aLineNumber,
                          nsIStackFrame* aCaller);
 
new file mode 100644
--- /dev/null
+++ b/dom/webidl/WorkerConsole.webidl
@@ -0,0 +1,35 @@
+/* -*- Mode: IDL; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+interface WorkerConsole {
+  void log(any... data);
+  void info(any... data);
+  void warn(any... data);
+  void error(any... data);
+  void _exception(any... data);
+  void debug(any... data);
+  void trace();
+  void dir(optional any data);
+  void group(any... data);
+  void groupCollapsed(any... data);
+  void groupEnd(any... data);
+  void time(optional any time);
+  void timeEnd(optional any time);
+  void profile(any... data);
+  void profileEnd(any... data);
+  void assert(boolean condition, any... data);
+  void ___noSuchMethod__();
+};
+
+// This dictionary is used internally to send the stack trace from the worker to
+// the main thread Console API implementation.
+dictionary WorkerConsoleStack {
+  DOMString filename = "";
+  unsigned long lineNumber = 0;
+  DOMString functionName = "";
+  unsigned long language = 0;
+};
+
--- a/dom/webidl/WorkerGlobalScope.webidl
+++ b/dom/webidl/WorkerGlobalScope.webidl
@@ -9,16 +9,17 @@
  * © Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and Opera
  * Software ASA.
  * You are granted a license to use, reproduce and create derivative works of
  * this document.
  */
 
 interface WorkerGlobalScope : EventTarget {
   readonly attribute WorkerGlobalScope self;
+  readonly attribute WorkerConsole console;
   readonly attribute WorkerLocation location;
 
   void close();
   attribute OnErrorEventHandler onerror;
 
   attribute EventHandler onoffline;
   attribute EventHandler ononline;
   // also has additional members in a partial interface
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -423,16 +423,17 @@ WEBIDL_FILES = [
     'VTTRegion.webidl',
     'VTTRegionList.webidl',
     'WaveShaperNode.webidl',
     'WebComponents.webidl',
     'WebSocket.webidl',
     'WheelEvent.webidl',
     'WifiOptions.webidl',
     'Worker.webidl',
+    'WorkerConsole.webidl',
     'WorkerGlobalScope.webidl',
     'WorkerLocation.webidl',
     'WorkerNavigator.webidl',
     'XMLDocument.webidl',
     'XMLHttpRequest.webidl',
     'XMLHttpRequestEventTarget.webidl',
     'XMLHttpRequestUpload.webidl',
     'XMLSerializer.webidl',
new file mode 100644
--- /dev/null
+++ b/dom/workers/Console.cpp
@@ -0,0 +1,561 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* 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 "Console.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/OldDebugAPI.h"
+
+#include "nsJSUtils.h"
+#include "WorkerRunnable.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIDOMGlobalPropertyInitializer.h"
+
+#include "mozilla/dom/WorkerConsoleBinding.h"
+#include "mozilla/dom/Exceptions.h"
+
+#define CONSOLE_TAG JS_SCTAG_USER_MIN
+
+// From dom/base/ConsoleAPI.js
+#define DEFAULT_MAX_STACKTRACE_DEPTH 200
+
+using namespace mozilla::dom::exceptions;
+
+BEGIN_WORKERS_NAMESPACE
+
+class ConsoleProxy
+{
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(URLProxy)
+
+  bool
+  Init(JSContext* aCx, nsPIDOMWindow* aWindow)
+  {
+    AssertIsOnMainThread();
+
+    // Console API:
+    nsCOMPtr<nsISupports> cInstance =
+      do_CreateInstance("@mozilla.org/console-api;1");
+
+    nsCOMPtr<nsIDOMGlobalPropertyInitializer> gpi =
+      do_QueryInterface(cInstance);
+    NS_ENSURE_TRUE(gpi, false);
+
+    // We don't do anything with the return value.
+    JS::Rooted<JS::Value> prop_val(aCx);
+    if (NS_FAILED(gpi->Init(aWindow, &prop_val))) {
+      return false;
+    }
+
+    mXpcwrappedjs = do_QueryInterface(cInstance);
+    NS_ENSURE_TRUE(mXpcwrappedjs, false);
+
+    return true;
+  }
+
+  nsIXPConnectWrappedJS*
+  GetWrappedJS() const
+  {
+    AssertIsOnMainThread();
+    return mXpcwrappedjs;
+  }
+
+  void ReleaseWrappedJS()
+  {
+    AssertIsOnMainThread();
+    mXpcwrappedjs = nullptr;
+  }
+
+private:
+  nsCOMPtr<nsIXPConnectWrappedJS> mXpcwrappedjs;
+};
+
+/**
+ * Console API in workers uses the Structured Clone Algorithm to move any value
+ * from the worker thread to the main-thread. Some object cannot be moved and,
+ * in these cases, we convert them to strings.
+ * It's not the best, but at least we are able to show something.
+ */
+
+// This method is called by the Structured Clone Algorithm when some data has
+// to be read.
+static JSObject*
+ConsoleStructuredCloneCallbacksRead(JSContext* aCx,
+                                    JSStructuredCloneReader* /* unused */,
+                                    uint32_t aTag, uint32_t aData,
+                                    void* aClosure)
+{
+  AssertIsOnMainThread();
+
+  if (aTag != CONSOLE_TAG) {
+    return nullptr;
+  }
+
+  nsTArray<nsString>* strings = static_cast<nsTArray<nsString>*>(aClosure);
+  if (strings->Length() <= aData) {
+    return nullptr;
+  }
+
+  JS::Rooted<JS::Value> value(aCx);
+  if (!xpc::StringToJsval(aCx, strings->ElementAt(aData), &value)) {
+    return nullptr;
+  }
+
+  JS::Rooted<JSObject*> obj(aCx);
+  if (!JS_ValueToObject(aCx, value, &obj)) {
+    return nullptr;
+  }
+
+  return obj;
+}
+
+// This method is called by the Structured Clone Algorithm when some data has
+// to be written.
+static bool
+ConsoleStructuredCloneCallbacksWrite(JSContext* aCx,
+                                     JSStructuredCloneWriter* aWriter,
+                                     JS::Handle<JSObject*> aObj,
+                                     void* aClosure)
+{
+  JS::Rooted<JS::Value> value(aCx, JS::ObjectOrNullValue(aObj));
+  JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value));
+  if (!jsString) {
+    return false;
+  }
+
+  nsDependentJSString string;
+  if (!string.init(aCx, jsString)) {
+    return false;
+  }
+
+  nsTArray<nsString>* strings = static_cast<nsTArray<nsString>*>(aClosure);
+
+  if (!JS_WriteUint32Pair(aWriter, CONSOLE_TAG, strings->Length())) {
+    return false;
+  }
+
+  strings->AppendElement(string);
+
+  return true;
+}
+
+static void
+ConsoleStructuredCloneCallbacksError(JSContext* /* aCx */,
+                                     uint32_t /* aErrorId */)
+{
+  NS_WARNING("Failed to clone data for the Console API in workers.");
+}
+
+JSStructuredCloneCallbacks gConsoleCallbacks = {
+  ConsoleStructuredCloneCallbacksRead,
+  ConsoleStructuredCloneCallbacksWrite,
+  ConsoleStructuredCloneCallbacksError
+};
+
+class ConsoleStackData
+{
+public:
+  ConsoleStackData()
+  : mLineNumber(0)
+  {}
+
+  nsCString mFilename;
+  uint32_t mLineNumber;
+  nsCString mFunctionName;
+};
+
+class ConsoleRunnable MOZ_FINAL : public nsRunnable
+{
+public:
+  explicit ConsoleRunnable(WorkerConsole* aConsole,
+                           WorkerPrivate* aWorkerPrivate)
+    : mConsole(aConsole)
+    , mWorkerPrivate(aWorkerPrivate)
+    , mMethod(nullptr)
+  {
+    mWorkerPrivate->AssertIsOnWorkerThread();
+  }
+
+  bool
+  Dispatch(JSContext* aCx,
+           const char* aMethod,
+           JS::Handle<JS::Value> aArguments,
+           nsTArray<ConsoleStackData>& aStackData)
+  {
+    mMethod = aMethod;
+    mStackData.SwapElements(aStackData);
+
+    if (!mArguments.write(aCx, aArguments, &gConsoleCallbacks, &mStrings)) {
+      JS_ClearPendingException(aCx);
+      return false;
+    }
+
+    mWorkerPrivate->AssertIsOnWorkerThread();
+
+    AutoSyncLoopHolder syncLoop(mWorkerPrivate);
+
+    mSyncLoopTarget = syncLoop.EventTarget();
+
+    if (NS_FAILED(NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL))) {
+      JS_ReportError(aCx,
+                     "Failed to dispatch to main thread for the "
+                     "Console API (method %s)!", mMethod);
+      return false;
+    }
+
+    return syncLoop.Run();
+  }
+
+private:
+  NS_IMETHOD Run()
+  {
+    AssertIsOnMainThread();
+
+    RunConsole();
+
+    nsRefPtr<MainThreadStopSyncLoopRunnable> response =
+      new MainThreadStopSyncLoopRunnable(mWorkerPrivate,
+                                         mSyncLoopTarget.forget(),
+                                         true);
+    if (!response->Dispatch(nullptr)) {
+      NS_WARNING("Failed to dispatch response!");
+    }
+
+    return NS_OK;
+  }
+
+  void
+  RunConsole()
+  {
+    // This class is used to clear any exception at the end of this method.
+    class ClearException
+    {
+    public:
+      ClearException(JSContext* aCx)
+        : mCx(aCx)
+      {
+      }
+
+      ~ClearException()
+      {
+        JS_ClearPendingException(mCx);
+      }
+
+    private:
+      JSContext* mCx;
+    };
+
+    // Walk up to our containing page
+    WorkerPrivate* wp = mWorkerPrivate;
+    while (wp->GetParent()) {
+      wp = wp->GetParent();
+    }
+
+    AutoPushJSContext cx(wp->ParentJSContext());
+    JSAutoRequest ar(cx);
+    ClearException ce(cx);
+
+    nsRefPtr<ConsoleProxy> proxy = mConsole->GetProxy();
+    if (!proxy) {
+      nsPIDOMWindow* window = wp->GetWindow();
+      NS_ENSURE_TRUE_VOID(window);
+
+      proxy = new ConsoleProxy();
+      if (!proxy->Init(cx, window)) {
+        return;
+      }
+
+      mConsole->SetProxy(proxy);
+    }
+
+    JS::Rooted<JSObject*> consoleObj(cx, proxy->GetWrappedJS()->GetJSObject());
+    NS_ENSURE_TRUE_VOID(consoleObj);
+
+    JSAutoCompartment ac(cx, consoleObj);
+
+    // 3 args for the queueCall.
+    nsDependentCString method(mMethod);
+
+    JS::Rooted<JS::Value> methodValue(cx);
+    if (!ByteStringToJsval(cx, method, &methodValue)) {
+      return;
+    }
+
+    JS::Rooted<JS::Value> argumentsValue(cx);
+    if (!mArguments.read(cx, &argumentsValue, &gConsoleCallbacks, &mStrings)) {
+      return;
+    }
+
+    JS::Rooted<JS::Value> stackValue(cx);
+    {
+      JS::Rooted<JSObject*> stackObj(cx,
+        JS_NewArrayObject(cx, mStackData.Length(), nullptr));
+      if (!stackObj) {
+        return;
+      }
+
+      for (uint32_t i = 0; i < mStackData.Length(); ++i) {
+        WorkerConsoleStack stack;
+
+        CopyUTF8toUTF16(mStackData[i].mFilename, stack.mFilename);
+
+        CopyUTF8toUTF16(mStackData[i].mFunctionName, stack.mFunctionName);
+
+        stack.mLineNumber = mStackData[i].mLineNumber;
+
+        stack.mLanguage = nsIProgrammingLanguage::JAVASCRIPT;
+
+        JS::Rooted<JS::Value> value(cx);
+        if (!stack.ToObject(cx, JS::NullPtr(), &value)) {
+          return;
+        }
+
+        if (!JS_DefineElement(cx, stackObj, i, value, nullptr, nullptr, 0)) {
+          return;
+        }
+      }
+
+      stackValue = JS::ObjectValue(*stackObj);
+    }
+
+    JS::AutoValueVector argv(cx);
+    if (!argv.resize(3)) {
+      return;
+    }
+
+    argv[0] = methodValue;
+    argv[1] = argumentsValue;
+    argv[2] = stackValue;
+
+    JS::Rooted<JS::Value> ret(cx);
+    JS_CallFunctionName(cx, consoleObj, "queueCall", argv.length(),
+                        argv.begin(), ret.address());
+  }
+
+  WorkerConsole* mConsole;
+  WorkerPrivate* mWorkerPrivate;
+  nsCOMPtr<nsIEventTarget> mSyncLoopTarget;
+
+  const char* mMethod;
+  JSAutoStructuredCloneBuffer mArguments;
+  nsTArray<ConsoleStackData> mStackData;
+
+  nsTArray<nsString> mStrings;
+};
+
+class TeardownRunnable : public nsRunnable
+{
+public:
+  TeardownRunnable(ConsoleProxy* aProxy)
+    : mProxy(aProxy)
+  {
+  }
+
+  NS_IMETHOD Run()
+  {
+    AssertIsOnMainThread();
+
+    mProxy->ReleaseWrappedJS();
+    mProxy = nullptr;
+
+    return NS_OK;
+  }
+
+private:
+  nsRefPtr<ConsoleProxy> mProxy;
+};
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(WorkerConsole)
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(WorkerConsole, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(WorkerConsole, Release)
+
+/* static */ already_AddRefed<WorkerConsole>
+WorkerConsole::Create()
+{
+  nsRefPtr<WorkerConsole> console = new WorkerConsole();
+  return console.forget();
+}
+
+JSObject*
+WorkerConsole::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
+{
+  return WorkerConsoleBinding_workers::Wrap(aCx, aScope, this);
+}
+
+WorkerConsole::WorkerConsole()
+{
+  MOZ_COUNT_CTOR(WorkerConsole);
+  SetIsDOMBinding();
+}
+
+WorkerConsole::~WorkerConsole()
+{
+  MOZ_COUNT_DTOR(WorkerConsole);
+
+  if (mProxy) {
+    nsRefPtr<TeardownRunnable> runnable = new TeardownRunnable(mProxy);
+    mProxy = nullptr;
+
+    if (NS_FAILED(NS_DispatchToMainThread(runnable))) {
+      NS_ERROR("Failed to dispatch teardown runnable!");
+    }
+  }
+}
+
+void
+WorkerConsole::SetProxy(ConsoleProxy* aProxy)
+{
+  MOZ_ASSERT(!mProxy);
+  mProxy = aProxy;
+}
+
+void
+WorkerConsole::Method(JSContext* aCx, const char* aMethodName,
+                      const Sequence<JS::Value>& aData,
+                      uint32_t aStackLevel)
+{
+  nsCOMPtr<nsIStackFrame> stack = CreateStack(aCx, aStackLevel);
+  if (!stack) {
+    return;
+  }
+
+  // nsIStackFrame is not thread-safe so we take what we need and we store in
+  // an array of ConsoleStackData objects.
+  nsTArray<ConsoleStackData> stackData;
+  while (stack) {
+    ConsoleStackData& data = *stackData.AppendElement();
+
+    if (NS_FAILED(stack->GetFilename(data.mFilename))) {
+      return;
+    }
+
+    int32_t lineNumber;
+    if (NS_FAILED(stack->GetLineNumber(&lineNumber))) {
+      return;
+    }
+
+    data.mLineNumber = lineNumber;
+
+    if (NS_FAILED(stack->GetName(data.mFunctionName))) {
+      return;
+    }
+
+    nsCOMPtr<nsIStackFrame> caller;
+    if (NS_FAILED(stack->GetCaller(getter_AddRefs(caller)))) {
+      return;
+    }
+
+    stack.swap(caller);
+  }
+
+  JS::Rooted<JSObject*> arguments(aCx,
+    JS_NewArrayObject(aCx, aData.Length(), nullptr));
+  if (!arguments) {
+    return;
+  }
+
+  for (uint32_t i = 0; i < aData.Length(); ++i) {
+    if (!JS_DefineElement(aCx, arguments, i, aData[i], nullptr, nullptr,
+                          JSPROP_ENUMERATE)) {
+      return;
+    }
+  }
+  JS::Rooted<JS::Value> argumentsValue(aCx, JS::ObjectValue(*arguments));
+
+  WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx);
+  MOZ_ASSERT(worker);
+
+  nsRefPtr<ConsoleRunnable> runnable = new ConsoleRunnable(this, worker);
+  runnable->Dispatch(aCx, aMethodName, argumentsValue, stackData);
+}
+
+#define METHOD(name, jsName)                                \
+  void                                                      \
+  WorkerConsole::name(JSContext* aCx,                       \
+                      const Sequence<JS::Value>& aData)     \
+  {                                                         \
+    Method(aCx, jsName, aData, 1);                          \
+  }
+
+METHOD(Log, "log")
+METHOD(Info, "info")
+METHOD(Warn, "warn")
+METHOD(Error, "error")
+METHOD(Exception, "exception")
+METHOD(Debug, "debug")
+
+void
+WorkerConsole::Trace(JSContext* aCx)
+{
+  Sequence<JS::Value> data;
+  Method(aCx, "trace", data, DEFAULT_MAX_STACKTRACE_DEPTH);
+}
+
+void
+WorkerConsole::Dir(JSContext* aCx,
+                   const Optional<JS::Handle<JS::Value>>& aValue)
+{
+  Sequence<JS::Value> data;
+
+  if (aValue.WasPassed()) {
+    data.AppendElement(aValue.Value());
+  }
+
+  Method(aCx, "dir", data, 1);
+}
+
+METHOD(Group, "group")
+METHOD(GroupCollapsed, "groupCollapsed")
+METHOD(GroupEnd, "groupEnd")
+
+void
+WorkerConsole::Time(JSContext* aCx,
+                    const Optional<JS::Handle<JS::Value>>& aTimer)
+{
+  Sequence<JS::Value> data;
+
+  if (aTimer.WasPassed()) {
+    data.AppendElement(aTimer.Value());
+  }
+
+  Method(aCx, "time", data, 1);
+}
+
+void
+WorkerConsole::TimeEnd(JSContext* aCx,
+                       const Optional<JS::Handle<JS::Value>>& aTimer)
+{
+  Sequence<JS::Value> data;
+
+  if (aTimer.WasPassed()) {
+    data.AppendElement(aTimer.Value());
+  }
+
+  Method(aCx, "timeEnd", data, 1);
+}
+
+METHOD(Profile, "profile")
+METHOD(ProfileEnd, "profileEnd")
+
+void
+WorkerConsole::Assert(JSContext* aCx, bool aCondition,
+                      const Sequence<JS::Value>& aData)
+{
+  if (!aCondition) {
+    Method(aCx, "assert", aData, 1);
+  }
+}
+
+void
+WorkerConsole::__noSuchMethod__()
+{
+  // Nothing to do.
+}
+
+#undef METHOD
+
+END_WORKERS_NAMESPACE
new file mode 100644
--- /dev/null
+++ b/dom/workers/Console.h
@@ -0,0 +1,112 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* 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/. */
+
+#ifndef mozilla_dom_workers_Console_h
+#define mozilla_dom_workers_Console_h
+
+#include "Workers.h"
+#include "WorkerPrivate.h"
+#include "nsWrapperCache.h"
+
+BEGIN_WORKERS_NAMESPACE
+
+class ConsoleProxy;
+class ConsoleStackData;
+
+class WorkerConsole MOZ_FINAL : public nsWrapperCache
+{
+  WorkerConsole();
+
+public:
+
+  NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(WorkerConsole)
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(WorkerConsole)
+
+  static already_AddRefed<WorkerConsole>
+  Create();
+
+  virtual JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope) MOZ_OVERRIDE;
+
+  nsISupports* GetParentObject() const
+  {
+    return nullptr;
+  }
+
+  ~WorkerConsole();
+
+  ConsoleProxy*
+  GetProxy() const
+  {
+    return mProxy;
+  }
+
+  void
+  SetProxy(ConsoleProxy* aProxy);
+
+  // WebIDL methods
+
+  void
+  Log(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  Info(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  Warn(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  Error(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  Exception(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  Debug(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  Trace(JSContext* aCx);
+
+  void
+  Dir(JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aValue);
+
+  void
+  Group(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  GroupCollapsed(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  GroupEnd(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  Time(JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aTimer);
+
+  void
+  TimeEnd(JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aTimer);
+
+  void
+  Profile(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  ProfileEnd(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
+  Assert(JSContext* aCx, bool aCondition, const Sequence<JS::Value>& aData);
+
+  void
+  __noSuchMethod__();
+
+private:
+  void
+  Method(JSContext* aCx, const char* aMethodName,
+         const Sequence<JS::Value>& aData, uint32_t aMaxStackDepth);
+
+  nsRefPtr<ConsoleProxy> mProxy;
+};
+
+END_WORKERS_NAMESPACE
+
+#endif // mozilla_dom_workers_Console_h
--- a/dom/workers/WorkerScope.cpp
+++ b/dom/workers/WorkerScope.cpp
@@ -10,16 +10,17 @@
 #include "mozilla/dom/FunctionBinding.h"
 #include "mozilla/dom/DedicatedWorkerGlobalScopeBinding.h"
 #include "mozilla/dom/SharedWorkerGlobalScopeBinding.h"
 
 #ifdef ANDROID
 #include <android/log.h>
 #endif
 
+#include "Console.h"
 #include "Location.h"
 #include "Navigator.h"
 #include "Principal.h"
 #include "RuntimeService.h"
 #include "ScriptLoader.h"
 #include "WorkerPrivate.h"
 
 #define UNWRAP_WORKER_OBJECT(Interface, obj, value)                           \
@@ -72,16 +73,29 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
 NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper)
 
 JSObject*
 WorkerGlobalScope::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
 {
   MOZ_CRASH("We should never get here!");
 }
 
+WorkerConsole*
+WorkerGlobalScope::Console()
+{
+  mWorkerPrivate->AssertIsOnWorkerThread();
+
+  if (!mConsole) {
+    mConsole = WorkerConsole::Create();
+    MOZ_ASSERT(mConsole);
+  }
+
+  return mConsole;
+}
+
 already_AddRefed<WorkerLocation>
 WorkerGlobalScope::Location()
 {
   mWorkerPrivate->AssertIsOnWorkerThread();
 
   if (!mLocation) {
     WorkerPrivate::LocationInfo& info = mWorkerPrivate->GetLocationInfo();
 
--- a/dom/workers/WorkerScope.h
+++ b/dom/workers/WorkerScope.h
@@ -15,22 +15,24 @@ namespace dom {
 class Function;
 
 } // namespace dom
 } // namespace mozilla
 
 BEGIN_WORKERS_NAMESPACE
 
 class WorkerPrivate;
+class WorkerConsole;
 class WorkerLocation;
 class WorkerNavigator;
 
 class WorkerGlobalScope : public nsDOMEventTargetHelper,
                           public nsIGlobalObject
 {
+  nsRefPtr<WorkerConsole> mConsole;
   nsRefPtr<WorkerLocation> mLocation;
   nsRefPtr<WorkerNavigator> mNavigator;
 
 protected:
   WorkerPrivate* mWorkerPrivate;
 
   WorkerGlobalScope(WorkerPrivate* aWorkerPrivate);
   virtual ~WorkerGlobalScope();
@@ -53,16 +55,19 @@ public:
                                                          nsDOMEventTargetHelper)
 
   already_AddRefed<WorkerGlobalScope>
   Self()
   {
     return nsRefPtr<WorkerGlobalScope>(this).forget();
   }
 
+  WorkerConsole*
+  Console();
+
   already_AddRefed<WorkerLocation>
   Location();
 
   already_AddRefed<WorkerNavigator>
   Navigator();
 
   already_AddRefed<WorkerNavigator>
   GetExistingNavigator() const;
--- a/dom/workers/moz.build
+++ b/dom/workers/moz.build
@@ -14,29 +14,31 @@ EXPORTS.mozilla.dom += [
 ]
 
 EXPORTS.mozilla.dom.workers += [
     'Workers.h',
 ]
 
 # Stuff needed for the bindings, not really public though.
 EXPORTS.mozilla.dom.workers.bindings += [
+    'Console.h',
     'FileReaderSync.h',
     'Location.h',
     'MessagePort.h',
     'Navigator.h',
     'SharedWorker.h',
     'URL.h',
     'WorkerFeature.h',
     'XMLHttpRequest.h',
     'XMLHttpRequestUpload.h',
 ]
 
 SOURCES += [
     'ChromeWorkerScope.cpp',
+    'Console.cpp',
     'File.cpp',
     'FileReaderSync.cpp',
     'Location.cpp',
     'MessagePort.cpp',
     'Navigator.cpp',
     'Principal.cpp',
     'RegisterBindings.cpp',
     'RuntimeService.cpp',
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/console_worker.js
@@ -0,0 +1,104 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+onmessage = function(event) {
+  // TEST: does console exist?
+  postMessage({event: 'console exists', status: !!console, last : false});
+
+  postMessage({event: 'trace without function', status: true, last : false});
+
+  for (var i = 0; i < 10; ++i) {
+    console.what('1', 123, 321);
+  }
+
+  for (var i = 0; i < 10; ++i) {
+    console.log(i, i, i);
+  }
+
+  function trace1() {
+    function trace2() {
+      function trace3() {
+        console.trace("trace " + i);
+      }
+      trace3();
+    }
+    trace2();
+  }
+  trace1();
+
+  foobar585956c = function(a) {
+    console.trace();
+    return a+"c";
+  };
+
+  function foobar585956b(a) {
+    return foobar585956c(a+"b");
+  }
+
+  function foobar585956a(omg) {
+    return foobar585956b(omg + "a");
+  }
+
+  function foobar646025(omg) {
+    console.log(omg, "o", "d");
+  }
+
+  function startTimer(timer) {
+    console.time(timer);
+  }
+
+  function stopTimer(timer) {
+    console.timeEnd(timer);
+  }
+
+  function testGroups() {
+    console.groupCollapsed("a", "group");
+    console.group("b", "group");
+    console.groupEnd("b", "group");
+  }
+
+  foobar585956a('omg');
+  foobar646025('omg');
+  testGroups();
+  startTimer('foo');
+  setTimeout(function() {
+    stopTimer('foo');
+    nextSteps(event);
+  }, 10);
+}
+
+function nextSteps(event) {
+
+  function namelessTimer() {
+    console.time();
+    console.timeEnd();
+  }
+
+  namelessTimer();
+
+  var str = "Test Message."
+  console.foobar(str); // if this throws, we don't execute following funcs
+  console.log(str);
+  console.info(str);
+  console.warn(str);
+  console.error(str);
+  console.exception(str);
+  console.assert(true, str);
+  console.assert(false, str);
+  console.profile(str);
+  console.profileEnd(str);
+  postMessage({event: '4 messages', status: true, last : false});
+
+  // Recursive:
+  if (event.data == true) {
+    var worker = new Worker('console_worker.js');
+    worker.onmessage = function(event) {
+      postMessage(event.data);
+    }
+    worker.postMessage(false);
+  } else {
+    postMessage({event: 'bye bye', status: true, last : true});
+  }
+}
--- a/dom/workers/test/mochitest.ini
+++ b/dom/workers/test/mochitest.ini
@@ -2,29 +2,31 @@
 support-files =
   WorkerTest_badworker.js
   atob_worker.js
   clearTimeouts_worker.js
   closeOnGC_server.sjs
   closeOnGC_worker.js
   close_worker.js
   content_worker.js
+  console_worker.js
   csp_worker.js
   errorPropagation_iframe.html
   errorPropagation_worker.js
   errorwarning_worker.js
   eventDispatch_worker.js
   fibonacci_worker.js
   importScripts_worker.js
   importScripts_worker_imported1.js
   importScripts_worker_imported2.js
   importScripts_worker_imported3.js
   importScripts_worker_imported4.js
   instanceof_worker.js
   json_worker.js
+  jsversion_worker.js
   loadEncoding_worker.js
   location_worker.js
   longThread_worker.js
   multi_sharedWorker_frame.html
   multi_sharedWorker_sharedWorker.js
   navigator_worker.js
   newError_worker.js
   onLine_worker.js
@@ -54,42 +56,43 @@ support-files =
   urlApi_worker.js
   url_worker.js
   workersDisabled_worker.js
   xhr2_worker.js
   xhrAbort_worker.js
   xhr_implicit_cancel_worker.js
   xhr_worker.js
   url_exceptions_worker.js
-  jsversion_worker.js
   urlSearchParams_worker.js
   subdir/relativeLoad_sub_worker.js
   subdir/relativeLoad_sub_worker2.js
   subdir/relativeLoad_sub_import.js
 
 [test_404.html]
 [test_atob.html]
 [test_blobConstructor.html]
 [test_blobWorkers.html]
 [test_chromeWorker.html]
 [test_clearTimeouts.html]
 [test_close.html]
 [test_closeOnGC.html]
+[test_console.html]
 [test_contentWorker.html]
 [test_csp.html]
 [test_csp.html^headers^]
 [test_csp.js]
 [test_dataURLWorker.html]
 [test_errorPropagation.html]
 [test_errorwarning.html]
 [test_eventDispatch.html]
 [test_fibonacci.html]
 [test_importScripts.html]
 [test_instanceof.html]
 [test_json.html]
+[test_jsversion.html]
 [test_loadEncoding.html]
 [test_loadError.html]
 [test_location.html]
 [test_longThread.html]
 [test_multi_sharedWorker.html]
 [test_multi_sharedWorker_lifetimes.html]
 [test_navigator.html]
 [test_newError.html]
@@ -120,9 +123,8 @@ support-files =
 [test_xhr_parameters.html]
 [test_xhr_parameters.js]
 [test_xhr_system.html]
 [test_xhr_system.js]
 [test_xhr_timeout.html]
 skip-if = (os == "win") || (os == "mac")
 [test_url_exceptions.html]
 [test_urlSearchParams.html]
-[test_jsversion.html]
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/test_console.html
@@ -0,0 +1,44 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests of DOM Worker Console
+-->
+<head>
+  <title>Test for DOM Worker Console</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" language="javascript">
+  var worker = new Worker("console_worker.js");
+
+  worker.onmessage = function(event) {
+    is(event.target, worker, "Worker and target match!");
+    ok(event.data.status, event.data.event);
+
+    if (!event.data.status || event.data.last)
+      SimpleTest.finish();
+  };
+
+  worker.onerror = function(event) {
+    ok(false, "Worker had an error: " + event.message);
+    SimpleTest.finish();
+  }
+
+  worker.postMessage(true);
+
+  SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>