| # 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]) |