Skip to content
Permalink
Browse files
Web Inspector: Debugger should have an option for showing asynchronou…
…s call stacks

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.


Canonical link: https://commits.webkit.org/182752@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@209062 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
LuckyKobold committed Nov 29, 2016
1 parent 7515fcb commit 924c49f68d5ce7cfe012c42434d46617e29504a4
Show file tree
Hide file tree
Showing 26 changed files with 835 additions and 36 deletions.
@@ -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.
@@ -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

@@ -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>
@@ -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
@@ -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()
@@ -50,6 +50,7 @@ class JS_EXPORT_PRIVATE ScriptCallFrame {
JSC::SourceID sourceID() const { return m_sourceID; }

bool isEqual(const ScriptCallFrame&) const;
bool isNative() const;

Ref<Inspector::Protocol::Console::CallFrame> buildInspectorObject() const;

@@ -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;

0 comments on commit 924c49f

Please sign in to comment.