Web Inspector: Debugger should have an option for showing asynchronous call stacks
authormattbaker@apple.com <mattbaker@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 29 Nov 2016 07:08:09 +0000 (07:08 +0000)
committermattbaker@apple.com <mattbaker@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 29 Nov 2016 07:08:09 +0000 (07:08 +0000)
https://bugs.webkit.org/show_bug.cgi?id=163230
<rdar://problem/28698683>

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

* inspector/ScriptCallFrame.cpp:
(Inspector::ScriptCallFrame::isNative):
Encapsulate check for native code source URL.

* inspector/ScriptCallFrame.h:
* inspector/ScriptCallStack.cpp:
(Inspector::ScriptCallStack::firstNonNativeCallFrame):
(Inspector::ScriptCallStack::buildInspectorArray):
* inspector/ScriptCallStack.h:
Replace use of Console::StackTrace with Array<Console::CallFrame>.

* inspector/agents/InspectorDebuggerAgent.cpp:
(Inspector::InspectorDebuggerAgent::disable):
(Inspector::InspectorDebuggerAgent::setAsyncStackTraceDepth):
Set number of async frames to store (including boundary frames).
A value of zero disables recording of async call stacks.

(Inspector::InspectorDebuggerAgent::buildAsyncStackTrace):
Helper function for building a linked list StackTraces.
(Inspector::InspectorDebuggerAgent::didScheduleAsyncCall):
Store a call stack for the script that scheduled the async call.
If the call repeats (e.g. setInterval), the starting reference count is
set to 1. This ensures that dereffing after dispatch won't clear the stack.
If another async call is currently being dispatched, increment the
AsyncCallData reference count for that call.

(Inspector::InspectorDebuggerAgent::didCancelAsyncCall):
Decrement the reference count for the canceled call.

(Inspector::InspectorDebuggerAgent::willDispatchAsyncCall):
Set the identifier for the async callback currently being dispatched,
so that if the debugger pauses during dispatch a stack trace can be
associated with the pause location. If an async call is already being
dispatched, which could be the case when a script schedules an async
call in a nested runloop, do nothing.

(Inspector::InspectorDebuggerAgent::didDispatchAsyncCall):
Decrement the reference count for the canceled call.
(Inspector::InspectorDebuggerAgent::didPause):
If a stored stack trace exists for this location, convert to a protocol
object and send to the frontend.

(Inspector::InspectorDebuggerAgent::didClearGlobalObject):
(Inspector::InspectorDebuggerAgent::clearAsyncStackTraceData):
(Inspector::InspectorDebuggerAgent::refAsyncCallData):
Increment AsyncCallData reference count.
(Inspector::InspectorDebuggerAgent::derefAsyncCallData):
Decrement AsyncCallData reference count. If zero, deref its parent
(if it exists) and remove the AsyncCallData entry.

* inspector/agents/InspectorDebuggerAgent.h:

* inspector/protocol/Console.json:
* inspector/protocol/Network.json:
Replace use of Console.StackTrace with array of Console.CallFrame.

* inspector/protocol/Debugger.json:
New protocol command and event data.

Source/WebCore:

Test: inspector/debugger/async-stack-trace.html

* inspector/InspectorInstrumentation.cpp:
(WebCore::didScheduleAsyncCall):
Helper function used by by instrumentation hooks. Informs the debugger
agent that an asynchronous call was scheduled for the current script
execution state.

(WebCore::InspectorInstrumentation::didInstallTimerImpl):
(WebCore::InspectorInstrumentation::didRemoveTimerImpl):
(WebCore::InspectorInstrumentation::willFireTimerImpl):
(WebCore::InspectorInstrumentation::didFireTimerImpl):
Asynchronous stack trace plumbing for timers (setTimeout, setInterval).
(WebCore::InspectorInstrumentation::didRequestAnimationFrameImpl):
(WebCore::InspectorInstrumentation::didCancelAnimationFrameImpl):
(WebCore::InspectorInstrumentation::willFireAnimationFrameImpl):
(WebCore::InspectorInstrumentation::didFireAnimationFrameImpl):
Asynchronous stack trace plumbing for requestAnimationFrame.

Source/WebInspectorUI:

* Localizations/en.lproj/localizedStrings.js:
New string for generic async call stack boundary label: "(async)".

* UserInterface/Controllers/DebuggerManager.js:
Create async stack depth setting and set default depth.
(WebInspector.DebuggerManager.prototype.get asyncStackTraceDepth):
(WebInspector.DebuggerManager.prototype.set asyncStackTraceDepth):
Make async stack depth setting accessible to the frontend.
(WebInspector.DebuggerManager.prototype.initializeTarget):
Set async stack depth value on the target.
(WebInspector.DebuggerManager.prototype.debuggerDidPause):
Plumbing for the async stack trace payload.

* UserInterface/Models/ConsoleMessage.js:
(WebInspector.ConsoleMessage):
Updated for new StackTrace.fromPayload use.

* UserInterface/Models/DebuggerData.js:
(WebInspector.DebuggerData):
(WebInspector.DebuggerData.prototype.get asyncStackTrace):
(WebInspector.DebuggerData.prototype.updateForPause):
(WebInspector.DebuggerData.prototype.updateForResume):
More plumbing.

* UserInterface/Models/StackTrace.js:
Update frontend model for use as new protocol object Console.StackTrace,
which was previously an alias for a simple array of Console.CallFrames.

(WebInspector.StackTrace):
(WebInspector.StackTrace.fromPayload):
(WebInspector.StackTrace.fromString):
(WebInspector.StackTrace.prototype.get topCallFrameIsBoundary):
(WebInspector.StackTrace.prototype.get parentStackTrace):

* UserInterface/Protocol/DebuggerObserver.js:
(WebInspector.DebuggerObserver.prototype.paused):
More plumbing.

* UserInterface/Views/CallFrameTreeElement.css:
(.tree-outline .item.call-frame.async-boundary):
Use default cursor since boundary element is not selectable.
(.tree-outline .item.call-frame.async-boundary .icon):
(.tree-outline .item.call-frame.async-boundary::before,):
(.tree-outline .item.call-frame.async-boundary::after):
(.tree-outline .item.call-frame.async-boundary::before):
Dimmed text and divider line styles for boundary element.

* UserInterface/Views/CallFrameTreeElement.js:
(WebInspector.CallFrameTreeElement):
Add a flag denoting whether the call frame is an async call trace
boundary, and set styles accordingly.

* UserInterface/Views/DebuggerSidebarPanel.js:
Set async stack trace depth, if supported.
(WebInspector.DebuggerSidebarPanel.prototype._updateSingleThreadCallStacks):
Add call frames for async stack traces to the call stack TreeOutline.
(WebInspector.DebuggerSidebarPanel.prototype._treeSelectionDidChange):
Ensure that async call frames cannot become the active call frame.

* UserInterface/Views/Variables.css:
(:root):
Add --text-color-gray-medium, for dimmed text in async boundary element.

LayoutTests:

Add basic tests for async stack trace data included in Debugger.paused, and
check that requestAnimationFrame, setTimeout, and setInterval are supported.

* inspector/debugger/async-stack-trace-expected.txt: Added.
* inspector/debugger/async-stack-trace.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@209062 268f45cc-cd09-0410-ab3c-d52691b4dbfc

26 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/debugger/async-stack-trace-expected.txt [new file with mode: 0644]
LayoutTests/inspector/debugger/async-stack-trace.html [new file with mode: 0644]
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/ScriptCallFrame.cpp
Source/JavaScriptCore/inspector/ScriptCallFrame.h
Source/JavaScriptCore/inspector/ScriptCallStack.cpp
Source/JavaScriptCore/inspector/ScriptCallStack.h
Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp
Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.h
Source/JavaScriptCore/inspector/protocol/Console.json
Source/JavaScriptCore/inspector/protocol/Debugger.json
Source/JavaScriptCore/inspector/protocol/Network.json
Source/WebCore/ChangeLog
Source/WebCore/inspector/InspectorInstrumentation.cpp
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Controllers/DebuggerManager.js
Source/WebInspectorUI/UserInterface/Models/ConsoleMessage.js
Source/WebInspectorUI/UserInterface/Models/DebuggerData.js
Source/WebInspectorUI/UserInterface/Models/StackTrace.js
Source/WebInspectorUI/UserInterface/Protocol/DebuggerObserver.js
Source/WebInspectorUI/UserInterface/Views/CallFrameTreeElement.css
Source/WebInspectorUI/UserInterface/Views/CallFrameTreeElement.js
Source/WebInspectorUI/UserInterface/Views/DebuggerSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/Variables.css

index bfe8d5f..060b610 100644 (file)
@@ -1,3 +1,17 @@
+2016-11-28  Matt Baker  <mattbaker@apple.com>
+
+        Web Inspector: Debugger should have an option for showing asynchronous call stacks
+        https://bugs.webkit.org/show_bug.cgi?id=163230
+        <rdar://problem/28698683>
+
+        Reviewed by Joseph Pecoraro.
+
+        Add basic tests for async stack trace data included in Debugger.paused, and
+        check that requestAnimationFrame, setTimeout, and setInterval are supported.
+
+        * inspector/debugger/async-stack-trace-expected.txt: Added.
+        * inspector/debugger/async-stack-trace.html: Added.
+
 2016-11-28  Ryan Haddad  <ryanhaddad@apple.com>
 
         Unreviewed, rolling out r209008.
diff --git a/LayoutTests/inspector/debugger/async-stack-trace-expected.txt b/LayoutTests/inspector/debugger/async-stack-trace-expected.txt
new file mode 100644 (file)
index 0000000..7e2749a
--- /dev/null
@@ -0,0 +1,74 @@
+Tests for async stack traces.
+
+
+== Running test suite: AsyncStackTrace
+-- Running test case: CheckAsyncStackTrace.RequestAnimationFrame
+PAUSE #1
+CALL STACK:
+0: [F] pauseThenFinishTest
+-- [N] requestAnimationFrame ----
+1: [F] testRequestAnimationFrame
+2: [P] Global Code
+
+-- Running test case: CheckAsyncStackTrace.SetTimeout
+PAUSE #1
+CALL STACK:
+0: [F] pauseThenFinishTest
+-- [N] setTimeout ----
+1: [F] testSetTimeout
+2: [P] Global Code
+
+-- Running test case: CheckAsyncStackTrace.SetInterval
+PAUSE #1
+CALL STACK:
+0: [F] intervalFired
+-- [N] setInterval ----
+1: [F] testSetInterval
+2: [P] Global Code
+PAUSE #2
+CALL STACK:
+0: [F] intervalFired
+-- [N] setInterval ----
+1: [F] testSetInterval
+2: [P] Global Code
+PAUSE #3
+CALL STACK:
+0: [F] intervalFired
+-- [N] setInterval ----
+1: [F] testSetInterval
+2: [P] Global Code
+
+-- Running test case: CheckAsyncStackTrace.ChainedRequestAnimationFrame
+PAUSE #1
+CALL STACK:
+0: [F] pauseThenFinishTest
+-- [N] requestAnimationFrame ----
+1: [F] testRequestAnimationFrame
+-- [N] requestAnimationFrame ----
+2: [F] testChainedRequestAnimationFrame
+3: [P] Global Code
+
+-- Running test case: CheckAsyncStackTrace.ReferenceCounting
+PAUSE #1
+CALL STACK:
+0: [F] pauseThenFinishTest
+-- [N] setTimeout ----
+1: [F] intervalFired
+-- [N] setInterval ----
+2: [F] testReferenceCounting
+3: [P] Global Code
+-- Running test setup.
+Save DebuggerManager.asyncStackTraceDepth
+
+-- Running test case: AsyncStackTrace.DisableStackTrace
+PASS: Async stack trace should be null.
+-- Running test teardown.
+Restore DebuggerManager.asyncStackTraceDepth
+-- Running test setup.
+Save DebuggerManager.asyncStackTraceDepth
+
+-- Running test case: AsyncStackTrace.SetStackTraceDepth
+PASS: Number of call frames should be equal to the depth setting.
+-- Running test teardown.
+Restore DebuggerManager.asyncStackTraceDepth
+
diff --git a/LayoutTests/inspector/debugger/async-stack-trace.html b/LayoutTests/inspector/debugger/async-stack-trace.html
new file mode 100644 (file)
index 0000000..3edb890
--- /dev/null
@@ -0,0 +1,189 @@
+<!doctype html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+const timerDelay = 20;
+
+function pauseThenFinishTest() {
+    debugger;
+    TestPage.dispatchEventToFrontend("AfterTestFunction");
+}
+
+function testRequestAnimationFrame() {
+    requestAnimationFrame(pauseThenFinishTest);
+}
+
+function testSetTimeout() {
+    setTimeout(pauseThenFinishTest, timerDelay);
+}
+
+function testChainedRequestAnimationFrame() {
+    requestAnimationFrame(testRequestAnimationFrame);
+}
+
+function testSetInterval(repeatCount) {
+    let pauses = 0;
+    let timerIdentifier = setInterval(function intervalFired() {
+        debugger;
+        if (++pauses === repeatCount) {
+            clearInterval(timerIdentifier);
+            TestPage.dispatchEventToFrontend("AfterTestFunction");
+        }
+    }, timerDelay);
+}
+
+function testReferenceCounting() {
+    let interval = setInterval(function intervalFired() {
+        clearInterval(interval);
+        setTimeout(pauseThenFinishTest, timerDelay * 2);
+    }, timerDelay);
+}
+
+function recursiveCallThenTest(testFunction, depth) {
+    if (depth) {
+        recursiveCallThenTest(testFunction, depth - 1);
+        return;
+    }
+    testFunction();
+}
+
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("AsyncStackTrace");
+
+    function activeTargetData() {
+        InspectorTest.assert(WebInspector.debuggerManager.activeCallFrame, "Active call frame should exist.");
+        if (!WebInspector.debuggerManager.activeCallFrame)
+            return null;
+
+        let targetData = WebInspector.debuggerManager.dataForTarget(WebInspector.debuggerManager.activeCallFrame.target);
+        InspectorTest.assert(targetData, "Data for active call frame target should exist.");
+        return targetData;
+    }
+
+    function logCallStack() {
+        function callFrameString(callFrame) {
+            let code = callFrame.nativeCode ? "N" : (callFrame.programCode ? "P" : "F");
+            return `[${code}] ${callFrame.functionName}`;
+        }
+
+        function logCallFrames(callFrames) {
+            for (let callFrame of callFrames) {
+                InspectorTest.log(`${callFrameIndex++}: ${callFrameString(callFrame)}`);
+                // Skip remaining call frames after the test harness entry point.
+                if (callFrame.programCode)
+                    break;
+            }
+        }
+
+        let {callFrames, asyncStackTrace} = activeTargetData();
+        InspectorTest.assert(callFrames);
+        InspectorTest.assert(asyncStackTrace);
+
+        let callFrameIndex = 0;
+        logCallFrames(callFrames);
+
+        while (asyncStackTrace) {
+            let callFrames = asyncStackTrace.callFrames;
+            let topCallFrameIsBoundary = asyncStackTrace.topCallFrameIsBoundary;
+            asyncStackTrace = asyncStackTrace.parentStackTrace;
+            if (!callFrames || !callFrames.length)
+                continue;
+
+            let boundaryLabel = topCallFrameIsBoundary ? callFrameString(callFrames.shift()) : "(async)";
+            InspectorTest.log(`-- ${boundaryLabel} ----`);
+            logCallFrames(callFrames);
+        }
+    }
+
+    function addSimpleTestCase(name, expression) {
+        suite.addTestCase({
+            name: `CheckAsyncStackTrace.${name}`,
+            test(resolve, reject) {
+                let pauseCount = 0;
+                function handlePaused() {
+                    InspectorTest.log(`PAUSE #${++pauseCount}`);
+                    InspectorTest.log("CALL STACK:");
+                    logCallStack();
+                    WebInspector.debuggerManager.resume();
+                }
+
+                WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.Paused, handlePaused);
+
+                InspectorTest.singleFireEventListener("AfterTestFunction", () => {
+                    WebInspector.debuggerManager.removeEventListener(WebInspector.DebuggerManager.Event.Paused, handlePaused);
+                    resolve();
+                });
+
+                InspectorTest.evaluateInPage(expression);
+            }
+        });
+    }
+
+    addSimpleTestCase("RequestAnimationFrame", "testRequestAnimationFrame()");
+    addSimpleTestCase("SetTimeout", "testSetTimeout()");
+    addSimpleTestCase("SetInterval", "testSetInterval(3)");
+    addSimpleTestCase("ChainedRequestAnimationFrame", "testChainedRequestAnimationFrame()");
+    addSimpleTestCase("ReferenceCounting", "testReferenceCounting()");
+
+    function setup(resolve) {
+        InspectorTest.log("Save DebuggerManager.asyncStackTraceDepth");
+        this.savedCallStackDepth = WebInspector.debuggerManager.asyncStackTraceDepth;
+        resolve();
+    }
+
+    function teardown(resolve) {
+        InspectorTest.log("Restore DebuggerManager.asyncStackTraceDepth");
+        WebInspector.debuggerManager.asyncStackTraceDepth = this.savedCallStackDepth;
+        resolve();
+    }
+
+    suite.addTestCase({
+        name: "AsyncStackTrace.DisableStackTrace",
+        setup,
+        teardown,
+        test(resolve, reject) {
+            WebInspector.debuggerManager.awaitEvent(WebInspector.DebuggerManager.Event.Paused)
+            .then((event) => {
+                let stackTrace = activeTargetData().asyncStackTrace;
+                InspectorTest.expectNull(stackTrace, "Async stack trace should be null.");
+                WebInspector.debuggerManager.resume().then(resolve, reject);
+            });
+
+            WebInspector.debuggerManager.asyncStackTraceDepth = 0;
+            InspectorTest.evaluateInPage("testRequestAnimationFrame()");
+        }
+    });
+
+    suite.addTestCase({
+        name: "AsyncStackTrace.SetStackTraceDepth",
+        setup,
+        teardown,
+        test(resolve, reject) {
+            WebInspector.debuggerManager.awaitEvent(WebInspector.DebuggerManager.Event.Paused)
+            .then((event) => {
+                let stackTrace = activeTargetData().asyncStackTrace;
+                InspectorTest.assert(stackTrace && stackTrace.callFrames);
+                if (!stackTrace || !stackTrace.callFrames)
+                    reject();
+
+                InspectorTest.expectEqual(stackTrace.callFrames.length, maxStackDepth, "Number of call frames should be equal to the depth setting.");
+                WebInspector.debuggerManager.resume().then(resolve, reject);
+            });
+
+            const maxStackDepth = 2;
+            const functionCallCount = maxStackDepth * 2;
+            WebInspector.debuggerManager.asyncStackTraceDepth = maxStackDepth;
+            InspectorTest.evaluateInPage(`recursiveCallThenTest(testRequestAnimationFrame, ${functionCallCount})`);
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+<p>Tests for async stack traces.</p>
+</body>
+</html>
index 072ac11..63ed220 100644 (file)
@@ -1,3 +1,70 @@
+2016-11-28  Matt Baker  <mattbaker@apple.com>
+
+        Web Inspector: Debugger should have an option for showing asynchronous call stacks
+        https://bugs.webkit.org/show_bug.cgi?id=163230
+        <rdar://problem/28698683>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/ScriptCallFrame.cpp:
+        (Inspector::ScriptCallFrame::isNative):
+        Encapsulate check for native code source URL.
+
+        * inspector/ScriptCallFrame.h:
+        * inspector/ScriptCallStack.cpp:
+        (Inspector::ScriptCallStack::firstNonNativeCallFrame):
+        (Inspector::ScriptCallStack::buildInspectorArray):
+        * inspector/ScriptCallStack.h:
+        Replace use of Console::StackTrace with Array<Console::CallFrame>.
+
+        * inspector/agents/InspectorDebuggerAgent.cpp:
+        (Inspector::InspectorDebuggerAgent::disable):
+        (Inspector::InspectorDebuggerAgent::setAsyncStackTraceDepth):
+        Set number of async frames to store (including boundary frames).
+        A value of zero disables recording of async call stacks.
+
+        (Inspector::InspectorDebuggerAgent::buildAsyncStackTrace):
+        Helper function for building a linked list StackTraces.
+        (Inspector::InspectorDebuggerAgent::didScheduleAsyncCall):
+        Store a call stack for the script that scheduled the async call.
+        If the call repeats (e.g. setInterval), the starting reference count is
+        set to 1. This ensures that dereffing after dispatch won't clear the stack.
+        If another async call is currently being dispatched, increment the
+        AsyncCallData reference count for that call.
+
+        (Inspector::InspectorDebuggerAgent::didCancelAsyncCall):
+        Decrement the reference count for the canceled call.
+
+        (Inspector::InspectorDebuggerAgent::willDispatchAsyncCall):
+        Set the identifier for the async callback currently being dispatched,
+        so that if the debugger pauses during dispatch a stack trace can be
+        associated with the pause location. If an async call is already being
+        dispatched, which could be the case when a script schedules an async
+        call in a nested runloop, do nothing.
+
+        (Inspector::InspectorDebuggerAgent::didDispatchAsyncCall):
+        Decrement the reference count for the canceled call.
+        (Inspector::InspectorDebuggerAgent::didPause):
+        If a stored stack trace exists for this location, convert to a protocol
+        object and send to the frontend.
+
+        (Inspector::InspectorDebuggerAgent::didClearGlobalObject):
+        (Inspector::InspectorDebuggerAgent::clearAsyncStackTraceData):
+        (Inspector::InspectorDebuggerAgent::refAsyncCallData):
+        Increment AsyncCallData reference count.
+        (Inspector::InspectorDebuggerAgent::derefAsyncCallData):
+        Decrement AsyncCallData reference count. If zero, deref its parent
+        (if it exists) and remove the AsyncCallData entry.
+
+        * inspector/agents/InspectorDebuggerAgent.h:
+
+        * inspector/protocol/Console.json:
+        * inspector/protocol/Network.json:
+        Replace use of Console.StackTrace with array of Console.CallFrame.
+
+        * inspector/protocol/Debugger.json:
+        New protocol command and event data.
+
 2016-11-28  Darin Adler  <darin@apple.com>
 
         Streamline and speed up tokenizer and segmented string classes
index b9e45e9..44627ab 100644 (file)
@@ -59,6 +59,11 @@ bool ScriptCallFrame::isEqual(const ScriptCallFrame& o) const
         && m_column == o.m_column;
 }
 
+bool ScriptCallFrame::isNative() const
+{
+    return m_scriptName == "[native code]";
+}
+
 Ref<Inspector::Protocol::Console::CallFrame> ScriptCallFrame::buildInspectorObject() const
 {
     return Inspector::Protocol::Console::CallFrame::create()
index 616fe43..f80b426 100644 (file)
@@ -50,6 +50,7 @@ public:
     JSC::SourceID sourceID() const { return m_sourceID; }
 
     bool isEqual(const ScriptCallFrame&) const;
+    bool isNative() const;
 
     Ref<Inspector::Protocol::Console::CallFrame> buildInspectorObject() const;
 
index 65ec9de..2f8e0f6 100644 (file)
@@ -77,7 +77,7 @@ const ScriptCallFrame* ScriptCallStack::firstNonNativeCallFrame() const
 
     for (size_t i = 0; i < m_frames.size(); ++i) {
         const ScriptCallFrame& frame = m_frames[i];
-        if (frame.sourceURL() != "[native code]")
+        if (!frame.isNative())
             return &frame;
     }
 
@@ -106,9 +106,9 @@ bool ScriptCallStack::isEqual(ScriptCallStack* o) const
     return true;
 }
 
-Ref<Inspector::Protocol::Console::StackTrace> ScriptCallStack::buildInspectorArray() const
+Ref<Inspector::Protocol::Array<Inspector::Protocol::Console::CallFrame>> ScriptCallStack::buildInspectorArray() const
 {
-    auto frames = Inspector::Protocol::Console::StackTrace::create();
+    auto frames = Inspector::Protocol::Array<Inspector::Protocol::Console::CallFrame>::create();
     for (size_t i = 0; i < m_frames.size(); i++)
         frames->addItem(m_frames.at(i).buildInspectorObject());
     return frames;
index ac6ac80..0f5e332 100644 (file)
@@ -57,7 +57,7 @@ public:
 
     bool isEqual(ScriptCallStack*) const;
 
-    Ref<Inspector::Protocol::Console::StackTrace> buildInspectorArray() const;
+    Ref<Inspector::Protocol::Array<Inspector::Protocol::Console::CallFrame>> buildInspectorArray() const;
 
 private:
     ScriptCallStack();
index 1e19b24..040fe5f 100644 (file)
@@ -37,6 +37,7 @@
 #include "InspectorValues.h"
 #include "JSCInlines.h"
 #include "RegularExpression.h"
+#include "ScriptCallStackFactory.h"
 #include "ScriptDebugServer.h"
 #include "ScriptObject.h"
 #include "ScriptValue.h"
@@ -113,6 +114,8 @@ void InspectorDebuggerAgent::disable(bool isBeingDestroyed)
     if (m_listener)
         m_listener->debuggerWasDisabled();
 
+    clearAsyncStackTraceData();
+
     m_pauseOnAssertionFailures = false;
 
     m_enabled = false;
@@ -133,6 +136,22 @@ bool InspectorDebuggerAgent::breakpointsActive() const
     return m_scriptDebugServer.breakpointsActive();
 }
 
+void InspectorDebuggerAgent::setAsyncStackTraceDepth(ErrorString& errorString, int depth)
+{
+    if (m_asyncStackTraceDepth == depth)
+        return;
+
+    if (depth < 0) {
+        errorString = ASCIILiteral("depth must be a positive number.");
+        return;
+    }
+
+    m_asyncStackTraceDepth = depth;
+
+    if (!m_asyncStackTraceDepth)
+        clearAsyncStackTraceData();
+}
+
 void InspectorDebuggerAgent::setBreakpointsActive(ErrorString&, bool active)
 {
     if (active)
@@ -193,12 +212,104 @@ RefPtr<InspectorObject> InspectorDebuggerAgent::buildExceptionPauseReason(JSC::J
     return injectedScript.wrapObject(exception, InspectorDebuggerAgent::backtraceObjectGroup)->openAccessors();
 }
 
+RefPtr<Inspector::Protocol::Console::StackTrace> InspectorDebuggerAgent::buildAsyncStackTrace(const AsyncCallIdentifier& identifier)
+{
+    RefPtr<Inspector::Protocol::Console::StackTrace> topStackTrace;
+    RefPtr<Inspector::Protocol::Console::StackTrace> previousStackTrace;
+
+    auto iterator = m_asyncCallIdentifierToData.find(identifier);
+    auto end = m_asyncCallIdentifierToData.end();
+    while (iterator != end) {
+        const auto& callData = iterator->value;
+        ASSERT(callData.callStack && callData.callStack->size());
+        if (!callData.callStack || !callData.callStack->size())
+            break;
+
+        RefPtr<Inspector::Protocol::Console::StackTrace> stackTrace = Inspector::Protocol::Console::StackTrace::create()
+            .setCallFrames(callData.callStack->buildInspectorArray())
+            .release();
+
+        if (callData.callStack->at(0).isNative())
+            stackTrace->setTopCallFrameIsBoundary(true);
+        if (!topStackTrace)
+            topStackTrace = stackTrace;
+        if (previousStackTrace)
+            previousStackTrace->setParentStackTrace(stackTrace);
+
+        if (!callData.parentAsyncCallIdentifier)
+            break;
+
+        previousStackTrace = stackTrace;
+        iterator = m_asyncCallIdentifierToData.find(callData.parentAsyncCallIdentifier.value());
+    }
+
+    return topStackTrace;
+}
+
 void InspectorDebuggerAgent::handleConsoleAssert(const String& message)
 {
     if (m_pauseOnAssertionFailures)
         breakProgram(DebuggerFrontendDispatcher::Reason::Assert, buildAssertPauseReason(message));
 }
 
+void InspectorDebuggerAgent::didScheduleAsyncCall(JSC::ExecState* exec, int asyncCallType, int callbackIdentifier, bool singleShot)
+{
+    if (!m_asyncStackTraceDepth)
+        return;
+
+    if (!m_scriptDebugServer.breakpointsActive())
+        return;
+
+    RefPtr<ScriptCallStack> callStack = createScriptCallStack(exec, m_asyncStackTraceDepth);
+    ASSERT(callStack && callStack->size());
+    if (!callStack || !callStack->size())
+        return;
+
+    if (m_currentAsyncCallIdentifier)
+        refAsyncCallData(m_currentAsyncCallIdentifier.value());
+
+    m_asyncCallIdentifierToData.set(std::make_pair(asyncCallType, callbackIdentifier), AsyncCallData(callStack, m_currentAsyncCallIdentifier, singleShot));
+}
+
+void InspectorDebuggerAgent::didCancelAsyncCall(int asyncCallType, int callbackIdentifier)
+{
+    if (!m_asyncStackTraceDepth)
+        return;
+
+    const auto asyncCallIdentifier = std::make_pair(asyncCallType, callbackIdentifier);
+    derefAsyncCallData(asyncCallIdentifier);
+}
+
+void InspectorDebuggerAgent::willDispatchAsyncCall(int asyncCallType, int callbackIdentifier)
+{
+    if (!m_asyncStackTraceDepth)
+        return;
+
+    if (m_currentAsyncCallIdentifier)
+        return;
+
+    // A call can be scheduled before the Inspector is opened, or while async stack
+    // traces are disabled. If no call data exists, do nothing.
+    auto asyncCallIdentifier = std::make_pair(asyncCallType, callbackIdentifier);
+    if (!m_asyncCallIdentifierToData.contains(asyncCallIdentifier))
+        return;
+
+    m_currentAsyncCallIdentifier = WTFMove(asyncCallIdentifier);
+    refAsyncCallData(asyncCallIdentifier);
+}
+
+void InspectorDebuggerAgent::didDispatchAsyncCall()
+{
+    if (!m_asyncStackTraceDepth)
+        return;
+
+    if (!m_currentAsyncCallIdentifier)
+        return;
+
+    derefAsyncCallData(m_currentAsyncCallIdentifier.value());
+    m_currentAsyncCallIdentifier = std::nullopt;
+}
+
 static Ref<InspectorObject> buildObjectForBreakpointCookie(const String& url, int lineNumber, int columnNumber, const String& condition, RefPtr<InspectorArray>& actions, bool isRegex, bool autoContinue, unsigned ignoreCount)
 {
     Ref<InspectorObject> breakpointObject = InspectorObject::create();
@@ -882,7 +993,11 @@ void InspectorDebuggerAgent::didPause(JSC::ExecState& scriptState, JSC::JSValue
     m_conditionToDispatchResumed = ShouldDispatchResumed::No;
     m_enablePauseWhenIdle = false;
 
-    m_frontendDispatcher->paused(currentCallFrames(injectedScript), m_breakReason, m_breakAuxData);
+    RefPtr<Inspector::Protocol::Console::StackTrace> asyncStackTrace;
+    if (m_currentAsyncCallIdentifier)
+        asyncStackTrace = buildAsyncStackTrace(m_currentAsyncCallIdentifier.value());
+
+    m_frontendDispatcher->paused(currentCallFrames(injectedScript), m_breakReason, m_breakAuxData, asyncStackTrace);
 
     m_javaScriptPauseScheduled = false;
 
@@ -985,6 +1100,8 @@ void InspectorDebuggerAgent::didClearGlobalObject()
     // pages have what breakpoints, as the mapping is only sent to DebuggerAgent once.
     clearDebuggerBreakpointState();
 
+    clearAsyncStackTraceData();
+
     m_frontendDispatcher->globalObjectCleared();
 }
 
@@ -1012,4 +1129,37 @@ void InspectorDebuggerAgent::clearExceptionValue()
     }
 }
 
+void InspectorDebuggerAgent::clearAsyncStackTraceData()
+{
+    m_asyncCallIdentifierToData.clear();
+    m_currentAsyncCallIdentifier = std::nullopt;
+}
+
+void InspectorDebuggerAgent::refAsyncCallData(const AsyncCallIdentifier& identifier)
+{
+    auto iterator = m_asyncCallIdentifierToData.find(identifier);
+    ASSERT(iterator != m_asyncCallIdentifierToData.end());
+    if (iterator == m_asyncCallIdentifierToData.end())
+        return;
+
+    iterator->value.referenceCount++;
+}
+
+void InspectorDebuggerAgent::derefAsyncCallData(const AsyncCallIdentifier& identifier)
+{
+    auto iterator = m_asyncCallIdentifierToData.find(identifier);
+    ASSERT(iterator != m_asyncCallIdentifierToData.end());
+    if (iterator == m_asyncCallIdentifierToData.end())
+        return;
+
+    auto& asyncCallData = iterator->value;
+    asyncCallData.referenceCount--;
+    if (asyncCallData.referenceCount)
+        return;
+
+    if (asyncCallData.parentAsyncCallIdentifier)
+        derefAsyncCallData(asyncCallData.parentAsyncCallIdentifier.value());
+    m_asyncCallIdentifierToData.remove(identifier);
+}
+
 } // namespace Inspector
index 5f40106..f14a53f 100644 (file)
@@ -35,6 +35,7 @@
 #include "debugger/Debugger.h"
 #include "inspector/InspectorAgentBase.h"
 #include "inspector/ScriptBreakpoint.h"
+#include "inspector/ScriptCallStack.h"
 #include "inspector/ScriptDebugListener.h"
 #include <wtf/Forward.h>
 #include <wtf/HashMap.h>
@@ -48,6 +49,7 @@ class InjectedScriptManager;
 class InspectorArray;
 class InspectorObject;
 class ScriptDebugServer;
+struct AsyncCallData;
 typedef String ErrorString;
 
 class JS_EXPORT_PRIVATE InspectorDebuggerAgent : public InspectorAgentBase, public ScriptDebugListener, public DebuggerBackendDispatcherHandler {
@@ -63,6 +65,7 @@ public:
 
     void enable(ErrorString&) final;
     void disable(ErrorString&) final;
+    void setAsyncStackTraceDepth(ErrorString&, int depth) final;
     void setBreakpointsActive(ErrorString&, bool active) final;
     void setBreakpointByUrl(ErrorString&, int lineNumber, const String* optionalURL, const String* optionalURLRegex, const int* optionalColumnNumber, const Inspector::InspectorObject* options, Inspector::Protocol::Debugger::BreakpointId*, RefPtr<Inspector::Protocol::Array<Inspector::Protocol::Debugger::Location>>& locations) final;
     void setBreakpoint(ErrorString&, const Inspector::InspectorObject& location, const Inspector::InspectorObject* options, Inspector::Protocol::Debugger::BreakpointId*, RefPtr<Inspector::Protocol::Debugger::Location>& actualLocation) final;
@@ -89,6 +92,11 @@ public:
 
     void handleConsoleAssert(const String& message);
 
+    void didScheduleAsyncCall(JSC::ExecState*, int asyncCallType, int callbackIdentifier, bool singleShot);
+    void didCancelAsyncCall(int asyncCallType, int callbackIdentifier);
+    void willDispatchAsyncCall(int asyncCallType, int callbackIdentifier);
+    void didDispatchAsyncCall();
+
     void schedulePauseOnNextStatement(DebuggerFrontendDispatcher::Reason breakReason, RefPtr<InspectorObject>&& data);
     void cancelPauseOnNextStatement();
     bool pauseOnNextStatementEnabled() const { return m_javaScriptPauseScheduled; }
@@ -142,6 +150,7 @@ private:
     void clearInspectorBreakpointState();
     void clearBreakDetails();
     void clearExceptionValue();
+    void clearAsyncStackTraceData();
 
     enum class ShouldDispatchResumed { No, WhenIdle, WhenContinued };
     void registerIdleHandler();
@@ -153,11 +162,32 @@ private:
 
     bool breakpointActionsFromProtocol(ErrorString&, RefPtr<InspectorArray>& actions, BreakpointActions* result);
 
+    typedef std::pair<int, int> AsyncCallIdentifier;
+
+    RefPtr<Inspector::Protocol::Console::StackTrace> buildAsyncStackTrace(const AsyncCallIdentifier&);
+    void refAsyncCallData(const AsyncCallIdentifier&);
+    void derefAsyncCallData(const AsyncCallIdentifier&);
+
     typedef HashMap<JSC::SourceID, Script> ScriptsMap;
     typedef HashMap<String, Vector<JSC::BreakpointID>> BreakpointIdentifierToDebugServerBreakpointIDsMap;
     typedef HashMap<String, RefPtr<InspectorObject>> BreakpointIdentifierToBreakpointMap;
     typedef HashMap<JSC::BreakpointID, String> DebugServerBreakpointIDToBreakpointIdentifier;
 
+    struct AsyncCallData {
+        AsyncCallData(RefPtr<ScriptCallStack> callStack, std::optional<AsyncCallIdentifier> parentAsyncCallIdentifier, bool singleShot)
+            : callStack(callStack)
+            , parentAsyncCallIdentifier(parentAsyncCallIdentifier)
+            , referenceCount(singleShot ? 0 : 1)
+        {
+        }
+
+        AsyncCallData() = default;
+
+        RefPtr<ScriptCallStack> callStack;
+        std::optional<AsyncCallIdentifier> parentAsyncCallIdentifier { std::nullopt };
+        unsigned referenceCount { 0 };
+    };
+
     InjectedScriptManager& m_injectedScriptManager;
     std::unique_ptr<DebuggerFrontendDispatcher> m_frontendDispatcher;
     RefPtr<DebuggerBackendDispatcher> m_backendDispatcher;
@@ -174,12 +204,15 @@ private:
     RefPtr<InspectorObject> m_breakAuxData;
     ShouldDispatchResumed m_conditionToDispatchResumed { ShouldDispatchResumed::No };
     bool m_enablePauseWhenIdle { false };
+    HashMap<AsyncCallIdentifier, AsyncCallData> m_asyncCallIdentifierToData;
+    std::optional<AsyncCallIdentifier> m_currentAsyncCallIdentifier { std::nullopt };
     bool m_enabled { false };
     bool m_javaScriptPauseScheduled { false };
     bool m_hasExceptionValue { false };
     bool m_didPauseStopwatch { false };
     bool m_pauseOnAssertionFailures { false };
     bool m_registeredIdleCallback { false };
+    int m_asyncStackTraceDepth { 0 };
 };
 
 } // namespace Inspector
index 542bcd9..bacfd80 100644 (file)
@@ -16,7 +16,7 @@
                 { "name": "column", "type": "integer", "optional": true, "description": "Column number on the line in the resource that generated this message." },
                 { "name": "repeatCount", "type": "integer", "optional": true, "description": "Repeat count for repeated messages." },
                 { "name": "parameters", "type": "array", "items": { "$ref": "Runtime.RemoteObject" }, "optional": true, "description": "Message parameters in case of the formatted message." },
-                { "name": "stackTrace", "$ref": "StackTrace", "optional": true, "description": "JavaScript stack trace for assertions and error messages." },
+                { "name": "stackTrace", "type": "array", "items": { "$ref": "CallFrame" }, "optional": true, "description": "JavaScript stack trace for assertions and error messages." },
                 { "name": "networkRequestId", "$ref": "Network.RequestId", "optional": true, "description": "Identifier of the network request associated with this message." }
             ]
         },
         },
         {
             "id": "StackTrace",
-            "type": "array",
-            "items": { "$ref": "CallFrame" },
-            "description": "Call frames for assertions or error messages."
+            "description": "Call frames for async function calls, console assertions, and error messages.",
+            "type": "object",
+            "properties": [
+                { "name": "callFrames", "type": "array", "items": { "$ref": "CallFrame" } },
+                { "name": "topCallFrameIsBoundary", "type": "boolean", "optional": true, "description": "Whether the first item in <code>callFrames</code> is the native function that scheduled the asynchronous operation (e.g. setTimeout)." },
+                { "name": "parentStackTrace", "$ref": "StackTrace", "optional": true, "description": "Parent StackTrace." }
+            ]
         }
     ],
     "commands": [
index be5a605..a6f2ccf 100644 (file)
             "description": "Disables debugger for given page."
         },
         {
+            "name": "setAsyncStackTraceDepth",
+            "description": "Set the async stack trace depth for the page. A value of zero disables recording of async stack traces.",
+            "parameters": [
+                { "name": "depth", "type": "integer", "description": "Async stack trace depth." }
+            ]
+        },
+        {
             "name": "setBreakpointsActive",
             "parameters": [
                 { "name": "active", "type": "boolean", "description": "New value for breakpoints active state." }
             "parameters": [
                 { "name": "callFrames", "type": "array", "items": { "$ref": "CallFrame" }, "description": "Call stack the virtual machine stopped on." },
                 { "name": "reason", "type": "string", "enum": ["XHR", "DOM", "EventListener", "exception", "assert", "CSPViolation", "DebuggerStatement", "Breakpoint", "PauseOnNextStatement", "other"], "description": "Pause reason." },
-                { "name": "data", "type": "object", "optional": true, "description": "Object containing break-specific auxiliary properties." }
+                { "name": "data", "type": "object", "optional": true, "description": "Object containing break-specific auxiliary properties." },
+                { "name": "asyncStackTrace", "$ref": "Console.StackTrace", "optional": true, "description": "Linked list of asynchronous StackTraces." }
             ],
             "description": "Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria."
         },
index 1910b1e..5b159ab 100644 (file)
             "description": "Information about the request initiator.",
             "properties": [
                 { "name": "type", "type": "string", "enum": ["parser", "script", "other"], "description": "Type of this initiator." },
-                { "name": "stackTrace", "$ref": "Console.StackTrace", "optional": true, "description": "Initiator JavaScript stack trace, set for Script only." },
+                { "name": "stackTrace", "type": "array", "items": { "$ref": "Console.CallFrame" }, "optional": true, "description": "Initiator JavaScript stack trace, set for Script only." },
                 { "name": "url", "type": "string", "optional": true, "description": "Initiator URL, set for Parser type only." },
                 { "name": "lineNumber", "type": "number", "optional": true, "description": "Initiator line number, set for Parser type only." }
             ]
index 8e184ac..0fcb246 100644 (file)
@@ -1,3 +1,30 @@
+2016-11-28  Matt Baker  <mattbaker@apple.com>
+
+        Web Inspector: Debugger should have an option for showing asynchronous call stacks
+        https://bugs.webkit.org/show_bug.cgi?id=163230
+        <rdar://problem/28698683>
+
+        Reviewed by Joseph Pecoraro.
+
+        Test: inspector/debugger/async-stack-trace.html
+
+        * inspector/InspectorInstrumentation.cpp:
+        (WebCore::didScheduleAsyncCall):
+        Helper function used by by instrumentation hooks. Informs the debugger
+        agent that an asynchronous call was scheduled for the current script
+        execution state.
+
+        (WebCore::InspectorInstrumentation::didInstallTimerImpl):
+        (WebCore::InspectorInstrumentation::didRemoveTimerImpl):
+        (WebCore::InspectorInstrumentation::willFireTimerImpl):
+        (WebCore::InspectorInstrumentation::didFireTimerImpl):
+        Asynchronous stack trace plumbing for timers (setTimeout, setInterval).
+        (WebCore::InspectorInstrumentation::didRequestAnimationFrameImpl):
+        (WebCore::InspectorInstrumentation::didCancelAnimationFrameImpl):
+        (WebCore::InspectorInstrumentation::willFireAnimationFrameImpl):
+        (WebCore::InspectorInstrumentation::didFireAnimationFrameImpl):
+        Asynchronous stack trace plumbing for requestAnimationFrame.
+
 2016-11-28  Jiewen Tan  <jiewen_tan@apple.com>
 
         Unreviewed, followup patch after r209059.
index f03e6a0..ad5494f 100644 (file)
@@ -83,12 +83,28 @@ static const char* const setTimerEventName = "setTimer";
 static const char* const clearTimerEventName = "clearTimer";
 static const char* const timerFiredEventName = "timerFired";
 
+enum AsyncCallType {
+    AsyncCallTypeRequestAnimationFrame,
+    AsyncCallTypeTimer,
+};
+
 namespace {
 static HashSet<InstrumentingAgents*>* s_instrumentingAgentsSet = nullptr;
 }
 
 int InspectorInstrumentation::s_frontendCounter = 0;
 
+static void didScheduleAsyncCall(InstrumentingAgents& instrumentingAgents, AsyncCallType type, int callbackId, ScriptExecutionContext& context, bool singleShot)
+{
+    if (InspectorDebuggerAgent* debuggerAgent = instrumentingAgents.inspectorDebuggerAgent()) {
+        JSC::ExecState* scriptState = context.execState();
+        if (!scriptState)
+            return;
+
+        debuggerAgent->didScheduleAsyncCall(scriptState, type, callbackId, singleShot);
+    }
+}
+
 static Frame* frameForScriptExecutionContext(ScriptExecutionContext* context)
 {
     Frame* frame = nullptr;
@@ -323,6 +339,8 @@ void InspectorInstrumentation::willSendXMLHttpRequestImpl(InstrumentingAgents& i
 void InspectorInstrumentation::didInstallTimerImpl(InstrumentingAgents& instrumentingAgents, int timerId, std::chrono::milliseconds timeout, bool singleShot, ScriptExecutionContext& context)
 {
     pauseOnNativeEventIfNeeded(instrumentingAgents, false, setTimerEventName, true);
+    didScheduleAsyncCall(instrumentingAgents, AsyncCallTypeTimer, timerId, context, singleShot);
+
     if (InspectorTimelineAgent* timelineAgent = instrumentingAgents.inspectorTimelineAgent())
         timelineAgent->didInstallTimer(timerId, timeout, singleShot, frameForScriptExecutionContext(context));
 }
@@ -330,6 +348,9 @@ void InspectorInstrumentation::didInstallTimerImpl(InstrumentingAgents& instrume
 void InspectorInstrumentation::didRemoveTimerImpl(InstrumentingAgents& instrumentingAgents, int timerId, ScriptExecutionContext& context)
 {
     pauseOnNativeEventIfNeeded(instrumentingAgents, false, clearTimerEventName, true);
+
+    if (InspectorDebuggerAgent* debuggerAgent = instrumentingAgents.inspectorDebuggerAgent())
+        debuggerAgent->didCancelAsyncCall(AsyncCallTypeTimer, timerId);
     if (InspectorTimelineAgent* timelineAgent = instrumentingAgents.inspectorTimelineAgent())
         timelineAgent->didRemoveTimer(timerId, frameForScriptExecutionContext(context));
 }
@@ -423,6 +444,9 @@ InspectorInstrumentationCookie InspectorInstrumentation::willFireTimerImpl(Instr
 {
     pauseOnNativeEventIfNeeded(instrumentingAgents, false, timerFiredEventName, false);
 
+    if (InspectorDebuggerAgent* debuggerAgent = instrumentingAgents.inspectorDebuggerAgent())
+        debuggerAgent->willDispatchAsyncCall(AsyncCallTypeTimer, timerId);
+
     int timelineAgentId = 0;
     if (InspectorTimelineAgent* timelineAgent = instrumentingAgents.inspectorTimelineAgent()) {
         timelineAgent->willFireTimer(timerId, frameForScriptExecutionContext(context));
@@ -433,6 +457,8 @@ InspectorInstrumentationCookie InspectorInstrumentation::willFireTimerImpl(Instr
 
 void InspectorInstrumentation::didFireTimerImpl(const InspectorInstrumentationCookie& cookie)
 {
+    if (InspectorDebuggerAgent* debuggerAgent = cookie.instrumentingAgents()->inspectorDebuggerAgent())
+        debuggerAgent->didDispatchAsyncCall();
     if (InspectorTimelineAgent* timelineAgent = retrieveTimelineAgent(cookie))
         timelineAgent->didFireTimer();
 }
@@ -1139,6 +1165,7 @@ void InspectorInstrumentation::pauseOnNativeEventIfNeeded(InstrumentingAgents& i
 void InspectorInstrumentation::didRequestAnimationFrameImpl(InstrumentingAgents& instrumentingAgents, int callbackId, Frame* frame)
 {
     pauseOnNativeEventIfNeeded(instrumentingAgents, false, requestAnimationFrameEventName, true);
+    didScheduleAsyncCall(instrumentingAgents, AsyncCallTypeRequestAnimationFrame, callbackId, *frame->document(), true);
 
     if (InspectorTimelineAgent* timelineAgent = instrumentingAgents.inspectorTimelineAgent())
         timelineAgent->didRequestAnimationFrame(callbackId, frame);
@@ -1148,6 +1175,8 @@ void InspectorInstrumentation::didCancelAnimationFrameImpl(InstrumentingAgents&
 {
     pauseOnNativeEventIfNeeded(instrumentingAgents, false, cancelAnimationFrameEventName, true);
 
+    if (InspectorDebuggerAgent* debuggerAgent = instrumentingAgents.inspectorDebuggerAgent())
+        debuggerAgent->didCancelAsyncCall(AsyncCallTypeRequestAnimationFrame, callbackId);
     if (InspectorTimelineAgent* timelineAgent = instrumentingAgents.inspectorTimelineAgent())
         timelineAgent->didCancelAnimationFrame(callbackId, frame);
 }
@@ -1156,6 +1185,9 @@ InspectorInstrumentationCookie InspectorInstrumentation::willFireAnimationFrameI
 {
     pauseOnNativeEventIfNeeded(instrumentingAgents, false, animationFrameFiredEventName, false);
 
+    if (InspectorDebuggerAgent* debuggerAgent = instrumentingAgents.inspectorDebuggerAgent())
+        debuggerAgent->willDispatchAsyncCall(AsyncCallTypeRequestAnimationFrame, callbackId);
+
     int timelineAgentId = 0;
     if (InspectorTimelineAgent* timelineAgent = instrumentingAgents.inspectorTimelineAgent()) {
         timelineAgent->willFireAnimationFrame(callbackId, frame);
@@ -1166,6 +1198,8 @@ InspectorInstrumentationCookie InspectorInstrumentation::willFireAnimationFrameI
 
 void InspectorInstrumentation::didFireAnimationFrameImpl(const InspectorInstrumentationCookie& cookie)
 {
+    if (InspectorDebuggerAgent* debuggerAgent = cookie.instrumentingAgents()->inspectorDebuggerAgent())
+        debuggerAgent->didDispatchAsyncCall();
     if (InspectorTimelineAgent* timelineAgent = retrieveTimelineAgent(cookie))
         timelineAgent->didFireAnimationFrame();
 }
index 6cd0c6c..7a49200 100644 (file)
@@ -1,3 +1,74 @@
+2016-11-28  Matt Baker  <mattbaker@apple.com>
+
+        Web Inspector: Debugger should have an option for showing asynchronous call stacks
+        https://bugs.webkit.org/show_bug.cgi?id=163230
+        <rdar://problem/28698683>
+
+        Reviewed by Joseph Pecoraro.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        New string for generic async call stack boundary label: "(async)".
+
+        * UserInterface/Controllers/DebuggerManager.js:
+        Create async stack depth setting and set default depth.
+        (WebInspector.DebuggerManager.prototype.get asyncStackTraceDepth):
+        (WebInspector.DebuggerManager.prototype.set asyncStackTraceDepth):
+        Make async stack depth setting accessible to the frontend.
+        (WebInspector.DebuggerManager.prototype.initializeTarget):
+        Set async stack depth value on the target.
+        (WebInspector.DebuggerManager.prototype.debuggerDidPause):
+        Plumbing for the async stack trace payload.
+
+        * UserInterface/Models/ConsoleMessage.js:
+        (WebInspector.ConsoleMessage):
+        Updated for new StackTrace.fromPayload use.
+
+        * UserInterface/Models/DebuggerData.js:
+        (WebInspector.DebuggerData):
+        (WebInspector.DebuggerData.prototype.get asyncStackTrace):
+        (WebInspector.DebuggerData.prototype.updateForPause):
+        (WebInspector.DebuggerData.prototype.updateForResume):
+        More plumbing.
+
+        * UserInterface/Models/StackTrace.js:
+        Update frontend model for use as new protocol object Console.StackTrace,
+        which was previously an alias for a simple array of Console.CallFrames.
+
+        (WebInspector.StackTrace):
+        (WebInspector.StackTrace.fromPayload):
+        (WebInspector.StackTrace.fromString):
+        (WebInspector.StackTrace.prototype.get topCallFrameIsBoundary):
+        (WebInspector.StackTrace.prototype.get parentStackTrace):
+
+        * UserInterface/Protocol/DebuggerObserver.js:
+        (WebInspector.DebuggerObserver.prototype.paused):
+        More plumbing.
+
+        * UserInterface/Views/CallFrameTreeElement.css:
+        (.tree-outline .item.call-frame.async-boundary):
+        Use default cursor since boundary element is not selectable.
+        (.tree-outline .item.call-frame.async-boundary .icon):
+        (.tree-outline .item.call-frame.async-boundary::before,):
+        (.tree-outline .item.call-frame.async-boundary::after):
+        (.tree-outline .item.call-frame.async-boundary::before):
+        Dimmed text and divider line styles for boundary element.
+
+        * UserInterface/Views/CallFrameTreeElement.js:
+        (WebInspector.CallFrameTreeElement):
+        Add a flag denoting whether the call frame is an async call trace
+        boundary, and set styles accordingly.
+
+        * UserInterface/Views/DebuggerSidebarPanel.js:
+        Set async stack trace depth, if supported.
+        (WebInspector.DebuggerSidebarPanel.prototype._updateSingleThreadCallStacks):
+        Add call frames for async stack traces to the call stack TreeOutline.
+        (WebInspector.DebuggerSidebarPanel.prototype._treeSelectionDidChange):
+        Ensure that async call frames cannot become the active call frame.
+
+        * UserInterface/Views/Variables.css:
+        (:root):
+        Add --text-color-gray-medium, for dimmed text in async boundary element.
+
 2016-11-18  Matt Baker  <mattbaker@apple.com>
 
         Web Inspector: TimelineDataGridNode assertions when refreshing page
index 57433ce..b78a9f6 100644 (file)
@@ -42,6 +42,7 @@ localizedStrings["%s interval"] = "%s interval";
 localizedStrings["(Index)"] = "(Index)";
 localizedStrings["(Tail Call)"] = "(Tail Call)";
 localizedStrings["(anonymous function)"] = "(anonymous function)";
+localizedStrings["(async)"] = "(async)";
 localizedStrings["(many)"] = "(many)";
 localizedStrings["(modify the boxes below to add a value)"] = "(modify the boxes below to add a value)";
 localizedStrings["(multiple)"] = "(multiple)";
index 8021dd1..32134f8 100644 (file)
@@ -52,6 +52,7 @@ WebInspector.DebuggerManager = class DebuggerManager extends WebInspector.Object
         this._allExceptionsBreakpointEnabledSetting = new WebInspector.Setting("break-on-all-exceptions", false);
         this._allUncaughtExceptionsBreakpointEnabledSetting = new WebInspector.Setting("break-on-all-uncaught-exceptions", false);
         this._assertionsBreakpointEnabledSetting = new WebInspector.Setting("break-on-assertions", false);
+        this._asyncStackTraceDepthSetting = new WebInspector.Setting("async-stack-trace-depth", 200);
 
         let specialBreakpointLocation = new WebInspector.SourceCodeLocation(null, Infinity, Infinity);
 
@@ -94,6 +95,10 @@ WebInspector.DebuggerManager = class DebuggerManager extends WebInspector.Object
         if (DebuggerAgent.setPauseOnAssertions)
             DebuggerAgent.setPauseOnAssertions(this._assertionsBreakpointEnabledSetting.value);
 
+        // COMPATIBILITY (iOS 10): Debugger.setAsyncStackTraceDepth did not exist yet.
+        if (DebuggerAgent.setAsyncStackTraceDepth)
+            DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value);
+
         this._ignoreBreakpointDisplayLocationDidChangeEvent = false;
 
         function restoreBreakpointsSoon() {
@@ -277,6 +282,22 @@ WebInspector.DebuggerManager = class DebuggerManager extends WebInspector.Object
         return knownScripts;
     }
 
+    get asyncStackTraceDepth()
+    {
+        return this._asyncStackTraceDepthSetting.value;
+    }
+
+    set asyncStackTraceDepth(x)
+    {
+        if (this._asyncStackTraceDepthSetting.value === x)
+            return;
+
+        this._asyncStackTraceDepthSetting.value = x;
+
+        for (let target of WebInspector.targets)
+            target.DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value);
+    }
+
     pause()
     {
         if (this.paused)
@@ -484,6 +505,7 @@ WebInspector.DebuggerManager = class DebuggerManager extends WebInspector.Object
         DebuggerAgent.setBreakpointsActive(this._breakpointsEnabledSetting.value);
         DebuggerAgent.setPauseOnAssertions(this._assertionsBreakpointEnabledSetting.value);
         DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState);
+        DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value);
 
         if (this.paused)
             targetData.pauseIfNeeded();
@@ -550,7 +572,7 @@ WebInspector.DebuggerManager = class DebuggerManager extends WebInspector.Object
             this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.Resumed);
     }
 
-    debuggerDidPause(target, callFramesPayload, reason, data)
+    debuggerDidPause(target, callFramesPayload, reason, data, asyncStackTracePayload)
     {
         // Called from WebInspector.DebuggerObserver.
 
@@ -599,7 +621,8 @@ WebInspector.DebuggerManager = class DebuggerManager extends WebInspector.Object
             return;
         }
 
-        targetData.updateForPause(callFrames, pauseReason, pauseData);
+        let asyncStackTrace = WebInspector.StackTrace.fromPayload(target, asyncStackTracePayload);
+        targetData.updateForPause(callFrames, pauseReason, pauseData, asyncStackTrace);
 
         // Pause other targets because at least one target has paused.
         // FIXME: Should this be done on the backend?
index bd0d6c2..b8154ae 100644 (file)
@@ -25,7 +25,7 @@
 
 WebInspector.ConsoleMessage = class ConsoleMessage extends WebInspector.Object
 {
-    constructor(target, source, level, message, type, url, line, column, repeatCount, parameters, stackTrace, request)
+    constructor(target, source, level, message, type, url, line, column, repeatCount, parameters, callFrames, request)
     {
         super();
 
@@ -49,7 +49,8 @@ WebInspector.ConsoleMessage = class ConsoleMessage extends WebInspector.Object
         this._repeatCount = repeatCount || 0;
         this._parameters = parameters;
 
-        this._stackTrace = WebInspector.StackTrace.fromPayload(this._target, stackTrace || []);
+        callFrames = callFrames || [];
+        this._stackTrace = WebInspector.StackTrace.fromPayload(this._target, {callFrames});
 
         this._request = request;
     }
index 07d0494..394b8ed 100644 (file)
@@ -38,6 +38,7 @@ WebInspector.DebuggerData = class DebuggerData extends WebInspector.Object
         this._pauseReason = null;
         this._pauseData = null;
         this._callFrames = [];
+        this._asyncStackTrace = null;
 
         this._scriptIdMap = new Map;
         this._scriptContentIdentifierMap = new Map;
@@ -53,6 +54,7 @@ WebInspector.DebuggerData = class DebuggerData extends WebInspector.Object
     get pauseReason() { return this._pauseReason; }
     get pauseData() { return this._pauseData; }
     get callFrames() { return this._callFrames; }
+    get asyncStackTrace() { return this._asyncStackTrace; }
 
     get scripts()
     {
@@ -122,13 +124,14 @@ WebInspector.DebuggerData = class DebuggerData extends WebInspector.Object
         return this._target.DebuggerAgent.continueUntilNextRunLoop();
     }
 
-    updateForPause(callFrames, pauseReason, pauseData)
+    updateForPause(callFrames, pauseReason, pauseData, asyncStackTrace)
     {
         this._paused = true;
         this._pausing = false;
         this._pauseReason = pauseReason;
         this._pauseData = pauseData;
         this._callFrames = callFrames;
+        this._asyncStackTrace = asyncStackTrace;
 
         // We paused, no need for auto-pausing.
         this._makePausingAfterNextResume = false;
@@ -141,6 +144,7 @@ WebInspector.DebuggerData = class DebuggerData extends WebInspector.Object
         this._pauseReason = null;
         this._pauseData = null;
         this._callFrames = [];
+        this._asyncStackTrace = null;
 
         // We resumed, but may be auto-pausing.
         if (this._makePausingAfterNextResume) {
index e38134b..486f485 100644 (file)
 
 WebInspector.StackTrace = class StackTrace extends WebInspector.Object
 {
-    constructor(callFrames)
+    constructor(callFrames, topCallFrameIsBoundary)
     {
         super();
 
         console.assert(callFrames && callFrames.every((callFrame) => callFrame instanceof WebInspector.CallFrame));
 
         this._callFrames = callFrames;
+        this._topCallFrameIsBoundary = topCallFrameIsBoundary || false;
+        this._parentStackTrace = null;
     }
 
     // Static
 
     static fromPayload(target, payload)
     {
-        let callFrames = payload.map((x) => WebInspector.CallFrame.fromPayload(target, x));
-        return new WebInspector.StackTrace(callFrames);
+        let result = null;
+        let previousStackTrace = null;
+
+        while (payload) {
+            let callFrames = payload.callFrames.map((x) => WebInspector.CallFrame.fromPayload(target, x));
+            let stackTrace = new WebInspector.StackTrace(callFrames, payload.topCallFrameIsBoundary);
+            if (!result)
+                result = stackTrace;
+            if (previousStackTrace)
+                previousStackTrace._parentStackTrace = stackTrace;
+
+            previousStackTrace = stackTrace;
+            payload = payload.parentStackTrace;
+        }
+
+        return result;
     }
 
     static fromString(target, stack)
     {
-        let payload = WebInspector.StackTrace._parseStackTrace(stack);
-        return WebInspector.StackTrace.fromPayload(target, payload);
+        let callFrames = WebInspector.StackTrace._parseStackTrace(stack);
+        return WebInspector.StackTrace.fromPayload(target, {callFrames});
     }
 
     // May produce false negatives; must not produce any false positives.
@@ -151,4 +167,7 @@ WebInspector.StackTrace = class StackTrace extends WebInspector.Object
 
         return null;
     }
+
+    get topCallFrameIsBoundary() { return this._topCallFrameIsBoundary; }
+    get parentStackTrace() { return this._parentStackTrace; }
 };
index 092d77b..0d093de 100644 (file)
@@ -63,9 +63,9 @@ WebInspector.DebuggerObserver = class DebuggerObserver
         WebInspector.debuggerManager.breakpointResolved(this.target, breakpointId, location);
     }
 
-    paused(callFrames, reason, data)
+    paused(callFrames, reason, data, asyncStackTrace)
     {
-        WebInspector.debuggerManager.debuggerDidPause(this.target, callFrames, reason, data);
+        WebInspector.debuggerManager.debuggerDidPause(this.target, callFrames, reason, data, asyncStackTrace);
     }
 
     resumed()
index d0a59a2..00b15e7 100644 (file)
 .tree-outline:matches(:focus, .force-focus) .item.call-frame.selected .status > .status-image {
     fill: var(--selected-foreground-color);
 }
+
+.tree-outline .item.call-frame.async-boundary {
+    cursor: default;
+    color: var(--text-color-gray-medium);
+    padding-left: 0;
+}
+
+.tree-outline .item.call-frame.async-boundary .icon {
+    float: none;
+    display: inline-block;
+    margin-left: 0 !important;
+}
+
+.tree-outline .item.call-frame.async-boundary::before,
+.tree-outline .item.call-frame.async-boundary::after {
+    content: "";
+    display: inline-block;
+    height: 0;
+    margin-top: 2px;
+    vertical-align: middle;
+    border-bottom: solid 0.5px var(--border-color);
+}
+
+.tree-outline .item.call-frame.async-boundary::after {
+    width: 100%;
+    margin-left: 2px;
+}
+
+.tree-outline .item.call-frame.async-boundary::before {
+    width: 20px;
+    margin-right: 2px;
+}
index 4b6c481..50ed877 100644 (file)
@@ -25,7 +25,7 @@
 
 WebInspector.CallFrameTreeElement = class CallFrameTreeElement extends WebInspector.GeneralTreeElement
 {
-    constructor(callFrame)
+    constructor(callFrame, isAsyncBoundaryCallFrame)
     {
         console.assert(callFrame instanceof WebInspector.CallFrame);
 
@@ -34,18 +34,26 @@ WebInspector.CallFrameTreeElement = class CallFrameTreeElement extends WebInspec
 
         super(["call-frame", className], title, null, callFrame, false);
 
-        if (!callFrame.nativeCode && callFrame.sourceCodeLocation) {
-            let displayScriptURL = callFrame.sourceCodeLocation.displaySourceCode.url;
-            if (displayScriptURL) {
-                this.subtitle = document.createElement("span");
-                callFrame.sourceCodeLocation.populateLiveDisplayLocationString(this.subtitle, "textContent");
-                // Set the tooltip on the entire tree element in onattach, once the element is created.
-                this.tooltipHandledSeparately = true;
-            }
-        }
-
         this._callFrame = callFrame;
         this._isActiveCallFrame = false;
+
+         if (isAsyncBoundaryCallFrame) {
+            this.addClassName("async-boundary");
+            this.selectable = false;
+         }
+
+        if (this._callFrame.nativeCode || !this._callFrame.sourceCodeLocation) {
+            this.subtitle = "";
+            return;
+        }
+
+        let displayScriptURL = this._callFrame.sourceCodeLocation.displaySourceCode.url;
+        if (displayScriptURL) {
+            this.subtitle = document.createElement("span");
+            this._callFrame.sourceCodeLocation.populateLiveDisplayLocationString(this.subtitle, "textContent");
+            // Set the tooltip on the entire tree element in onattach, once the element is created.
+            this.tooltipHandledSeparately = true;
+        }
     }
 
     // Public
index 47479ad..71d9f4d 100644 (file)
@@ -628,6 +628,36 @@ WebInspector.DebuggerSidebarPanel = class DebuggerSidebarPanel extends WebInspec
             if (this._showingSingleThreadCallStack)
                 activeCallFrameTreeElement.select(true, true);
         }
+
+        if (!targetData.asyncStackTrace)
+            return;
+
+        let currentStackTrace = targetData.asyncStackTrace;
+        while (currentStackTrace) {
+            console.assert(currentStackTrace.callFrames.length, "StackTrace should have non-empty call frames array.");
+            if (!currentStackTrace.callFrames.length)
+                break;
+
+            let boundaryCallFrame;
+            if (currentStackTrace.topCallFrameIsBoundary) {
+                boundaryCallFrame = currentStackTrace.callFrames[0];
+                console.assert(boundaryCallFrame.nativeCode && !boundaryCallFrame.sourceCodeLocation);
+            } else {
+                // Create a generic native CallFrame for the asynchronous boundary.
+                const functionName = WebInspector.UIString("(async)");
+                const nativeCode = true;
+                boundaryCallFrame = new WebInspector.CallFrame(null, null, null, functionName, null, null, nativeCode);
+            }
+
+            const isAsyncBoundaryCallFrame = true;
+            this._singleThreadCallStackTreeOutline.appendChild(new WebInspector.CallFrameTreeElement(boundaryCallFrame, isAsyncBoundaryCallFrame));
+
+            let startIndex = currentStackTrace.topCallFrameIsBoundary ? 1 : 0;
+            for (let i = startIndex; i < currentStackTrace.callFrames.length; ++i)
+                this._singleThreadCallStackTreeOutline.appendChild(new WebInspector.CallFrameTreeElement(currentStackTrace.callFrames[i]));
+
+            currentStackTrace = currentStackTrace.parentStackTrace;
+        }
     }
 
     _selectActiveCallFrameTreeElement(treeOutline)
@@ -859,8 +889,11 @@ WebInspector.DebuggerSidebarPanel = class DebuggerSidebarPanel extends WebInspec
 
         if (treeElement instanceof WebInspector.CallFrameTreeElement) {
             let callFrame = treeElement.callFrame;
-            WebInspector.debuggerManager.activeCallFrame = callFrame;
-            WebInspector.showSourceCodeLocation(callFrame.sourceCodeLocation);
+            if (callFrame.id)
+                WebInspector.debuggerManager.activeCallFrame = callFrame;
+
+            if (callFrame.sourceCodeLocation)
+                WebInspector.showSourceCodeLocation(callFrame.sourceCodeLocation);
             return;
         }
 
index fe50618..19d9294 100644 (file)
@@ -53,6 +53,7 @@
     --console-secondary-text-color: hsla(0, 0%, 0%, 0.33);
     --console-prompt-min-height: 30px;
 
+    --text-color-gray-medium: hsl(0, 0%, 50%);
     --error-text-color: hsl(0, 86%, 47%);
 
     --syntax-highlight-number-color: hsl(248, 100%, 40%);