| OLD | NEW |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Models for loading in chrome. | 5 """Models for loading in chrome. |
| 6 | 6 |
| 7 (Redirect the following to the general model module once we have one) | 7 (Redirect the following to the general model module once we have one) |
| 8 A model is an object with the following methods. | 8 A model is an object with the following methods. |
| 9 CostMs(): return the cost of the model in milliseconds. | 9 CostMs(): return the cost of the model in milliseconds. |
| 10 Set(): set model-specific parameters. | 10 Set(): set model-specific parameters. |
| 11 | 11 |
| 12 ResourceGraph | 12 ResourceGraph |
| 13 This creates a DAG of resource dependencies from loading.log_requests to model | 13 This creates a DAG of resource dependencies from loading.log_requests to model |
| 14 loading time. The model may be parameterized by changing the loading time of | 14 loading time. The model may be parameterized by changing the loading time of |
| 15 a particular or all resources. | 15 a particular or all resources. |
| 16 """ | 16 """ |
| 17 | 17 |
| 18 import logging | 18 import logging |
| 19 import os | 19 import os |
| 20 import urlparse | 20 import urlparse |
| 21 import sys | 21 import sys |
| 22 | 22 |
| 23 import dag | 23 import dag |
| 24 import loading_trace |
| 24 import request_dependencies_lens | 25 import request_dependencies_lens |
| 25 | 26 |
| 26 class ResourceGraph(object): | 27 class ResourceGraph(object): |
| 27 """A model of loading by a DAG (tree?) of resource dependancies. | 28 """A model of loading by a DAG (tree?) of resource dependancies. |
| 28 | 29 |
| 29 Set parameters: | 30 Set parameters: |
| 30 cache_all: if true, assume zero loading time for all resources. | 31 cache_all: if true, assume zero loading time for all resources. |
| 31 """ | 32 """ |
| 32 def __init__(self, trace): | 33 def __init__(self, trace): |
| 33 """Create from a LoadingTrace. | 34 """Create from a LoadingTrace (or json of a trace). |
| 34 | 35 |
| 35 Args: | 36 Args: |
| 36 trace: (LoadingTrace) Loading trace. | 37 trace: (LoadingTrace/JSON) Loading trace or JSON of a trace. |
| 37 """ | 38 """ |
| 39 if type(trace) == dict: |
| 40 trace = loading_trace.LoadingTrace.FromJsonDict(trace) |
| 38 self._BuildDag(trace) | 41 self._BuildDag(trace) |
| 39 self._global_start = min([n.StartTime() for n in self._node_info]) | 42 self._global_start = min([n.StartTime() for n in self._node_info]) |
| 40 # Sort before splitting children so that we can correctly dectect if a | 43 # Sort before splitting children so that we can correctly dectect if a |
| 41 # reparented child is actually a dependency for a child of its new parent. | 44 # reparented child is actually a dependency for a child of its new parent. |
| 42 try: | 45 try: |
| 43 for n in dag.TopologicalSort(self._nodes): | 46 for n in dag.TopologicalSort(self._nodes): |
| 44 self._SplitChildrenByTime(self._node_info[n.Index()]) | 47 self._SplitChildrenByTime(self._node_info[n.Index()]) |
| 45 except AssertionError as exc: | 48 except AssertionError as exc: |
| 46 sys.stderr.write('Bad topological sort: %s\n' | 49 sys.stderr.write('Bad topological sort: %s\n' |
| 47 'Skipping child split\n' % str(exc)) | 50 'Skipping child split\n' % str(exc)) |
| (...skipping 235 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 283 for c in children: | 286 for c in children: |
| 284 assert n in c.Predecessors() # Integrity checking | 287 assert n in c.Predecessors() # Integrity checking |
| 285 queue.append(c) | 288 queue.append(c) |
| 286 assert len(visited) == len(self._nodes) | 289 assert len(visited) == len(self._nodes) |
| 287 return '\n'.join(output) | 290 return '\n'.join(output) |
| 288 | 291 |
| 289 ## | 292 ## |
| 290 ## Internal items | 293 ## Internal items |
| 291 ## | 294 ## |
| 292 | 295 |
| 293 _CONTENT_TYPE_TO_COLOR = {'html': 'red', 'css': 'green', 'script': 'blue', | 296 _CONTENT_KIND_TO_COLOR = { |
| 294 'json': 'purple', 'gif_image': 'grey', | 297 'application': 'blue', # Scripts. |
| 295 'image': 'orange', 'other': 'white'} | 298 'font': 'grey70', |
| 299 'image': 'orange', # This probably catches gifs? |
| 300 'video': 'hotpink1', |
| 301 } |
| 302 |
| 303 _CONTENT_TYPE_TO_COLOR = { |
| 304 'html': 'red', |
| 305 'css': 'green', |
| 306 'script': 'blue', |
| 307 'javascript': 'blue', |
| 308 'json': 'purple', |
| 309 'gif': 'grey', |
| 310 'image': 'orange', |
| 311 'jpeg': 'orange', |
| 312 'png': 'orange', |
| 313 'plain': 'brown3', |
| 314 'octet-stream': 'brown3', |
| 315 'other': 'white', |
| 316 } |
| 296 | 317 |
| 297 # This resource type may induce a timing dependency. See _SplitChildrenByTime | 318 # This resource type may induce a timing dependency. See _SplitChildrenByTime |
| 298 # for details. | 319 # for details. |
| 299 # TODO(mattcary): are these right? | 320 # TODO(mattcary): are these right? |
| 300 _CAN_BE_TIMING_PARENT = set(['script', 'magic-debug-content']) | 321 _CAN_BE_TIMING_PARENT = set(['script', 'magic-debug-content']) |
| 301 _CAN_MAKE_TIMING_DEPENDENCE = set(['json', 'other', 'magic-debug-content']) | 322 _CAN_MAKE_TIMING_DEPENDENCE = set(['json', 'other', 'magic-debug-content']) |
| 302 | 323 |
| 303 class _NodeInfo(object): | 324 class _NodeInfo(object): |
| 304 """Our internal class that adds cost and other information to nodes. | 325 """Our internal class that adds cost and other information to nodes. |
| 305 | 326 |
| (...skipping 10 matching lines...) Expand all Loading... |
| 316 Args: | 337 Args: |
| 317 node: The node to augment. | 338 node: The node to augment. |
| 318 request: The request associated with this node. | 339 request: The request associated with this node. |
| 319 """ | 340 """ |
| 320 self._request = request | 341 self._request = request |
| 321 self._node = node | 342 self._node = node |
| 322 self._edge_costs = {} | 343 self._edge_costs = {} |
| 323 self._edge_annotations = {} | 344 self._edge_annotations = {} |
| 324 # All fields in timing are millis relative to request_time, which is epoch | 345 # All fields in timing are millis relative to request_time, which is epoch |
| 325 # seconds. | 346 # seconds. |
| 326 self._node_cost = max([t for f, t in request.timing._asdict().iteritems() | 347 self._node_cost = max( |
| 327 if f != 'request_time']) | 348 [0] + [t for f, t in request.timing._asdict().iteritems() |
| 349 if f != 'request_time']) |
| 328 | 350 |
| 329 def __str__(self): | 351 def __str__(self): |
| 330 return self.ShortName() | 352 return self.ShortName() |
| 331 | 353 |
| 332 def Node(self): | 354 def Node(self): |
| 333 return self._node | 355 return self._node |
| 334 | 356 |
| 335 def Index(self): | 357 def Index(self): |
| 336 return self._node.Index() | 358 return self._node.Index() |
| 337 | 359 |
| (...skipping 18 matching lines...) Expand all Loading... |
| 356 | 378 |
| 357 def ContentType(self): | 379 def ContentType(self): |
| 358 return self._request.GetContentType() | 380 return self._request.GetContentType() |
| 359 | 381 |
| 360 def ShortName(self): | 382 def ShortName(self): |
| 361 """Returns either the hostname of the resource, or the filename, | 383 """Returns either the hostname of the resource, or the filename, |
| 362 or the end of the path. Tries to include the domain as much as possible. | 384 or the end of the path. Tries to include the domain as much as possible. |
| 363 """ | 385 """ |
| 364 parsed = urlparse.urlparse(self._request.url) | 386 parsed = urlparse.urlparse(self._request.url) |
| 365 path = parsed.path | 387 path = parsed.path |
| 388 hostname = parsed.hostname if parsed.hostname else '?.?.?' |
| 366 if path != '' and path != '/': | 389 if path != '' and path != '/': |
| 367 last_path = parsed.path.split('/')[-1] | 390 last_path = parsed.path.split('/')[-1] |
| 368 if len(last_path) < 10: | 391 if len(last_path) < 10: |
| 369 if len(path) < 10: | 392 if len(path) < 10: |
| 370 return parsed.hostname + '/' + path | 393 return hostname + '/' + path |
| 371 else: | 394 else: |
| 372 return parsed.hostname + '/..' + parsed.path[-10:] | 395 return hostname + '/..' + parsed.path[-10:] |
| 373 elif len(last_path) > 10: | 396 elif len(last_path) > 10: |
| 374 return parsed.hostname + '/..' + last_path[:5] | 397 return hostname + '/..' + last_path[:5] |
| 375 else: | 398 else: |
| 376 return parsed.hostname + '/..' + last_path | 399 return hostname + '/..' + last_path |
| 377 else: | 400 else: |
| 378 return parsed.hostname | 401 return hostname |
| 379 | 402 |
| 380 def Url(self): | 403 def Url(self): |
| 381 return self._request.url | 404 return self._request.url |
| 382 | 405 |
| 383 def SetEdgeCost(self, child, cost): | 406 def SetEdgeCost(self, child, cost): |
| 384 assert child.Node() in self._node.Successors() | 407 assert child.Node() in self._node.Successors() |
| 385 self._edge_costs[child] = cost | 408 self._edge_costs[child] = cost |
| 386 | 409 |
| 387 def AddEdgeAnnotation(self, s, annotation): | 410 def AddEdgeAnnotation(self, s, annotation): |
| 388 assert s.Node() in self._node.Successors() | 411 assert s.Node() in self._node.Successors() |
| (...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 456 next_index = len(self._nodes) | 479 next_index = len(self._nodes) |
| 457 assert request not in index_by_request | 480 assert request not in index_by_request |
| 458 index_by_request[request] = next_index | 481 index_by_request[request] = next_index |
| 459 node = dag.Node(next_index) | 482 node = dag.Node(next_index) |
| 460 node_info = self._NodeInfo(node, request) | 483 node_info = self._NodeInfo(node, request) |
| 461 self._nodes.append(node) | 484 self._nodes.append(node) |
| 462 self._node_info.append(node_info) | 485 self._node_info.append(node_info) |
| 463 | 486 |
| 464 dependencies = request_dependencies_lens.RequestDependencyLens( | 487 dependencies = request_dependencies_lens.RequestDependencyLens( |
| 465 trace).GetRequestDependencies() | 488 trace).GetRequestDependencies() |
| 466 for child_rq, parent_rq, reason in dependencies: | 489 for parent_rq, child_rq, reason in dependencies: |
| 467 parent = self._node_info[index_by_request[parent_rq]] | 490 parent = self._node_info[index_by_request[parent_rq]] |
| 468 child = self._node_info[index_by_request[child_rq]] | 491 child = self._node_info[index_by_request[child_rq]] |
| 469 edge_cost = child.StartTime() - parent.EndTime() | 492 edge_cost = child.StartTime() - parent.EndTime() |
| 470 if edge_cost < 0: | 493 if edge_cost < 0: |
| 471 edge_cost = 0 | 494 edge_cost = 0 |
| 472 if child.StartTime() < parent.StartTime(): | 495 if child.StartTime() < parent.StartTime(): |
| 473 logging.error('Inverted dependency: %s->%s', | 496 logging.error('Inverted dependency: %s->%s', |
| 474 parent.ShortName(), child.ShortName()) | 497 parent.ShortName(), child.ShortName()) |
| 475 # Note that child.StartTime() < parent.EndTime() appears to happen a | 498 # Note that child.StartTime() < parent.EndTime() appears to happen a |
| 476 # fair amount in practice. | 499 # fair amount in practice. |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 542 break | 565 break |
| 543 if end_mark >= len(children_by_end_time): | 566 if end_mark >= len(children_by_end_time): |
| 544 break # It's not possible to rearrange any more children. | 567 break # It's not possible to rearrange any more children. |
| 545 if go_to_next_child: | 568 if go_to_next_child: |
| 546 continue # We can't rearrange this child, but the next child may be | 569 continue # We can't rearrange this child, but the next child may be |
| 547 # eligible. | 570 # eligible. |
| 548 if children_by_end_time[end_mark].EndTime() <= current.StartTime(): | 571 if children_by_end_time[end_mark].EndTime() <= current.StartTime(): |
| 549 current.ReparentTo(parent, children_by_end_time[end_mark]) | 572 current.ReparentTo(parent, children_by_end_time[end_mark]) |
| 550 children_by_end_time[end_mark].AddEdgeAnnotation(current, 'timing') | 573 children_by_end_time[end_mark].AddEdgeAnnotation(current, 'timing') |
| 551 | 574 |
| 575 def _ContentTypeToColor(self, content_type): |
| 576 if not content_type: |
| 577 type_str = 'other' |
| 578 elif '/' in content_type: |
| 579 kind, type_str = content_type.split('/') |
| 580 if kind in self._CONTENT_KIND_TO_COLOR: |
| 581 return self._CONTENT_KIND_TO_COLOR[kind] |
| 582 else: |
| 583 type_str = content_type |
| 584 return self._CONTENT_TYPE_TO_COLOR[type_str] |
| 585 |
| 552 def _GraphvizNode(self, index, highlight): | 586 def _GraphvizNode(self, index, highlight): |
| 553 """Returns a graphviz node description for a given node. | 587 """Returns a graphviz node description for a given node. |
| 554 | 588 |
| 555 Args: | 589 Args: |
| 556 index: index of the node. | 590 index: index of the node. |
| 557 highlight: a list of node items to emphasize. Any resource url which | 591 highlight: a list of node items to emphasize. Any resource url which |
| 558 contains any highlight text will be distinguished in the output. | 592 contains any highlight text will be distinguished in the output. |
| 559 | 593 |
| 560 Returns: | 594 Returns: |
| 561 A string describing the resource in graphviz format. | 595 A string describing the resource in graphviz format. |
| 562 The resource is color-coded according to its content type, and its shape | 596 The resource is color-coded according to its content type, and its shape |
| 563 is oval if its max-age is less than 300s (or if it's not cacheable). | 597 is oval if its max-age is less than 300s (or if it's not cacheable). |
| 564 """ | 598 """ |
| 565 node_info = self._node_info[index] | 599 node_info = self._node_info[index] |
| 566 color = self._CONTENT_TYPE_TO_COLOR[node_info.ContentType()] | 600 color = self._ContentTypeToColor(node_info.ContentType()) |
| 567 max_age = node_info.Request().MaxAge() | 601 max_age = node_info.Request().MaxAge() |
| 568 shape = 'polygon' if max_age > 300 else 'oval' | 602 shape = 'polygon' if max_age > 300 else 'oval' |
| 569 styles = ['filled'] | 603 styles = ['filled'] |
| 570 if highlight: | 604 if highlight: |
| 571 for fragment in highlight: | 605 for fragment in highlight: |
| 572 if fragment in node_info.Url(): | 606 if fragment in node_info.Url(): |
| 573 styles.append('dotted') | 607 styles.append('dotted') |
| 574 break | 608 break |
| 575 return ('%d [label = "%s\\n%.2f->%.2f (%.2f)"; style = "%s"; ' | 609 return ('%d [label = "%s\\n%.2f->%.2f (%.2f)"; style = "%s"; ' |
| 576 'fillcolor = %s; shape = %s];\n' | 610 'fillcolor = %s; shape = %s];\n' |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 666 Dict of image url + short name to NodeInfo. | 700 Dict of image url + short name to NodeInfo. |
| 667 """ | 701 """ |
| 668 image_to_info = {} | 702 image_to_info = {} |
| 669 for n in self._node_info: | 703 for n in self._node_info: |
| 670 if (n.ContentType().startswith('image') and | 704 if (n.ContentType().startswith('image') and |
| 671 not self._IsAdUrl(n.Url())): | 705 not self._IsAdUrl(n.Url())): |
| 672 key = str((n.Url(), n.ShortName(), n.StartTime())) | 706 key = str((n.Url(), n.ShortName(), n.StartTime())) |
| 673 assert key not in image_to_info, n.Url() | 707 assert key not in image_to_info, n.Url() |
| 674 image_to_info[key] = n | 708 image_to_info[key] = n |
| 675 return image_to_info | 709 return image_to_info |
| OLD | NEW |