# Copyright 2015 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 time from profile_creators import profile_extender from telemetry.core import exceptions from telemetry.core import util class FastNavigationProfileExtender(profile_extender.ProfileExtender): """Extends a Chrome profile. This class creates or extends an existing profile by performing a set of tab navigations in large batches. This is accomplished by opening a large number of tabs, simultaneously navigating all the tabs, and then waiting for all the tabs to load. This provides two benefits: - Takes advantage of the high number of logical cores on modern CPUs. - The total time spent waiting for navigations to time out scales linearly with the number of batches, but does not scale with the size of the batch. """ def __init__(self, finder_options, maximum_batch_size): """Initializer. Args: maximum_batch_size: A positive integer indicating the number of tabs to simultaneously perform navigations. """ super(FastNavigationProfileExtender, self).__init__(finder_options) # The instance keeps a list of Tabs that can be navigated successfully. # This means that the Tab is not crashed, and is processing JavaScript in a # timely fashion. self._navigation_tabs = [] # The number of tabs to use. self._NUM_TABS = maximum_batch_size # The amount of additional time to wait for a batch of pages to finish # loading for each page in the batch. self._BATCH_TIMEOUT_PER_PAGE_IN_SECONDS = 20 # The amount of time to wait for a page to quiesce. Some pages will never # quiesce. self._TIME_TO_WAIT_FOR_PAGE_TO_QUIESCE_IN_SECONDS = 10 def Run(self): """Superclass override.""" try: self.SetUpBrowser() self._PerformNavigations() finally: self.TearDownBrowser() # When there hasn't been an exception, verify that the profile was # correctly extended. # TODO(erikchen): I've intentionally omitted my implementation of # VerifyProfileWasExtended() in small_profile_extender, since the profile # is not being correctly extended. http://crbug.com/484833 # http://crbug.com/484880 self.VerifyProfileWasExtended() def VerifyProfileWasExtended(self): """Verifies that the profile was correctly extended. Can be overridden by subclasses. """ pass def GetUrlIterator(self): """Gets URLs for the browser to navigate to. Intended for subclass override. Returns: An iterator whose elements are urls to be navigated to. """ raise NotImplementedError() def ShouldExitAfterBatchNavigation(self): """Returns a boolean indicating whether profile extension is finished. Intended for subclass override. """ raise NotImplementedError() def CleanUpAfterBatchNavigation(self): """A hook for subclasses to perform cleanup after each batch of navigations. Can be overridden by subclasses. """ pass def _RefreshNavigationTabs(self): """Updates the member self._navigation_tabs to contain self._NUM_TABS elements, each of which is not crashed. The crashed tabs are intentionally leaked, since Telemetry doesn't have a good way of killing crashed tabs. It is also possible for a tab to be stalled in an infinite JavaScript loop. These tabs will be in self.browser.tabs, but not in self._navigation_tabs. There is no way to kill these tabs, so they are also leaked. This method is careful to only use tabs in self._navigation_tabs, or newly created tabs. """ live_tabs = [tab for tab in self._navigation_tabs if tab.IsAlive()] self._navigation_tabs = live_tabs while len(self._navigation_tabs) < self._NUM_TABS: self._navigation_tabs.append(self._browser.tabs.New()) def _RemoveNavigationTab(self, tab): """Removes a tab which is no longer in a useable state from self._navigation_tabs. The tab is not removed from self.browser.tabs, since there is no guarantee that the tab can be safely removed.""" self._navigation_tabs.remove(tab) def _RetrieveTabUrl(self, tab, timeout): """Retrives the URL of the tab.""" # TODO(erikchen): Use tab.url instead, which talks to the browser process # instead of the renderer process. http://crbug.com/486119 return tab.EvaluateJavaScript('document.URL', timeout) def _WaitForUrlToChange(self, tab, initial_url, end_time): """Waits for the tab to navigate away from its initial url. If time.time() is larger than end_time, the function does nothing. Otherwise, the function tries to return no later than end_time. """ while True: seconds_to_wait = end_time - time.time() if seconds_to_wait <= 0: break current_url = self._RetrieveTabUrl(tab, seconds_to_wait) if current_url != initial_url and current_url != "": break # Retrieving the current url is a non-trivial operation. Add a small # sleep here to prevent this method from contending with the actual # navigation. time.sleep(0.01) def _WaitForTabToBeReady(self, tab, end_time): """Waits for the tab to be ready. If time.time() is larger than end_time, the function does nothing. Otherwise, the function tries to return no later than end_time. """ seconds_to_wait = end_time - time.time() if seconds_to_wait <= 0: return tab.WaitForDocumentReadyStateToBeComplete(seconds_to_wait) # Wait up to 10 seconds for the page to quiesce. If the page hasn't # quiesced in 10 seconds, it will probably never quiesce. seconds_to_wait = end_time - time.time() seconds_to_wait = max(0, seconds_to_wait) try: util.WaitFor(tab.HasReachedQuiescence, seconds_to_wait) except exceptions.TimeoutException: pass def _BatchNavigateTabs(self, batch): """Performs a batch of tab navigations with minimal delay. Args: batch: A list of tuples (tab, url). Returns: A list of tuples (tab, initial_url). |initial_url| is the url of the |tab| prior to a navigation command being sent to it. """ # Attempting to pass in a timeout of 0 seconds results in a synchronous # socket error from the websocket library. Pass in a very small timeout # instead so that the websocket library raises a Timeout exception. This # prevents the logic from accidentally catching different socket # exceptions. timeout_in_seconds = 0.01 queued_tabs = [] for tab, url in batch: initial_url = self._RetrieveTabUrl(tab, 20) try: tab.Navigate(url, None, timeout_in_seconds) except exceptions.TimeoutException: # We expect to receive a timeout exception, since we're not waiting for # the navigation to complete. pass queued_tabs.append((tab, initial_url)) return queued_tabs def _WaitForQueuedTabsToLoad(self, queued_tabs): """Waits for all the batch navigated tabs to finish loading. Args: queued_tabs: A list of tuples (tab, initial_url). Each tab is guaranteed to have already been sent a navigation command. """ total_batch_timeout = (len(queued_tabs) * self._BATCH_TIMEOUT_PER_PAGE_IN_SECONDS) end_time = time.time() + total_batch_timeout for tab, initial_url in queued_tabs: # Since we didn't wait any time for the tab url navigation to commit, it's # possible that the tab hasn't started navigating yet. self._WaitForUrlToChange(tab, initial_url, end_time) self._WaitForTabToBeReady(tab, end_time) def _GetUrlsToNavigate(self, url_iterator): """Returns an array of urls to navigate to, given a url_iterator.""" urls = [] for _ in xrange(self._NUM_TABS): try: urls.append(url_iterator.next()) except StopIteration: break return urls def _PerformNavigations(self): """Repeatedly fetches a batch of urls, and navigates to those urls. This will run until an empty batch is returned, or ShouldExitAfterBatchNavigation() returns True. """ url_iterator = self.GetUrlIterator() while True: self._RefreshNavigationTabs() urls = self._GetUrlsToNavigate(url_iterator) if len(urls) == 0: break batch = [] for i in range(len(urls)): url = urls[i] tab = self._navigation_tabs[i] batch.append((tab, url)) queued_tabs = self._BatchNavigateTabs(batch) self._WaitForQueuedTabsToLoad(queued_tabs) self.CleanUpAfterBatchNavigation() if self.ShouldExitAfterBatchNavigation(): break