| # 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. |
| |
| """Models the effect of prefetching resources from a loading trace. |
| |
| For example, this can be used to evaluate NoState Prefetch |
| (https://goo.gl/B3nRUR). |
| |
| When executed as a script, takes a trace as a command-line arguments and shows |
| statistics about it. |
| """ |
| |
| import itertools |
| import operator |
| |
| import common_util |
| import dependency_graph |
| import graph |
| import loading_trace |
| import user_satisfied_lens |
| import request_dependencies_lens |
| import request_track |
| |
| |
| class RequestNode(dependency_graph.RequestNode): |
| """Simulates the effect of prefetching resources discoverable by the preload |
| scanner. |
| """ |
| _ATTRS = ['preloaded', 'before'] |
| def __init__(self, request=None): |
| super(RequestNode, self).__init__(request) |
| self.preloaded = False |
| self.before = False |
| |
| def ToJsonDict(self): |
| result = super(RequestNode, self).ToJsonDict() |
| return common_util.SerializeAttributesToJsonDict(result, self, self._ATTRS) |
| |
| @classmethod |
| def FromJsonDict(cls, json_dict): |
| result = super(RequestNode, cls).FromJsonDict(json_dict) |
| return common_util.DeserializeAttributesFromJsonDict( |
| json_dict, result, cls._ATTRS) |
| |
| |
| class PrefetchSimulationView(object): |
| """Simulates the effect of prefetch.""" |
| def __init__(self, trace, dependencies_lens, user_lens): |
| self.postload_msec = None |
| self.graph = None |
| if trace is None: |
| return |
| requests = trace.request_track.GetEvents() |
| critical_requests_ids = user_lens.CriticalRequestIds() |
| self.postload_msec = user_lens.PostloadTimeMsec() |
| self.graph = dependency_graph.RequestDependencyGraph( |
| requests, dependencies_lens, node_class=RequestNode) |
| preloaded_requests = [r.request_id for r in self.PreloadedRequests( |
| requests[0], dependencies_lens, trace)] |
| self._AnnotateNodes(self.graph.graph.Nodes(), preloaded_requests, |
| critical_requests_ids) |
| |
| def Cost(self): |
| """Returns the cost of the graph, restricted to the critical requests.""" |
| pruned_graph = self._PrunedGraph() |
| return pruned_graph.Cost() + self.postload_msec |
| |
| def UpdateNodeCosts(self, node_to_cost): |
| """Updates the cost of nodes, according to |node_to_cost|. |
| |
| Args: |
| node_to_cost: (Callable) RequestNode -> float. Callable returning the cost |
| of a node. |
| """ |
| pruned_graph = self._PrunedGraph() |
| for node in pruned_graph.Nodes(): |
| node.cost = node_to_cost(node) |
| |
| def ToJsonDict(self): |
| """Returns a dict representing this instance.""" |
| result = {'graph': self.graph.ToJsonDict()} |
| return common_util.SerializeAttributesToJsonDict( |
| result, self, ['postload_msec']) |
| |
| @classmethod |
| def FromJsonDict(cls, json_dict): |
| """Returns an instance of PrefetchSimulationView from a dict dumped by |
| ToJSonDict(). |
| """ |
| result = cls(None, None, None) |
| result.graph = dependency_graph.RequestDependencyGraph.FromJsonDict( |
| json_dict['graph'], RequestNode, dependency_graph.Edge) |
| return common_util.DeserializeAttributesFromJsonDict( |
| json_dict, result, ['postload_msec']) |
| |
| @classmethod |
| def _AnnotateNodes(cls, nodes, preloaded_requests_ids, |
| critical_requests_ids,): |
| for node in nodes: |
| node.preloaded = node.request.request_id in preloaded_requests_ids |
| node.before = node.request.request_id in critical_requests_ids |
| |
| @classmethod |
| def ParserDiscoverableRequests( |
| cls, request, dependencies_lens, recurse=False): |
| """Returns a list of requests IDs dicovered by the parser. |
| |
| Args: |
| request: (Request) Root request. |
| |
| Returns: |
| [Request] |
| """ |
| # TODO(lizeb): handle the recursive case. |
| assert not recurse |
| discoverable_requests = [request] |
| first_request = dependencies_lens.GetRedirectChain(request)[-1] |
| deps = dependencies_lens.GetRequestDependencies() |
| for (first, second, reason) in deps: |
| if first.request_id == first_request.request_id and reason == 'parser': |
| discoverable_requests.append(second) |
| return discoverable_requests |
| |
| @classmethod |
| def _ExpandRedirectChains(cls, requests, dependencies_lens): |
| return list(itertools.chain.from_iterable( |
| [dependencies_lens.GetRedirectChain(r) for r in requests])) |
| |
| @classmethod |
| def PreloadedRequests(cls, request, dependencies_lens, trace): |
| """Returns the requests that have been preloaded from a given request. |
| |
| This list is the set of request that are: |
| - Discoverable by the parser |
| - Found in the trace log. |
| |
| Before looking for dependencies, this follows the redirect chain. |
| |
| Args: |
| request: (Request) Root request. |
| |
| Returns: |
| A list of Request. Does not include the root request. This list is a |
| subset of the one returned by ParserDiscoverableRequests(). |
| """ |
| # Preload step events are emitted in ResourceFetcher::preloadStarted(). |
| resource_events = trace.tracing_track.Filter( |
| categories=set([u'blink.net'])) |
| preload_step_events = filter( |
| lambda e: e.args.get('step') == 'Preload', |
| resource_events.GetEvents()) |
| preloaded_urls = set() |
| for preload_step_event in preload_step_events: |
| preload_event = resource_events.EventFromStep(preload_step_event) |
| if preload_event: |
| preloaded_urls.add(preload_event.args['data']['url']) |
| parser_requests = cls.ParserDiscoverableRequests( |
| request, dependencies_lens) |
| preloaded_root_requests = filter( |
| lambda r: r.url in preloaded_urls, parser_requests) |
| # We can actually fetch the whole redirect chain. |
| return [request] + list(itertools.chain.from_iterable( |
| [dependencies_lens.GetRedirectChain(r) |
| for r in preloaded_root_requests])) |
| |
| def _PrunedGraph(self): |
| roots = self.graph.graph.RootNodes() |
| nodes = self.graph.graph.ReachableNodes( |
| roots, should_stop=lambda n: not n.before) |
| return graph.DirectedGraph(nodes, self.graph.graph.Edges()) |
| |
| |
| def _PrintSumamry(trace, dependencies_lens, user_lens): |
| prefetch_view = PrefetchSimulationView(trace, dependencies_lens, user_lens) |
| print 'Time to First Contentful Paint = %.02fms' % prefetch_view.Cost() |
| print 'Set costs of prefetched requests to 0.' |
| prefetch_view.UpdateNodeCosts(lambda n: 0 if n.preloaded else n.cost) |
| print 'Time to First Contentful Paint = %.02fms' % prefetch_view.Cost() |
| |
| |
| def main(filename): |
| trace = loading_trace.LoadingTrace.FromJsonFile(filename) |
| dependencies_lens = request_dependencies_lens.RequestDependencyLens(trace) |
| user_lens = user_satisfied_lens.FirstContentfulPaintLens(trace) |
| _PrintSumamry(trace, dependencies_lens, user_lens) |
| |
| |
| if __name__ == '__main__': |
| import sys |
| main(sys.argv[1]) |