blob: 7a89f6bf6fcee14691c297852c0cef6a33d269b0 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Visualize a loading_graph_view.LoadingGraphView.
When executed as a script, takes a loading trace and generates a png of the
loading graph."""
import activity_lens
import request_track
class LoadingGraphViewVisualization(object):
"""Manipulate visual representations of a request graph.
Currently only DOT output is supported.
"""
_LONG_EDGE_THRESHOLD_MS = 2000 # Time in milliseconds.
_CONTENT_KIND_TO_COLOR = {
'application': 'blue', # Scripts.
'font': 'grey70',
'image': 'orange', # This probably catches gifs?
'video': 'hotpink1',
'audio': 'hotpink2',
}
_CONTENT_TYPE_TO_COLOR = {
'html': 'red',
'css': 'green',
'script': 'blue',
'javascript': 'blue',
'json': 'purple',
'gif': 'grey',
'image': 'orange',
'jpeg': 'orange',
'ping': 'cyan', # Empty response
'redirect': 'forestgreen',
'png': 'orange',
'plain': 'brown3',
'octet-stream': 'brown3',
'other': 'white',
}
_EDGE_REASON_TO_COLOR = {
'redirect': 'black',
'parser': 'red',
'script': 'blue',
'script_inferred': 'purple',
}
_ACTIVITY_TYPE_LABEL = (
('idle', 'I'), ('unrelated_work', 'W'), ('script', 'S'),
('parsing', 'P'), ('other_url', 'O'), ('unknown_url', 'U'))
def __init__(self, graph_view):
"""Initialize.
Args:
graph_view: (loading_graph_view.LoadingGraphView) the graph to visualize.
"""
self._graph_view = graph_view
self._global_start = None
def OutputDot(self, output):
"""Output DOT (graphviz) representation.
Args:
output: a file-like output stream to receive the dot file.
"""
nodes = self._graph_view.deps_graph.graph.Nodes()
self._global_start = min(n.request.start_msec for n in nodes)
g = self._graph_view.deps_graph.graph
output.write("""digraph dependencies {
rankdir = LR;
""")
isolated_nodes = [
n for n in nodes if (
len(g.InEdges(n)) == 0 and len(g.OutEdges(n)) == 0)]
if isolated_nodes:
output.write("""subgraph cluster_isolated {
color=black;
label="Isolated Nodes";
""")
for n in isolated_nodes:
output.write(self._DotNode(n))
output.write('}\n')
output.write("""subgraph cluster_nodes {
color=invis;
""")
for n in nodes:
if n in isolated_nodes:
continue
output.write(self._DotNode(n))
edges = g.Edges()
for edge in edges:
output.write(self._DotEdge(edge))
output.write('}\n')
output.write('}\n')
def _ContentTypeToColor(self, content_type):
if not content_type:
type_str = 'other'
elif '/' in content_type:
kind, type_str = content_type.split('/', 1)
if kind in self._CONTENT_KIND_TO_COLOR:
return self._CONTENT_KIND_TO_COLOR[kind]
else:
type_str = content_type
return self._CONTENT_TYPE_TO_COLOR[type_str]
def _DotNode(self, node):
"""Returns a graphviz node description for a given node.
Args:
node: (RequestNode)
Returns:
A string describing the resource in graphviz format.
The resource is color-coded according to its content type, and its shape
is oval if its max-age is less than 300s (or if it's not cacheable).
"""
color = self._ContentTypeToColor(node.request.GetContentType())
request = node.request
max_age = request.MaxAge()
shape = 'polygon' if max_age > 300 else 'oval'
styles = ['filled']
if node.is_ad or node.is_tracking:
styles += ['bold', 'diagonals']
return ('"%s" [label = "%s\\n%.2f->%.2f (%.2f)"; style = "%s"; '
'fillcolor = %s; shape = %s];\n'
% (request.request_id, request_track.ShortName(request.url),
request.start_msec - self._global_start,
request.end_msec - self._global_start,
request.end_msec - request.start_msec,
','.join(styles), color, shape))
def _DotEdge(self, edge):
"""Returns a graphviz edge description for a given edge.
Args:
edge: (Edge)
Returns:
A string encoding the graphviz representation of the edge.
"""
style = {'color': 'orange'}
label = '%.02f' % edge.cost
if edge.is_timing:
style['style'] = 'dashed'
style['color'] = self._EDGE_REASON_TO_COLOR[edge.reason]
if edge.cost > self._LONG_EDGE_THRESHOLD_MS:
style['penwidth'] = '5'
style['weight'] = '2'
style_str = '; '.join('%s=%s' % (k, v) for (k, v) in style.items())
label = '%.02f' % edge.cost
if edge.activity:
separator = ' - '
for activity_type, activity_label in self._ACTIVITY_TYPE_LABEL:
label += '%s%s:%.02f ' % (
separator, activity_label, edge.activity[activity_type])
separator = ' '
arrow = '[%s; label="%s"]' % (style_str, label)
from_request_id = edge.from_node.request.request_id
to_request_id = edge.to_node.request.request_id
return '"%s" -> "%s" %s;\n' % (from_request_id, to_request_id, arrow)
def main(trace_file):
import subprocess
import loading_graph_view
import loading_trace
import request_dependencies_lens
trace = loading_trace.LoadingTrace.FromJsonFile(trace_file)
dependencies_lens = request_dependencies_lens.RequestDependencyLens(trace)
activity = activity_lens.ActivityLens(trace)
graph_view = loading_graph_view.LoadingGraphView(trace, dependencies_lens,
activity=activity)
visualization = LoadingGraphViewVisualization(graph_view)
dotfile = trace_file + '.dot'
pngfile = trace_file + '.png'
with file(dotfile, 'w') as output:
visualization.OutputDot(output)
subprocess.check_call(['dot', '-Tpng', dotfile, '-o', pngfile])
if __name__ == '__main__':
import sys
main(sys.argv[1])