OLD | NEW |
| (Empty) |
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 from telemetry.value import scalar | |
18 | |
19 | |
20 class NetworkMetricException(page_measurement.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)) | |
OLD | NEW |