blob: cbd882a6748a6c9fbd49075ec36820dc94cc8682 [file] [log] [blame]
# 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])