blob: 23a39a457448135e36d241988b51131cdf86d675 [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.
"""Generates a loading report.
When executed as a script, takes a trace filename and print the report.
"""
from activity_lens import ActivityLens
from content_classification_lens import ContentClassificationLens
from loading_graph_view import LoadingGraphView
import loading_trace
import metrics
from network_activity_lens import NetworkActivityLens
from prefetch_view import PrefetchSimulationView
from queuing_lens import QueuingLens
import request_dependencies_lens
from user_satisfied_lens import (
FirstTextPaintLens, FirstContentfulPaintLens, FirstSignificantPaintLens,
PLTLens)
def _ComputeCpuBusyness(activity, load_start, satisfied_end):
"""Generates a breakdown of CPU activity between |load_start| and
|satisfied_end|."""
duration = float(satisfied_end - load_start)
result = {
'activity_frac': (
activity.MainRendererThreadBusyness(load_start, satisfied_end)
/ duration),
}
activity_breakdown = activity.ComputeActivity(load_start, satisfied_end)
result['parsing_frac'] = (
sum(activity_breakdown['parsing'].values()) / duration)
result['script_frac'] = (
sum(activity_breakdown['script'].values()) / duration)
return result
class PerUserLensReport(object):
"""Generates a variety of metrics relative to a passed in user lens."""
def __init__(self, trace, user_lens, activity_lens, network_lens,
navigation_start_msec):
requests = trace.request_track.GetEvents()
dependencies_lens = request_dependencies_lens.RequestDependencyLens(
trace)
prefetch_view = PrefetchSimulationView(trace, dependencies_lens, user_lens)
preloaded_requests = prefetch_view.PreloadedRequests(
requests[0], dependencies_lens, trace)
self._navigation_start_msec = navigation_start_msec
self._satisfied_msec = user_lens.SatisfiedMs()
graph = LoadingGraphView.FromTrace(trace)
self._inversions = graph.GetInversionsAtTime(self._satisfied_msec)
self._byte_frac = self._GenerateByteFrac(network_lens)
self._requests = user_lens.CriticalRequests()
self._preloaded_requests = (
[r for r in preloaded_requests if r in self._requests])
self._cpu_busyness = _ComputeCpuBusyness(activity_lens,
navigation_start_msec,
self._satisfied_msec)
prefetch_view.UpdateNodeCosts(lambda n: 0 if n.preloaded else n.cost)
self._no_state_prefetch_ms = prefetch_view.Cost()
def GenerateReport(self):
report = {}
report['ms'] = self._satisfied_msec - self._navigation_start_msec
report['byte_frac'] = self._byte_frac
report['requests'] = len(self._requests)
report['preloaded_requests'] = len(self._preloaded_requests)
report['requests_cost'] = reduce(lambda x,y: x + y.Cost(),
self._requests, 0)
report['preloaded_requests_cost'] = reduce(lambda x,y: x + y.Cost(),
self._preloaded_requests, 0)
report['predicted_no_state_prefetch_ms'] = self._no_state_prefetch_ms
# Take the first (earliest) inversion.
report['inversion'] = self._inversions[0].url if self._inversions else ''
report.update(self._cpu_busyness)
return report
def _GenerateByteFrac(self, network_lens):
if not network_lens.total_download_bytes:
return float('Nan')
byte_frac = (network_lens.DownloadedBytesAt(self._satisfied_msec)
/ float(network_lens.total_download_bytes))
return byte_frac
class LoadingReport(object):
"""Generates a loading report from a loading trace."""
def __init__(self, trace, ad_rules=None, tracking_rules=None):
"""Constructor.
Args:
trace: (LoadingTrace) a loading trace.
ad_rules: ([str]) List of ad filtering rules.
tracking_rules: ([str]) List of tracking filtering rules.
"""
self.trace = trace
navigation_start_events = trace.tracing_track.GetMatchingEvents(
'blink.user_timing', 'navigationStart')
self._navigation_start_msec = min(
e.start_msec for e in navigation_start_events)
self._dns_requests, self._dns_cost_msec = metrics.DnsRequestsAndCost(trace)
self._connection_stats = metrics.ConnectionMetrics(trace)
self._user_lens_reports = {}
plt_lens = PLTLens(self.trace)
first_text_paint_lens = FirstTextPaintLens(self.trace)
first_contentful_paint_lens = FirstContentfulPaintLens(self.trace)
first_significant_paint_lens = FirstSignificantPaintLens(self.trace)
activity = ActivityLens(trace)
network_lens = NetworkActivityLens(self.trace)
for key, user_lens in [['plt', plt_lens],
['first_text', first_text_paint_lens],
['contentful', first_contentful_paint_lens],
['significant', first_significant_paint_lens]]:
self._user_lens_reports[key] = PerUserLensReport(self.trace,
user_lens, activity, network_lens, self._navigation_start_msec)
self._transfer_size = metrics.TotalTransferSize(trace)[1]
self._request_count = len(trace.request_track.GetEvents())
content_lens = ContentClassificationLens(
trace, ad_rules or [], tracking_rules or [])
has_ad_rules = bool(ad_rules)
has_tracking_rules = bool(tracking_rules)
self._ad_report = self._AdRequestsReport(
trace, content_lens, has_ad_rules, has_tracking_rules)
self._ads_cost = self._AdsAndTrackingCpuCost(
self._navigation_start_msec,
(self._navigation_start_msec
+ self._user_lens_reports['plt'].GenerateReport()['ms']),
content_lens, activity, has_tracking_rules or has_ad_rules)
self._queue_stats = self._ComputeQueueStats(QueuingLens(trace))
def GenerateReport(self):
"""Returns a report as a dict."""
# NOTE: When changing the return value here, also update the schema
# (bigquery_schema.json) accordingly. See cloud/frontend/README.md for
# details.
report = {
'url': self.trace.url,
'transfer_size': self._transfer_size,
'dns_requests': self._dns_requests,
'dns_cost_ms': self._dns_cost_msec,
'total_requests': self._request_count}
for user_lens_type, user_lens_report in self._user_lens_reports.iteritems():
for key, value in user_lens_report.GenerateReport().iteritems():
report[user_lens_type + '_' + key] = value
report.update(self._ad_report)
report.update(self._ads_cost)
report.update(self._connection_stats)
report.update(self._queue_stats)
return report
@classmethod
def FromTraceFilename(cls, filename, ad_rules_filename,
tracking_rules_filename):
"""Returns a LoadingReport from a trace filename."""
trace = loading_trace.LoadingTrace.FromJsonFile(filename)
return LoadingReport(trace, ad_rules_filename, tracking_rules_filename)
@classmethod
def _AdRequestsReport(
cls, trace, content_lens, has_ad_rules, has_tracking_rules):
requests = trace.request_track.GetEvents()
has_rules = has_ad_rules or has_tracking_rules
result = {
'ad_requests': 0 if has_ad_rules else None,
'tracking_requests': 0 if has_tracking_rules else None,
'ad_or_tracking_requests': 0 if has_rules else None,
'ad_or_tracking_initiated_requests': 0 if has_rules else None,
'ad_or_tracking_initiated_transfer_size': 0 if has_rules else None}
if not has_rules:
return result
for request in requests:
is_ad = content_lens.IsAdRequest(request)
is_tracking = content_lens.IsTrackingRequest(request)
if has_ad_rules:
result['ad_requests'] += int(is_ad)
if has_tracking_rules:
result['tracking_requests'] += int(is_tracking)
result['ad_or_tracking_requests'] += int(is_ad or is_tracking)
ad_tracking_requests = content_lens.AdAndTrackingRequests()
result['ad_or_tracking_initiated_requests'] = len(ad_tracking_requests)
result['ad_or_tracking_initiated_transfer_size'] = metrics.TransferSize(
ad_tracking_requests)[1]
return result
@classmethod
def _ComputeQueueStats(cls, queue_lens):
queuing_info = queue_lens.GenerateRequestQueuing()
total_blocked_msec = 0
total_loading_msec = 0
num_blocking_requests = []
for queue_info in queuing_info.itervalues():
try:
total_blocked_msec += max(0, queue_info.ready_msec -
queue_info.start_msec)
total_loading_msec += max(0, queue_info.end_msec -
queue_info.start_msec)
except TypeError:
pass # Invalid queue info timings.
num_blocking_requests.append(len(queue_info.blocking))
if num_blocking_requests:
num_blocking_requests.sort()
avg_blocking = (float(sum(num_blocking_requests)) /
len(num_blocking_requests))
mid = len(num_blocking_requests) / 2
if len(num_blocking_requests) & 1:
median_blocking = num_blocking_requests[mid]
else:
median_blocking = (num_blocking_requests[mid-1] +
num_blocking_requests[mid]) / 2
else:
avg_blocking = 0
median_blocking = 0
return {
'total_queuing_blocked_msec': int(total_blocked_msec),
'total_queuing_load_msec': int(total_loading_msec),
'average_blocking_request_count': avg_blocking,
'median_blocking_request_count': median_blocking,
}
@classmethod
def _AdsAndTrackingCpuCost(
cls, start_msec, end_msec, content_lens, activity, has_rules):
"""Returns the CPU cost associated with Ads and tracking between timestamps.
Can return an overestimate, as execution slices are tagged by URL, and not
by requests.
Args:
start_msec: (float)
end_msec: (float)
content_lens: (ContentClassificationLens)
activity: (ActivityLens)
Returns:
{'ad_and_tracking_script_frac': float,
'ad_and_tracking_parsing_frac': float}
"""
result = {'ad_or_tracking_script_frac': None,
'ad_or_tracking_parsing_frac': None}
if not has_rules:
return result
duration = float(end_msec - start_msec)
requests = content_lens.AdAndTrackingRequests()
urls = {r.url for r in requests}
cpu_breakdown = activity.ComputeActivity(start_msec, end_msec)
result['ad_or_tracking_script_frac'] = sum(
value for (url, value) in cpu_breakdown['script'].items()
if url in urls) / duration
result['ad_or_tracking_parsing_frac'] = sum(
value for (url, value) in cpu_breakdown['parsing'].items()
if url in urls) / duration
return result
def _Main(args):
if len(args) not in (2, 4):
print 'Usage: report.py trace.json (ad_rules tracking_rules)'
sys.exit(1)
trace_filename = args[1]
ad_rules = None
tracking_rules = None
if len(args) == 4:
ad_rules = open(args[2]).readlines()
tracking_rules = open(args[3]).readlines()
report = LoadingReport.FromTraceFilename(
trace_filename, ad_rules, tracking_rules)
print json.dumps(report.GenerateReport(), indent=2, sort_keys=True)
if __name__ == '__main__':
import sys
import json
_Main(sys.argv)