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 165630 99c61f40109b72615ac80d44f98a7933870cd46a
parent 165629 faca83ad9b80a37932601bd484bbe7edfebc19bb
child 165631 b182f29bb7b1ebb63554dbfb5155548190fe3648
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersehsan, bz
bugs620935
milestone29.0a1
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>