Index: tools/perf/metrics/network.py |
diff --git a/tools/perf/metrics/network.py b/tools/perf/metrics/network.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..bd84c4f6730e850ab5bcbe48735bf2bbd4965b4f |
--- /dev/null |
+++ b/tools/perf/metrics/network.py |
@@ -0,0 +1,191 @@ |
+# Copyright 2014 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. |
+ |
+import base64 |
+import gzip |
+import hashlib |
+import io |
+import logging |
+import zlib |
+ |
+from metrics import Metric |
+from telemetry.page import page_measurement |
+# All network metrics are Chrome only for now. |
+from telemetry.core.backends.chrome import inspector_network |
+from telemetry.core.timeline import recording_options |
+ |
+ |
+class NetworkMetricException(page_measurement.MeasurementFailure): |
+ pass |
+ |
+ |
+class HTTPResponse(object): |
+ """ Represents an HTTP response from a timeline event.""" |
+ def __init__(self, event): |
+ self._response = ( |
+ inspector_network.InspectorNetworkResponseData.FromTimelineEvent(event)) |
+ self._content_length = None |
+ |
+ @property |
+ def response(self): |
+ return self._response |
+ |
+ @property |
+ def url_signature(self): |
+ return hashlib.md5(self.response.url).hexdigest() |
+ |
+ @property |
+ def content_length(self): |
+ if self._content_length is None: |
+ self._content_length = self.GetContentLength() |
+ return self._content_length |
+ |
+ @property |
+ def has_original_content_length(self): |
+ return 'X-Original-Content-Length' in self.response.headers |
+ |
+ @property |
+ def original_content_length(self): |
+ if self.has_original_content_length: |
+ return int(self.response.GetHeader('X-Original-Content-Length')) |
+ return 0 |
+ |
+ @property |
+ def data_saving_rate(self): |
+ if (self.response.served_from_cache or |
+ not self.has_original_content_length or |
+ self.original_content_length <= 0): |
+ return 0.0 |
+ return (float(self.original_content_length - self.content_length) / |
+ self.original_content_length) |
+ |
+ def GetContentLengthFromBody(self): |
+ resp = self.response |
+ body, base64_encoded = resp.GetBody() |
+ if not body: |
+ return 0 |
+ # The binary data like images, etc is base64_encoded. Decode it to get |
+ # the actualy content length. |
+ if base64_encoded: |
+ decoded = base64.b64decode(body) |
+ return len(decoded) |
+ |
+ encoding = resp.GetHeader('Content-Encoding') |
+ if not encoding: |
+ return len(body) |
+ # The response body returned from a timeline event is always decompressed. |
+ # So, we need to compress it to get the actual content length if headers |
+ # say so. |
+ encoding = encoding.lower() |
+ if encoding == 'gzip': |
+ return self.GetGizppedBodyLength(body) |
+ elif encoding == 'deflate': |
+ return len(zlib.compress(body, 9)) |
+ else: |
+ raise NetworkMetricException, ( |
+ 'Unknown Content-Encoding %s for %s' % (encoding, resp.url)) |
+ |
+ def GetContentLength(self): |
+ cl = 0 |
+ try: |
+ cl = self.GetContentLengthFromBody() |
+ except Exception, e: |
+ resp = self.response |
+ logging.warning('Fail to get content length for %s from body: %s', |
+ resp.url[:100], e) |
+ cl_header = resp.GetHeader('Content-Length') |
+ if cl_header: |
+ cl = int(cl_header) |
+ else: |
+ body, _ = resp.GetBody() |
+ if body: |
+ cl = len(body) |
+ return cl |
+ |
+ @staticmethod |
+ def GetGizppedBodyLength(body): |
+ if not body: |
+ return 0 |
+ bio = io.BytesIO() |
+ try: |
+ with gzip.GzipFile(fileobj=bio, mode="wb", compresslevel=9) as f: |
+ f.write(body.encode('utf-8')) |
+ except Exception, e: |
+ logging.warning('Fail to gzip response body: %s', e) |
+ raise e |
+ return len(bio.getvalue()) |
+ |
+ |
+class NetworkMetric(Metric): |
+ """A network metric based on timeline events.""" |
+ |
+ def __init__(self): |
+ super(NetworkMetric, self).__init__() |
+ |
+ # Whether to add detailed result for each sub-resource in a page. |
+ self.add_result_for_resource = False |
+ self.compute_data_saving = False |
+ self._events = None |
+ |
+ def Start(self, page, tab): |
+ self._events = None |
+ opts = recording_options.TimelineRecordingOptions() |
+ opts.record_network = True |
+ tab.StartTimelineRecording(opts) |
+ |
+ def Stop(self, page, tab): |
+ assert self._events is None |
+ tab.StopTimelineRecording() |
+ |
+ def IterResponses(self, tab): |
+ if self._events is None: |
+ self._events = tab.timeline_model.GetAllEventsOfName('HTTPResponse') |
+ if len(self._events) == 0: |
+ return |
+ for e in self._events: |
+ yield self.ResponseFromEvent(e) |
+ |
+ def ResponseFromEvent(self, event): |
+ return HTTPResponse(event) |
+ |
+ def AddResults(self, tab, results): |
+ content_length = 0 |
+ original_content_length = 0 |
+ |
+ for resp in self.IterResponses(tab): |
+ # Ignore content length calculation for cache hit. |
+ if resp.response.served_from_cache: |
+ continue |
+ |
+ resource = resp.response.url |
+ resource_signature = resp.url_signature |
+ cl = resp.content_length |
+ if resp.has_original_content_length: |
+ ocl = resp.original_content_length |
+ if ocl < cl: |
+ logging.warning('original content length (%d) is less than content ' |
+ 'lenght(%d) for resource %s', ocl, cl, resource) |
+ if self.add_result_for_resource: |
+ results.Add('resource_data_saving_' + resource_signature, |
+ 'percent', resp.data_saving_rate * 100) |
+ results.Add('resource_original_content_length_' + resource_signature, |
+ 'bytes', ocl) |
+ original_content_length += ocl |
+ else: |
+ original_content_length += cl |
+ if self.add_result_for_resource: |
+ results.Add( |
+ 'resource_content_length_' + resource_signature, 'bytes', cl) |
+ content_length += cl |
+ |
+ results.Add('content_length', 'bytes', content_length) |
+ results.Add('original_content_length', 'bytes', original_content_length) |
+ if self.compute_data_saving: |
+ if (original_content_length > 0 and |
+ original_content_length >= content_length): |
+ saving = (float(original_content_length-content_length) * 100 / |
+ original_content_length) |
+ results.Add('data_saving', 'percent', saving) |
+ else: |
+ results.Add('data_saving', 'percent', 0.0) |