1# Copyright (c) 2013 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.
4import os
5import time
6
7import context_lost_expectations
8
9from telemetry import benchmark as benchmark_module
10from telemetry.core import exceptions
11from telemetry.core import util
12from telemetry.page import page
13from telemetry.page import page_set
14from telemetry.page import page_test
15
16data_path = os.path.join(
17    util.GetChromiumSrcDir(), 'content', 'test', 'data', 'gpu')
18
19wait_timeout = 20  # seconds
20
21harness_script = r"""
22  var domAutomationController = {};
23
24  domAutomationController._loaded = false;
25  domAutomationController._succeeded = false;
26  domAutomationController._finished = false;
27
28  domAutomationController.setAutomationId = function(id) {}
29
30  domAutomationController.send = function(msg) {
31    msg = msg.toLowerCase()
32    if (msg == "loaded") {
33      domAutomationController._loaded = true;
34    } else if (msg == "success") {
35      domAutomationController._succeeded = true;
36      domAutomationController._finished = true;
37    } else {
38      domAutomationController._succeeded = false;
39      domAutomationController._finished = true;
40    }
41  }
42
43  domAutomationController.reset = function() {
44    domAutomationController._succeeded = false;
45    domAutomationController._finished = false;
46  }
47
48  window.domAutomationController = domAutomationController;
49  console.log("Harness injected.");
50"""
51
52class _ContextLostValidator(page_test.PageTest):
53  def __init__(self):
54    # Strictly speaking this test doesn't yet need a browser restart
55    # after each run, but if more tests are added which crash the GPU
56    # process, then it will.
57    super(_ContextLostValidator, self).__init__(
58      needs_browser_restart_after_each_page=True)
59
60  def CustomizeBrowserOptions(self, options):
61    options.AppendExtraBrowserArgs(
62        '--disable-domain-blocking-for-3d-apis')
63    options.AppendExtraBrowserArgs(
64        '--disable-gpu-process-crash-limit')
65    # Required for about:gpucrash handling from Telemetry.
66    options.AppendExtraBrowserArgs('--enable-gpu-benchmarking')
67
68  def ValidateAndMeasurePage(self, page, tab, results):
69    def WaitForPageToFinish():
70      print "Waiting for page to finish."
71      try:
72        util.WaitFor(lambda: tab.EvaluateJavaScript(
73            'window.domAutomationController._finished'), wait_timeout)
74        return True
75      except util.TimeoutException:
76        return False
77
78    if page.kill_gpu_process:
79      # Doing the GPU process kill operation cooperatively -- in the
80      # same page's context -- is much more stressful than restarting
81      # the browser every time.
82      for x in range(page.number_of_gpu_process_kills):
83        if not tab.browser.supports_tab_control:
84          raise page_test.Failure('Browser must support tab control')
85
86        expected_kills = x + 1
87
88        # Reset the test's state.
89        tab.EvaluateJavaScript(
90          'window.domAutomationController.reset()');
91
92        # If we're running the GPU process crash test, we need the
93        # test to have fully reset before crashing the GPU process.
94        if page.check_crash_count:
95          util.WaitFor(lambda: tab.EvaluateJavaScript(
96              'window.domAutomationController._finished'), wait_timeout)
97
98        # Crash the GPU process.
99        gpucrash_tab = tab.browser.tabs.New()
100        # To access these debug URLs from Telemetry, they have to be
101        # written using the chrome:// scheme.
102        # The try/except is a workaround for crbug.com/368107.
103        try:
104          gpucrash_tab.Navigate('chrome://gpucrash')
105        except (exceptions.TabCrashException, Exception):
106          print 'Tab crashed while navigating to chrome://gpucrash'
107        # Activate the original tab and wait for completion.
108        tab.Activate()
109        completed = WaitForPageToFinish()
110
111        if page.check_crash_count:
112          if not tab.browser.supports_system_info:
113            raise page_test.Failure('Browser must support system info')
114
115          if not tab.EvaluateJavaScript(
116            'window.domAutomationController._succeeded'):
117            raise page_test.Failure(
118              'Test failed (didn\'t render content properly?)')
119
120          number_of_crashes = -1
121          # To allow time for a gpucrash to complete, wait up to 20s,
122          # polling repeatedly.
123          start_time = time.time()
124          current_time = time.time()
125          while current_time - start_time < 20:
126            system_info = tab.browser.GetSystemInfo()
127            number_of_crashes = \
128                system_info.gpu.aux_attributes[u'process_crash_count']
129            if number_of_crashes >= expected_kills:
130              break
131            time.sleep(1)
132            current_time = time.time()
133
134          # Wait 5 more seconds and re-read process_crash_count, in
135          # attempt to catch latent process crashes.
136          time.sleep(5)
137          system_info = tab.browser.GetSystemInfo()
138          number_of_crashes = \
139              system_info.gpu.aux_attributes[u'process_crash_count']
140
141          if number_of_crashes < expected_kills:
142            raise page_test.Failure(
143                'Timed out waiting for a gpu process crash')
144          elif number_of_crashes != expected_kills:
145            raise page_test.Failure(
146                'Expected %d gpu process crashes; got: %d' %
147                (expected_kills, number_of_crashes))
148
149        # The try/except is a workaround for crbug.com/368107.
150        try:
151          gpucrash_tab.Close()
152        except (exceptions.TabCrashException, Exception):
153          print 'Tab crashed while closing chrome://gpucrash'
154        if not completed:
155          raise page_test.Failure(
156              'Test didn\'t complete (no context lost event?)')
157        if not tab.EvaluateJavaScript(
158          'window.domAutomationController._succeeded'):
159          raise page_test.Failure(
160            'Test failed (context not restored properly?)')
161    elif page.force_garbage_collection:
162      # Try to corce GC to clean up any contexts not attached to the page.
163      # This method seem unreliable, so the page will also attempt to force
164      # GC through excessive allocations.
165      tab.CollectGarbage()
166      completed = WaitForPageToFinish()
167
168      if not completed:
169        raise page_test.Failure(
170            'Test didn\'t complete (no context restored event?)')
171      if not tab.EvaluateJavaScript(
172        'window.domAutomationController._succeeded'):
173        raise page_test.Failure(
174          'Test failed (context not restored properly?)')
175    elif page.hide_tab_and_lose_context:
176      if not tab.browser.supports_tab_control:
177        raise page_test.Failure('Browser must support tab control')
178
179      # Test losing a context in a hidden tab. This test passes if the tab
180      # doesn't crash.
181      dummy_tab = tab.browser.tabs.New()
182      tab.EvaluateJavaScript('loseContextUsingExtension()')
183      tab.Activate()
184
185      completed = WaitForPageToFinish()
186
187      if not completed:
188        raise page_test.Failure('Test didn\'t complete')
189      if not tab.EvaluateJavaScript(
190        'window.domAutomationController._succeeded'):
191        raise page_test.Failure('Test failed')
192    else:
193      completed = WaitForPageToFinish()
194
195      if not completed:
196        raise page_test.Failure('Test didn\'t complete')
197      if not tab.EvaluateJavaScript(
198        'window.domAutomationController._succeeded'):
199        raise page_test.Failure('Test failed')
200
201# Test that navigating to chrome://gpucrash causes the GPU process to crash
202# exactly one time per navigation.
203class GPUProcessCrashesExactlyOnce(page.Page):
204  def __init__(self, page_set, base_dir):
205    super(GPUProcessCrashesExactlyOnce, self).__init__(
206      url='file://gpu_process_crash.html',
207      page_set=page_set,
208      base_dir=base_dir,
209      name='GpuCrash.GPUProcessCrashesExactlyOnce')
210    self.script_to_evaluate_on_commit = harness_script
211    self.kill_gpu_process = True
212    self.number_of_gpu_process_kills = 2
213    self.check_crash_count = True
214    self.force_garbage_collection = False
215    self.hide_tab_and_lose_context = False
216
217  def RunNavigateSteps(self, action_runner):
218    action_runner.NavigateToPage(self)
219    action_runner.WaitForJavaScriptCondition(
220        'window.domAutomationController._loaded')
221
222class WebGLContextLostFromGPUProcessExitPage(page.Page):
223  def __init__(self, page_set, base_dir):
224    super(WebGLContextLostFromGPUProcessExitPage, self).__init__(
225      url='file://webgl.html?query=kill_after_notification',
226      page_set=page_set,
227      base_dir=base_dir,
228      name='ContextLost.WebGLContextLostFromGPUProcessExit')
229    self.script_to_evaluate_on_commit = harness_script
230    self.kill_gpu_process = True
231    self.check_crash_count = False
232    self.number_of_gpu_process_kills = 1
233    self.force_garbage_collection = False
234    self.hide_tab_and_lose_context = False
235
236  def RunNavigateSteps(self, action_runner):
237    action_runner.NavigateToPage(self)
238    action_runner.WaitForJavaScriptCondition(
239        'window.domAutomationController._loaded')
240
241
242class WebGLContextLostFromLoseContextExtensionPage(page.Page):
243  def __init__(self, page_set, base_dir):
244    super(WebGLContextLostFromLoseContextExtensionPage, self).__init__(
245      url='file://webgl.html?query=WEBGL_lose_context',
246      page_set=page_set,
247      base_dir=base_dir,
248      name='ContextLost.WebGLContextLostFromLoseContextExtension')
249    self.script_to_evaluate_on_commit = harness_script
250    self.kill_gpu_process = False
251    self.check_crash_count = False
252    self.force_garbage_collection = False
253    self.hide_tab_and_lose_context = False
254
255  def RunNavigateSteps(self, action_runner):
256    action_runner.NavigateToPage(self)
257    action_runner.WaitForJavaScriptCondition(
258        'window.domAutomationController._finished')
259
260
261class WebGLContextLostInHiddenTabPage(page.Page):
262  def __init__(self, page_set, base_dir):
263    super(WebGLContextLostInHiddenTabPage, self).__init__(
264      url='file://webgl.html?query=kill_after_notification',
265      page_set=page_set,
266      base_dir=base_dir,
267      name='ContextLost.WebGLContextLostInHiddenTab')
268    self.script_to_evaluate_on_commit = harness_script
269    self.kill_gpu_process = False
270    self.check_crash_count = False
271    self.force_garbage_collection = False
272    self.hide_tab_and_lose_context = True
273
274  def RunNavigateSteps(self, action_runner):
275    action_runner.NavigateToPage(self)
276    action_runner.WaitForJavaScriptCondition(
277        'window.domAutomationController._loaded')
278
279
280class WebGLContextLostFromQuantityPage(page.Page):
281  def __init__(self, page_set, base_dir):
282    super(WebGLContextLostFromQuantityPage, self).__init__(
283      url='file://webgl.html?query=forced_quantity_loss',
284      page_set=page_set,
285      base_dir=base_dir,
286      name='ContextLost.WebGLContextLostFromQuantity')
287    self.script_to_evaluate_on_commit = harness_script
288    self.kill_gpu_process = False
289    self.check_crash_count = False
290    self.force_garbage_collection = True
291    self.hide_tab_and_lose_context = False
292
293  def RunNavigateSteps(self, action_runner):
294    action_runner.NavigateToPage(self)
295    action_runner.WaitForJavaScriptCondition(
296        'window.domAutomationController._loaded')
297
298class WebGLContextLostFromSelectElementPage(page.Page):
299  def __init__(self, page_set, base_dir):
300    super(WebGLContextLostFromSelectElementPage, self).__init__(
301      url='file://webgl_with_select_element.html',
302      page_set=page_set,
303      base_dir=base_dir,
304      name='ContextLost.WebGLContextLostFromSelectElement')
305    self.script_to_evaluate_on_commit = harness_script
306    self.kill_gpu_process = False
307    self.check_crash_count = False
308    self.force_garbage_collection = False
309    self.hide_tab_and_lose_context = False
310
311  def RunNavigateSteps(self, action_runner):
312    action_runner.NavigateToPage(self)
313    action_runner.WaitForJavaScriptCondition(
314        'window.domAutomationController._loaded')
315
316class ContextLost(benchmark_module.Benchmark):
317  enabled = True
318  test = _ContextLostValidator
319
320  def CreateExpectations(self, page_set):
321    return context_lost_expectations.ContextLostExpectations()
322
323  # For the record, this would have been another way to get the pages
324  # to repeat. pageset_repeat would be another option.
325  # options = {'page_repeat': 5}
326  def CreatePageSet(self, options):
327    ps = page_set.PageSet(
328      file_path=data_path,
329      user_agent_type='desktop',
330      serving_dirs=set(['']))
331    ps.AddPage(GPUProcessCrashesExactlyOnce(ps, ps.base_dir))
332    ps.AddPage(WebGLContextLostFromGPUProcessExitPage(ps, ps.base_dir))
333    ps.AddPage(WebGLContextLostFromLoseContextExtensionPage(ps, ps.base_dir))
334    ps.AddPage(WebGLContextLostFromQuantityPage(ps, ps.base_dir))
335    ps.AddPage(WebGLContextLostFromSelectElementPage(ps, ps.base_dir))
336    ps.AddPage(WebGLContextLostInHiddenTabPage(ps, ps.base_dir))
337    return ps
338