summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--tools/android/loading/devtools_monitor.py32
-rw-r--r--tools/android/loading/request_dependencies_lens.py31
-rw-r--r--tools/android/loading/request_dependencies_lens_unittest.py18
-rw-r--r--tools/android/loading/request_track.py4
4 files changed, 84 insertions, 1 deletions
diff --git a/tools/android/loading/devtools_monitor.py b/tools/android/loading/devtools_monitor.py
index 5279e4f..68c785f 100644
--- a/tools/android/loading/devtools_monitor.py
+++ b/tools/android/loading/devtools_monitor.py
@@ -92,6 +92,7 @@ class DevToolsConnection(object):
self._ws = self._Connect(hostname, port)
self._event_listeners = {}
self._domain_listeners = {}
+ self._scoped_states = {}
self._domains_to_enable = set()
self._tearing_down_tracing = False
self._set_up = False
@@ -130,6 +131,30 @@ class DevToolsConnection(object):
if key in self._domain_listeners:
del(self._domain_listeners[key])
+ def SetScopedState(self, method, params, default_params, enable_domain):
+ """Changes state at the beginning the monitoring and resets it at the end.
+
+ |method| is called with |params| at the beginning of the monitoring. After
+ the monitoring completes, the state is reset by calling |method| with
+ |default_params|.
+
+ Args:
+ method: (str) Method.
+ params: (dict) Parameters to set when the monitoring starts.
+ default_params: (dict) Parameters to reset the state at the end.
+ enable_domain: (bool) True if enabling the domain is required.
+ """
+ if enable_domain:
+ if '.' in method:
+ domain = method[:method.index('.')]
+ assert domain, 'No valid domain'
+ self._domains_to_enable.add(domain)
+ scoped_state_value = (params, default_params)
+ if self._scoped_states.has_key(method):
+ assert self._scoped_states[method] == scoped_state_value
+ else:
+ self._scoped_states[method] = scoped_state_value
+
def SyncRequest(self, method, params=None):
"""Issues a synchronous request to the DevTools server.
@@ -186,6 +211,9 @@ class DevToolsConnection(object):
self.SyncRequestNoResponse('%s.enable' % domain)
# Tracing setup must be done by the tracing track to control filtering
# and output.
+ for scoped_state in self._scoped_states:
+ self.SyncRequestNoResponse(scoped_state,
+ self._scoped_states[scoped_state][0])
self._tearing_down_tracing = False
self._set_up = True
@@ -218,6 +246,9 @@ class DevToolsConnection(object):
self.SyncRequestNoResponse(self.TRACING_END_METHOD)
self._tearing_down_tracing = True
self._Dispatch(kind='Tracing', timeout=self.TRACING_TIMEOUT)
+ for scoped_state in self._scoped_states:
+ self.SyncRequestNoResponse(scoped_state,
+ self._scoped_states[scoped_state][1])
for domain in self._domains_to_enable:
if domain != self.TRACING_DOMAIN:
self.SyncRequest('%s.disable' % domain)
@@ -225,6 +256,7 @@ class DevToolsConnection(object):
self._domains_to_enable.clear()
self._domain_listeners.clear()
self._event_listeners.clear()
+ self._scoped_states.clear()
def _OnDataReceived(self, msg):
if 'method' not in msg:
diff --git a/tools/android/loading/request_dependencies_lens.py b/tools/android/loading/request_dependencies_lens.py
index b255656..4a72a4f 100644
--- a/tools/android/loading/request_dependencies_lens.py
+++ b/tools/android/loading/request_dependencies_lens.py
@@ -8,6 +8,7 @@ When executed as a script, loads a trace and outputs the dependencies.
"""
import collections
+import copy
import logging
import operator
@@ -18,6 +19,7 @@ import request_track
class RequestDependencyLens(object):
"""Analyses and infers request dependencies."""
DEPENDENCIES = ('redirect', 'parser', 'script', 'inferred', 'other')
+ CALLFRAMES_KEY = 'callFrames'
def __init__(self, trace):
"""Initializes an instance of RequestDependencyLens.
@@ -89,6 +91,30 @@ class RequestDependencyLens(object):
initiating_request = self._FindBestMatchingInitiator(request, candidates)
return (initiating_request, request, 'parser')
+ def _FlattenScriptStack(self, stack):
+ """Recursively collapses the stack of asynchronous callstacks.
+
+ A stack has a list of call frames and optionnally a "parent" stack.
+ This function recursively folds the parent stacks into the root stack by
+ concatening all the call frames.
+
+ Args:
+ stack: (dict) the stack that must be flattened
+
+ Returns:
+ A stack with no parent, which is a dictionary with a single "callFrames"
+ key, and no "parent" key.
+ """
+ PARENT_KEY = 'parent'
+ if not PARENT_KEY in stack:
+ return stack
+ stack[self.CALLFRAMES_KEY] += stack[PARENT_KEY][self.CALLFRAMES_KEY]
+ if not PARENT_KEY in stack[PARENT_KEY]:
+ stack.pop(PARENT_KEY)
+ else:
+ stack[PARENT_KEY] = stack[PARENT_KEY][PARENT_KEY]
+ return self._FlattenScriptStack(stack)
+
def _GetInitiatingRequestScript(self, request):
STACK_KEY = 'stack'
if not STACK_KEY in request.initiator:
@@ -96,7 +122,10 @@ class RequestDependencyLens(object):
return None
initiating_request = None
timestamp = request.timing.request_time
- call_frames = request.initiator[STACK_KEY]['callFrames']
+ # Deep copy the initiator's stack to avoid mutating the input request.
+ stack = self._FlattenScriptStack(
+ copy.deepcopy(request.initiator[STACK_KEY]))
+ call_frames = stack[self.CALLFRAMES_KEY]
for frame in call_frames:
url = frame['url']
candidates = self._FindMatchingRequests(url, timestamp)
diff --git a/tools/android/loading/request_dependencies_lens_unittest.py b/tools/android/loading/request_dependencies_lens_unittest.py
index 4035abf..1429a0e 100644
--- a/tools/android/loading/request_dependencies_lens_unittest.py
+++ b/tools/android/loading/request_dependencies_lens_unittest.py
@@ -83,6 +83,24 @@ class RequestDependencyLensTestCase(unittest.TestCase):
deps[0],
self._JS_REQUEST.request_id, self._JS_REQUEST_2.request_id, 'script')
+ def testAsyncScriptDependency(self):
+ JS_REQUEST_WITH_ASYNC_STACK = Request.FromJsonDict(
+ {'url': 'http://bla.com/cat.js', 'request_id': '1234.14',
+ 'initiator': {
+ 'type': 'script',
+ 'stack': {'callFrames': [],
+ 'parent': {'callFrames': [
+ {'url': 'http://bla.com/nyancat.js'}]}}},
+ 'timestamp': 10, 'timing': TimingFromDict({})})
+ loading_trace = test_utils.LoadingTraceFromEvents(
+ [self._JS_REQUEST, JS_REQUEST_WITH_ASYNC_STACK])
+ request_dependencies_lens = RequestDependencyLens(loading_trace)
+ deps = request_dependencies_lens.GetRequestDependencies()
+ self.assertEquals(1, len(deps))
+ self._AssertDependencyIs(
+ deps[0], self._JS_REQUEST.request_id,
+ JS_REQUEST_WITH_ASYNC_STACK.request_id, 'script')
+
def testParserDependency(self):
loading_trace = test_utils.LoadingTraceFromEvents(
[self._REQUEST, self._JS_REQUEST])
diff --git a/tools/android/loading/request_track.py b/tools/android/loading/request_track.py
index 89e3a5d..2c94703 100644
--- a/tools/android/loading/request_track.py
+++ b/tools/android/loading/request_track.py
@@ -273,6 +273,10 @@ class RequestTrack(devtools_monitor.Track):
if connection: # Optional for testing.
for method in RequestTrack._METHOD_TO_HANDLER:
self._connection.RegisterListener(method, self)
+ # Enable asynchronous callstacks to get full javascript callstacks in
+ # initiators
+ self._connection.SetScopedState('Debugger.setAsyncCallStackDepth',
+ {'maxDepth': 4}, {'maxDepth': 0}, True)
# responseReceived message are sometimes duplicated. Records the message to
# detect this.
self._request_id_to_response_received = {}