• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import base64
6import gzip
7import hashlib
8import io
9import logging
10import zlib
11
12from metrics import Metric
13from telemetry.page import page_measurement
14# All network metrics are Chrome only for now.
15from telemetry.core.backends.chrome import inspector_network
16from telemetry.timeline import recording_options
17
18
19class NetworkMetricException(page_measurement.MeasurementFailure):
20  pass
21
22
23class HTTPResponse(object):
24  """ Represents an HTTP response from a timeline event."""
25  def __init__(self, event):
26    self._response = (
27        inspector_network.InspectorNetworkResponseData.FromTimelineEvent(event))
28    self._content_length = None
29
30  @property
31  def response(self):
32    return self._response
33
34  @property
35  def url_signature(self):
36    return hashlib.md5(self.response.url).hexdigest()
37
38  @property
39  def content_length(self):
40    if self._content_length is None:
41      self._content_length = self.GetContentLength()
42    return self._content_length
43
44  @property
45  def has_original_content_length(self):
46    return 'X-Original-Content-Length' in self.response.headers
47
48  @property
49  def original_content_length(self):
50    if self.has_original_content_length:
51      return int(self.response.GetHeader('X-Original-Content-Length'))
52    return 0
53
54  @property
55  def data_saving_rate(self):
56    if (self.response.served_from_cache or
57        not self.has_original_content_length or
58        self.original_content_length <= 0):
59      return 0.0
60    return (float(self.original_content_length - self.content_length) /
61            self.original_content_length)
62
63  def GetContentLengthFromBody(self):
64    resp = self.response
65    body, base64_encoded = resp.GetBody()
66    if not body:
67      return 0
68    # The binary data like images, etc is base64_encoded. Decode it to get
69    # the actualy content length.
70    if base64_encoded:
71      decoded = base64.b64decode(body)
72      return len(decoded)
73
74    encoding = resp.GetHeader('Content-Encoding')
75    if not encoding:
76      return len(body)
77    # The response body returned from a timeline event is always decompressed.
78    # So, we need to compress it to get the actual content length if headers
79    # say so.
80    encoding = encoding.lower()
81    if encoding == 'gzip':
82      return self.GetGizppedBodyLength(body)
83    elif encoding == 'deflate':
84      return len(zlib.compress(body, 9))
85    else:
86      raise NetworkMetricException, (
87          'Unknown Content-Encoding %s for %s' % (encoding, resp.url))
88
89  def GetContentLength(self):
90    cl = 0
91    try:
92      cl = self.GetContentLengthFromBody()
93    except Exception, e:
94      resp = self.response
95      logging.warning('Fail to get content length for %s from body: %s',
96                      resp.url[:100], e)
97      cl_header = resp.GetHeader('Content-Length')
98      if cl_header:
99        cl = int(cl_header)
100      else:
101        body, _ = resp.GetBody()
102        if body:
103          cl = len(body)
104    return cl
105
106  @staticmethod
107  def GetGizppedBodyLength(body):
108    if not body:
109      return 0
110    bio = io.BytesIO()
111    try:
112      with gzip.GzipFile(fileobj=bio, mode="wb", compresslevel=9) as f:
113        f.write(body.encode('utf-8'))
114    except Exception, e:
115      logging.warning('Fail to gzip response body: %s', e)
116      raise e
117    return len(bio.getvalue())
118
119
120class NetworkMetric(Metric):
121  """A network metric based on timeline events."""
122
123  def __init__(self):
124    super(NetworkMetric, self).__init__()
125
126    # Whether to add detailed result for each sub-resource in a page.
127    self.add_result_for_resource = False
128    self.compute_data_saving = False
129    self._events = None
130
131  def Start(self, page, tab):
132    self._events = None
133    opts = recording_options.TimelineRecordingOptions()
134    opts.record_network = True
135    tab.StartTimelineRecording(opts)
136
137  def Stop(self, page, tab):
138    assert self._events is None
139    tab.StopTimelineRecording()
140
141  def IterResponses(self, tab):
142    if self._events is None:
143      self._events = tab.timeline_model.GetAllEventsOfName('HTTPResponse')
144    if len(self._events) == 0:
145      return
146    for e in self._events:
147      yield self.ResponseFromEvent(e)
148
149  def ResponseFromEvent(self, event):
150    return HTTPResponse(event)
151
152  def AddResults(self, tab, results):
153    content_length = 0
154    original_content_length = 0
155
156    for resp in self.IterResponses(tab):
157      # Ignore content length calculation for cache hit.
158      if resp.response.served_from_cache:
159        continue
160
161      resource = resp.response.url
162      resource_signature = resp.url_signature
163      cl = resp.content_length
164      if resp.has_original_content_length:
165        ocl = resp.original_content_length
166        if ocl < cl:
167          logging.warning('original content length (%d) is less than content '
168                        'lenght(%d) for resource %s', ocl, cl, resource)
169        if self.add_result_for_resource:
170          results.Add('resource_data_saving_' + resource_signature,
171                      'percent', resp.data_saving_rate * 100)
172          results.Add('resource_original_content_length_' + resource_signature,
173                      'bytes', ocl)
174        original_content_length += ocl
175      else:
176        original_content_length += cl
177      if self.add_result_for_resource:
178        results.Add(
179            'resource_content_length_' + resource_signature, 'bytes', cl)
180      content_length += cl
181
182    results.Add('content_length', 'bytes', content_length)
183    results.Add('original_content_length', 'bytes', original_content_length)
184    if self.compute_data_saving:
185      if (original_content_length > 0 and
186          original_content_length >= content_length):
187        saving = (float(original_content_length-content_length) * 100 /
188                  original_content_length)
189        results.Add('data_saving', 'percent', saving)
190      else:
191        results.Add('data_saving', 'percent', 0.0)
192