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_test | |
14 # All network metrics are Chrome only for now. | |
15 from telemetry.core.backends.chrome_inspector import inspector_network | |
16 from telemetry.value import scalar | |
17 | |
18 | |
19 class NetworkMetricException(page_test.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._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 | |
32 | |
33 @property | |
34 def response(self): | |
35 return self._response | |
36 | |
37 @property | |
38 def remote_port(self): | |
39 return self._remote_port | |
40 | |
41 @property | |
42 def url_signature(self): | |
43 return hashlib.md5(self.response.url).hexdigest() | |
44 | |
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 | |
50 | |
51 @property | |
52 def has_original_content_length(self): | |
53 return 'X-Original-Content-Length' in self.response.headers | |
54 | |
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 | |
60 | |
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) | |
69 | |
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) | |
80 | |
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)) | |
95 | |
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 | |
113 | |
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()) | |
126 | |
127 | |
128 class NetworkMetric(Metric): | |
129 """A network metric based on timeline events.""" | |
130 | |
131 def __init__(self): | |
132 super(NetworkMetric, self).__init__() | |
133 | |
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 | |
138 | |
139 def Start(self, page, tab): | |
140 self._events = None | |
141 tab.StartTimelineRecording() | |
142 | |
143 def Stop(self, page, tab): | |
144 assert self._events is None | |
145 tab.StopTimelineRecording() | |
146 | |
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) | |
154 | |
155 def ResponseFromEvent(self, event): | |
156 return HTTPResponse(event) | |
157 | |
158 def AddResults(self, tab, results): | |
159 content_length = 0 | |
160 original_content_length = 0 | |
161 | |
162 for resp in self.IterResponses(tab): | |
163 # Ignore content length calculation for cache hit. | |
164 if resp.response.served_from_cache: | |
165 continue | |
166 | |
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 | |
192 | |
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)) | |
OLD | NEW |