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.
8 from integration_tests
import network_metrics
9 from telemetry
.page
import page_test
10 from telemetry
.value
import scalar
13 class ChromeProxyMetricException(page_test
.MeasurementFailure
):
17 CHROME_PROXY_VIA_HEADER
= 'Chrome-Compression-Proxy'
20 class ChromeProxyResponse(network_metrics
.HTTPResponse
):
21 """ Represents an HTTP response from a timeleine event."""
22 def __init__(self
, event
):
23 super(ChromeProxyResponse
, self
).__init
__(event
)
25 def ShouldHaveChromeProxyViaHeader(self
):
27 # Ignore https and data url
28 if resp
.url
.startswith('https') or resp
.url
.startswith('data:'):
30 # Ignore 304 Not Modified and cache hit.
31 if resp
.status
== 304 or resp
.served_from_cache
:
33 # Ignore invalid responses that don't have any header. Log a warning.
35 logging
.warning('response for %s does not any have header '
36 '(refer=%s, status=%s)',
37 resp
.url
, resp
.GetHeader('Referer'), resp
.status
)
41 def HasChromeProxyViaHeader(self
):
42 via_header
= self
.response
.GetHeader('Via')
45 vias
= [v
.strip(' ') for v
in via_header
.split(',')]
46 # The Via header is valid if it has a 4-character version prefix followed by
47 # the proxy name, for example, "1.1 Chrome-Compression-Proxy".
48 return any(v
[4:] == CHROME_PROXY_VIA_HEADER
for v
in vias
)
50 def IsValidByViaHeader(self
):
51 return (not self
.ShouldHaveChromeProxyViaHeader() or
52 self
.HasChromeProxyViaHeader())
54 def GetChromeProxyClientType(self
):
55 """Get the client type directive from the Chrome-Proxy request header.
58 The client type directive from the Chrome-Proxy request header for the
59 request that lead to this response. For example, if the request header
60 "Chrome-Proxy: c=android" is present, then this method would return
61 "android". Returns None if no client type directive is present.
63 if 'Chrome-Proxy' not in self
.response
.request_headers
:
66 chrome_proxy_request_header
= self
.response
.request_headers
['Chrome-Proxy']
67 values
= [v
.strip() for v
in chrome_proxy_request_header
.split(',')]
69 kvp
= value
.split('=', 1)
70 if len(kvp
) == 2 and kvp
[0].strip() == 'c':
74 def HasChromeProxyLoFi(self
):
75 if 'Chrome-Proxy' not in self
.response
.request_headers
:
77 chrome_proxy_request_header
= self
.response
.request_headers
['Chrome-Proxy']
78 values
= [v
.strip() for v
in chrome_proxy_request_header
.split(',')]
80 if len(value
) == 5 and value
== 'q=low':
84 class ChromeProxyMetric(network_metrics
.NetworkMetric
):
85 """A Chrome proxy timeline metric."""
88 super(ChromeProxyMetric
, self
).__init
__()
89 self
.compute_data_saving
= True
91 def SetEvents(self
, events
):
92 """Used for unittest."""
95 def ResponseFromEvent(self
, event
):
96 return ChromeProxyResponse(event
)
98 def AddResults(self
, tab
, results
):
99 raise NotImplementedError
101 def AddResultsForDataSaving(self
, tab
, results
):
102 resources_via_proxy
= 0
103 resources_from_cache
= 0
106 super(ChromeProxyMetric
, self
).AddResults(tab
, results
)
107 for resp
in self
.IterResponses(tab
):
108 if resp
.response
.served_from_cache
:
109 resources_from_cache
+= 1
110 if resp
.HasChromeProxyViaHeader():
111 resources_via_proxy
+= 1
113 resources_direct
+= 1
115 if resources_from_cache
+ resources_via_proxy
+ resources_direct
== 0:
116 raise ChromeProxyMetricException
, (
117 'Expected at least one response, but zero responses were received.')
119 results
.AddValue(scalar
.ScalarValue(
120 results
.current_page
, 'resources_via_proxy', 'count',
121 resources_via_proxy
))
122 results
.AddValue(scalar
.ScalarValue(
123 results
.current_page
, 'resources_from_cache', 'count',
124 resources_from_cache
))
125 results
.AddValue(scalar
.ScalarValue(
126 results
.current_page
, 'resources_direct', 'count', resources_direct
))
128 def AddResultsForHeaderValidation(self
, tab
, results
):
131 for resp
in self
.IterResponses(tab
):
132 if resp
.IsValidByViaHeader():
136 raise ChromeProxyMetricException
, (
137 '%s: Via header (%s) is not valid (refer=%s, status=%d)' % (
138 r
.url
, r
.GetHeader('Via'), r
.GetHeader('Referer'), r
.status
))
141 raise ChromeProxyMetricException
, (
142 'Expected at least one response through the proxy, but zero such '
143 'responses were received.')
144 results
.AddValue(scalar
.ScalarValue(
145 results
.current_page
, 'checked_via_header', 'count', via_count
))
147 def AddResultsForLatency(self
, tab
, results
):
148 # TODO(bustamante): This is a hack to workaround crbug.com/467174,
149 # once fixed just pull down window.performance.timing object and
150 # reference that everywhere.
151 load_event_start
= tab
.EvaluateJavaScript(
152 'window.performance.timing.loadEventStart')
153 navigation_start
= tab
.EvaluateJavaScript(
154 'window.performance.timing.navigationStart')
155 dom_content_loaded_event_start
= tab
.EvaluateJavaScript(
156 'window.performance.timing.domContentLoadedEventStart')
157 fetch_start
= tab
.EvaluateJavaScript(
158 'window.performance.timing.fetchStart')
159 request_start
= tab
.EvaluateJavaScript(
160 'window.performance.timing.requestStart')
161 domain_lookup_end
= tab
.EvaluateJavaScript(
162 'window.performance.timing.domainLookupEnd')
163 domain_lookup_start
= tab
.EvaluateJavaScript(
164 'window.performance.timing.domainLookupStart')
165 connect_end
= tab
.EvaluateJavaScript(
166 'window.performance.timing.connectEnd')
167 connect_start
= tab
.EvaluateJavaScript(
168 'window.performance.timing.connectStart')
169 response_end
= tab
.EvaluateJavaScript(
170 'window.performance.timing.responseEnd')
171 response_start
= tab
.EvaluateJavaScript(
172 'window.performance.timing.responseStart')
174 # NavigationStart relative markers in milliseconds.
175 load_start
= (float(load_event_start
) - navigation_start
)
176 results
.AddValue(scalar
.ScalarValue(
177 results
.current_page
, 'load_start', 'ms', load_start
))
179 dom_content_loaded_start
= (
180 float(dom_content_loaded_event_start
) - navigation_start
)
181 results
.AddValue(scalar
.ScalarValue(
182 results
.current_page
, 'dom_content_loaded_start', 'ms',
183 dom_content_loaded_start
))
185 fetch_start
= (float(fetch_start
) - navigation_start
)
186 results
.AddValue(scalar
.ScalarValue(
187 results
.current_page
, 'fetch_start', 'ms', fetch_start
,
190 request_start
= (float(request_start
) - navigation_start
)
191 results
.AddValue(scalar
.ScalarValue(
192 results
.current_page
, 'request_start', 'ms', request_start
,
195 # Phase measurements in milliseconds.
196 domain_lookup_duration
= (float(domain_lookup_end
) - domain_lookup_start
)
197 results
.AddValue(scalar
.ScalarValue(
198 results
.current_page
, 'domain_lookup_duration', 'ms',
199 domain_lookup_duration
, important
=False))
201 connect_duration
= (float(connect_end
) - connect_start
)
202 results
.AddValue(scalar
.ScalarValue(
203 results
.current_page
, 'connect_duration', 'ms', connect_duration
,
206 request_duration
= (float(response_start
) - request_start
)
207 results
.AddValue(scalar
.ScalarValue(
208 results
.current_page
, 'request_duration', 'ms', request_duration
,
211 response_duration
= (float(response_end
) - response_start
)
212 results
.AddValue(scalar
.ScalarValue(
213 results
.current_page
, 'response_duration', 'ms', response_duration
,
216 def AddResultsForClientVersion(self
, tab
, results
):
218 for resp
in self
.IterResponses(tab
):
220 if resp
.response
.status
!= 200:
221 raise ChromeProxyMetricException
, ('%s: Response is not 200: %d' %
223 if not resp
.IsValidByViaHeader():
224 raise ChromeProxyMetricException
, ('%s: Response missing via header' %
229 raise ChromeProxyMetricException
, (
230 'Expected at least one response through the proxy, but zero such '
231 'responses were received.')
232 results
.AddValue(scalar
.ScalarValue(
233 results
.current_page
, 'responses_via_proxy', 'count', via_count
))
235 def GetClientTypeFromRequests(self
, tab
):
236 """Get the Chrome-Proxy client type value from requests made in this tab.
239 The client type value from the first request made in this tab that
240 specifies a client type in the Chrome-Proxy request header. See
241 ChromeProxyResponse.GetChromeProxyClientType for more details about the
242 Chrome-Proxy client type. Returns None if none of the requests made in
243 this tab specify a client type.
245 for resp
in self
.IterResponses(tab
):
246 client_type
= resp
.GetChromeProxyClientType()
251 def AddResultsForClientType(self
, tab
, results
, client_type
,
252 bypass_for_client_type
):
256 for resp
in self
.IterResponses(tab
):
257 if resp
.HasChromeProxyViaHeader():
259 if client_type
.lower() == bypass_for_client_type
.lower():
260 raise ChromeProxyMetricException
, (
261 '%s: Response for client of type "%s" has via header, but should '
263 resp
.response
.url
, bypass_for_client_type
, client_type
))
264 elif resp
.ShouldHaveChromeProxyViaHeader():
266 if client_type
.lower() != bypass_for_client_type
.lower():
267 raise ChromeProxyMetricException
, (
268 '%s: Response missing via header. Only "%s" clients should '
269 'bypass for this page, but this client is "%s".' % (
270 resp
.response
.url
, bypass_for_client_type
, client_type
))
272 if via_count
+ bypass_count
== 0:
273 raise ChromeProxyMetricException
, (
274 'Expected at least one response that was eligible to be proxied, but '
275 'zero such responses were received.')
277 results
.AddValue(scalar
.ScalarValue(
278 results
.current_page
, 'via', 'count', via_count
))
279 results
.AddValue(scalar
.ScalarValue(
280 results
.current_page
, 'bypass', 'count', bypass_count
))
282 def AddResultsForLoFi(self
, tab
, results
):
285 for resp
in self
.IterResponses(tab
):
286 if resp
.HasChromeProxyLoFi():
289 raise ChromeProxyMetricException
, (
290 '%s: LoFi not in request header.' % (resp
.response
.url
))
292 if resp
.content_length
> 100:
293 raise ChromeProxyMetricException
, (
294 'Image %s is %d bytes. Expecting less than 100 bytes.' %
295 (resp
.response
.url
, resp
.content_length
))
298 raise ChromeProxyMetricException
, (
299 'Expected at least one LoFi response, but zero such responses were '
302 results
.AddValue(scalar
.ScalarValue(
303 results
.current_page
, 'lo_fi', 'count', lo_fi_count
))
304 super(ChromeProxyMetric
, self
).AddResults(tab
, results
)
306 def AddResultsForBypass(self
, tab
, results
):
309 for resp
in self
.IterResponses(tab
):
310 if resp
.HasChromeProxyViaHeader():
312 raise ChromeProxyMetricException
, (
313 '%s: Should not have Via header (%s) (refer=%s, status=%d)' % (
314 r
.url
, r
.GetHeader('Via'), r
.GetHeader('Referer'), r
.status
))
317 if bypass_count
== 0:
318 raise ChromeProxyMetricException
, (
319 'Expected at least one response to be bypassed, but zero such '
320 'responses were received.')
321 results
.AddValue(scalar
.ScalarValue(
322 results
.current_page
, 'bypass', 'count', bypass_count
))
324 def AddResultsForCorsBypass(self
, tab
, results
):
325 eligible_response_count
= 0
328 for resp
in self
.IterResponses(tab
):
329 logging
.warn('got a resource %s' % (resp
.response
.url
))
331 for resp
in self
.IterResponses(tab
):
332 if resp
.ShouldHaveChromeProxyViaHeader():
333 eligible_response_count
+= 1
334 if not resp
.HasChromeProxyViaHeader():
336 elif resp
.response
.status
== 502:
337 bypasses
[resp
.response
.url
] = 0
339 for resp
in self
.IterResponses(tab
):
340 if resp
.ShouldHaveChromeProxyViaHeader():
341 if not resp
.HasChromeProxyViaHeader():
342 if resp
.response
.status
== 200:
343 if (bypasses
.has_key(resp
.response
.url
)):
344 bypasses
[resp
.response
.url
] = bypasses
[resp
.response
.url
] + 1
347 if bypasses
[url
] == 0:
348 raise ChromeProxyMetricException
, (
349 '%s: Got a 502 without a subsequent 200' % (url
))
350 elif bypasses
[url
] > 1:
351 raise ChromeProxyMetricException
, (
352 '%s: Got a 502 and multiple 200s: %d' % (url
, bypasses
[url
]))
353 if bypass_count
== 0:
354 raise ChromeProxyMetricException
, (
355 'At least one response should be bypassed. '
356 '(eligible_response_count=%d, bypass_count=%d)\n' % (
357 eligible_response_count
, bypass_count
))
359 results
.AddValue(scalar
.ScalarValue(
360 results
.current_page
, 'cors_bypass', 'count', bypass_count
))
362 def AddResultsForBlockOnce(self
, tab
, results
):
363 eligible_response_count
= 0
366 for resp
in self
.IterResponses(tab
):
367 if resp
.ShouldHaveChromeProxyViaHeader():
368 eligible_response_count
+= 1
369 if not resp
.HasChromeProxyViaHeader():
372 if eligible_response_count
<= 1:
373 raise ChromeProxyMetricException
, (
374 'There should be more than one DRP eligible response '
375 '(eligible_response_count=%d, bypass_count=%d)\n' % (
376 eligible_response_count
, bypass_count
))
377 elif bypass_count
!= 1:
378 raise ChromeProxyMetricException
, (
379 'Exactly one response should be bypassed. '
380 '(eligible_response_count=%d, bypass_count=%d)\n' % (
381 eligible_response_count
, bypass_count
))
383 results
.AddValue(scalar
.ScalarValue(
384 results
.current_page
, 'eligible_responses', 'count',
385 eligible_response_count
))
386 results
.AddValue(scalar
.ScalarValue(
387 results
.current_page
, 'bypass', 'count', bypass_count
))
389 def AddResultsForSafebrowsingOn(self
, tab
, results
):
390 results
.AddValue(scalar
.ScalarValue(
391 results
.current_page
, 'safebrowsing', 'timeout responses', 1))
393 def AddResultsForSafebrowsingOff(self
, tab
, results
):
395 for resp
in self
.IterResponses(tab
):
396 # Data reduction proxy should return the real response for sites with
399 if not resp
.HasChromeProxyViaHeader():
401 raise ChromeProxyMetricException
, (
402 '%s: Safebrowsing feature should be off for desktop and webview.\n'
403 'Reponse: status=(%d, %s)\nHeaders:\n %s' % (
404 r
.url
, r
.status
, r
.status_text
, r
.headers
))
406 if response_count
== 0:
407 raise ChromeProxyMetricException
, (
408 'Safebrowsing test failed: No valid responses received')
410 results
.AddValue(scalar
.ScalarValue(
411 results
.current_page
, 'safebrowsing', 'responses', response_count
))
413 def AddResultsForHTTPFallback(self
, tab
, results
):
414 via_fallback_count
= 0
416 for resp
in self
.IterResponses(tab
):
417 if resp
.ShouldHaveChromeProxyViaHeader():
418 # All responses should have come through the HTTP fallback proxy, which
419 # means that they should have the via header, and if a remote port is
420 # defined, it should be port 80.
421 if (not resp
.HasChromeProxyViaHeader() or
422 (resp
.remote_port
and resp
.remote_port
!= 80)):
424 raise ChromeProxyMetricException
, (
425 '%s: Should have come through the fallback proxy.\n'
426 'Reponse: remote_port=%s status=(%d, %s)\nHeaders:\n %s' % (
427 r
.url
, str(resp
.remote_port
), r
.status
, r
.status_text
,
429 via_fallback_count
+= 1
431 if via_fallback_count
== 0:
432 raise ChromeProxyMetricException
, (
433 'Expected at least one response through the fallback proxy, but zero '
434 'such responses were received.')
435 results
.AddValue(scalar
.ScalarValue(
436 results
.current_page
, 'via_fallback', 'count', via_fallback_count
))
438 def AddResultsForHTTPToDirectFallback(self
, tab
, results
,
439 fallback_response_host
):
440 via_fallback_count
= 0
442 responses
= self
.IterResponses(tab
)
444 # The first response(s) coming from fallback_response_host should be
445 # through the HTTP fallback proxy.
446 resp
= next(responses
, None)
447 while resp
and fallback_response_host
in resp
.response
.url
:
448 if fallback_response_host
in resp
.response
.url
:
449 if (not resp
.HasChromeProxyViaHeader() or resp
.remote_port
!= 80):
451 raise ChromeProxyMetricException
, (
452 'Response for %s should have come through the fallback proxy.\n'
453 'Response: remote_port=%s status=(%d, %s)\nHeaders:\n %s' % (
454 r
.url
, str(resp
.remote_port
), r
.status
, r
.status_text
,
457 via_fallback_count
+= 1
458 resp
= next(responses
, None)
460 # All other responses should be bypassed.
462 if resp
.HasChromeProxyViaHeader():
464 raise ChromeProxyMetricException
, (
465 'Response for %s should not have via header.\n'
466 'Response: status=(%d, %s)\nHeaders:\n %s' % (
467 r
.url
, r
.status
, r
.status_text
, r
.headers
))
470 resp
= next(responses
, None)
472 # At least one response should go through the http proxy and be bypassed.
473 if via_fallback_count
== 0 or bypass_count
== 0:
474 raise ChromeProxyMetricException(
475 'There should be at least one response through the fallback proxy '
476 '(actual %s) and at least one bypassed response (actual %s)' %
477 (via_fallback_count
, bypass_count
))
479 results
.AddValue(scalar
.ScalarValue(
480 results
.current_page
, 'via_fallback', 'count', via_fallback_count
))
481 results
.AddValue(scalar
.ScalarValue(
482 results
.current_page
, 'bypass', 'count', bypass_count
))
484 def AddResultsForReenableAfterBypass(
485 self
, tab
, results
, bypass_seconds_min
, bypass_seconds_max
):
486 """Verify results for a re-enable after bypass test.
489 tab: the tab for the test.
490 results: the results object to add the results values to.
491 bypass_seconds_min: the minimum duration of the bypass.
492 bypass_seconds_max: the maximum duration of the bypass.
497 for resp
in self
.IterResponses(tab
):
498 if resp
.HasChromeProxyViaHeader():
500 raise ChromeProxyMetricException
, (
501 'Response for %s should not have via header.\n'
502 'Reponse: status=(%d, %s)\nHeaders:\n %s' % (
503 r
.url
, r
.status
, r
.status_text
, r
.headers
))
507 # Wait until 30 seconds before the bypass should expire, and fetch a page.
508 # It should not have the via header because the proxy should still be
510 time
.sleep(bypass_seconds_min
- 30)
512 tab
.ClearCache(force
=True)
513 before_metrics
= ChromeProxyMetric()
514 before_metrics
.Start(results
.current_page
, tab
)
515 tab
.Navigate('http://chromeproxy-test.appspot.com/default')
516 tab
.WaitForJavaScriptExpression('performance.timing.loadEventStart', 10)
517 before_metrics
.Stop(results
.current_page
, tab
)
519 for resp
in before_metrics
.IterResponses(tab
):
520 if resp
.HasChromeProxyViaHeader():
522 raise ChromeProxyMetricException
, (
523 'Response for %s should not have via header; proxy should still '
524 'be bypassed.\nReponse: status=(%d, %s)\nHeaders:\n %s' % (
525 r
.url
, r
.status
, r
.status_text
, r
.headers
))
528 if bypass_count
== 0:
529 raise ChromeProxyMetricException
, (
530 'Expected at least one response to be bypassed before the bypass '
531 'expired, but zero such responses were received.')
533 # Wait until 30 seconds after the bypass should expire, and fetch a page. It
534 # should have the via header since the proxy should no longer be bypassed.
535 time
.sleep((bypass_seconds_max
+ 30) - (bypass_seconds_min
- 30))
537 tab
.ClearCache(force
=True)
538 after_metrics
= ChromeProxyMetric()
539 after_metrics
.Start(results
.current_page
, tab
)
540 tab
.Navigate('http://chromeproxy-test.appspot.com/default')
541 tab
.WaitForJavaScriptExpression('performance.timing.loadEventStart', 10)
542 after_metrics
.Stop(results
.current_page
, tab
)
544 for resp
in after_metrics
.IterResponses(tab
):
545 if not resp
.HasChromeProxyViaHeader():
547 raise ChromeProxyMetricException
, (
548 'Response for %s should have via header; proxy should no longer '
549 'be bypassed.\nReponse: status=(%d, %s)\nHeaders:\n %s' % (
550 r
.url
, r
.status
, r
.status_text
, r
.headers
))
554 raise ChromeProxyMetricException
, (
555 'Expected at least one response through the proxy after the bypass '
556 'expired, but zero such responses were received.')
558 results
.AddValue(scalar
.ScalarValue(
559 results
.current_page
, 'bypass', 'count', bypass_count
))
560 results
.AddValue(scalar
.ScalarValue(
561 results
.current_page
, 'via', 'count', via_count
))