Bug 1050500 - Add callee information to Javascript timeline markers. r=smaug, a=sledru
--- 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;