Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | |
|
tonyg
2014/03/12 02:08:37
2014
bolian
2014/03/19 00:20:03
Done.
| |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 import base64 | |
| 5 import logging | |
|
tonyg
2014/03/12 02:08:37
nit: alphabetize
bolian
2014/03/19 00:20:03
Done.
| |
| 6 import gzip | |
| 7 import hashlib | |
| 8 | |
| 9 from io import BytesIO | |
| 10 from metrics import Metric | |
| 11 from metrics import loading | |
| 12 from telemetry.page import page_measurement | |
| 13 # All chrome_proxy metrics are Chrome only. | |
|
bengr
2014/03/12 18:17:36
Is there a check somewhere that Chrome is being te
bolian
2014/03/19 00:20:03
No. But an exception will be thrown if a non-Chrom
| |
| 14 from telemetry.core.backends.chrome import inspector_network | |
| 15 from telemetry.core.timeline import recording_options | |
| 16 | |
| 17 | |
| 18 class ChromeProxyMetricException(page_measurement.MeasurementFailure): | |
| 19 pass | |
| 20 | |
| 21 | |
| 22 class ChromeProxyLatency(Metric): | |
|
tonyg
2014/03/12 02:08:37
Let's remove this Metric, fold your new results in
bolian
2014/03/19 00:20:03
Done.
| |
| 23 """The metrics for page loading latency based on window.performance""" | |
| 24 | |
| 25 def __init__(self): | |
| 26 super(ChromeProxyLatency, self).__init__() | |
| 27 | |
| 28 def Start(self, page, tab): | |
| 29 raise NotImplementedError() | |
| 30 | |
| 31 def Stop(self, page, tab): | |
| 32 raise NotImplementedError() | |
| 33 | |
| 34 def AddResults(self, tab, results): | |
| 35 loading.LoadingMetric().AddResults(tab, results) | |
| 36 | |
| 37 load_timings = tab.EvaluateJavaScript('window.performance.timing') | |
| 38 # Navigation timing segments. Named after corresponding histograms. | |
| 39 # See chrome/renderer/page_load_histograms.cc. | |
| 40 nt_delay_before_fetch = (float(load_timings['fetchStart']) - | |
| 41 load_timings['navigationStart']) | |
| 42 results.Add('nt_delay_before_fetch', 'ms', nt_delay_before_fetch) | |
| 43 | |
| 44 nt_delay_before_request = (float(load_timings['requestStart']) - | |
| 45 load_timings['navigationStart']) | |
| 46 results.Add('nt_delay_before_request', 'ms', nt_delay_before_request) | |
| 47 | |
| 48 nt_domain_lookup = (float(load_timings['domainLookupEnd']) - | |
| 49 load_timings['domainLookupStart']) | |
| 50 results.Add('nt_domain_lookup', 'ms', nt_domain_lookup) | |
| 51 | |
| 52 nt_connect = (float(load_timings['connectEnd']) - | |
| 53 load_timings['connectStart']) | |
| 54 results.Add('nt_connect', 'ms', nt_connect) | |
| 55 | |
| 56 nt_request = (float(load_timings['responseStart']) - | |
| 57 load_timings['requestStart']) | |
| 58 results.Add('nt_request', 'ms', nt_request) | |
| 59 | |
| 60 nt_response = (float(load_timings['responseEnd']) - | |
| 61 load_timings['responseStart']) | |
| 62 results.Add('nt_response', 'ms', nt_response) | |
| 63 | |
| 64 | |
| 65 _CHROME_PROXY_VIA_HEADER = 'Chrome-Compression-Proxy' | |
| 66 _CHROME_PROXY_VIA_HEADER_OLD = '1.1 Chrome Compression Proxy' | |
| 67 | |
| 68 | |
| 69 class ChromeProxyResponse(object): | |
| 70 """ Represents an HTTP response from a timeleine event.""" | |
| 71 def __init__(self, event): | |
| 72 self._response = ( | |
| 73 inspector_network.InspectorNetworkResponseData.FromTimelineEvent(event)) | |
| 74 self._content_length = None | |
| 75 | |
| 76 @property | |
| 77 def response(self): | |
| 78 return self._response | |
| 79 | |
| 80 @property | |
| 81 def url_signature(self): | |
| 82 return hashlib.md5(self.response.url).hexdigest() | |
| 83 | |
| 84 @property | |
| 85 def content_length(self): | |
| 86 if self._content_length is None: | |
| 87 self._content_length = self.GetContentLength(self.response) | |
| 88 return self._content_length | |
| 89 | |
| 90 @property | |
| 91 def has_original_content_length(self): | |
| 92 return 'X-Original-Content-Length' in self.response.headers | |
| 93 | |
| 94 @property | |
| 95 def original_content_length(self): | |
| 96 if self.has_original_content_length: | |
| 97 return int(self.response.GetHeader('X-Original-Content-Length')) | |
| 98 return 0 | |
| 99 | |
| 100 @property | |
| 101 def data_saving_rate(self): | |
| 102 if (not self.has_original_content_length or | |
| 103 self.original_content_length <= 0): | |
| 104 return 0.0 | |
| 105 return (float(self.original_content_length - self.content_length) / | |
| 106 self.original_content_length) | |
| 107 | |
| 108 @staticmethod | |
| 109 def GetGizppedBodyLength(body): | |
| 110 if not body: | |
| 111 return 0 | |
| 112 bio = BytesIO() | |
| 113 try: | |
| 114 with gzip.GzipFile(fileobj=bio, mode="wb") as f: | |
| 115 f.write(body.encode('utf-8')) | |
| 116 except Exception, e: | |
| 117 logging.warning('Fail to gzip response body: %s', e) | |
| 118 raise e | |
| 119 return len(bio.getvalue()) | |
| 120 | |
| 121 @staticmethod | |
| 122 def ShouldGzipContent(content_type): | |
| 123 """Returns True if we need to gzip the content.""" | |
| 124 if not content_type: | |
| 125 return False | |
| 126 if 'text/' in content_type: | |
| 127 return True | |
| 128 if ('application/' in content_type and | |
| 129 ('javascript' in content_type or 'json' in content_type)): | |
| 130 return True | |
| 131 return False | |
| 132 | |
| 133 @staticmethod | |
| 134 def GetContentLengthFromBody(body, base64_encoded, content_type): | |
| 135 if not body: | |
| 136 return 0 | |
| 137 if base64_encoded: | |
| 138 decoded = base64.b64decode(body) | |
|
bengr
2014/03/12 18:48:27
Why do you decode a base64 encoded body?
bolian
2014/03/19 00:20:03
Added comments for that. The binary body (like tha
| |
| 139 return len(decoded) | |
| 140 else: | |
| 141 # Use gzipped content length if we can gzip the body based on | |
| 142 # Content-Type and the gzipped length is less than body length. | |
| 143 if ChromeProxyResponse.ShouldGzipContent(content_type): | |
|
bengr
2014/03/12 18:48:27
I don't understand. What if it is possible to gzip
bolian
2014/03/19 00:20:03
Fixed and added comments.
The logic now is that if
| |
| 144 gzipped = ChromeProxyResponse.GetGizppedBodyLength(body) | |
| 145 return gzipped if gzipped <= len(body) else len(body) | |
| 146 else: | |
| 147 return len(body) | |
| 148 | |
| 149 @staticmethod | |
| 150 def GetContentLength(resp): | |
| 151 cl = 0 | |
| 152 body, base64_encoded = resp.GetBody() | |
| 153 try: | |
| 154 cl = ChromeProxyResponse.GetContentLengthFromBody( | |
| 155 body, base64_encoded, resp.GetHeader('Content-Type')) | |
| 156 except Exception, e: | |
| 157 logging.warning('Fail to get content length for %s from body: %s', | |
| 158 resp.url[:100], e) | |
| 159 cl_header = resp.GetHeader('Content-Length') | |
| 160 if cl_header: | |
| 161 cl = int(cl_header) | |
| 162 elif body: | |
| 163 cl = len(body) | |
| 164 return cl | |
| 165 | |
| 166 @staticmethod | |
| 167 def ShouldHaveChromeProxyViaHeader(resp): | |
| 168 # Ignore https and data url | |
| 169 if resp.url.startswith('https') or resp.url.startswith('data:'): | |
|
bengr
2014/03/12 18:48:27
This reminds me. We should have an integration tes
bolian
2014/03/19 00:20:03
I think cached resource has Via header and "data"
| |
| 170 return False | |
| 171 # Ignore 304 Not Modified. | |
| 172 if resp.status == 304: | |
| 173 return False | |
| 174 return True | |
| 175 | |
| 176 @staticmethod | |
| 177 def HasChromeProxyViaHeader(resp): | |
| 178 via_header = resp.GetHeader('Via') | |
| 179 if not via_header: | |
| 180 return False | |
| 181 vias = [v.strip(' ') for v in via_header.split(',')] | |
| 182 # The Via header is valid if it is the old format or the new format | |
| 183 # with 4-character version prefix, for example, | |
| 184 # "1.1 Chrome-Compression-Proxy". | |
| 185 return (_CHROME_PROXY_VIA_HEADER_OLD in vias or | |
| 186 any(v[4:] == _CHROME_PROXY_VIA_HEADER for v in vias)) | |
| 187 | |
| 188 def IsValidByViaHeader(self): | |
|
bengr
2014/03/12 18:48:27
I don't know why you need a function like this one
bolian
2014/03/19 00:20:03
Yes, I am using this to tell whether Chrome proxy
| |
| 189 return (not self.ShouldHaveChromeProxyViaHeader(self.response) or | |
| 190 self.HasChromeProxyViaHeader(self.response)) | |
| 191 | |
| 192 def IsSafebrowsingResponse(self): | |
| 193 if (self.response.status == 307 and | |
| 194 self.response.GetHeader('X-Malware-Url') == '1' and | |
| 195 self.IsValidByViaHeader() and | |
| 196 self.response.GetHeader('Location') == self.response.url): | |
| 197 return True | |
| 198 return False | |
| 199 | |
| 200 | |
| 201 class ChromeProxyTimelineMetrics(Metric): | |
| 202 """A Chrome proxy timeline metric.""" | |
| 203 | |
| 204 def __init__(self): | |
| 205 super(ChromeProxyTimelineMetrics, self).__init__() | |
| 206 | |
| 207 # Whether to add detailed result for each sub-resource in a page. | |
| 208 self.add_result_for_resource = False | |
| 209 self._events = None | |
| 210 | |
| 211 def Start(self, page, tab): | |
| 212 self._events = None | |
| 213 opts = recording_options.TimelineRecordingOptions() | |
| 214 opts.record_network = True | |
| 215 tab.StartTimelineRecording(opts) | |
| 216 | |
| 217 def Stop(self, page, tab): | |
| 218 assert self._events is None | |
| 219 tab.StopTimelineRecording() | |
| 220 | |
| 221 def AddResults(self, tab, results): | |
| 222 raise NotImplementedError | |
| 223 | |
| 224 def IterResponses(self, tab): | |
| 225 if self._events is None: | |
| 226 self._events = tab.timeline_model.GetAllEventsOfName('HTTPResponse') | |
| 227 if len(self._events) == 0: | |
| 228 return | |
| 229 for e in self._events: | |
| 230 yield ChromeProxyResponse(e) | |
| 231 | |
| 232 def AddResultsForDataSaving(self, tab, results): | |
| 233 resources_via_proxy = 0 | |
| 234 resources_from_cache = 0 | |
| 235 resources_other = 0 | |
| 236 content_length = 0 | |
| 237 original_content_length = 0 | |
| 238 | |
| 239 for resp in self.IterResponses(tab): | |
| 240 if resp.response.served_from_cache: | |
| 241 resources_from_cache += 1 | |
| 242 continue | |
| 243 if ChromeProxyResponse.HasChromeProxyViaHeader(resp.response): | |
| 244 resources_via_proxy += 1 | |
| 245 else: | |
| 246 resources_other += 1 | |
| 247 | |
| 248 resource = resp.response.url | |
| 249 resource_signature = resp.url_signature | |
| 250 cl = resp.content_length | |
| 251 if resp.has_original_content_length: | |
| 252 ocl = resp.original_content_length | |
| 253 if ocl < cl: | |
| 254 logging.warning('original content length (%d) is less than content ' | |
| 255 'lenght(%d) for resource %s', ocl, cl, resource) | |
| 256 if self.add_result_for_resource: | |
| 257 results.Add('resource_data_saving_' + resource_signature, | |
| 258 'percent', resp.data_saving_rate * 100) | |
| 259 results.Add('resource_original_content_length_' + resource_signature, | |
| 260 'bytes', ocl) | |
| 261 original_content_length += ocl | |
| 262 else: | |
| 263 original_content_length += cl | |
| 264 if self.add_result_for_resource: | |
| 265 results.Add('resource_content_length_' + resource_signature, | |
| 266 'bytes', cl) | |
| 267 content_length += cl | |
| 268 | |
| 269 results.Add('resources_via_proxy', 'count', resources_via_proxy) | |
| 270 results.Add('resources_from_cache', 'count', resources_from_cache) | |
| 271 results.Add('resources_other', 'count', resources_other) | |
| 272 results.Add('content_length', 'bytes', content_length) | |
| 273 results.Add('original_content_length', 'bytes', original_content_length) | |
|
tonyg
2014/03/12 02:08:37
Let's generalize this a bit and have a Network(Met
bolian
2014/03/19 00:20:03
Done. Added a new network metric.
| |
| 274 if (original_content_length > 0 and | |
| 275 original_content_length >= content_length): | |
| 276 saving = (float(original_content_length-content_length) / | |
| 277 original_content_length * 100) | |
| 278 results.Add('data_saving', 'percent', saving) | |
| 279 else: | |
| 280 results.Add('data_saving', 'percent', 0.0) | |
| 281 | |
| 282 def AddResultsForHeaderValidation(self, tab, results): | |
| 283 via_count = 0 | |
| 284 for resp in self.IterResponses(tab): | |
| 285 if resp.IsValidByViaHeader(): | |
| 286 via_count += 1 | |
| 287 else: | |
| 288 r = resp.response | |
| 289 raise ChromeProxyMetricException, ( | |
| 290 '%s: Via header (%s) is not valid (refer=%s, status=%d)' % ( | |
| 291 r.url, r.GetHeader('Via'), r.GetHeader('Referer'), r.status)) | |
| 292 results.Add('checked_via_header', 'count', via_count) | |
| 293 | |
| 294 def AddResultsForBypass(self, tab, results): | |
| 295 bypass_count = 0 | |
| 296 for resp in self.IterResponses(tab): | |
| 297 r = resp.response | |
| 298 if ChromeProxyResponse.HasChromeProxyViaHeader(r): | |
| 299 raise ChromeProxyMetricException, ( | |
| 300 '%s: Should not have Via header (%s) (refer=%s, status=%d)' % ( | |
| 301 r.url, r.GetHeader('Via'), r.GetHeader('Referer'), r.status)) | |
| 302 bypass_count += 1 | |
| 303 results.Add('bypass_count', 'count', bypass_count) | |
| 304 | |
| 305 def AddResultsForSafebrowsing(self, tab, results): | |
| 306 count = 0 | |
| 307 safebrowsing_count = 0 | |
| 308 for resp in self.IterResponses(tab): | |
| 309 count += 1 | |
| 310 if resp.IsSafebrowsingResponse(): | |
| 311 safebrowsing_count += 1 | |
| 312 else: | |
| 313 r = resp.response | |
| 314 raise ChromeProxyMetricException, ( | |
| 315 '%s: Not a valid safe browsing response.\n' | |
| 316 'Reponse: status=(%d, %s)\nHeaders:\n %s' % ( | |
| 317 r.url, r.status, r.status_text, r.headers)) | |
| 318 if count == safebrowsing_count: | |
| 319 results.Add('safebrowsing', 'boolean', True) | |
| 320 else: | |
| 321 raise ChromeProxyMetricException, ( | |
| 322 'Safebrowsing failed (count=%d, safebrowsing_count=%d)\n' % ( | |
| 323 count, safebrowsing_count)) | |
| OLD | NEW |