1#!/usr/bin/env python 2 3# This Source Code Form is subject to the terms of the Mozilla Public 4# License, v. 2.0. If a copy of the MPL was not distributed with this 5# file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7from __future__ import absolute_import, division 8 9import json 10import os 11import requests 12import time 13 14from benchmark import Benchmark 15from cmdline import CHROMIUM_DISTROS 16from control_server import RaptorControlServer 17from gen_test_config import gen_test_config 18from logger.logger import RaptorLogger 19from memory import generate_android_memory_profile 20from perftest import Perftest 21from results import RaptorResultsHandler 22 23LOG = RaptorLogger(component="raptor-webext") 24 25here = os.path.abspath(os.path.dirname(__file__)) 26webext_dir = os.path.join(here, "..", "..", "webext") 27 28 29class WebExtension(Perftest): 30 """Container class for WebExtension.""" 31 32 def __init__(self, *args, **kwargs): 33 self.raptor_webext = None 34 self.control_server = None 35 self.cpu_profiler = None 36 37 super(WebExtension, self).__init__(*args, **kwargs) 38 39 # set up the results handler 40 self.results_handler = RaptorResultsHandler(**self.config) 41 browser_name, browser_version = self.get_browser_meta() 42 self.results_handler.add_browser_meta(self.config["app"], browser_version) 43 44 self.start_control_server() 45 46 def run_test_setup(self, test): 47 super(WebExtension, self).run_test_setup(test) 48 49 LOG.info("starting web extension test: %s" % test["name"]) 50 LOG.info("test settings: %s" % str(test)) 51 LOG.info("web extension config: %s" % str(self.config)) 52 53 if test.get("type") == "benchmark": 54 self.serve_benchmark_source(test) 55 56 gen_test_config( 57 test["name"], 58 self.control_server.port, 59 self.post_startup_delay, 60 host=self.config["host"], 61 b_port=int(self.benchmark.port) if self.benchmark else 0, 62 debug_mode=1 if self.debug_mode else 0, 63 browser_cycle=test.get("browser_cycle", 1), 64 ) 65 66 self.install_raptor_webext() 67 68 def wait_for_test_finish(self, test, timeout, process_exists_callback=None): 69 # this is a 'back-stop' i.e. if for some reason Raptor doesn't finish for some 70 # serious problem; i.e. the test was unable to send a 'page-timeout' to the control 71 # server, etc. Therefore since this is a 'back-stop' we want to be generous here; 72 # we don't want this timeout occurring unless abosultely necessary 73 74 # convert timeout to seconds and account for page cycles 75 # pylint --py3k W1619 76 timeout = int(timeout / 1000) * int(test.get("page_cycles", 1)) 77 # account for the pause the raptor webext runner takes after browser startup 78 # and the time an exception is propagated through the framework 79 # pylint --py3k W1619 80 timeout += int(self.post_startup_delay / 1000) + 10 81 82 # for page-load tests we don't start the page-timeout timer until the pageload.js content 83 # is successfully injected and invoked; which differs per site being tested; therefore we 84 # need to be generous here - let's add 10 seconds extra per page-cycle 85 if test.get("type") == "pageload": 86 timeout += 10 * int(test.get("page_cycles", 1)) 87 88 # if geckoProfile enabled, give browser more time for profiling 89 if self.config["gecko_profile"] is True: 90 timeout += 5 * 60 91 92 # we also need to give time for results processing, not just page/browser cycles! 93 timeout += 60 94 95 # stop 5 seconds early 96 end_time = time.time() + timeout - 5 97 98 while not self.control_server._finished: 99 # Ignore check if the control server shutdown the app 100 if not self.control_server._is_shutting_down: 101 # If the application is no longer running immediately bail out 102 if callable(process_exists_callback) and not process_exists_callback(): 103 raise RuntimeError("Process has been unexpectedly closed") 104 105 if self.config["enable_control_server_wait"]: 106 response = self.control_server_wait_get() 107 if response == "webext_shutdownBrowser": 108 if self.config["memory_test"]: 109 generate_android_memory_profile(self, test["name"]) 110 if self.cpu_profiler: 111 self.cpu_profiler.generate_android_cpu_profile(test["name"]) 112 113 self.control_server_wait_continue() 114 115 # Sleep for a moment to not check the process too often 116 time.sleep(1) 117 118 # we only want to force browser-shutdown on timeout if not in debug mode; 119 # in debug-mode we leave the browser running (require manual shutdown) 120 if not self.debug_mode and end_time < time.time(): 121 self.control_server.wait_for_quit() 122 123 if not self.control_server.is_webextension_loaded: 124 raise RuntimeError("Connection to Raptor webextension failed!") 125 126 raise RuntimeError( 127 "Test failed to finish. " 128 "Application timed out after {} seconds".format(timeout) 129 ) 130 131 if self.control_server._runtime_error: 132 raise RuntimeError( 133 "Failed to run {}: {}\nStack:\n{}".format( 134 test["name"], 135 self.control_server._runtime_error["error"], 136 self.control_server._runtime_error["stack"], 137 ) 138 ) 139 140 def run_test_teardown(self, test): 141 super(WebExtension, self).run_test_teardown(test) 142 143 if self.playback is not None: 144 self.playback.stop() 145 self.playback = None 146 147 self.remove_raptor_webext() 148 149 def set_browser_test_prefs(self, raw_prefs): 150 # add test specific preferences 151 LOG.info("setting test-specific Firefox preferences") 152 self.profile.set_preferences(json.loads(raw_prefs)) 153 154 def build_browser_profile(self): 155 super(WebExtension, self).build_browser_profile() 156 157 if self.control_server: 158 # The control server and the browser profile are not well factored 159 # at this time, so the start-up process overlaps. Accommodate. 160 self.control_server.user_profile = self.profile 161 162 def start_control_server(self): 163 self.control_server = RaptorControlServer(self.results_handler, self.debug_mode) 164 self.control_server.user_profile = self.profile 165 self.control_server.start() 166 167 if self.config["enable_control_server_wait"]: 168 self.control_server_wait_set("webext_shutdownBrowser") 169 170 def serve_benchmark_source(self, test): 171 # benchmark-type tests require the benchmark test to be served out 172 self.benchmark = Benchmark(self.config, test) 173 174 def install_raptor_webext(self): 175 # must intall raptor addon each time because we dynamically update some content 176 # the webext is installed into the browser profile 177 # note: for chrome the addon is just a list of paths that ultimately are added 178 # to the chromium command line '--load-extension' argument 179 self.raptor_webext = os.path.join(webext_dir, "raptor") 180 LOG.info("installing webext %s" % self.raptor_webext) 181 self.profile.addons.install(self.raptor_webext) 182 183 # on firefox we can get an addon id; chrome addon actually is just cmd line arg 184 try: 185 self.webext_id = self.profile.addons.addon_details(self.raptor_webext)["id"] 186 except AttributeError: 187 self.webext_id = None 188 189 self.control_server.startup_handler(False) 190 191 def remove_raptor_webext(self): 192 # remove the raptor webext; as it must be reloaded with each subtest anyway 193 if not self.raptor_webext: 194 LOG.info("raptor webext not installed - not attempting removal") 195 return 196 197 LOG.info("removing webext %s" % self.raptor_webext) 198 if self.config["app"] in ["firefox", "geckoview", "refbrow", "fenix"]: 199 self.profile.addons.remove_addon(self.webext_id) 200 201 # for chrome the addon is just a list (appended to cmd line) 202 chrome_apps = CHROMIUM_DISTROS + ["chrome-android", "chromium-android"] 203 if self.config["app"] in chrome_apps: 204 self.profile.addons.remove(self.raptor_webext) 205 206 self.raptor_webext = None 207 208 def clean_up(self): 209 super(WebExtension, self).clean_up() 210 211 if self.config["enable_control_server_wait"]: 212 self.control_server_wait_clear("all") 213 214 self.control_server.stop() 215 LOG.info("finished") 216 217 def control_server_wait_set(self, state): 218 response = requests.post( 219 "http://127.0.0.1:%s/" % self.control_server.port, 220 json={"type": "wait-set", "data": state}, 221 ) 222 return response.text 223 224 def control_server_wait_timeout(self, timeout): 225 response = requests.post( 226 "http://127.0.0.1:%s/" % self.control_server.port, 227 json={"type": "wait-timeout", "data": timeout}, 228 ) 229 return response.text 230 231 def control_server_wait_get(self): 232 response = requests.post( 233 "http://127.0.0.1:%s/" % self.control_server.port, 234 json={"type": "wait-get", "data": ""}, 235 ) 236 return response.text 237 238 def control_server_wait_continue(self): 239 response = requests.post( 240 "http://127.0.0.1:%s/" % self.control_server.port, 241 json={"type": "wait-continue", "data": ""}, 242 ) 243 return response.text 244 245 def control_server_wait_clear(self, state): 246 response = requests.post( 247 "http://127.0.0.1:%s/" % self.control_server.port, 248 json={"type": "wait-clear", "data": state}, 249 ) 250 return response.text 251