OLD | NEW |
---|---|
(Empty) | |
1 # Copyright 2015 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 import time | |
5 | |
6 from telemetry.core import browser_finder | |
7 from telemetry.core import browser_finder_exceptions | |
8 from telemetry.core import exceptions | |
9 from telemetry.core import util | |
10 | |
11 | |
12 class FastNavigationProfileExtender(object): | |
13 """Extends a Chrome profile. | |
14 | |
15 This class creates or extends an existing profile by performing a set of tab | |
16 navigations in large batches. This is accomplished by opening a large number | |
17 of tabs, simultaneously navigating all the tabs, and then waiting for all the | |
18 tabs to load. This provides two benefits: | |
19 - Takes advantage of the high number of logical cores on modern CPUs. | |
20 - The total time spent waiting for navigations to time out scales linearly | |
21 with the number of batches, but does not scale with the size of the | |
22 batch. | |
23 """ | |
24 def __init__(self): | |
25 super(FastNavigationProfileExtender, self).__init__() | |
26 | |
27 # A reference to the browser that will be performing all of the tab | |
28 # navigations. | |
29 self._browser = None | |
30 | |
31 # A static copy of the urls that this class is going to navigate to. | |
32 self._navigation_urls = None | |
33 | |
34 # The number of tabs to use. | |
35 self._NUM_TABS = 15 | |
36 | |
37 # The number of pages to load in parallel. | |
38 self._NUM_PARALLEL_PAGES = 15 | |
39 | |
40 assert self._NUM_PARALLEL_PAGES <= self._NUM_TABS, (' the batch size can\'t' | |
41 ' be larger than the number of available tabs') | |
42 | |
43 # The amount of time to wait for a batch of pages to finish loading. | |
44 self._BATCH_PAGE_LOAD_TIMEOUT_IN_SECONDS = 10 | |
45 | |
46 # The default amount of time to wait for the retrieval of the URL of a tab. | |
47 self._TAB_URL_RETRIEVAL_TIMEOUT_IN_SECONDS = 1 | |
48 | |
49 def Run(self, finder_options): | |
50 """Extends the profile. | |
51 | |
52 Args: | |
53 finder_options: An instance of BrowserFinderOptions that contains the | |
54 directory of the input profile, the directory to place the output | |
55 profile, and sufficient information to choose a specific browser binary. | |
56 """ | |
57 try: | |
58 self._navigation_urls = self.GetUrlsToNavigate() | |
59 self._SetUpBrowser(finder_options) | |
60 self._PerformNavigations() | |
61 finally: | |
62 self._TearDownBrowser() | |
63 | |
64 def GetUrlsToNavigate(self): | |
65 """Returns a list of urls to be navigated to. | |
66 | |
67 Intended for subclass override. | |
68 """ | |
69 raise NotImplementedError() | |
70 | |
71 | |
72 def _GetPossibleBrowser(self, finder_options): | |
73 """Return a possible_browser with the given options.""" | |
74 possible_browser = browser_finder.FindBrowser(finder_options) | |
75 if not possible_browser: | |
76 raise browser_finder_exceptions.BrowserFinderException( | |
77 'No browser found.\n\nAvailable browsers:\n%s\n' % | |
78 '\n'.join(browser_finder.GetAllAvailableBrowserTypes(finder_options))) | |
79 finder_options.browser_options.browser_type = ( | |
80 possible_browser.browser_type) | |
81 | |
82 return possible_browser | |
83 | |
84 def _RetrieveTabUrl(self, tab, timeout): | |
85 """Retrives the URL of the tab.""" | |
86 try: | |
87 return tab.EvaluateJavaScript('document.URL', timeout) | |
88 except exceptions.DevtoolsTargetCrashException: | |
89 return None | |
90 | |
91 def _WaitForUrlToChange(self, tab, initial_url, timeout): | |
nednguyen
2015/02/17 23:38:10
telemetry already has a util.Waitfor method that d
erikchen
2015/02/17 23:46:10
The function util.WaitFor() does not work well her
nednguyen
2015/02/17 23:50:10
You can making it configurable by making those par
erikchen
2015/02/17 23:59:38
The conditional itself requires a dynamic variable
| |
92 """Waits for the tab to navigate away from its initial url.""" | |
93 end_time = time.time() + timeout | |
94 while True: | |
95 seconds_to_wait = end_time - time.time() | |
96 seconds_to_wait = max(0, seconds_to_wait) | |
97 | |
98 if seconds_to_wait == 0: | |
99 break | |
100 | |
101 current_url = self._RetrieveTabUrl(tab, seconds_to_wait) | |
102 if current_url != initial_url: | |
103 break | |
104 | |
105 # Retrieving the current url is a non-trivial operation. Add a small | |
106 # sleep here to prevent this method from contending with the actual | |
107 # navigation. | |
108 time.sleep(0.01) | |
109 | |
110 def _BatchNavigateTabs(self, batch): | |
111 """Performs a batch of tab navigations with minimal delay. | |
112 | |
113 Args: | |
114 batch: A list of tuples (tab, url). | |
115 | |
116 Returns: | |
117 A list of tuples (tab, initial_url). |initial_url| is the url of the | |
118 |tab| prior to a navigation command being sent to it. | |
119 """ | |
120 timeout_in_seconds = 0 | |
121 | |
122 queued_tabs = [] | |
123 for tab, url in batch: | |
124 initial_url = self._RetrieveTabUrl(tab, | |
125 self._TAB_URL_RETRIEVAL_TIMEOUT_IN_SECONDS) | |
126 | |
127 try: | |
128 tab.Navigate(url, None, timeout_in_seconds) | |
129 except exceptions.DevtoolsTargetCrashException: | |
130 # We expect a time out, and don't mind if the webpage crashes. Ignore | |
131 # both exceptions. | |
132 pass | |
133 | |
134 queued_tabs.append((tab, initial_url)) | |
135 return queued_tabs | |
136 | |
137 def _WaitForQueuedTabsToLoad(self, queued_tabs): | |
138 """Waits for all the batch navigated tabs to finish loading. | |
139 | |
140 Args: | |
141 queued_tabs: A list of tuples (tab, initial_url). Each tab is guaranteed | |
142 to have already been sent a navigation command. | |
143 """ | |
144 end_time = time.time() + self._BATCH_PAGE_LOAD_TIMEOUT_IN_SECONDS | |
145 for tab, initial_url in queued_tabs: | |
146 seconds_to_wait = end_time - time.time() | |
147 seconds_to_wait = max(0, seconds_to_wait) | |
148 | |
149 if seconds_to_wait == 0: | |
150 break | |
151 | |
152 # Since we don't wait any time for the tab url navigation to commit, it's | |
153 # possible that the tab hasn't started navigating yet. | |
154 self._WaitForUrlToChange(tab, initial_url, seconds_to_wait) | |
155 | |
156 seconds_to_wait = end_time - time.time() | |
157 seconds_to_wait = max(0, seconds_to_wait) | |
158 | |
159 try: | |
160 tab.WaitForDocumentReadyStateToBeComplete(seconds_to_wait) | |
161 except (util.TimeoutException, exceptions.DevtoolsTargetCrashException): | |
162 # Ignore time outs and web page crashes. | |
163 pass | |
164 | |
165 def _SetUpBrowser(self, finder_options): | |
166 """Finds the browser, starts the browser, and opens the requisite number of | |
167 tabs.""" | |
168 possible_browser = self._GetPossibleBrowser(finder_options) | |
169 self._browser = possible_browser.Create(finder_options) | |
170 | |
171 for _ in range(self._NUM_TABS): | |
172 self._browser.tabs.New() | |
173 | |
174 def _PerformNavigations(self): | |
175 """Performs the navigations specified by |_navigation_urls| in large | |
176 batches.""" | |
177 # The index of the first url that has not yet been navigated to. | |
178 navigation_url_index = 0 | |
179 while True: | |
180 # Generate the next batch of navigations. | |
181 batch = [] | |
182 max_index = min(navigation_url_index + self._NUM_PARALLEL_PAGES, | |
183 len(self._navigation_urls)) | |
184 for i in range(navigation_url_index, max_index): | |
185 url = self._navigation_urls[i] | |
186 tab = self._browser.tabs[i % self._NUM_TABS] | |
187 batch.append((tab, url)) | |
188 navigation_url_index = max_index | |
189 | |
190 queued_tabs = self._BatchNavigateTabs(batch) | |
191 self._WaitForQueuedTabsToLoad(queued_tabs) | |
192 | |
193 if navigation_url_index == len(self._navigation_urls): | |
194 break | |
195 | |
196 def _TearDownBrowser(self): | |
197 """Teardown that is guaranteed to be executed before the instance is | |
198 destroyed.""" | |
199 if self._browser: | |
200 self._browser.Close() | |
201 self._browser = None | |
OLD | NEW |