OLD | NEW |
| (Empty) |
1 #! /usr/bin/python | |
2 # Copyright 2015 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """Creates a Graphviz file visualizing the resource dependencies from a JSON | |
7 file dumped by log_requests.py. | |
8 """ | |
9 | |
10 import collections | |
11 import sys | |
12 import urlparse | |
13 | |
14 import log_parser | |
15 from log_parser import Resource | |
16 | |
17 | |
18 def _BuildResourceDependencyGraph(requests): | |
19 """Builds the graph of resource dependencies. | |
20 | |
21 Args: | |
22 requests: [RequestData, ...] | |
23 | |
24 Returns: | |
25 A tuple ([Resource], [(resource1, resource2, reason), ...]) | |
26 """ | |
27 resources = log_parser.GetResources(requests) | |
28 resources_from_url = {resource.url: resource for resource in resources} | |
29 requests_by_completion = log_parser.SortedByCompletion(requests) | |
30 deps = [] | |
31 for r in requests: | |
32 resource = Resource.FromRequest(r) | |
33 initiator = r.initiator | |
34 initiator_type = initiator['type'] | |
35 dep = None | |
36 if initiator_type == 'parser': | |
37 url = initiator['url'] | |
38 blocking_resource = resources_from_url.get(url, None) | |
39 if blocking_resource is None: | |
40 continue | |
41 dep = (blocking_resource, resource, 'parser') | |
42 elif initiator_type == 'script' and 'stackTrace' in initiator: | |
43 for frame in initiator['stackTrace']: | |
44 url = frame['url'] | |
45 blocking_resource = resources_from_url.get(url, None) | |
46 if blocking_resource is None: | |
47 continue | |
48 dep = (blocking_resource, resource, 'stack') | |
49 break | |
50 else: | |
51 # When the initiator is a script without a stackTrace, infer that it comes | |
52 # from the most recent script from the same hostname. | |
53 # TLD+1 might be better, but finding what is a TLD requires a database. | |
54 request_hostname = urlparse.urlparse(r.url).hostname | |
55 sorted_script_requests_from_hostname = [ | |
56 r for r in requests_by_completion | |
57 if (resource.GetContentType() in ('script', 'html', 'json') | |
58 and urlparse.urlparse(r.url).hostname == request_hostname)] | |
59 most_recent = None | |
60 # Linear search is bad, but this shouldn't matter here. | |
61 for request in sorted_script_requests_from_hostname: | |
62 if request.timestamp < r.timing.requestTime: | |
63 most_recent = request | |
64 else: | |
65 break | |
66 if most_recent is not None: | |
67 blocking = resources_from_url.get(most_recent.url, None) | |
68 if blocking is not None: | |
69 dep = (blocking, resource, 'script_inferred') | |
70 if dep is not None: | |
71 deps.append(dep) | |
72 return (resources, deps) | |
73 | |
74 | |
75 def PrefetchableResources(requests): | |
76 """Returns a list of resources that are discoverable without JS. | |
77 | |
78 Args: | |
79 requests: List of requests. | |
80 | |
81 Returns: | |
82 List of discoverable resources, with their initial request. | |
83 """ | |
84 resource_to_request = log_parser.ResourceToRequestMap(requests) | |
85 (_, all_deps) = _BuildResourceDependencyGraph(requests) | |
86 # Only keep "parser" arcs | |
87 deps = [(first, second) for (first, second, reason) in all_deps | |
88 if reason == 'parser'] | |
89 deps_per_resource = collections.defaultdict(list) | |
90 for (first, second) in deps: | |
91 deps_per_resource[first].append(second) | |
92 result = [] | |
93 visited = set() | |
94 to_visit = [deps[0][0]] | |
95 while len(to_visit) != 0: | |
96 r = to_visit.pop() | |
97 visited.add(r) | |
98 to_visit += deps_per_resource[r] | |
99 result.append(resource_to_request[r]) | |
100 return result | |
101 | |
102 | |
103 _CONTENT_TYPE_TO_COLOR = {'html': 'red', 'css': 'green', 'script': 'blue', | |
104 'json': 'purple', 'gif_image': 'grey', | |
105 'image': 'orange', 'other': 'white'} | |
106 | |
107 | |
108 def _ResourceGraphvizNode(resource, request, resource_to_index): | |
109 """Returns the node description for a given resource. | |
110 | |
111 Args: | |
112 resource: Resource. | |
113 request: RequestData associated with the resource. | |
114 resource_to_index: {Resource: int}. | |
115 | |
116 Returns: | |
117 A string describing the resource in graphviz format. | |
118 The resource is color-coded according to its content type, and its shape is | |
119 oval if its max-age is less than 300s (or if it's not cacheable). | |
120 """ | |
121 color = _CONTENT_TYPE_TO_COLOR[resource.GetContentType()] | |
122 max_age = log_parser.MaxAge(request) | |
123 shape = 'polygon' if max_age > 300 else 'oval' | |
124 return ('%d [label = "%s"; style = "filled"; fillcolor = %s; shape = %s];\n' | |
125 % (resource_to_index[resource], resource.GetShortName(), color, | |
126 shape)) | |
127 | |
128 | |
129 def _GraphvizFileFromDeps(resources, requests, deps, output_filename): | |
130 """Writes a graphviz file from a set of resource dependencies. | |
131 | |
132 Args: | |
133 resources: [Resource, ...] | |
134 requests: list of requests | |
135 deps: [(resource1, resource2, reason), ...] | |
136 output_filename: file to write the graph to. | |
137 """ | |
138 with open(output_filename, 'w') as f: | |
139 f.write("""digraph dependencies { | |
140 rankdir = LR; | |
141 """) | |
142 resource_to_request = log_parser.ResourceToRequestMap(requests) | |
143 resource_to_index = {r: i for (i, r) in enumerate(resources)} | |
144 resources_with_edges = set() | |
145 for (first, second, reason) in deps: | |
146 resources_with_edges.add(first) | |
147 resources_with_edges.add(second) | |
148 if len(resources_with_edges) != len(resources): | |
149 f.write("""subgraph cluster_orphans { | |
150 color=black; | |
151 label="Orphans"; | |
152 """) | |
153 for resource in resources: | |
154 if resource not in resources_with_edges: | |
155 request = resource_to_request[resource] | |
156 f.write(_ResourceGraphvizNode(resource, request, resource_to_index)) | |
157 f.write('}\n') | |
158 | |
159 f.write("""subgraph cluster_nodes { | |
160 color=invis; | |
161 """) | |
162 for resource in resources: | |
163 request = resource_to_request[resource] | |
164 print resource.url | |
165 if resource in resources_with_edges: | |
166 f.write(_ResourceGraphvizNode(resource, request, resource_to_index)) | |
167 for (first, second, reason) in deps: | |
168 arrow = '' | |
169 if reason == 'parser': | |
170 arrow = '[color = red]' | |
171 elif reason == 'stack': | |
172 arrow = '[color = blue]' | |
173 elif reason == 'script_inferred': | |
174 arrow = '[color = blue; style=dotted]' | |
175 f.write('%d -> %d %s;\n' % ( | |
176 resource_to_index[first], resource_to_index[second], arrow)) | |
177 f.write('}\n}\n') | |
178 | |
179 | |
180 def main(): | |
181 filename = sys.argv[1] | |
182 requests = log_parser.ParseJsonFile(filename) | |
183 requests = log_parser.FilterRequests(requests) | |
184 (resources, deps) = _BuildResourceDependencyGraph(requests) | |
185 _GraphvizFileFromDeps(resources, requests, deps, filename + '.dot') | |
186 | |
187 | |
188 if __name__ == '__main__': | |
189 main() | |
OLD | NEW |