Bug 1458466 - Implement Console.timeLog(optional DOMString label = "default"), r=bgrins
authorAndrea Marchesini <amarchesini@mozilla.com>
Tue, 15 May 2018 13:00:49 +0200
changeset 418311 3facc042d3210a74dec7651208f7acee82585ade
parent 418310 48d6f36d141287c064af3a24e9968b6cc4ac92a0
child 418312 66fa28c2f438f1b8cd14c63f9557090d54ad5230
push id103273
push useramarchesini@mozilla.com
push dateTue, 15 May 2018 11:01:21 +0000
treeherdermozilla-inbound@3facc042d321 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1458466
milestone62.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 1458466 - Implement Console.timeLog(optional DOMString label = "default"), r=bgrins
dom/base/UseCounters.conf
dom/console/Console.cpp
dom/console/Console.h
dom/console/ConsoleInstance.cpp
dom/console/ConsoleInstance.h
dom/console/tests/mochitest.ini
dom/console/tests/test_timer.html
dom/tests/mochitest/general/test_consoleAPI.html
dom/webidl/Console.webidl
--- a/dom/base/UseCounters.conf
+++ b/dom/base/UseCounters.conf
@@ -103,13 +103,14 @@ method console.table
 method console.trace
 method console.warn
 method console.dir
 method console.dirxml
 method console.group
 method console.groupCollapsed
 method console.groupEnd
 method console.time
+method console.timeLog
 method console.timeEnd
 method console.exception
 method console.timeStamp
 method console.profile
 method console.profileEnd
--- a/dom/console/Console.cpp
+++ b/dom/console/Console.cpp
@@ -89,18 +89,18 @@ class ConsoleCallData final
 public:
   NS_INLINE_DECL_REFCOUNTING(ConsoleCallData)
 
   ConsoleCallData()
     : mMethodName(Console::MethodLog)
     , mTimeStamp(JS_Now() / PR_USEC_PER_MSEC)
     , mStartTimerValue(0)
     , mStartTimerStatus(Console::eTimerUnknown)
-    , mStopTimerDuration(0)
-    , mStopTimerStatus(Console::eTimerUnknown)
+    , mLogTimerDuration(0)
+    , mLogTimerStatus(Console::eTimerUnknown)
     , mCountValue(MAX_PAGE_COUNTERS)
     , mIDType(eUnknown)
     , mOuterIDNumber(0)
     , mInnerIDNumber(0)
     , mStatus(eUnused)
   {}
 
   bool
@@ -216,24 +216,24 @@ public:
   // They will be set on the owning thread and never touched again on that
   // thread. They will be used in order to create a ConsoleTimerStart dictionary
   // when console.time() is used.
   DOMHighResTimeStamp mStartTimerValue;
   nsString mStartTimerLabel;
   Console::TimerStatus mStartTimerStatus;
 
   // These values are set in the owning thread and they contain the duration,
-  // the name and the status of the StopTimer method. If status is false,
+  // the name and the status of the LogTimer method. If status is false,
   // something went wrong. They will be set on the owning thread and never
   // touched again on that thread. They will be used in order to create a
-  // ConsoleTimerEnd dictionary. This members are set when
-  // console.timeEnd() is called.
-  double mStopTimerDuration;
-  nsString mStopTimerLabel;
-  Console::TimerStatus mStopTimerStatus;
+  // ConsoleTimerLogOrEnd dictionary. This members are set when
+  // console.timeEnd() or console.timeLog() are called.
+  double mLogTimerDuration;
+  nsString mLogTimerLabel;
+  Console::TimerStatus mLogTimerStatus;
 
   // These 2 values are set by IncreaseCounter on the owning thread and they are
   // used CreateCounterValue. These members are set when console.count() is
   // called.
   nsString mCountLabel;
   uint32_t mCountValue;
 
   // The concept of outerID and innerID is misleading because when a
@@ -1226,40 +1226,52 @@ Console::GroupEnd(const GlobalObject& aG
 {
   const Sequence<JS::Value> data;
   Method(aGlobal, MethodGroupEnd, NS_LITERAL_STRING("groupEnd"), data);
 }
 
 /* static */ void
 Console::Time(const GlobalObject& aGlobal, const nsAString& aLabel)
 {
-  StringMethod(aGlobal, aLabel, MethodTime, NS_LITERAL_STRING("time"));
+  StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), MethodTime,
+               NS_LITERAL_STRING("time"));
 }
 
 /* static */ void
 Console::TimeEnd(const GlobalObject& aGlobal, const nsAString& aLabel)
 {
-  StringMethod(aGlobal, aLabel, MethodTimeEnd, NS_LITERAL_STRING("timeEnd"));
+  StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), MethodTimeEnd,
+               NS_LITERAL_STRING("timeEnd"));
+}
+
+/* static */ void
+Console::TimeLog(const GlobalObject& aGlobal, const nsAString& aLabel,
+                 const Sequence<JS::Value>& aData)
+{
+  StringMethod(aGlobal, aLabel, aData, MethodTimeLog,
+               NS_LITERAL_STRING("timeLog"));
 }
 
 /* static */ void
 Console::StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel,
-                      MethodName aMethodName, const nsAString& aMethodString)
+                      const Sequence<JS::Value>& aData, MethodName aMethodName,
+                      const nsAString& aMethodString)
 {
   RefPtr<Console> console = GetConsole(aGlobal);
   if (!console) {
     return;
   }
 
-  console->StringMethodInternal(aGlobal.Context(), aLabel, aMethodName,
+  console->StringMethodInternal(aGlobal.Context(), aLabel, aData, aMethodName,
                                 aMethodString);
 }
 
 void
 Console::StringMethodInternal(JSContext* aCx, const nsAString& aLabel,
+                              const Sequence<JS::Value>& aData,
                               MethodName aMethodName,
                               const nsAString& aMethodString)
 {
   ConsoleCommon::ClearException ce(aCx);
 
   Sequence<JS::Value> data;
   SequenceRooter<JS::Value> rooter(aCx, &data);
 
@@ -1267,16 +1279,22 @@ Console::StringMethodInternal(JSContext*
   if (!dom::ToJSValue(aCx, aLabel, &value)) {
     return;
   }
 
   if (!data.AppendElement(value, fallible)) {
     return;
   }
 
+  for (uint32_t i = 0; i < aData.Length(); ++i) {
+    if (!data.AppendElement(aData[i], fallible)) {
+      return;
+    }
+  }
+
   MethodInternal(aCx, aMethodName, aMethodString, data);
 }
 
 /* static */ void
 Console::TimeStamp(const GlobalObject& aGlobal,
                    const JS::Handle<JS::Value> aData)
 {
   JSContext* cx = aGlobal.Context();
@@ -1417,17 +1435,18 @@ Console::Assert(const GlobalObject& aGlo
   if (!aCondition) {
     Method(aGlobal, MethodAssert, NS_LITERAL_STRING("assert"), aData);
   }
 }
 
 /* static */ void
 Console::Count(const GlobalObject& aGlobal, const nsAString& aLabel)
 {
-  StringMethod(aGlobal, aLabel, MethodCount, NS_LITERAL_STRING("count"));
+  StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), MethodCount,
+               NS_LITERAL_STRING("count"));
 }
 
 namespace {
 
 void
 StackFrameToStackEntry(JSContext* aCx, nsIStackFrame* aStackFrame,
                        ConsoleStackEntry& aStackEntry)
 {
@@ -1568,36 +1587,46 @@ Console::MethodInternal(JSContext* aCx, 
     // nsIStackFrame is not threadsafe, so we need to snapshot it now,
     // before we post our runnable to the main thread.
     callData->mReifiedStack.emplace();
     ReifyStack(aCx, stack, *callData->mReifiedStack);
   }
 
   DOMHighResTimeStamp monotonicTimer;
 
-  // Monotonic timer for 'time' and 'timeEnd'
+  // Monotonic timer for 'time', 'timeLog' and 'timeEnd'
   if ((aMethodName == MethodTime ||
+       aMethodName == MethodTimeLog ||
        aMethodName == MethodTimeEnd ||
        aMethodName == MethodTimeStamp) &&
       !MonotonicTimer(aCx, aMethodName, aData, &monotonicTimer)) {
     return;
   }
 
   if (aMethodName == MethodTime && !aData.IsEmpty()) {
     callData->mStartTimerStatus = StartTimer(aCx, aData[0],
                                              monotonicTimer,
                                              callData->mStartTimerLabel,
                                              &callData->mStartTimerValue);
   }
 
   else if (aMethodName == MethodTimeEnd && !aData.IsEmpty()) {
-    callData->mStopTimerStatus = StopTimer(aCx, aData[0],
-                                           monotonicTimer,
-                                           callData->mStopTimerLabel,
-                                           &callData->mStopTimerDuration);
+    callData->mLogTimerStatus = LogTimer(aCx, aData[0],
+                                         monotonicTimer,
+                                         callData->mLogTimerLabel,
+                                         &callData->mLogTimerDuration,
+                                         true /* Cancel timer */);
+  }
+
+  else if (aMethodName == MethodTimeLog && !aData.IsEmpty()) {
+    callData->mLogTimerStatus = LogTimer(aCx, aData[0],
+                                         monotonicTimer,
+                                         callData->mLogTimerLabel,
+                                         &callData->mLogTimerDuration,
+                                         false /* Cancel timer */);
   }
 
   else if (aMethodName == MethodCount) {
     callData->mCountValue = IncreaseCounter(aCx, aData, callData->mCountLabel);
     if (!callData->mCountValue) {
       return;
     }
   }
@@ -1858,20 +1887,21 @@ Console::PopulateConsoleNotificationInTh
     }
   }
 
   else if (aData->mMethodName == MethodTime && !aArguments.IsEmpty()) {
     event.mTimer = CreateStartTimerValue(aCx, aData->mStartTimerLabel,
                                          aData->mStartTimerStatus);
   }
 
-  else if (aData->mMethodName == MethodTimeEnd && !aArguments.IsEmpty()) {
-    event.mTimer = CreateStopTimerValue(aCx, aData->mStopTimerLabel,
-                                        aData->mStopTimerDuration,
-                                        aData->mStopTimerStatus);
+  else if ((aData->mMethodName == MethodTimeEnd ||
+            aData->mMethodName == MethodTimeLog) && !aArguments.IsEmpty()) {
+    event.mTimer = CreateLogOrEndTimerValue(aCx, aData->mLogTimerLabel,
+                                            aData->mLogTimerDuration,
+                                            aData->mLogTimerStatus);
   }
 
   else if (aData->mMethodName == MethodCount) {
     event.mCounter = CreateCounterValue(aCx, aData->mCountLabel,
                                         aData->mCountValue);
   }
 
   JSAutoCompartment ac2(aCx, targetScope);
@@ -2316,20 +2346,21 @@ Console::CreateStartTimerValue(JSContext
   if (!ToJSValue(aCx, timer, &value)) {
     return JS::UndefinedValue();
   }
 
   return value;
 }
 
 Console::TimerStatus
-Console::StopTimer(JSContext* aCx, const JS::Value& aName,
-                   DOMHighResTimeStamp aTimestamp,
-                   nsAString& aTimerLabel,
-                   double* aTimerDuration)
+Console::LogTimer(JSContext* aCx, const JS::Value& aName,
+                  DOMHighResTimeStamp aTimestamp,
+                  nsAString& aTimerLabel,
+                  double* aTimerDuration,
+                  bool aCancelTimer)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(aTimerDuration);
 
   *aTimerDuration = 0;
 
   JS::Rooted<JS::Value> name(aCx, aName);
   JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, name));
@@ -2340,34 +2371,42 @@ Console::StopTimer(JSContext* aCx, const
   nsAutoJSString key;
   if (NS_WARN_IF(!key.init(aCx, jsString))) {
     return eTimerJSException;
   }
 
   aTimerLabel = key;
 
   DOMHighResTimeStamp value = 0;
-  if (!mTimerRegistry.Remove(key, &value)) {
-    NS_WARNING("mTimerRegistry entry not found");
-    return eTimerDoesntExist;
+
+  if (aCancelTimer) {
+    if (!mTimerRegistry.Remove(key, &value)) {
+      NS_WARNING("mTimerRegistry entry not found");
+      return eTimerDoesntExist;
+    }
+  } else {
+    if (!mTimerRegistry.Get(key, &value)) {
+      NS_WARNING("mTimerRegistry entry not found");
+      return eTimerDoesntExist;
+    }
   }
 
   *aTimerDuration = aTimestamp - value;
   return eTimerDone;
 }
 
 JS::Value
-Console::CreateStopTimerValue(JSContext* aCx, const nsAString& aLabel,
-                              double aDuration, TimerStatus aStatus) const
+Console::CreateLogOrEndTimerValue(JSContext* aCx, const nsAString& aLabel,
+                                  double aDuration, TimerStatus aStatus) const
 {
   if (aStatus != eTimerDone) {
     return CreateTimerError(aCx, aLabel, aStatus);
   }
 
-  RootedDictionary<ConsoleTimerEnd> timer(aCx);
+  RootedDictionary<ConsoleTimerLogOrEnd> timer(aCx);
   timer.mName = aLabel;
   timer.mDuration = aDuration;
 
   JS::Rooted<JS::Value> value(aCx);
   if (!ToJSValue(aCx, timer, &value)) {
     return JS::UndefinedValue();
   }
 
@@ -2991,16 +3030,17 @@ Console::WebIDLLogLevelToInteger(Console
 {
   switch (aLevel) {
     case ConsoleLogLevel::All: return 0;
     case ConsoleLogLevel::Debug: return 2;
     case ConsoleLogLevel::Log: return 3;
     case ConsoleLogLevel::Info: return 3;
     case ConsoleLogLevel::Clear: return 3;
     case ConsoleLogLevel::Trace: return 3;
+    case ConsoleLogLevel::TimeLog: return 3;
     case ConsoleLogLevel::TimeEnd: return 3;
     case ConsoleLogLevel::Time: return 3;
     case ConsoleLogLevel::Group: return 3;
     case ConsoleLogLevel::GroupEnd: return 3;
     case ConsoleLogLevel::Profile: return 3;
     case ConsoleLogLevel::ProfileEnd: return 3;
     case ConsoleLogLevel::Dir: return 3;
     case ConsoleLogLevel::Dirxml: return 3;
@@ -3028,16 +3068,17 @@ Console::InternalLogLevelToInteger(Metho
     case MethodTable: return 3;
     case MethodTrace: return 3;
     case MethodDir: return 3;
     case MethodDirxml: return 3;
     case MethodGroup: return 3;
     case MethodGroupCollapsed: return 3;
     case MethodGroupEnd: return 3;
     case MethodTime: return 3;
+    case MethodTimeLog: return 3;
     case MethodTimeEnd: return 3;
     case MethodTimeStamp: return 3;
     case MethodAssert: return 3;
     case MethodCount: return 3;
     case MethodClear: return 3;
     case MethodProfile: return 3;
     case MethodProfileEnd: return 3;
     default:
--- a/dom/console/Console.h
+++ b/dom/console/Console.h
@@ -92,16 +92,20 @@ public:
 
   static void
   GroupEnd(const GlobalObject& aGlobal);
 
   static void
   Time(const GlobalObject& aGlobal, const nsAString& aLabel);
 
   static void
+  TimeLog(const GlobalObject& aGlobal, const nsAString& aLabel,
+          const Sequence<JS::Value>& aData);
+
+  static void
   TimeEnd(const GlobalObject& aGlobal, const nsAString& aLabel);
 
   static void
   TimeStamp(const GlobalObject& aGlobal, const JS::Handle<JS::Value> aData);
 
   static void
   Profile(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
 
@@ -154,16 +158,17 @@ private:
     MethodTable,
     MethodTrace,
     MethodDir,
     MethodDirxml,
     MethodGroup,
     MethodGroupCollapsed,
     MethodGroupEnd,
     MethodTime,
+    MethodTimeLog,
     MethodTimeEnd,
     MethodTimeStamp,
     MethodAssert,
     MethodCount,
     MethodClear,
     MethodProfile,
     MethodProfileEnd,
   };
@@ -188,20 +193,22 @@ private:
          const nsAString& aString, const Sequence<JS::Value>& aData);
 
   void
   MethodInternal(JSContext* aCx, MethodName aName,
                  const nsAString& aString, const Sequence<JS::Value>& aData);
 
   static void
   StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel,
-               MethodName aMethodName, const nsAString& aMethodString);
+               const Sequence<JS::Value>& aData, MethodName aMethodName,
+               const nsAString& aMethodString);
 
   void
   StringMethodInternal(JSContext* aCx, const nsAString& aLabel,
+                       const Sequence<JS::Value>& aData,
                        MethodName aMethodName, const nsAString& aMethodString);
 
   // This method must receive aCx and aArguments in the same JSCompartment.
   void
   ProcessCallData(JSContext* aCx,
                   ConsoleCallData* aData,
                   const Sequence<JS::Value>& aArguments);
 
@@ -312,43 +319,45 @@ private:
   // thread.
   // * aCx - this is the context that will root the returned value.
   // * aTimerLabel - this label must be what StartTimer received as aTimerLabel.
   // * aTimerStatus - the return value of StartTimer.
   JS::Value
   CreateStartTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
                         TimerStatus aTimerStatus) const;
 
-  // StopTimer follows the same pattern as StartTimer: it runs on the
+  // LogTimer follows the same pattern as StartTimer: it runs on the
   // owning thread and populates aTimerLabel and aTimerDuration, used by
-  // CreateStopTimerValue.
+  // CreateLogOrEndTimerValue.
   // * aCx - the JSContext rooting aName.
   // * aName - this is (should be) the name of the timer as JS::Value.
   // * aTimestamp - the monotonicTimer for this context taken from
   //                performance.now().
   // * aTimerLabel - This label will be populated with the aName converted to a
   //                 string.
   // * aTimerDuration - the difference between aTimestamp and when the timer
   //                    started (see StartTimer).
+  // * aCancelTimer - if true, the timer is removed from the table.
   TimerStatus
-  StopTimer(JSContext* aCx, const JS::Value& aName,
-            DOMHighResTimeStamp aTimestamp,
-            nsAString& aTimerLabel,
-            double* aTimerDuration);
+  LogTimer(JSContext* aCx, const JS::Value& aName,
+           DOMHighResTimeStamp aTimestamp,
+           nsAString& aTimerLabel,
+           double* aTimerDuration,
+           bool aCancelTimer);
 
   // This method generates a ConsoleTimerEnd dictionary exposed as JS::Value, or
-  // a ConsoleTimerError dictionary if aTimerStatus is false. See StopTimer.
+  // a ConsoleTimerError dictionary if aTimerStatus is false. See LogTimer.
   // * aCx - this is the context that will root the returned value.
-  // * aTimerLabel - this label must be what StopTimer received as aTimerLabel.
-  // * aTimerDuration - this is what StopTimer received as aTimerDuration
-  // * aTimerStatus - the return value of StopTimer.
+  // * aTimerLabel - this label must be what LogTimer received as aTimerLabel.
+  // * aTimerDuration - this is what LogTimer received as aTimerDuration
+  // * aTimerStatus - the return value of LogTimer.
   JS::Value
-  CreateStopTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
-                       double aTimerDuration,
-                       TimerStatus aTimerStatus) const;
+  CreateLogOrEndTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
+                           double aTimerDuration,
+                           TimerStatus aTimerStatus) const;
 
   // The method populates a Sequence from an array of JS::Value.
   bool
   ArgumentsToValueList(const Sequence<JS::Value>& aData,
                        Sequence<JS::Value>& aSequence) const;
 
   // This method follows the same pattern as StartTimer: its runs on the owning
   // thread and populate aCountLabel, used by CreateCounterValue. Returns
--- a/dom/console/ConsoleInstance.cpp
+++ b/dom/console/ConsoleInstance.cpp
@@ -142,24 +142,34 @@ ConsoleInstance::GroupEnd(JSContext* aCx
   const Sequence<JS::Value> data;
   mConsole->MethodInternal(aCx, Console::MethodGroupEnd,
                            NS_LITERAL_STRING("groupEnd"), data);
 }
 
 void
 ConsoleInstance::Time(JSContext* aCx, const nsAString& aLabel)
 {
-  mConsole->StringMethodInternal(aCx, aLabel, Console::MethodTime,
+  mConsole->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(),
+                                 Console::MethodTime,
                                  NS_LITERAL_STRING("time"));
 }
 
 void
+ConsoleInstance::TimeLog(JSContext* aCx, const nsAString& aLabel,
+                         const Sequence<JS::Value>& aData)
+{
+  mConsole->StringMethodInternal(aCx, aLabel, aData, Console::MethodTimeLog,
+                                 NS_LITERAL_STRING("timeLog"));
+}
+
+void
 ConsoleInstance::TimeEnd(JSContext* aCx, const nsAString& aLabel)
 {
-  mConsole->StringMethodInternal(aCx, aLabel, Console::MethodTimeEnd,
+  mConsole->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(),
+                                 Console::MethodTimeEnd,
                                  NS_LITERAL_STRING("timeEnd"));
 }
 
 void
 ConsoleInstance::TimeStamp(JSContext* aCx, const JS::Handle<JS::Value> aData)
 {
   ConsoleCommon::ClearException ce(aCx);
 
@@ -196,17 +206,18 @@ ConsoleInstance::Assert(JSContext* aCx, 
     mConsole->MethodInternal(aCx, Console::MethodAssert,
                              NS_LITERAL_STRING("assert"), aData);
   }
 }
 
 void
 ConsoleInstance::Count(JSContext* aCx, const nsAString& aLabel)
 {
-  mConsole->StringMethodInternal(aCx, aLabel, Console::MethodCount,
+  mConsole->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(),
+                                 Console::MethodCount,
                                  NS_LITERAL_STRING("count"));
 }
 
 void
 ConsoleInstance::Clear(JSContext* aCx)
 {
   const Sequence<JS::Value> data;
   mConsole->MethodInternal(aCx, Console::MethodClear,
--- a/dom/console/ConsoleInstance.h
+++ b/dom/console/ConsoleInstance.h
@@ -70,16 +70,20 @@ public:
 
   void
   GroupEnd(JSContext* aCx);
 
   void
   Time(JSContext* aCx, const nsAString& aLabel);
 
   void
+  TimeLog(JSContext* aCx, const nsAString& aLabel,
+          const Sequence<JS::Value>& aData);
+
+  void
   TimeEnd(JSContext* aCx, const nsAString& aLabel);
 
   void
   TimeStamp(JSContext* aCx, const JS::Handle<JS::Value> aData);
 
   void
   Profile(JSContext* aCx, const Sequence<JS::Value>& aData);
 
--- a/dom/console/tests/mochitest.ini
+++ b/dom/console/tests/mochitest.ini
@@ -5,8 +5,9 @@ support-files =
 [test_bug659625.html]
 [test_bug978522.html]
 [test_bug979109.html]
 [test_bug989665.html]
 [test_consoleEmptyStack.html]
 [test_console_binding.html]
 [test_console_proto.html]
 [test_devtools_pref.html]
+[test_timer.html]
new file mode 100644
--- /dev/null
+++ b/dom/console/tests/test_timer.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for timeStart/timeLog/timeEnd in console</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function consoleListener(cb) {
+  return new Promise(resolve => {
+    let observer = {
+      observe: function listener(aSubject, aTopic, aData) {
+        var obj = aSubject.wrappedJSObject;
+        if (obj.arguments[0] == 'test' && cb(obj)) {
+          SpecialPowers.removeObserver(observer, "console-api-log-event");
+          resolve();
+        }
+      }
+    };
+    SpecialPowers.addObserver(observer, "console-api-log-event");
+  });
+}
+
+// Timer creation:
+async function runTest() {
+  var cl = consoleListener(function(obj) {
+    return ("timer" in obj) &&
+           ("name" in obj.timer) &&
+           obj.timer.name == 'test';
+  });
+  console.time('test');
+  await cl;
+  ok(true, "Console.time received!");
+
+  // Timer check:
+  cl = consoleListener(obj => {
+    return ("timer" in obj) &&
+           ("name" in obj.timer) &&
+           obj.timer.name == 'test' &&
+           ("duration" in obj.timer) &&
+           obj.timer.duration > 0 &&
+           obj.arguments[1] == 1 &&
+           obj.arguments[2] == 2 &&
+           obj.arguments[3] == 3 &&
+           obj.arguments[4] == 4;
+  });
+  console.timeLog('test', 1, 2, 3, 4);
+  await cl;
+  ok(true, "Console.timeLog received!");
+
+  // Time deleted:
+  cl = consoleListener(obj => {
+    return ("timer" in obj) &&
+           ("name" in obj.timer) &&
+           obj.timer.name == 'test' &&
+           ("duration" in obj.timer) &&
+           obj.timer.duration > 0;
+  });
+  console.timeEnd('test');
+  await cl;
+  ok(true, "Console.timeEnd received!");
+
+  // Here an error:
+  cl = consoleListener(obj => {
+    return ("timer" in obj) &&
+           ("name" in obj.timer) &&
+           obj.timer.name == 'test' &&
+           ("error" in obj.timer);
+  });
+  console.timeLog('test');
+  await cl;
+  ok(true, "Console.timeLog with error received!");
+}
+
+runTest().then(SimpleTest.finish);
+
+  </script>
+</body>
+</html>
--- a/dom/tests/mochitest/general/test_consoleAPI.html
+++ b/dom/tests/mochitest/general/test_consoleAPI.html
@@ -27,16 +27,17 @@ function doTest() {
     "exception": "function",
     "debug": "function",
     "trace": "function",
     "dir": "function",
     "group": "function",
     "groupCollapsed": "function",
     "groupEnd": "function",
     "time": "function",
+    "timeLog": "function",
     "timeEnd": "function",
     "profile": "function",
     "profileEnd": "function",
     "assert": "function",
     "count": "function",
     "table": "function",
     "clear": "function",
     "dirxml": "function",
--- a/dom/webidl/Console.webidl
+++ b/dom/webidl/Console.webidl
@@ -49,16 +49,18 @@ namespace console {
   void groupCollapsed(any... data);
   [UseCounter]
   void groupEnd();
 
   // Timing
   [UseCounter]
   void time(optional DOMString label = "default");
   [UseCounter]
+  void timeLog(optional DOMString label = "default", any... data);
+  [UseCounter]
   void timeEnd(optional DOMString label = "default");
 
   // Mozilla only or Webcompat methods
 
   [UseCounter]
   void _exception(any... data);
   [UseCounter]
   void timeStamp(optional any data);
@@ -116,17 +118,17 @@ dictionary ConsoleStackEntry {
   DOMString functionName = "";
   DOMString? asyncCause;
 };
 
 dictionary ConsoleTimerStart {
   DOMString name = "";
 };
 
-dictionary ConsoleTimerEnd {
+dictionary ConsoleTimerLogOrEnd {
   DOMString name = "";
   double duration = 0;
 };
 
 dictionary ConsoleTimerError {
   DOMString error = "";
   DOMString name = "";
 };
@@ -160,32 +162,34 @@ interface ConsoleInstance {
 
   // Grouping
   void group(any... data);
   void groupCollapsed(any... data);
   void groupEnd();
 
   // Timing
   void time(optional DOMString label = "default");
+  void timeLog(optional DOMString label = "default", any... data);
   void timeEnd(optional DOMString label = "default");
 
   // Mozilla only or Webcompat methods
 
   void _exception(any... data);
   void timeStamp(optional any data);
 
   void profile(any... data);
   void profileEnd(any... data);
 };
 
 callback ConsoleInstanceDumpCallback = void (DOMString message);
 
 enum ConsoleLogLevel {
-  "All", "Debug", "Log", "Info", "Clear", "Trace", "TimeEnd", "Time", "Group",
-  "GroupEnd", "Profile", "ProfileEnd", "Dir", "Dirxml", "Warn", "Error", "Off"
+  "All", "Debug", "Log", "Info", "Clear", "Trace", "TimeLog", "TimeEnd", "Time",
+  "Group", "GroupEnd", "Profile", "ProfileEnd", "Dir", "Dirxml", "Warn", "Error",
+  "Off"
 };
 
 dictionary ConsoleInstanceOptions {
   // An optional function to intercept all strings written to stdout.
   ConsoleInstanceDumpCallback dump;
 
   // An optional prefix string to be printed before the actual logged message.
   DOMString prefix = "";