Home | History | Annotate | Download | only in metrics
      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_measurement
     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 
     18 
     19 class NetworkMetricException(page_measurement.MeasurementFailure):
     20   pass
     21 
     22 
     23 class 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 
    120 class 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