Bug 1050500 - Add callee information to Javascript timeline markers. r=smaug
authorTom Tromey <tromey@mozilla.com>
Wed, 20 May 2015 05:28:00 -0400 (2015-05-20)
changeset 245282 23e8df452487eb978654e17ee785039d58a19d9b
parent 245281 2d38fecce226b96c37f5b6da75be2172232080b2
child 245283 76c0e3be4fe8cbfb34251046a2872569f5ea6bd2
push id28800
push userphilringnalda@gmail.com
push dateSat, 23 May 2015 21:28:40 +0000 (2015-05-23)
treeherdermozilla-central@373fecc9d868 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1050500
milestone41.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 1050500 - Add callee information to Javascript timeline markers. r=smaug
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;
           }
         }
@@ -13907,37 +13907,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;