| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import logging |
| import os |
| import shutil |
| import sys |
| import tempfile |
| import zipfile |
| |
| from catapult_base import cloud_storage # pylint: disable=import-error |
| |
| from telemetry.core import exceptions |
| from telemetry.core import util |
| from telemetry import decorators |
| from telemetry.internal.browser import browser_finder |
| from telemetry.internal.browser import browser_finder_exceptions |
| from telemetry.internal.browser import browser_info as browser_info_module |
| from telemetry.internal.platform.profiler import profiler_finder |
| from telemetry.internal.util import exception_formatter |
| from telemetry.internal.util import file_handle |
| from telemetry.page import page_test |
| from telemetry import story |
| from telemetry.util import image_util |
| from telemetry.util import wpr_modes |
| from telemetry.web_perf import timeline_based_measurement |
| |
| |
| def _PrepareFinderOptions(finder_options, test, device_type): |
| browser_options = finder_options.browser_options |
| # Set up user agent. |
| browser_options.browser_user_agent_type = device_type |
| |
| test.CustomizeBrowserOptions(finder_options.browser_options) |
| if finder_options.profiler: |
| profiler_class = profiler_finder.FindProfiler(finder_options.profiler) |
| profiler_class.CustomizeBrowserOptions(browser_options.browser_type, |
| finder_options) |
| |
| |
| class SharedPageState(story.SharedState): |
| """ |
| This class contains all specific logic necessary to run a Chrome browser |
| benchmark. |
| """ |
| |
| _device_type = None |
| |
| def __init__(self, test, finder_options, story_set): |
| super(SharedPageState, self).__init__(test, finder_options, story_set) |
| if isinstance(test, timeline_based_measurement.TimelineBasedMeasurement): |
| assert not finder_options.profiler, ( |
| 'This is a Timeline Based Measurement benchmark. You cannot run it ' |
| 'with the --profiler flag. If you need trace data, tracing is always ' |
| ' enabled in Timeline Based Measurement benchmarks and you can get ' |
| 'the trace data by using --output-format=json.') |
| # This is to avoid the cyclic-import caused by timeline_based_page_test. |
| from telemetry.web_perf import timeline_based_page_test |
| self._test = timeline_based_page_test.TimelineBasedPageTest(test) |
| else: |
| self._test = test |
| device_type = self._device_type |
| # TODO(aiolos, nednguyen): Remove this logic of pulling out user_agent_type |
| # from story_set once all page_set are converted to story_set |
| # (crbug.com/439512). |
| |
| def _IsPageSetInstance(s): |
| # This is needed to avoid importing telemetry.page.page_set which will |
| # cause cyclic import. |
| return 'PageSet' == s.__class__.__name__ or 'PageSet' in ( |
| list(c.__name__ for c in s.__class__.__bases__)) |
| if not device_type and _IsPageSetInstance(story_set): |
| device_type = story_set.user_agent_type |
| _PrepareFinderOptions(finder_options, self._test, device_type) |
| self._browser = None |
| self._finder_options = finder_options |
| self._possible_browser = self._GetPossibleBrowser( |
| self._test, finder_options) |
| |
| self._first_browser = True |
| self._did_login_for_current_page = False |
| self._current_page = None |
| self._current_tab = None |
| self._migrated_profile = None |
| |
| self._pregenerated_profile_archive_dir = None |
| self._test.SetOptions(self._finder_options) |
| |
| # TODO(crbug/404771): Move network controller options out of |
| # browser_options and into finder_options. |
| browser_options = self._finder_options.browser_options |
| if self._finder_options.use_live_sites: |
| wpr_mode = wpr_modes.WPR_OFF |
| elif browser_options.wpr_mode == wpr_modes.WPR_RECORD: |
| wpr_mode = wpr_modes.WPR_RECORD |
| else: |
| wpr_mode = wpr_modes.WPR_REPLAY |
| |
| self.platform.network_controller.Open(wpr_mode, |
| browser_options.extra_wpr_args) |
| |
| |
| @property |
| def browser(self): |
| return self._browser |
| |
| def _FindBrowser(self, finder_options): |
| possible_browser = browser_finder.FindBrowser(finder_options) |
| if not possible_browser: |
| raise browser_finder_exceptions.BrowserFinderException( |
| 'No browser found.\n\nAvailable browsers:\n%s\n' % |
| '\n'.join(browser_finder.GetAllAvailableBrowserTypes(finder_options))) |
| return possible_browser |
| |
| def _GetPossibleBrowser(self, test, finder_options): |
| """Return a possible_browser with the given options for |test|. """ |
| possible_browser = self._FindBrowser(finder_options) |
| finder_options.browser_options.browser_type = ( |
| possible_browser.browser_type) |
| |
| enabled, msg = decorators.IsEnabled(test, possible_browser) |
| if not enabled and not finder_options.run_disabled_tests: |
| logging.warning(msg) |
| logging.warning('You are trying to run a disabled test.') |
| logging.warning( |
| 'Pass --also-run-disabled-tests to squelch this message.') |
| sys.exit(0) |
| |
| if possible_browser.IsRemote(): |
| possible_browser.RunRemote() |
| sys.exit(0) |
| return possible_browser |
| |
| def _TryCaptureScreenShot(self, page, tab, results): |
| try: |
| # TODO(nednguyen): once all platforms support taking screenshot, |
| # remove the tab checking logic and consider moving this to story_runner. |
| # (crbug.com/369490) |
| if tab.browser.platform.CanTakeScreenshot(): |
| tf = tempfile.NamedTemporaryFile(delete=False, suffix='.png') |
| tf.close() |
| tab.browser.platform.TakeScreenshot(tf.name) |
| results.AddProfilingFile(page, file_handle.FromTempFile(tf)) |
| elif tab.IsAlive() and tab.screenshot_supported: |
| tf = tempfile.NamedTemporaryFile(delete=False, suffix='.png') |
| tf.close() |
| image = tab.Screenshot() |
| image_util.WritePngFile(image, tf.name) |
| results.AddProfilingFile(page, file_handle.FromTempFile(tf)) |
| else: |
| logging.warning( |
| 'Either tab has crashed or browser does not support taking tab ' |
| 'screenshot. Skip taking screenshot on failure.') |
| except Exception as e: |
| logging.warning('Exception when trying to capture screenshot: %s', |
| repr(e)) |
| |
| def DidRunStory(self, results): |
| if self._finder_options.profiler: |
| self._StopProfiling(results) |
| # We might hang while trying to close the connection, and need to guarantee |
| # the page will get cleaned up to avoid future tests failing in weird ways. |
| try: |
| if self._current_tab and self._current_tab.IsAlive(): |
| self._current_tab.CloseConnections() |
| except Exception: |
| if self._current_tab: |
| self._current_tab.Close() |
| finally: |
| if self._current_page.credentials and self._did_login_for_current_page: |
| self.browser.credentials.LoginNoLongerNeeded( |
| self._current_tab, self._current_page.credentials) |
| if self._test.StopBrowserAfterPage(self.browser, self._current_page): |
| self._StopBrowser() |
| self._current_page = None |
| self._current_tab = None |
| |
| @property |
| def platform(self): |
| return self._possible_browser.platform |
| |
| def _StartBrowser(self, page): |
| assert self._browser is None |
| self._possible_browser.SetCredentialsPath(page.credentials_path) |
| |
| self._test.WillStartBrowser(self.platform) |
| if page.startup_url: |
| self._finder_options.browser_options.startup_url = page.startup_url |
| self._browser = self._possible_browser.Create(self._finder_options) |
| self._test.DidStartBrowser(self.browser) |
| |
| if self._first_browser: |
| self._first_browser = False |
| self.browser.credentials.WarnIfMissingCredentials(page) |
| logging.info('OS: %s %s', |
| self.platform.GetOSName(), |
| self.platform.GetOSVersionName()) |
| if self.browser.supports_system_info: |
| system_info = self.browser.GetSystemInfo() |
| if system_info.model_name: |
| logging.info('Model: %s', system_info.model_name) |
| if system_info.gpu: |
| for i, device in enumerate(system_info.gpu.devices): |
| logging.info('GPU device %d: %s', i, device) |
| if system_info.gpu.aux_attributes: |
| logging.info('GPU Attributes:') |
| for k, v in sorted(system_info.gpu.aux_attributes.iteritems()): |
| logging.info(' %-20s: %s', k, v) |
| if system_info.gpu.feature_status: |
| logging.info('Feature Status:') |
| for k, v in sorted(system_info.gpu.feature_status.iteritems()): |
| logging.info(' %-20s: %s', k, v) |
| if system_info.gpu.driver_bug_workarounds: |
| logging.info('Driver Bug Workarounds:') |
| for workaround in system_info.gpu.driver_bug_workarounds: |
| logging.info(' %s', workaround) |
| else: |
| logging.info('No GPU devices') |
| else: |
| logging.warning('System info not supported') |
| |
| def WillRunStory(self, page): |
| if not self.platform.tracing_controller.is_tracing_running: |
| # For TimelineBasedMeasurement benchmarks, tracing has already started. |
| # For PageTest benchmarks, tracing has not yet started. We need to make |
| # sure no tracing state is left before starting the browser for PageTest |
| # benchmarks. |
| self.platform.tracing_controller.ClearStateIfNeeded() |
| |
| if self._ShouldDownloadPregeneratedProfileArchive(): |
| self._DownloadPregeneratedProfileArchive() |
| |
| if self._ShouldMigrateProfile(): |
| self._MigratePregeneratedProfile() |
| |
| page_set = page.page_set |
| self._current_page = page |
| if self._browser and (self._test.RestartBrowserBeforeEachPage() |
| or page.startup_url): |
| assert not self.platform.tracing_controller.is_tracing_running, ( |
| 'Should not restart browser when tracing is already running. For ' |
| 'TimelineBasedMeasurement (TBM) benchmarks, you should not use ' |
| 'startup_url. Use benchmark.ShouldTearDownStateAfterEachStoryRun ' |
| 'instead.') |
| self._StopBrowser() |
| started_browser = not self.browser |
| |
| archive_path = page_set.WprFilePathForStory(page) |
| # TODO(nednguyen, perezju): Ideally we should just let the network |
| # controller raise an exception when the archive_path is not found. |
| if archive_path is not None and not os.path.isfile(archive_path): |
| logging.warning('WPR archive missing: %s', archive_path) |
| archive_path = None |
| self.platform.network_controller.StartReplay( |
| archive_path, page.make_javascript_deterministic) |
| |
| if self.browser: |
| # Set new credential path for browser. |
| self.browser.credentials.credentials_path = page.credentials_path |
| else: |
| self._StartBrowser(page) |
| if self.browser.supports_tab_control and self._test.close_tabs_before_run: |
| # Create a tab if there's none. |
| if len(self.browser.tabs) == 0: |
| self.browser.tabs.New() |
| |
| # Ensure only one tab is open, unless the test is a multi-tab test. |
| if not self._test.is_multi_tab_test: |
| while len(self.browser.tabs) > 1: |
| self.browser.tabs[-1].Close() |
| |
| # Must wait for tab to commit otherwise it can commit after the next |
| # navigation has begun and RenderFrameHostManager::DidNavigateMainFrame() |
| # will cancel the next navigation because it's pending. This manifests as |
| # the first navigation in a PageSet freezing indefinitely because the |
| # navigation was silently canceled when |self.browser.tabs[0]| was |
| # committed. Only do this when we just started the browser, otherwise |
| # there are cases where previous pages in a PageSet never complete |
| # loading so we'll wait forever. |
| if started_browser: |
| self.browser.tabs[0].WaitForDocumentReadyStateToBeComplete() |
| |
| # Start profiling if needed. |
| if self._finder_options.profiler: |
| self._StartProfiling(self._current_page) |
| |
| def CanRunStory(self, page): |
| return self.CanRunOnBrowser(browser_info_module.BrowserInfo(self.browser), |
| page) |
| |
| def CanRunOnBrowser(self, browser_info, |
| page): # pylint: disable=unused-argument |
| """Override this to return whether the browser brought up by this state |
| instance is suitable for running the given page. |
| |
| Args: |
| browser_info: an instance of telemetry.core.browser_info.BrowserInfo |
| page: an instance of telemetry.page.Page |
| """ |
| del browser_info, page # unused |
| return True |
| |
| def _PreparePage(self): |
| self._current_tab = self._test.TabForPage(self._current_page, self.browser) |
| if self._current_page.is_file: |
| self.platform.SetHTTPServerDirectories( |
| self._current_page.page_set.serving_dirs | |
| set([self._current_page.serving_dir])) |
| |
| if self._current_page.credentials: |
| if not self.browser.credentials.LoginNeeded( |
| self._current_tab, self._current_page.credentials): |
| raise page_test.Failure( |
| 'Login as ' + self._current_page.credentials + ' failed') |
| self._did_login_for_current_page = True |
| |
| if self._test.clear_cache_before_each_run: |
| self._current_tab.ClearCache(force=True) |
| |
| @property |
| def current_page(self): |
| return self._current_page |
| |
| @property |
| def current_tab(self): |
| return self._current_tab |
| |
| @property |
| def page_test(self): |
| return self._test |
| |
| def RunStory(self, results): |
| try: |
| self._PreparePage() |
| self._current_page.Run(self) |
| self._test.ValidateAndMeasurePage( |
| self._current_page, self._current_tab, results) |
| except exceptions.Error: |
| if self._finder_options.browser_options.take_screenshot_for_failed_page: |
| self._TryCaptureScreenShot(self._current_page, self._current_tab, |
| results) |
| if self._test.is_multi_tab_test: |
| # Avoid trying to recover from an unknown multi-tab state. |
| exception_formatter.PrintFormattedException( |
| msg='Telemetry Error during multi tab test:') |
| raise page_test.MultiTabTestAppCrashError |
| raise |
| except Exception: |
| if self._finder_options.browser_options.take_screenshot_for_failed_page: |
| self._TryCaptureScreenShot(self._current_page, self._current_tab, |
| results) |
| raise |
| |
| def TearDownState(self): |
| if self._migrated_profile: |
| shutil.rmtree(self._migrated_profile) |
| self._migrated_profile = None |
| |
| self._StopBrowser() |
| self.platform.StopAllLocalServers() |
| self.platform.network_controller.Close() |
| |
| def _StopBrowser(self): |
| if self._browser: |
| self._browser.Close() |
| self._browser = None |
| |
| def _StartProfiling(self, page): |
| output_file = os.path.join(self._finder_options.output_dir, |
| page.file_safe_name) |
| is_repeating = (self._finder_options.page_repeat != 1 or |
| self._finder_options.pageset_repeat != 1) |
| if is_repeating: |
| output_file = util.GetSequentialFileName(output_file) |
| self.browser.profiling_controller.Start( |
| self._finder_options.profiler, output_file) |
| |
| def _StopProfiling(self, results): |
| if self.browser: |
| profiler_files = self.browser.profiling_controller.Stop() |
| for f in profiler_files: |
| if os.path.isfile(f): |
| results.AddProfilingFile(self._current_page, |
| file_handle.FromFilePath(f)) |
| |
| def _ShouldMigrateProfile(self): |
| return not self._migrated_profile |
| |
| def _MigrateProfile(self, finder_options, found_browser, |
| initial_profile, final_profile): |
| """Migrates a profile to be compatible with a newer version of Chrome. |
| |
| Launching Chrome with the old profile will perform the migration. |
| """ |
| # Save the current input and output profiles. |
| saved_input_profile = finder_options.browser_options.profile_dir |
| saved_output_profile = finder_options.output_profile_path |
| |
| # Set the input and output profiles. |
| finder_options.browser_options.profile_dir = initial_profile |
| finder_options.output_profile_path = final_profile |
| |
| # Launch the browser, then close it. |
| browser = found_browser.Create(finder_options) |
| browser.Close() |
| |
| # Load the saved input and output profiles. |
| finder_options.browser_options.profile_dir = saved_input_profile |
| finder_options.output_profile_path = saved_output_profile |
| |
| def _MigratePregeneratedProfile(self): |
| """Migrates the pre-generated profile by launching Chrome with it. |
| |
| On success, updates self._migrated_profile and |
| self._finder_options.browser_options.profile_dir with the directory of the |
| migrated profile. |
| """ |
| self._migrated_profile = tempfile.mkdtemp() |
| logging.info("Starting migration of pre-generated profile to %s", |
| self._migrated_profile) |
| pregenerated_profile = self._finder_options.browser_options.profile_dir |
| |
| possible_browser = self._FindBrowser(self._finder_options) |
| self._MigrateProfile(self._finder_options, possible_browser, |
| pregenerated_profile, self._migrated_profile) |
| self._finder_options.browser_options.profile_dir = self._migrated_profile |
| logging.info("Finished migration of pre-generated profile to %s", |
| self._migrated_profile) |
| |
| def GetPregeneratedProfileArchiveDir(self): |
| return self._pregenerated_profile_archive_dir |
| |
| def SetPregeneratedProfileArchiveDir(self, archive_path): |
| """ |
| Benchmarks can set a pre-generated profile archive to indicate that when |
| Chrome is launched, it should have a --user-data-dir set to the |
| pre-generated profile, rather than to an empty profile. |
| |
| If the benchmark is invoked with the option --profile-dir=<dir>, that |
| option overrides this value. |
| """ |
| self._pregenerated_profile_archive_dir = archive_path |
| |
| def _ShouldDownloadPregeneratedProfileArchive(self): |
| """Whether to download a pre-generated profile archive.""" |
| # There is no pre-generated profile archive. |
| if not self.GetPregeneratedProfileArchiveDir(): |
| return False |
| |
| # If profile dir is specified on command line, use that instead. |
| if self._finder_options.browser_options.profile_dir: |
| logging.warning("Profile directory specified on command line: %s, this" |
| "overrides the benchmark's default profile directory.", |
| self._finder_options.browser_options.profile_dir) |
| return False |
| |
| # If the browser is remote, a local download has no effect. |
| if self._possible_browser.IsRemote(): |
| return False |
| |
| return True |
| |
| def _DownloadPregeneratedProfileArchive(self): |
| """Download and extract the profile directory archive if one exists. |
| |
| On success, updates self._finder_options.browser_options.profile_dir with |
| the directory of the extracted profile. |
| """ |
| # Download profile directory from cloud storage. |
| generated_profile_archive_path = self.GetPregeneratedProfileArchiveDir() |
| |
| try: |
| cloud_storage.GetIfChanged(generated_profile_archive_path, |
| cloud_storage.PUBLIC_BUCKET) |
| except (cloud_storage.CredentialsError, |
| cloud_storage.PermissionError) as e: |
| if os.path.exists(generated_profile_archive_path): |
| # If the profile directory archive exists, assume the user has their |
| # own local copy simply warn. |
| logging.warning('Could not download Profile archive: %s', |
| generated_profile_archive_path) |
| else: |
| # If the archive profile directory doesn't exist, this is fatal. |
| logging.error('Can not run without required profile archive: %s. ' |
| 'If you believe you have credentials, follow the ' |
| 'instructions below.', |
| generated_profile_archive_path) |
| logging.error(str(e)) |
| sys.exit(-1) |
| |
| # Check to make sure the zip file exists. |
| if not os.path.isfile(generated_profile_archive_path): |
| raise Exception("Profile directory archive not downloaded: ", |
| generated_profile_archive_path) |
| |
| # The location to extract the profile into. |
| extracted_profile_dir_path = ( |
| os.path.splitext(generated_profile_archive_path)[0]) |
| |
| # Unzip profile directory. |
| with zipfile.ZipFile(generated_profile_archive_path) as f: |
| try: |
| f.extractall(os.path.dirname(generated_profile_archive_path)) |
| except e: |
| # Cleanup any leftovers from unzipping. |
| if os.path.exists(extracted_profile_dir_path): |
| shutil.rmtree(extracted_profile_dir_path) |
| logging.error("Error extracting profile directory zip file: %s", e) |
| sys.exit(-1) |
| |
| # Run with freshly extracted profile directory. |
| logging.info("Using profile archive directory: %s", |
| extracted_profile_dir_path) |
| self._finder_options.browser_options.profile_dir = ( |
| extracted_profile_dir_path) |
| |
| |
| class SharedMobilePageState(SharedPageState): |
| _device_type = 'mobile' |
| |
| |
| class SharedDesktopPageState(SharedPageState): |
| _device_type = 'desktop' |
| |
| |
| class SharedTabletPageState(SharedPageState): |
| _device_type = 'tablet' |
| |
| |
| class Shared10InchTabletPageState(SharedPageState): |
| _device_type = 'tablet_10_inch' |