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