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