tab.py revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
1# Copyright (c) 2012 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 5from telemetry.core import bitmap 6from telemetry.core import web_contents 7 8DEFAULT_TAB_TIMEOUT = 60 9 10 11class BoundingBoxNotFoundException(Exception): 12 pass 13 14 15class Tab(web_contents.WebContents): 16 """Represents a tab in the browser 17 18 The important parts of the Tab object are in the runtime and page objects. 19 E.g.: 20 # Navigates the tab to a given url. 21 tab.Navigate('http://www.google.com/') 22 23 # Evaluates 1+1 in the tab's JavaScript context. 24 tab.Evaluate('1+1') 25 """ 26 def __init__(self, inspector_backend): 27 super(Tab, self).__init__(inspector_backend) 28 self._previous_tab_contents_bounding_box = None 29 30 def __del__(self): 31 super(Tab, self).__del__() 32 33 @property 34 def browser(self): 35 """The browser in which this tab resides.""" 36 return self._inspector_backend.browser 37 38 @property 39 def url(self): 40 return self._inspector_backend.url 41 42 @property 43 def dom_stats(self): 44 """A dictionary populated with measured DOM statistics. 45 46 Currently this dictionary contains: 47 { 48 'document_count': integer, 49 'node_count': integer, 50 'event_listener_count': integer 51 } 52 """ 53 dom_counters = self._inspector_backend.GetDOMStats( 54 timeout=DEFAULT_TAB_TIMEOUT) 55 assert (len(dom_counters) == 3 and 56 all([x in dom_counters for x in ['document_count', 'node_count', 57 'event_listener_count']])) 58 return dom_counters 59 60 61 def Activate(self): 62 """Brings this tab to the foreground asynchronously. 63 64 Not all browsers or browser versions support this method. 65 Be sure to check browser.supports_tab_control. 66 67 Please note: this is asynchronous. There is a delay between this call 68 and the page's documentVisibilityState becoming 'visible', and yet more 69 delay until the actual tab is visible to the user. None of these delays 70 are included in this call.""" 71 self._inspector_backend.Activate() 72 73 @property 74 def screenshot_supported(self): 75 """True if the browser instance is capable of capturing screenshots.""" 76 return self._inspector_backend.screenshot_supported 77 78 def Screenshot(self, timeout=DEFAULT_TAB_TIMEOUT): 79 """Capture a screenshot of the tab's contents. 80 81 Returns: 82 A telemetry.core.Bitmap. 83 """ 84 return self._inspector_backend.Screenshot(timeout) 85 86 @property 87 def video_capture_supported(self): 88 """True if the browser instance is capable of capturing video.""" 89 return self.browser.platform.CanCaptureVideo() 90 91 def Highlight(self, color): 92 """Synchronously highlights entire tab contents with the given RgbaColor. 93 94 TODO(tonyg): It is possible that the z-index hack here might not work for 95 all pages. If this happens, DevTools also provides a method for this. 96 """ 97 self.ExecuteJavaScript(""" 98 (function() { 99 var screen = document.createElement('div'); 100 screen.style.background = 'rgba(%d, %d, %d, %d)'; 101 screen.style.position = 'fixed'; 102 screen.style.top = '0'; 103 screen.style.left = '0'; 104 screen.style.width = '100%%'; 105 screen.style.height = '100%%'; 106 screen.style.zIndex = '2147483638'; 107 document.body.appendChild(screen); 108 requestAnimationFrame(function() { 109 window.__telemetry_screen_%d = screen; 110 }); 111 })(); 112 """ % (color.r, color.g, color.b, color.a, int(color))) 113 self.WaitForJavaScriptExpression( 114 '!!window.__telemetry_screen_%d' % int(color), 5) 115 116 def ClearHighlight(self, color): 117 """Clears a highlight of the given bitmap.RgbaColor.""" 118 self.ExecuteJavaScript(""" 119 (function() { 120 document.body.removeChild(window.__telemetry_screen_%d); 121 requestAnimationFrame(function() { 122 window.__telemetry_screen_%d = null; 123 }); 124 })(); 125 """ % (int(color), int(color))) 126 self.WaitForJavaScriptExpression( 127 '!window.__telemetry_screen_%d' % int(color), 5) 128 129 def StartVideoCapture(self, min_bitrate_mbps): 130 """Starts capturing video of the tab's contents. 131 132 This works by flashing the entire tab contents to a arbitrary color and then 133 starting video recording. When the frames are processed, we can look for 134 that flash as the content bounds. 135 136 Args: 137 min_bitrate_mbps: The minimum caputre bitrate in MegaBits Per Second. 138 The platform is free to deliver a higher bitrate if it can do so 139 without increasing overhead. 140 """ 141 self.Highlight(bitmap.WEB_PAGE_TEST_ORANGE) 142 self.browser.platform.StartVideoCapture(min_bitrate_mbps) 143 self.ClearHighlight(bitmap.WEB_PAGE_TEST_ORANGE) 144 145 def _FindHighlightBoundingBox(self, bmp, color, bounds_tolerance=4, 146 color_tolerance=8): 147 """Returns the bounding box of the content highlight of the given color. 148 149 Raises: 150 BoundingBoxNotFoundException if the hightlight could not be found. 151 """ 152 content_box, pixel_count = bmp.GetBoundingBox(color, 153 tolerance=color_tolerance) 154 155 if not content_box: 156 return None 157 158 # We assume arbitrarily that tabs are all larger than 200x200. If this 159 # fails it either means that assumption has changed or something is 160 # awry with our bounding box calculation. 161 if content_box[2] < 200 or content_box[3] < 200: 162 raise BoundingBoxNotFoundException('Unexpectedly small tab contents.') 163 164 # TODO(tonyg): Can this threshold be increased? 165 if pixel_count < 0.9 * content_box[2] * content_box[3]: 166 raise BoundingBoxNotFoundException( 167 'Low count of pixels in tab contents matching expected color.') 168 169 # Since Telemetry doesn't know how to resize the window, we assume 170 # that we should always get the same content box for a tab. If this 171 # fails, it means either that assumption has changed or something is 172 # awry with our bounding box calculation. If this assumption changes, 173 # this can be removed. 174 # 175 # TODO(tonyg): This assert doesn't seem to work. 176 if (self._previous_tab_contents_bounding_box and 177 self._previous_tab_contents_bounding_box != content_box): 178 # Check if there's just a minor variation on the bounding box. If it's 179 # just a few pixels, we can assume it's probably due to 180 # compression artifacts. 181 for i in xrange(len(content_box)): 182 bounds_difference = abs(content_box[i] - 183 self._previous_tab_contents_bounding_box[i]) 184 if bounds_difference > bounds_tolerance: 185 raise BoundingBoxNotFoundException( 186 'Unexpected change in tab contents box.') 187 self._previous_tab_contents_bounding_box = content_box 188 189 return content_box 190 191 def StopVideoCapture(self): 192 """Stops recording video of the tab's contents. 193 194 This looks for the initial color flash in the first frame to establish the 195 tab content boundaries and then omits all frames displaying the flash. 196 197 Yields: 198 (time_ms, bitmap) tuples representing each video keyframe. Only the first 199 frame in a run of sequential duplicate bitmaps is typically included. 200 time_ms is milliseconds since navigationStart. 201 bitmap is a telemetry.core.Bitmap. 202 """ 203 frame_generator = self.browser.platform.StopVideoCapture() 204 205 # Flip through frames until we find the initial tab contents flash. 206 content_box = None 207 for _, bmp in frame_generator: 208 content_box = self._FindHighlightBoundingBox( 209 bmp, bitmap.WEB_PAGE_TEST_ORANGE) 210 if content_box: 211 break 212 213 if not content_box: 214 raise BoundingBoxNotFoundException( 215 'Failed to identify tab contents in video capture.') 216 217 # Flip through frames until the flash goes away and emit that as frame 0. 218 timestamp = 0 219 for timestamp, bmp in frame_generator: 220 if not self._FindHighlightBoundingBox(bmp, bitmap.WEB_PAGE_TEST_ORANGE): 221 yield 0, bmp.Crop(*content_box) 222 break 223 224 start_time = timestamp 225 for timestamp, bmp in frame_generator: 226 yield timestamp - start_time, bmp.Crop(*content_box) 227 228 def PerformActionAndWaitForNavigate( 229 self, action_function, timeout=DEFAULT_TAB_TIMEOUT): 230 """Executes action_function, and waits for the navigation to complete. 231 232 action_function must be a Python function that results in a navigation. 233 This function returns when the navigation is complete or when 234 the timeout has been exceeded. 235 """ 236 self._inspector_backend.PerformActionAndWaitForNavigate( 237 action_function, timeout) 238 239 def Navigate(self, url, script_to_evaluate_on_commit=None, 240 timeout=DEFAULT_TAB_TIMEOUT): 241 """Navigates to url. 242 243 If |script_to_evaluate_on_commit| is given, the script source string will be 244 evaluated when the navigation is committed. This is after the context of 245 the page exists, but before any script on the page itself has executed. 246 """ 247 self._inspector_backend.Navigate(url, script_to_evaluate_on_commit, timeout) 248 249 def GetCookieByName(self, name, timeout=DEFAULT_TAB_TIMEOUT): 250 """Returns the value of the cookie by the given |name|.""" 251 return self._inspector_backend.GetCookieByName(name, timeout) 252 253 def CollectGarbage(self): 254 self._inspector_backend.CollectGarbage() 255 256 def ClearCache(self, force): 257 """Clears the browser's networking related disk, memory and other caches. 258 259 Args: 260 force: Iff true, navigates to about:blank which destroys the previous 261 renderer, ensuring that even "live" resources in the memory cache are 262 cleared. 263 """ 264 self.ExecuteJavaScript(""" 265 if (window.chrome && chrome.benchmarking && 266 chrome.benchmarking.clearCache) { 267 chrome.benchmarking.clearCache(); 268 chrome.benchmarking.clearPredictorCache(); 269 chrome.benchmarking.clearHostResolverCache(); 270 } 271 """) 272 if force: 273 self.Navigate('about:blank') 274