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