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.
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
):
23 class HTTPResponse(object):
24 """ Represents an HTTP response from a timeline event."""
25 def __init__(self
, event
):
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
38 def remote_port(self
):
39 return self
._remote
_port
42 def url_signature(self
):
43 return hashlib
.md5(self
.response
.url
).hexdigest()
46 def content_length(self
):
47 if self
._content
_length
is None:
48 self
._content
_length
= self
.GetContentLength()
49 return self
._content
_length
52 def has_original_content_length(self
):
53 return 'X-Original-Content-Length' in self
.response
.headers
56 def original_content_length(self
):
57 if self
.has_original_content_length
:
58 return int(self
.response
.GetHeader('X-Original-Content-Length'))
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):
67 return (float(self
.original_content_length
- self
.content_length
) /
68 self
.original_content_length
)
70 def GetContentLengthFromBody(self
):
72 body
, base64_encoded
= resp
.GetBody()
75 # The binary data like images, etc is base64_encoded. Decode it to get
76 # the actualy content length.
78 decoded
= base64
.b64decode(body
)
81 encoding
= resp
.GetHeader('Content-Encoding')
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
87 encoding
= encoding
.lower()
88 if encoding
== 'gzip':
89 return self
.GetGizppedBodyLength(body
)
90 elif encoding
== 'deflate':
91 return len(zlib
.compress(body
, 9))
93 raise NetworkMetricException
, (
94 'Unknown Content-Encoding %s for %s' % (encoding
, resp
.url
))
96 def GetContentLength(self
):
99 cl
= self
.GetContentLengthFromBody()
101 logging
.warning('Fail to get content length for %s from body: %s',
102 self
.response
.url
[:100], e
)
105 cl_header
= resp
.GetHeader('Content-Length')
109 body
, _
= resp
.GetBody()
115 def GetGizppedBodyLength(body
):
120 with gzip
.GzipFile(fileobj
=bio
, mode
="wb", compresslevel
=9) as f
:
121 f
.write(body
.encode('utf-8'))
123 logging
.warning('Fail to gzip response body: %s', e
)
125 return len(bio
.getvalue())
128 class NetworkMetric(Metric
):
129 """A network metric based on timeline events."""
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
139 def Start(self
, page
, tab
):
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:
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
):
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
:
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
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',
184 original_content_length
+= ocl
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
))
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
))
206 results
.AddValue(scalar
.ScalarValue(
207 results
.current_page
, 'data_saving', 'percent', 0.0))