Bug 1050500 - Add callee information to Javascript timeline markers. r=smaug, a=sledru
authorTom Tromey <tromey@mozilla.com>
Wed, 20 May 2015 05:28:00 -0400
changeset 274861 98b12f19c7b3a1ff214bc881dc19c72fff86caf0
parent 274860 11c00eb7c82aca85b1cafd45d12fcf90b6c16d79
child 274862 de16e4cd139ad060023e30042e05ec588cb34391
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, sledru
bugs1050500
milestone40.0a2
Bug 1050500 - Add callee information to Javascript timeline markers. r=smaug, a=sledru
docshell/base/TimelineMarker.cpp
docshell/base/TimelineMarker.h
docshell/base/nsDocShell.cpp
docshell/base/nsIDocShell.idl
docshell/test/browser/browser_timelineMarkers-frame-04.js
dom/base/Console.cpp
dom/base/ScriptSettings.cpp
dom/base/ScriptSettings.h
dom/events/EventListenerManager.cpp
dom/webidl/ProfileTimelineMarker.webidl
layout/base/RestyleTracker.cpp
--- a/docshell/base/TimelineMarker.cpp
+++ b/docshell/base/TimelineMarker.cpp
@@ -17,25 +17,26 @@ TimelineMarker::TimelineMarker(nsDocShel
   aDocShell->Now(&mTime);
   if (aMetaData == TRACING_INTERVAL_START) {
     CaptureStack();
   }
 }
 
 TimelineMarker::TimelineMarker(nsDocShell* aDocShell, const char* aName,
                                TracingMetadata aMetaData,
-                               const nsAString& aCause)
+                               const nsAString& aCause,
+                               TimelineStackRequest aStackRequest)
   : mName(aName)
   , mMetaData(aMetaData)
   , mCause(aCause)
 {
   MOZ_COUNT_CTOR(TimelineMarker);
   MOZ_ASSERT(aName);
   aDocShell->Now(&mTime);
-  if (aMetaData == TRACING_INTERVAL_START) {
+  if (aMetaData == TRACING_INTERVAL_START && aStackRequest != NO_STACK) {
     CaptureStack();
   }
 }
 
 TimelineMarker::~TimelineMarker()
 {
   MOZ_COUNT_DTOR(TimelineMarker);
 }
--- a/docshell/base/TimelineMarker.h
+++ b/docshell/base/TimelineMarker.h
@@ -16,39 +16,44 @@
 class nsDocShell;
 
 // Objects of this type can be added to the timeline.  The class can
 // also be subclassed to let a given marker creator provide custom
 // details.
 class TimelineMarker
 {
 public:
+  enum TimelineStackRequest { STACK, NO_STACK };
+
   TimelineMarker(nsDocShell* aDocShell, const char* aName,
                  TracingMetadata aMetaData);
 
   TimelineMarker(nsDocShell* aDocShell, const char* aName,
                  TracingMetadata aMetaData,
-                 const nsAString& aCause);
+                 const nsAString& aCause,
+                 TimelineStackRequest aStackRequest = STACK);
 
   virtual ~TimelineMarker();
 
   // Check whether two markers should be considered the same,
   // for the purpose of pairing start and end markers.  Normally
   // this definition suffices.
   virtual bool Equals(const TimelineMarker& aOther)
   {
     return strcmp(mName, aOther.mName) == 0;
   }
 
   // Add details specific to this marker type to aMarker.  The
   // standard elements have already been set.  This method is
   // called on both the starting and ending markers of a pair.
   // Ordinarily the ending marker doesn't need to do anything
   // here.
-  virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) {}
+  virtual void AddDetails(JSContext* aCx,
+                          mozilla::dom::ProfileTimelineMarker& aMarker)
+  {}
 
   virtual void AddLayerRectangles(
       mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&)
   {
     MOZ_ASSERT_UNREACHABLE("can only be called on layer markers");
   }
 
   const char* GetName() const { return mName; }
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -2992,17 +2992,17 @@ nsDocShell::PopProfileTimelineMarkers(
     // marker here.
     if (startPayload->GetMetaData() == TRACING_TIMESTAMP) {
       mozilla::dom::ProfileTimelineMarker* marker =
         profileTimelineMarkers.AppendElement();
 
       marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
       marker->mStart = startPayload->GetTime();
       marker->mEnd = startPayload->GetTime();
-      startPayload->AddDetails(*marker);
+      startPayload->AddDetails(aCx, *marker);
       continue;
     }
 
     if (startPayload->GetMetaData() == TRACING_INTERVAL_START) {
       bool hasSeenEnd = false;
 
       // DOM events can be nested, so we must take care when searching
       // for the matching end.  It doesn't hurt to apply this logic to
@@ -3040,18 +3040,18 @@ nsDocShell::PopProfileTimelineMarkers(
 
               marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
               marker->mStart = startPayload->GetTime();
               marker->mEnd = endPayload->GetTime();
               marker->mStack = startPayload->GetStack();
               if (isPaint) {
                 marker->mRectangles.Construct(layerRectangles);
               }
-              startPayload->AddDetails(*marker);
-              endPayload->AddDetails(*marker);
+              startPayload->AddDetails(aCx, *marker);
+              endPayload->AddDetails(aCx, *marker);
             }
 
             // We want the start to be dropped either way.
             hasSeenEnd = true;
 
             break;
           }
         }
@@ -13913,37 +13913,71 @@ nsDocShell::GetURLSearchParams()
 {
   return mURLSearchParams;
 }
 
 class JavascriptTimelineMarker : public TimelineMarker
 {
 public:
   JavascriptTimelineMarker(nsDocShell* aDocShell, const char* aName,
-                           const char* aReason)
+                           const char* aReason,
+                           const char16_t* aFunctionName,
+                           const char16_t* aFileName,
+                           uint32_t aLineNumber)
     : TimelineMarker(aDocShell, aName, TRACING_INTERVAL_START,
-                     NS_ConvertUTF8toUTF16(aReason))
+                     NS_ConvertUTF8toUTF16(aReason),
+                     NO_STACK)
+    , mFunctionName(aFunctionName)
+    , mFileName(aFileName)
+    , mLineNumber(aLineNumber)
   {
   }
 
-  void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) override
+  void AddDetails(JSContext* aCx, mozilla::dom::ProfileTimelineMarker& aMarker)
+    override
   {
     aMarker.mCauseName.Construct(GetCause());
-  }
+
+    if (!mFunctionName.IsEmpty() || !mFileName.IsEmpty()) {
+      ProfileTimelineStackFrame stackFrame;
+      stackFrame.mLine.Construct(mLineNumber);
+      stackFrame.mSource.Construct(mFileName);
+      stackFrame.mFunctionDisplayName.Construct(mFunctionName);
+
+      JS::Rooted<JS::Value> newStack(aCx);
+      if (ToJSValue(aCx, stackFrame, &newStack)) {
+        if (newStack.isObject()) {
+          aMarker.mStack = &newStack.toObject();
+        }
+      } else {
+        JS_ClearPendingException(aCx);
+      }
+    }
+  }
+
+private:
+  nsString mFunctionName;
+  nsString mFileName;
+  uint32_t mLineNumber;
 };
 
 void
-nsDocShell::NotifyJSRunToCompletionStart(const char* aReason)
+nsDocShell::NotifyJSRunToCompletionStart(const char* aReason,
+                                         const char16_t* aFunctionName,
+                                         const char16_t* aFilename,
+                                         const uint32_t aLineNumber)
 {
   bool timelineOn = nsIDocShell::GetRecordProfileTimelineMarkers();
 
   // If first start, mark interval start.
   if (timelineOn && mJSRunToCompletionDepth == 0) {
     mozilla::UniquePtr<TimelineMarker> marker =
-      MakeUnique<JavascriptTimelineMarker>(this, "Javascript", aReason);
+      MakeUnique<JavascriptTimelineMarker>(this, "Javascript", aReason,
+                                           aFunctionName, aFilename,
+                                           aLineNumber);
     AddProfileTimelineMarker(Move(marker));
   }
   mJSRunToCompletionDepth++;
 }
 
 void
 nsDocShell::NotifyJSRunToCompletionStop()
 {
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -49,17 +49,17 @@ interface nsIWebBrowserPrint;
 interface nsIVariant;
 interface nsIPrivacyTransitionObserver;
 interface nsIReflowObserver;
 interface nsIScrollObserver;
 interface nsITabParent;
  
 typedef unsigned long nsLoadFlags;
 
-[scriptable, builtinclass, uuid(bf78de98-9e88-498d-bc19-0e138f683939)]
+[scriptable, builtinclass, uuid(696b32a1-3cf1-4909-b501-474b25fc7954)]
 interface nsIDocShell : nsIDocShellTreeItem
 {
   /**
    * Loads a given URI.  This will give priority to loading the requested URI
    * in the object implementing	this interface.  If it can't be loaded here
    * however, the URL dispatcher will go through its normal process of content
    * loading.
    *
@@ -1034,17 +1034,20 @@ interface nsIDocShell : nsIDocShellTreeI
   // URLSearchParams for the window.location is owned by the docShell.
   [noscript,notxpcom] URLSearchParams getURLSearchParams();
 
   /**
    * Notify DocShell when the browser is about to start executing JS, and after
    * that execution has stopped.  This only occurs when the Timeline devtool
    * is collecting information.
    */
-  [noscript,notxpcom,nostdcall] void notifyJSRunToCompletionStart(in string aReason);
+  [noscript,notxpcom,nostdcall] void notifyJSRunToCompletionStart(in string aReason,
+                                                                  in wstring functionName,
+                                                                  in wstring fileName,
+                                                                  in unsigned long lineNumber);
   [noscript,notxpcom,nostdcall] void notifyJSRunToCompletionStop();
 
   /**
    * This attribute determines whether a document which is not about:blank has
    * already be loaded by this docShell.
    */
   [infallible] readonly attribute boolean hasLoadedNonBlankURI;
 
--- a/docshell/test/browser/browser_timelineMarkers-frame-04.js
+++ b/docshell/test/browser/browser_timelineMarkers-frame-04.js
@@ -21,12 +21,14 @@ let TESTS = [{
     // first state transition is reported synchronously and so should
     // show up as a nested marker.
     is(domMarkers.length, 5, "Got 5 markers");
 
     // We should see some Javascript markers, and they should have a
     // cause.
     let jsMarkers = markers.filter(m => m.name == "Javascript" && m.causeName);
     ok(jsMarkers.length > 0, "Got some Javascript markers");
+    is(jsMarkers[0].stack.functionDisplayName, "do_xhr",
+       "Javascript marker has entry point name");
   }
 }];
 
 timelineContentTest(TESTS);
--- a/dom/base/Console.cpp
+++ b/dom/base/Console.cpp
@@ -1014,17 +1014,18 @@ public:
   {
     if (!TimelineMarker::Equals(aOther)) {
       return false;
     }
     // Console markers must have matching causes as well.
     return GetCause() == aOther.GetCause();
   }
 
-  virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) override
+  virtual void AddDetails(JSContext* aCx,
+                          mozilla::dom::ProfileTimelineMarker& aMarker) override
   {
     if (GetMetaData() == TRACING_INTERVAL_START) {
       aMarker.mCauseName.Construct(GetCause());
     } else {
       aMarker.mEndStack = GetStack();
     }
   }
 };
@@ -1036,17 +1037,18 @@ public:
                           TracingMetadata aMetaData,
                           const nsAString& aCause)
     : TimelineMarker(aDocShell, "TimeStamp", aMetaData, aCause)
   {
     CaptureStack();
     MOZ_ASSERT(aMetaData == TRACING_TIMESTAMP);
   }
 
-  virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) override
+  virtual void AddDetails(JSContext* aCx,
+                          mozilla::dom::ProfileTimelineMarker& aMarker) override
   {
     if (!GetCause().IsEmpty()) {
       aMarker.mCauseName.Construct(GetCause());
     }
     aMarker.mEndStack = GetStack();
   }
 };
 
--- a/dom/base/ScriptSettings.cpp
+++ b/dom/base/ScriptSettings.cpp
@@ -524,54 +524,112 @@ AutoJSAPI::StealException(JS::MutableHan
 AutoEntryScript::AutoEntryScript(nsIGlobalObject* aGlobalObject,
                                  const char *aReason,
                                  bool aIsMainThread,
                                  JSContext* aCx)
   : AutoJSAPI(aGlobalObject, aIsMainThread,
               aCx ? aCx : FindJSContext(aGlobalObject))
   , ScriptSettingsStackEntry(aGlobalObject, /* aCandidate = */ true)
   , mWebIDLCallerPrincipal(nullptr)
-  , mDocShellForJSRunToCompletion(nullptr)
   , mIsMainThread(aIsMainThread)
 {
   MOZ_ASSERT(aGlobalObject);
   MOZ_ASSERT_IF(!aCx, aIsMainThread); // cx is mandatory off-main-thread.
   MOZ_ASSERT_IF(aCx && aIsMainThread, aCx == FindJSContext(aGlobalObject));
   if (aIsMainThread) {
     nsContentUtils::EnterMicroTask();
   }
 
   if (aIsMainThread && gRunToCompletionListeners > 0) {
-    nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aGlobalObject);
-    if (window) {
-        mDocShellForJSRunToCompletion = window->GetDocShell();
-    }
-  }
-
-  if (mDocShellForJSRunToCompletion) {
-    mDocShellForJSRunToCompletion->NotifyJSRunToCompletionStart(aReason);
+    mDocShellEntryMonitor.emplace(cx(), aReason);
   }
 }
 
 AutoEntryScript::~AutoEntryScript()
 {
-  if (mDocShellForJSRunToCompletion) {
-    mDocShellForJSRunToCompletion->NotifyJSRunToCompletionStop();
-  }
-
   if (mIsMainThread) {
     nsContentUtils::LeaveMicroTask();
   }
 
   // GC when we pop a script entry point. This is a useful heuristic that helps
   // us out on certain (flawed) benchmarks like sunspider, because it lets us
   // avoid GCing during the timing loop.
   JS_MaybeGC(cx());
 }
 
+AutoEntryScript::DocshellEntryMonitor::DocshellEntryMonitor(JSContext* aCx,
+                                                            const char* aReason)
+  : JS::dbg::AutoEntryMonitor(aCx)
+  , mReason(aReason)
+{
+}
+
+void
+AutoEntryScript::DocshellEntryMonitor::Entry(JSContext* aCx, JSFunction* aFunction,
+                                             JSScript* aScript)
+{
+  JS::Rooted<JSFunction*> rootedFunction(aCx);
+  if (aFunction) {
+    rootedFunction = aFunction;
+  }
+  JS::Rooted<JSScript*> rootedScript(aCx);
+  if (aScript) {
+    rootedScript = aScript;
+  }
+
+  nsCOMPtr<nsPIDOMWindow> window =
+    do_QueryInterface(xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx)));
+  if (!window || !window->GetDocShell() ||
+      !window->GetDocShell()->GetRecordProfileTimelineMarkers()) {
+    return;
+  }
+
+  nsCOMPtr<nsIDocShell> docShellForJSRunToCompletion = window->GetDocShell();
+  nsString filename;
+  uint32_t lineNumber = 0;
+
+  js::AutoStableStringChars functionName(aCx);
+  if (rootedFunction) {
+    JS::Rooted<JSString*> displayId(aCx, JS_GetFunctionDisplayId(rootedFunction));
+    if (displayId) {
+      functionName.initTwoByte(aCx, displayId);
+    }
+  }
+
+  if (!rootedScript) {
+    rootedScript = JS_GetFunctionScript(aCx, rootedFunction);
+  }
+  if (rootedScript) {
+    filename = NS_ConvertUTF8toUTF16(JS_GetScriptFilename(rootedScript));
+    lineNumber = JS_GetScriptBaseLineNumber(aCx, rootedScript);
+  }
+
+  if (!filename.IsEmpty() || functionName.isTwoByte()) {
+    const char16_t* functionNameChars = functionName.isTwoByte() ?
+      functionName.twoByteChars() : nullptr;
+
+    docShellForJSRunToCompletion->NotifyJSRunToCompletionStart(mReason,
+                                                               functionNameChars,
+                                                               filename.BeginReading(),
+                                                               lineNumber);
+  }
+}
+
+void
+AutoEntryScript::DocshellEntryMonitor::Exit(JSContext* aCx)
+{
+  nsCOMPtr<nsPIDOMWindow> window =
+    do_QueryInterface(xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx)));
+  // Not really worth checking GetRecordProfileTimelineMarkers here.
+  if (window && window->GetDocShell()) {
+    nsCOMPtr<nsIDocShell> docShellForJSRunToCompletion = window->GetDocShell();
+    docShellForJSRunToCompletion->NotifyJSRunToCompletionStop();
+  }
+}
+
 AutoIncumbentScript::AutoIncumbentScript(nsIGlobalObject* aGlobalObject)
   : ScriptSettingsStackEntry(aGlobalObject, /* aCandidate = */ false)
   , mCallerOverride(nsContentUtils::GetCurrentJSContextForThread())
 {
 }
 
 AutoNoJSAPI::AutoNoJSAPI(bool aIsMainThread)
   : ScriptSettingsStackEntry()
--- a/dom/base/ScriptSettings.h
+++ b/dom/base/ScriptSettings.h
@@ -11,16 +11,17 @@
 
 #include "MainThreadUtils.h"
 #include "nsIGlobalObject.h"
 #include "nsIPrincipal.h"
 
 #include "mozilla/Maybe.h"
 
 #include "jsapi.h"
+#include "js/Debug.h"
 
 class nsPIDOMWindow;
 class nsGlobalWindow;
 class nsIScriptContext;
 class nsIDocument;
 class nsIDocShell;
 
 namespace mozilla {
@@ -334,27 +335,51 @@ public:
 
   ~AutoEntryScript();
 
   void SetWebIDLCallerPrincipal(nsIPrincipal *aPrincipal) {
     mWebIDLCallerPrincipal = aPrincipal;
   }
 
 private:
+  // A subclass of AutoEntryMonitor that notifies the docshell.
+  class DocshellEntryMonitor : public JS::dbg::AutoEntryMonitor
+  {
+  public:
+    DocshellEntryMonitor(JSContext* aCx, const char* aReason);
+
+    void Entry(JSContext* aCx, JSFunction* aFunction) override
+    {
+      Entry(aCx, aFunction, nullptr);
+    }
+
+    void Entry(JSContext* aCx, JSScript* aScript) override
+    {
+      Entry(aCx, nullptr, aScript);
+    }
+
+    void Exit(JSContext* aCx) override;
+
+  private:
+    void Entry(JSContext* aCx, JSFunction* aFunction, JSScript* aScript);
+
+    const char* mReason;
+  };
+
   // It's safe to make this a weak pointer, since it's the subject principal
   // when we go on the stack, so can't go away until after we're gone.  In
   // particular, this is only used from the CallSetup constructor, and only in
   // the aIsJSImplementedWebIDL case.  And in that case, the subject principal
   // is the principal of the callee function that is part of the CallArgs just a
   // bit up the stack, and which will outlive us.  So we know the principal
   // can't go away until then either.
   nsIPrincipal* MOZ_NON_OWNING_REF mWebIDLCallerPrincipal;
   friend nsIPrincipal* GetWebIDLCallerPrincipal();
 
-  nsCOMPtr<nsIDocShell> mDocShellForJSRunToCompletion;
+  Maybe<DocshellEntryMonitor> mDocShellEntryMonitor;
 
   bool mIsMainThread;
 };
 
 /*
  * A class that can be used to force a particular incumbent script on the stack.
  */
 class AutoIncumbentScript : protected ScriptSettingsStackEntry {
--- a/dom/events/EventListenerManager.cpp
+++ b/dom/events/EventListenerManager.cpp
@@ -1032,17 +1032,18 @@ class EventTimelineMarker : public Timel
 public:
   EventTimelineMarker(nsDocShell* aDocShell, TracingMetadata aMetaData,
                       uint16_t aPhase, const nsAString& aCause)
     : TimelineMarker(aDocShell, "DOMEvent", aMetaData, aCause)
     , mPhase(aPhase)
   {
   }
 
-  virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) override
+  virtual void AddDetails(JSContext* aCx,
+                          mozilla::dom::ProfileTimelineMarker& aMarker) override
   {
     if (GetMetaData() == TRACING_INTERVAL_START) {
       aMarker.mType.Construct(GetCause());
       aMarker.mEventPhase.Construct(mPhase);
     }
   }
 
 private:
--- a/dom/webidl/ProfileTimelineMarker.webidl
+++ b/dom/webidl/ProfileTimelineMarker.webidl
@@ -1,14 +1,25 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
+// For Javascript markers, the |stack| of a ProfileTimelineMarker
+// holds an object of this type.  It intentionally looks like a
+// SavedStack object and is a representation of the frame that is
+// about to be constructed at the entry point.
+dictionary ProfileTimelineStackFrame {
+  long line;
+  long column = 0;
+  DOMString source;
+  DOMString functionDisplayName;
+};
+
 dictionary ProfileTimelineLayerRect {
   long x = 0;
   long y = 0;
   long width = 0;
   long height = 0;
 };
 
 dictionary ProfileTimelineMarker {
--- a/layout/base/RestyleTracker.cpp
+++ b/layout/base/RestyleTracker.cpp
@@ -104,17 +104,18 @@ public:
                         nsRestyleHint aRestyleHint)
     : TimelineMarker(aDocShell, "Styles", aMetaData)
   {
     if (aRestyleHint) {
       mRestyleHint.AssignWithConversion(RestyleManager::RestyleHintToString(aRestyleHint));
     }
   }
 
-  virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) override
+  virtual void AddDetails(JSContext* aCx,
+                          mozilla::dom::ProfileTimelineMarker& aMarker) override
   {
     if (GetMetaData() == TRACING_INTERVAL_START) {
       aMarker.mRestyleHint.Construct(mRestyleHint);
     }
   }
 
 private:
   nsAutoString mRestyleHint;