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