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
5import logging
6import os
7import sys
8
9from telemetry.core import exceptions
10from telemetry.core import util
11from telemetry import decorators
12from telemetry.internal.browser import browser_finder
13from telemetry.internal.browser import browser_finder_exceptions
14from telemetry.internal.browser import browser_info as browser_info_module
15from telemetry.internal.platform.profiler import profiler_finder
16from telemetry.internal.util import exception_formatter
17from telemetry.internal.util import file_handle
18from telemetry.page import cache_temperature
19from telemetry.page import traffic_setting
20from telemetry.page import legacy_page_test
21from telemetry import story
22from telemetry.util import screenshot
23from telemetry.util import wpr_modes
24from telemetry.web_perf import timeline_based_measurement
25
26
27def _PrepareFinderOptions(finder_options, test, device_type):
28  browser_options = finder_options.browser_options
29  # Set up user agent.
30  browser_options.browser_user_agent_type = device_type
31
32  test.CustomizeBrowserOptions(finder_options.browser_options)
33  if finder_options.profiler:
34    profiler_class = profiler_finder.FindProfiler(finder_options.profiler)
35    profiler_class.CustomizeBrowserOptions(browser_options.browser_type,
36                                           finder_options)
37
38
39class SharedPageState(story.SharedState):
40  """
41  This class contains all specific logic necessary to run a Chrome browser
42  benchmark.
43  """
44
45  _device_type = None
46
47  def __init__(self, test, finder_options, story_set):
48    super(SharedPageState, self).__init__(test, finder_options, story_set)
49    if isinstance(test, timeline_based_measurement.TimelineBasedMeasurement):
50      if finder_options.profiler:
51        assert not 'trace' in finder_options.profiler, (
52            'This is a Timeline Based Measurement benchmark. You cannot run it '
53            'with trace profiler enabled. If you need trace data, tracing is '
54            'always enabled in Timeline Based Measurement benchmarks and you '
55            'can get the trace data by adding --output-format=json.')
56      # This is to avoid the cyclic-import caused by timeline_based_page_test.
57      from telemetry.web_perf import timeline_based_page_test
58      self._test = timeline_based_page_test.TimelineBasedPageTest(test)
59    else:
60      self._test = test
61    _PrepareFinderOptions(finder_options, self._test, self._device_type)
62    self._browser = None
63    self._finder_options = finder_options
64    self._possible_browser = self._GetPossibleBrowser(
65        self._test, finder_options)
66
67    self._first_browser = True
68    self._did_login_for_current_page = False
69    self._previous_page = None
70    self._current_page = None
71    self._current_tab = None
72
73    self._test.SetOptions(self._finder_options)
74
75    # TODO(crbug/404771): Move network controller options out of
76    # browser_options and into finder_options.
77    browser_options = self._finder_options.browser_options
78    if self._finder_options.use_live_sites:
79      wpr_mode = wpr_modes.WPR_OFF
80    elif browser_options.wpr_mode == wpr_modes.WPR_RECORD:
81      wpr_mode = wpr_modes.WPR_RECORD
82    else:
83      wpr_mode = wpr_modes.WPR_REPLAY
84
85    use_live_traffic = wpr_mode == wpr_modes.WPR_OFF
86
87    if self.platform.network_controller.is_open:
88      self.platform.network_controller.Close()
89    self.platform.network_controller.InitializeIfNeeded(
90        use_live_traffic=use_live_traffic)
91    self.platform.network_controller.Open(wpr_mode,
92                                          browser_options.extra_wpr_args)
93    self.platform.Initialize()
94
95  @property
96  def possible_browser(self):
97    return self._possible_browser
98
99  @property
100  def browser(self):
101    return self._browser
102
103  def _FindBrowser(self, finder_options):
104    possible_browser = browser_finder.FindBrowser(finder_options)
105    if not possible_browser:
106      raise browser_finder_exceptions.BrowserFinderException(
107          'No browser found.\n\nAvailable browsers:\n%s\n' %
108          '\n'.join(browser_finder.GetAllAvailableBrowserTypes(finder_options)))
109    return possible_browser
110
111  def _GetPossibleBrowser(self, test, finder_options):
112    """Return a possible_browser with the given options for |test|. """
113    possible_browser = self._FindBrowser(finder_options)
114    finder_options.browser_options.browser_type = (
115        possible_browser.browser_type)
116
117    enabled, msg = decorators.IsEnabled(test, possible_browser)
118    if not enabled and not finder_options.run_disabled_tests:
119      logging.warning(msg)
120      logging.warning('You are trying to run a disabled test.')
121
122    if possible_browser.IsRemote():
123      possible_browser.RunRemote()
124      sys.exit(0)
125    return possible_browser
126
127  def DumpStateUponFailure(self, page, results):
128    # Dump browser standard output and log.
129    if self._browser:
130      self._browser.DumpStateUponFailure()
131    else:
132      logging.warning('Cannot dump browser state: No browser.')
133
134    # Capture a screenshot
135    if self._finder_options.browser_options.take_screenshot_for_failed_page:
136      fh = screenshot.TryCaptureScreenShot(self.platform, self._current_tab)
137      if fh is not None:
138        results.AddProfilingFile(page, fh)
139    else:
140      logging.warning('Taking screenshots upon failures disabled.')
141
142  def DidRunStory(self, results):
143    if self._finder_options.profiler:
144      self._StopProfiling(results)
145    # We might hang while trying to close the connection, and need to guarantee
146    # the page will get cleaned up to avoid future tests failing in weird ways.
147    try:
148      if self._current_tab and self._current_tab.IsAlive():
149        self._current_tab.CloseConnections()
150      self._previous_page = self._current_page
151    except Exception:
152      if self._current_tab:
153        self._current_tab.Close()
154    finally:
155      if self._current_page.credentials and self._did_login_for_current_page:
156        self.browser.credentials.LoginNoLongerNeeded(
157            self._current_tab, self._current_page.credentials)
158      if self._test.StopBrowserAfterPage(self.browser, self._current_page):
159        self._StopBrowser()
160      self._current_page = None
161      self._current_tab = None
162
163  @property
164  def platform(self):
165    return self._possible_browser.platform
166
167  def _StartBrowser(self, page):
168    assert self._browser is None
169    self._possible_browser.SetCredentialsPath(page.credentials_path)
170
171    self._test.WillStartBrowser(self.platform)
172    if page.startup_url:
173      self._finder_options.browser_options.startup_url = page.startup_url
174    self._browser = self._possible_browser.Create(self._finder_options)
175    self._test.DidStartBrowser(self.browser)
176
177    if self._first_browser:
178      self._first_browser = False
179      self.browser.credentials.WarnIfMissingCredentials(page)
180
181  def WillRunStory(self, page):
182    if not self.platform.tracing_controller.is_tracing_running:
183      # For TimelineBasedMeasurement benchmarks, tracing has already started.
184      # For PageTest benchmarks, tracing has not yet started. We need to make
185      # sure no tracing state is left before starting the browser for PageTest
186      # benchmarks.
187      self.platform.tracing_controller.ClearStateIfNeeded()
188
189    page_set = page.page_set
190    self._current_page = page
191    if self._browser and (self._test.RestartBrowserBeforeEachPage()
192                          or page.startup_url):
193      assert not self.platform.tracing_controller.is_tracing_running, (
194          'Should not restart browser when tracing is already running. For '
195          'TimelineBasedMeasurement (TBM) benchmarks, you should not use '
196          'startup_url. Use benchmark.ShouldTearDownStateAfterEachStoryRun '
197          'instead.')
198      self._StopBrowser()
199    started_browser = not self.browser
200
201    archive_path = page_set.WprFilePathForStory(page, self.platform.GetOSName())
202    # TODO(nednguyen, perezju): Ideally we should just let the network
203    # controller raise an exception when the archive_path is not found.
204    if archive_path is not None and not os.path.isfile(archive_path):
205      logging.warning('WPR archive missing: %s', archive_path)
206      archive_path = None
207    self.platform.network_controller.StartReplay(
208        archive_path, page.make_javascript_deterministic)
209
210    if self.browser:
211      # Set new credential path for browser.
212      self.browser.credentials.credentials_path = page.credentials_path
213    else:
214      self._StartBrowser(page)
215    if self.browser.supports_tab_control and self._test.close_tabs_before_run:
216      # Create a tab if there's none.
217      if len(self.browser.tabs) == 0:
218        self.browser.tabs.New()
219
220      # Ensure only one tab is open, unless the test is a multi-tab test.
221      if not self._test.is_multi_tab_test:
222        while len(self.browser.tabs) > 1:
223          self.browser.tabs[-1].Close()
224
225      # Must wait for tab to commit otherwise it can commit after the next
226      # navigation has begun and RenderFrameHostManager::DidNavigateMainFrame()
227      # will cancel the next navigation because it's pending. This manifests as
228      # the first navigation in a PageSet freezing indefinitely because the
229      # navigation was silently canceled when |self.browser.tabs[0]| was
230      # committed. Only do this when we just started the browser, otherwise
231      # there are cases where previous pages in a PageSet never complete
232      # loading so we'll wait forever.
233      if started_browser:
234        self.browser.tabs[0].WaitForDocumentReadyStateToBeComplete()
235
236    # Reset traffic shaping to speed up cache temperature setup.
237    self.platform.network_controller.UpdateTrafficSettings(0, 0, 0)
238    cache_temperature.EnsurePageCacheTemperature(
239        self._current_page, self.browser, self._previous_page)
240    if self._current_page.traffic_setting != traffic_setting.NONE:
241      s = traffic_setting.NETWORK_CONFIGS[self._current_page.traffic_setting]
242      self.platform.network_controller.UpdateTrafficSettings(
243          round_trip_latency_ms=s.round_trip_latency_ms,
244          download_bandwidth_kbps=s.download_bandwidth_kbps,
245          upload_bandwidth_kbps=s.upload_bandwidth_kbps)
246
247    # Start profiling if needed.
248    if self._finder_options.profiler:
249      self._StartProfiling(self._current_page)
250
251  def CanRunStory(self, page):
252    return self.CanRunOnBrowser(browser_info_module.BrowserInfo(self.browser),
253                                page)
254
255  def CanRunOnBrowser(self, browser_info,
256                      page):  # pylint: disable=unused-argument
257    """Override this to return whether the browser brought up by this state
258    instance is suitable for running the given page.
259
260    Args:
261      browser_info: an instance of telemetry.core.browser_info.BrowserInfo
262      page: an instance of telemetry.page.Page
263    """
264    del browser_info, page  # unused
265    return True
266
267  def _PreparePage(self):
268    self._current_tab = self._test.TabForPage(self._current_page, self.browser)
269    if self._current_page.is_file:
270      self.platform.SetHTTPServerDirectories(
271          self._current_page.page_set.serving_dirs |
272          set([self._current_page.serving_dir]))
273
274    if self._current_page.credentials:
275      if not self.browser.credentials.LoginNeeded(
276          self._current_tab, self._current_page.credentials):
277        raise legacy_page_test.Failure(
278            'Login as ' + self._current_page.credentials + ' failed')
279      self._did_login_for_current_page = True
280
281    if self._test.clear_cache_before_each_run:
282      self._current_tab.ClearCache(force=True)
283
284  @property
285  def current_page(self):
286    return self._current_page
287
288  @property
289  def current_tab(self):
290    return self._current_tab
291
292  @property
293  def page_test(self):
294    return self._test
295
296  def RunStory(self, results):
297    try:
298      self._PreparePage()
299      self._current_page.Run(self)
300      self._test.ValidateAndMeasurePage(
301          self._current_page, self._current_tab, results)
302    except exceptions.Error:
303      if self._test.is_multi_tab_test:
304        # Avoid trying to recover from an unknown multi-tab state.
305        exception_formatter.PrintFormattedException(
306            msg='Telemetry Error during multi tab test:')
307        raise legacy_page_test.MultiTabTestAppCrashError
308      raise
309
310  def TearDownState(self):
311    self._StopBrowser()
312    self.platform.StopAllLocalServers()
313    self.platform.network_controller.Close()
314
315  def _StopBrowser(self):
316    if self._browser:
317      self._browser.Close()
318      self._browser = None
319
320  def _StartProfiling(self, page):
321    output_file = os.path.join(self._finder_options.output_dir,
322                               page.file_safe_name)
323    if self._finder_options.pageset_repeat != 1:
324      output_file = util.GetSequentialFileName(output_file)
325    self.browser.profiling_controller.Start(
326        self._finder_options.profiler, output_file)
327
328  def _StopProfiling(self, results):
329    if self.browser:
330      profiler_files = self.browser.profiling_controller.Stop()
331      for f in profiler_files:
332        if os.path.isfile(f):
333          results.AddProfilingFile(self._current_page,
334                                   file_handle.FromFilePath(f))
335
336
337class SharedMobilePageState(SharedPageState):
338  _device_type = 'mobile'
339
340
341class SharedDesktopPageState(SharedPageState):
342  _device_type = 'desktop'
343
344
345class SharedTabletPageState(SharedPageState):
346  _device_type = 'tablet'
347
348
349class Shared10InchTabletPageState(SharedPageState):
350  _device_type = 'tablet_10_inch'
351