Bug 1522972 - Make existing main Raptor code more modular; r=davehunt
authorRob Wood <rwood@mozilla.com>
Fri, 15 Feb 2019 14:55:16 +0000
changeset 459547 da98ba6d879b0318bdb5e68c0abcfb1fae181125
parent 459546 4cb13cbe5f051f6b55d5e7d089916e6d86fd6383
child 459548 2d5fbe3bb7f6773ec20384c23178515a9013ad17
push id35563
push userccoroiu@mozilla.com
push dateSat, 16 Feb 2019 09:36:04 +0000
treeherdermozilla-central@1cfd69d05aa1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdavehunt
bugs1522972
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1522972 - Make existing main Raptor code more modular; r=davehunt Differential Revision: https://phabricator.services.mozilla.com/D18689
testing/raptor/raptor/manifest.py
testing/raptor/raptor/raptor.py
testing/raptor/raptor/utils.py
testing/raptor/test/conftest.py
testing/raptor/test/test_raptor.py
--- a/testing/raptor/raptor/manifest.py
+++ b/testing/raptor/raptor/manifest.py
@@ -46,16 +46,18 @@ def validate_test_ini(test_details):
         # measure setting not required for benchmark type tests
         if setting == 'measure' and test_details['type'] == 'benchmark':
             continue
         if setting not in test_details:
             valid_settings = False
             LOG.error("ERROR: setting '%s' is required but not found in %s"
                       % (setting, test_details['manifest']))
 
+    test_details.setdefault("page_timeout", 30000)
+
     # if playback is specified, we need more playback settings
     if 'playback' in test_details:
         for setting in playback_settings:
             if setting not in test_details:
                 valid_settings = False
                 LOG.error("ERROR: setting '%s' is required but not found in %s"
                           % (setting, test_details['manifest']))
 
--- a/testing/raptor/raptor/raptor.py
+++ b/testing/raptor/raptor/raptor.py
@@ -4,17 +4,16 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 from __future__ import absolute_import
 
 import json
 import os
 import posixpath
 import shutil
-import subprocess
 import sys
 import tempfile
 import time
 
 import mozcrash
 import mozinfo
 
 from mozdevice import ADBDevice
@@ -46,25 +45,27 @@ from cmdline import parse_args
 from control_server import RaptorControlServer
 from gen_test_config import gen_test_config
 from outputhandler import OutputHandler
 from manifest import get_raptor_test_list
 from playback import get_playback
 from results import RaptorResultsHandler
 from gecko_profile import GeckoProfile
 from power import init_geckoview_power_test, finish_geckoview_power_test
+from utils import view_gecko_profile
 
 
 class Raptor(object):
     """Container class for Raptor"""
 
     def __init__(self, app, binary, run_local=False, obj_path=None,
                  gecko_profile=False, gecko_profile_interval=None, gecko_profile_entries=None,
                  symbols_path=None, host=None, power_test=False, is_release_build=False,
                  debug_mode=False):
+
         # Override the magic --host HOST_IP with the value of the environment variable.
         if host == 'HOST_IP':
             host = os.environ['HOST_IP']
         self.config = {}
         self.config['app'] = app
         self.config['binary'] = binary
         self.config['platform'] = mozinfo.os
         self.config['processor'] = mozinfo.processor
@@ -77,84 +78,107 @@ class Raptor(object):
         self.config['host'] = host
         self.config['power_test'] = power_test
         self.config['is_release_build'] = is_release_build
         self.raptor_venv = os.path.join(os.getcwd(), 'raptor-venv')
         self.log = get_default_logger(component='raptor-main')
         self.control_server = None
         self.playback = None
         self.benchmark = None
+        self.benchmark_port = 0
         self.gecko_profiler = None
         self.post_startup_delay = 30000
         self.device = None
+        self.profile_class = app
 
         # debug mode is currently only supported when running locally
         self.debug_mode = debug_mode if self.config['run_local'] else False
 
         # if running debug-mode reduce the pause after browser startup
         if self.debug_mode:
             self.post_startup_delay = 3000
             self.log.info("debug-mode enabled, reducing post-browser startup pause to %d ms"
                           % self.post_startup_delay)
 
-        # Create the profile; for geckoview/fennec we want a firefox profile type
-        if self.config['app'] in ["geckoview", "fennec"]:
-            self.profile = create_profile('firefox')
+        self.log.info("main raptor init, config is: %s" % str(self.config))
+
+        # create results holder
+        self.results_handler = RaptorResultsHandler()
+
+    @property
+    def profile_data_dir(self):
+        if 'MOZ_DEVELOPER_REPO_DIR' in os.environ:
+            return os.path.join(os.environ['MOZ_DEVELOPER_REPO_DIR'], 'testing', 'profiles')
+        if build:
+            return os.path.join(build.topsrcdir, 'testing', 'profiles')
+        return os.path.join(here, 'profile_data')
+
+    def run_test_setup(self, test):
+        self.log.info("starting raptor test: %s" % test['name'])
+        self.log.info("test settings: %s" % str(test))
+        self.log.info("raptor config: %s" % str(self.config))
+
+        if test.get('type') == "benchmark":
+            self.serve_benchmark_source(test)
+
+        gen_test_config(self.config['app'],
+                        test['name'],
+                        self.control_server.port,
+                        self.post_startup_delay,
+                        host=self.config['host'],
+                        b_port=self.benchmark_port,
+                        debug_mode=1 if self.debug_mode else 0)
+
+        self.install_raptor_webext()
+
+        if test.get("preferences", None) is not None:
+            self.set_browser_test_prefs(test['preferences'])
+
+        # if 'alert_on' was provided in the test INI, add to our config for results/output
+        self.config['subtest_alert_on'] = test.get('alert_on', None)
+
+    def set_browser_test_prefs(self, raw_prefs):
+        # add test specific preferences
+        if self.config['app'] == "firefox":
+            self.log.info("setting test-specific browser preferences")
+            self.profile.set_preferences(json.loads(raw_prefs))
         else:
-            self.profile = create_profile(self.config['app'])
+            self.log.info("preferences were configured for the test, however \
+                          we currently do not install them on non Firefox browsers.")
+
+    def run_test_teardown(self):
+        self.check_for_crashes()
+
+        if self.playback is not None:
+            self.playback.stop()
+
+        self.remove_raptor_webext()
+
+        # gecko profiling symbolication
+        if self.config['gecko_profile'] is True:
+            self.gecko_profiler.symbolicate()
+            # clean up the temp gecko profiling folders
+            self.log.info("cleaning up after gecko profiling")
+            self.gecko_profiler.clean()
+
+    def create_browser_profile(self):
+        self.profile = create_profile(self.profile_class)
 
         # Merge in base profiles
         with open(os.path.join(self.profile_data_dir, 'profiles.json'), 'r') as fh:
             base_profiles = json.load(fh)['raptor']
 
         for name in base_profiles:
             path = os.path.join(self.profile_data_dir, name)
             self.log.info("Merging profile: {}".format(path))
             self.profile.merge(path)
 
         # add profile dir to our config
         self.config['local_profile_dir'] = self.profile.profile
 
-        # create results holder
-        self.results_handler = RaptorResultsHandler()
-
-        # when testing desktop browsers we use mozrunner to start the browser; when
-        # testing on android (i.e. geckoview) we use mozdevice to control the device app
-
-        if self.config['app'] in ["geckoview", "fennec"]:
-            # create the android device handler; it gets initiated and sets up adb etc
-            self.log.info("creating android device handler using mozdevice")
-            self.device = ADBDevice(verbose=True)
-            self.device.clear_logcat()
-            self.log.info("Clear %s app data" % self.config['binary'])
-            self.device.shell("pm clear %s" % self.config['binary'])
-            if self.config['power_test']:
-                init_geckoview_power_test(self)
-        else:
-            # create the desktop browser runner
-            self.log.info("creating browser runner using mozrunner")
-            self.output_handler = OutputHandler()
-            process_args = {
-                'processOutputLine': [self.output_handler],
-            }
-            runner_cls = runners[app]
-            self.runner = runner_cls(
-                binary, profile=self.profile, process_args=process_args,
-                symbols_path=self.config['symbols_path'])
-
-        self.log.info("raptor config: %s" % str(self.config))
-
-    @property
-    def profile_data_dir(self):
-        if 'MOZ_DEVELOPER_REPO_DIR' in os.environ:
-            return os.path.join(os.environ['MOZ_DEVELOPER_REPO_DIR'], 'testing', 'profiles')
-        if build:
-            return os.path.join(build.topsrcdir, 'testing', 'profiles')
-        return os.path.join(here, 'profile_data')
-
     def start_control_server(self):
         self.control_server = RaptorControlServer(self.results_handler, self.debug_mode)
         self.control_server.start()
 
         # for android we must make the control server available to the device
         if self.config['app'] in ['geckoview', 'fennec'] and \
                 self.config['host'] in ('localhost', '127.0.0.1'):
             self.log.info("making the raptor control server port available to device")
@@ -167,309 +191,113 @@ class Raptor(object):
         self.config['playback_binary_manifest'] = test.get('playback_binary_manifest', None)
         _key = 'playback_binary_zip_%s' % self.config['platform']
         self.config['playback_binary_zip'] = test.get(_key, None)
         self.config['playback_pageset_manifest'] = test.get('playback_pageset_manifest', None)
         _key = 'playback_pageset_zip_%s' % self.config['platform']
         self.config['playback_pageset_zip'] = test.get(_key, None)
         self.config['playback_recordings'] = test.get('playback_recordings', None)
 
-    def run_test(self, test, timeout=None):
-        self.log.info("starting raptor test: %s" % test['name'])
-        self.log.info("test settings: %s" % str(test))
-        self.log.info("raptor config: %s" % str(self.config))
-
+    def serve_benchmark_source(self, test):
         # benchmark-type tests require the benchmark test to be served out
-        if test.get('type') == "benchmark":
-            self.benchmark = Benchmark(self.config, test)
-            benchmark_port = int(self.benchmark.port)
+        self.benchmark = Benchmark(self.config, test)
+        self.benchmark_port = int(self.benchmark.port)
 
-            # for android we must make the benchmarks server available to the device
-            if self.config['app'] in ['geckoview', 'fennec'] and \
-                    self.config['host'] in ('localhost', '127.0.0.1'):
-                self.log.info("making the raptor benchmarks server port available to device")
-                _tcp_port = "tcp:%s" % benchmark_port
-                self.device.create_socket_connection('reverse', _tcp_port, _tcp_port)
-
-        else:
-            benchmark_port = 0
+        # for android we must make the benchmarks server available to the device
+        if self.config['app'] in ['geckoview', 'fennec'] and \
+                self.config['host'] in ('localhost', '127.0.0.1'):
+            self.log.info("making the raptor benchmarks server port available to device")
+            _tcp_port = "tcp:%s" % self.benchmark_port
+            self.device.create_socket_connection('reverse', _tcp_port, _tcp_port)
 
-        gen_test_config(self.config['app'],
-                        test['name'],
-                        self.control_server.port,
-                        self.post_startup_delay,
-                        host=self.config['host'],
-                        b_port=benchmark_port,
-                        debug_mode=1 if self.debug_mode else 0)
-
+    def install_raptor_webext(self):
         # must intall raptor addon each time because we dynamically update some content
+        # the webext is installed into the browser profile
         # note: for chrome the addon is just a list of paths that ultimately are added
         # to the chromium command line '--load-extension' argument
-        raptor_webext = os.path.join(webext_dir, 'raptor')
-        self.log.info("installing webext %s" % raptor_webext)
-        self.profile.addons.install(raptor_webext)
-
-        # add test specific preferences
-        if test.get("preferences", None) is not None:
-            if self.config['app'] == "firefox":
-                self.profile.set_preferences(json.loads(test['preferences']))
-            else:
-                self.log.info("preferences were configured for the test, \
-                              but we do not install them on non Firefox browsers.")
-
-        # if 'alert_on' was provided in the test INI, we must add that to our config
-        # for use in our results.py and output.py
-        # test['alert_on'] has already been converted to a list and stripped of spaces
-        self.config['subtest_alert_on'] = test.get('alert_on', None)
+        self.raptor_webext = os.path.join(webext_dir, 'raptor')
+        self.log.info("installing webext %s" % self.raptor_webext)
+        self.profile.addons.install(self.raptor_webext)
 
         # on firefox we can get an addon id; chrome addon actually is just cmd line arg
         if self.config['app'] in ['firefox', 'geckoview', 'fennec']:
-            webext_id = self.profile.addons.addon_details(raptor_webext)['id']
-
-        # for android/geckoview, create a top-level raptor folder on the device
-        # sdcard; if it already exists remove it so we start fresh each time
-        if self.config['app'] in ["geckoview", "fennec"]:
-            self.device_raptor_dir = "/sdcard/raptor"
-            self.config['device_raptor_dir'] = self.device_raptor_dir
-            if self.device.is_dir(self.device_raptor_dir):
-                self.log.info("deleting existing device raptor dir: %s" % self.device_raptor_dir)
-                self.device.rm(self.device_raptor_dir, recursive=True)
-            self.log.info("creating raptor folder on sdcard: %s" % self.device_raptor_dir)
-            self.device.mkdir(self.device_raptor_dir)
-            self.device.chmod(self.device_raptor_dir, recursive=True)
-
-        # some tests require tools to playback the test pages
-        if test.get('playback', None) is not None:
-            # startup the playback tool
-            self.get_playback_config(test)
-            self.playback = get_playback(self.config, self.device)
-
-            # for android we must make the playback server available to the device
-            if self.config['app'] == "geckoview" and self.config['host'] \
-                    in ('localhost', '127.0.0.1'):
-                self.log.info("making the raptor playback server port available to device")
-                _tcp_port = "tcp:8080"
-                self.device.create_socket_connection('reverse', _tcp_port, _tcp_port)
-
-        if self.config['app'] in ('geckoview', 'firefox', 'fennec') and \
-           self.config['host'] not in ('localhost', '127.0.0.1'):
-            # Must delete the proxy settings from the profile if running
-            # the test with a host different from localhost.
-            userjspath = os.path.join(self.profile.profile, 'user.js')
-            with open(userjspath) as userjsfile:
-                prefs = userjsfile.readlines()
-            prefs = [pref for pref in prefs if 'network.proxy' not in pref]
-            with open(userjspath, 'w') as userjsfile:
-                userjsfile.writelines(prefs)
-
-        # for geckoview/android pageload playback we can't use a policy to turn on the
-        # proxy; we need to set prefs instead; note that the 'host' may be different
-        # than '127.0.0.1' so we must set the prefs accordingly
-        if self.config['app'] == "geckoview" and test.get('playback', None) is not None:
-            self.log.info("setting profile prefs to turn on the geckoview browser proxy")
-            no_proxies_on = "localhost, 127.0.0.1, %s" % self.config['host']
-            proxy_prefs = {}
-            proxy_prefs["network.proxy.type"] = 1
-            proxy_prefs["network.proxy.http"] = self.config['host']
-            proxy_prefs["network.proxy.http_port"] = 8080
-            proxy_prefs["network.proxy.ssl"] = self.config['host']
-            proxy_prefs["network.proxy.ssl_port"] = 8080
-            proxy_prefs["network.proxy.no_proxies_on"] = no_proxies_on
-            self.profile.set_preferences(proxy_prefs)
-
-        # now some final settings, and then startup of the browser under test
-        if self.config['app'] in ["geckoview", "fennec"]:
-            # for geckoview/fennec we must copy the profile onto the device and set perms
-            if not self.device.is_app_installed(self.config['binary']):
-                raise Exception('%s is not installed' % self.config['binary'])
-            self.device_profile = os.path.join(self.device_raptor_dir, "profile")
-
-            if self.device.is_dir(self.device_profile):
-                self.log.info("deleting existing device profile folder: %s" % self.device_profile)
-                self.device.rm(self.device_profile, recursive=True)
-            self.log.info("creating profile folder on device: %s" % self.device_profile)
-            self.device.mkdir(self.device_profile)
-
-            self.log.info("copying firefox profile onto the device")
-            self.log.info("note: the profile folder being copied is: %s" % self.profile.profile)
-            self.log.info('the adb push cmd copies that profile dir to a new temp dir before copy')
-            self.device.push(self.profile.profile, self.device_profile)
-            self.device.chmod(self.device_profile, recursive=True)
-
-            # now start the geckoview/fennec app
-            self.log.info("starting %s" % self.config['app'])
-
-            extra_args = ["-profile", self.device_profile,
-                          "--es", "env0", "LOG_VERBOSE=1",
-                          "--es", "env1", "R_LOG_LEVEL=6"]
+            self.webext_id = self.profile.addons.addon_details(self.raptor_webext)['id']
 
-            if self.config['app'] == 'geckoview':
-                # launch geckoview example app
-                try:
-                    # make sure the geckoview app is not running before
-                    # attempting to start.
-                    self.device.stop_application(self.config['binary'])
-                    self.device.launch_activity(self.config['binary'],
-                                                "GeckoViewActivity",
-                                                extra_args=extra_args,
-                                                url='about:blank',
-                                                e10s=True,
-                                                fail_if_running=False)
-                except Exception:
-                    self.log.error("Exception launching %s" % self.config['binary'])
-                    if self.config['power_test']:
-                        finish_geckoview_power_test(self)
-                    raise
-            else:
-                # launch fennec
-                try:
-                    # if fennec is already running, shut it down first
-                    self.device.stop_application(self.config['binary'])
-                    self.device.launch_fennec(self.config['binary'],
-                                              extra_args=extra_args,
-                                              url='about:blank',
-                                              fail_if_running=False)
-                except Exception:
-                    self.log.error("Exception launching %s" % self.config['binary'])
-                    if self.config['power_test']:
-                        finish_geckoview_power_test(self)
-                    raise
-
-            self.control_server.device = self.device
-            self.control_server.app_name = self.config['binary']
-
-        else:
-            # For Firefox we need to set
-            # MOZ_DISABLE_NONLOCAL_CONNECTIONS=1 env var before
-            # startup when testing release builds from mozilla-beta or
-            # mozilla-release. This is because of restrictions on
-            # release builds that require webextensions to be signed
-            # unless MOZ_DISABLE_NONLOCAL_CONNECTIONS is set to '1'.
-            if self.config['app'] == "firefox" and self.config['is_release_build']:
-                self.log.info("setting MOZ_DISABLE_NONLOCAL_CONNECTIONS=1")
-                os.environ['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = "1"
-
-            # if running debug-mode, tell Firefox to open the browser console on startup
-            # for google chrome, open the devtools on the raptor test tab
-            if self.debug_mode:
-                if self.config['app'] == "firefox":
-                    self.runner.cmdargs.extend(['-jsconsole'])
-                if self.config['app'] == "chrome":
-                    self.runner.cmdargs.extend(['--auto-open-devtools-for-tabs'])
-
-            # now start the desktop browser
-            self.log.info("starting %s" % self.config['app'])
-
-            # if running a pageload test on google chrome, add the cmd line options
-            # to turn on the proxy and ignore security certificate errors
-            # if using host localhost, 127.0.0.1.
-            if self.config['app'] == "chrome" and test.get('playback', None) is not None:
-                chrome_args = [
-                    '--proxy-server=127.0.0.1:8080',
-                    '--proxy-bypass-list=localhost;127.0.0.1',
-                    '--ignore-certificate-errors',
-                    '--no-default-browser-check',
-                ]
-                if self.config['host'] not in ('localhost', '127.0.0.1'):
-                    chrome_args[0] = chrome_args[0].replace('127.0.0.1', self.config['host'])
-                if ' '.join(chrome_args) not in ' '.join(self.runner.cmdargs):
-                    self.runner.cmdargs.extend(chrome_args)
-
-            self.runner.start()
-            proc = self.runner.process_handler
-            self.output_handler.proc = proc
-
-            self.control_server.browser_proc = proc
-
-            # pageload tests need to be able to access non-local connections via mitmproxy
-            if self.config['app'] == "firefox" and self.config['is_release_build'] and \
-               test.get('playback', None) is not None:
-                self.log.info("setting MOZ_DISABLE_NONLOCAL_CONNECTIONS=0")
-                os.environ['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = "0"
-
-            # if geckoProfile is enabled, initialize it
-            if self.config['gecko_profile'] is True:
-                self._init_gecko_profiling(test)
-                # tell the control server the gecko_profile dir; the control server will
-                # receive the actual gecko profiles from the web ext and will write them
-                # to disk; then profiles are picked up by gecko_profile.symbolicate
-                self.control_server.gecko_profile_dir = self.gecko_profiler.gecko_profile_dir
-
-        # set our cs flag to indicate we are running the browser/app
-        self.control_server._finished = False
-
-        # convert to seconds and account for page cycles
-        timeout = int(timeout / 1000) * int(test['page_cycles'])
-        # account for the pause the raptor webext runner takes after browser startup
-        timeout += int(self.post_startup_delay / 1000)
-
-        # if geckoProfile enabled, give browser more time for profiling
-        if self.config['gecko_profile'] is True:
-            timeout += 5 * 60
-
-        try:
-            elapsed_time = 0
-            while not self.control_server._finished:
-                time.sleep(1)
-                # we only want to force browser-shutdown on timeout if not in debug mode;
-                # in debug-mode we leave the browser running (require manual shutdown)
-                if not self.debug_mode:
-                    elapsed_time += 1
-                    if elapsed_time > (timeout) - 5:  # stop 5 seconds early
-                        self.log.info("application timed out after {} seconds".format(timeout))
-                        self.control_server.wait_for_quit()
-                        break
-        finally:
-            if self.config['app'] == "geckoview":
-                if self.config['power_test']:
-                    finish_geckoview_power_test(self)
-            self.check_for_crashes()
-
-        if self.playback is not None:
-            self.playback.stop()
-
+    def remove_raptor_webext(self):
         # remove the raptor webext; as it must be reloaded with each subtest anyway
-        self.log.info("removing webext %s" % raptor_webext)
+        self.log.info("removing webext %s" % self.raptor_webext)
         if self.config['app'] in ['firefox', 'geckoview', 'fennec']:
-            self.profile.addons.remove_addon(webext_id)
+            self.profile.addons.remove_addon(self.webext_id)
 
         # for chrome the addon is just a list (appended to cmd line)
         if self.config['app'] in ["chrome", "chrome-android"]:
-            self.profile.addons.remove(raptor_webext)
+            self.profile.addons.remove(self.raptor_webext)
 
-        # gecko profiling symbolication
-        if self.config['gecko_profile'] is True:
-            self.gecko_profiler.symbolicate()
-            # clean up the temp gecko profiling folders
-            self.log.info("cleaning up after gecko profiling")
-            self.gecko_profiler.clean()
+    def set_test_browser_prefs(self, test_prefs):
+        # if the test has any specific browser prefs specified, set those in browser profiile
+        if self.config['app'] == "firefox":
+            self.profile.set_preferences(json.loads(test_prefs))
+        else:
+            self.log.info("preferences were configured for the test, \
+                          but we do not install them on non Firefox browsers.")
+
+    def start_playback(self, test):
+        # startup the playback tool
+        self.get_playback_config(test)
+        self.playback = get_playback(self.config, self.device)
 
-        # browser should be closed by now but this is a backup-shutdown (if not in debug-mode)
-        if not self.debug_mode:
-            if self.config['app'] not in ['geckoview', 'fennec']:
-                if self.runner.is_running():
-                    self.runner.stop()
-            # TODO the geckoview app should have been shutdown by this point by the
-            # control server, but we can double-check here to make sure
-        else:
-            # in debug mode, and running locally, leave the browser running
-            if self.config['run_local']:
-                self.log.info("* debug-mode enabled - please shutdown the browser manually...")
-                self.runner.wait(timeout=None)
+        # for android we must make the playback server available to the device
+        if self.config['app'] == "geckoview" and self.config['host'] \
+                in ('localhost', '127.0.0.1'):
+            self.log.info("making the raptor playback server port available to device")
+            _tcp_port = "tcp:8080"
+            self.device.create_socket_connection('reverse', _tcp_port, _tcp_port)
+
+    def delete_proxy_settings_from_profile(self):
+        # Must delete the proxy settings from the profile if running
+        # the test with a host different from localhost.
+        userjspath = os.path.join(self.profile.profile, 'user.js')
+        with open(userjspath) as userjsfile:
+            prefs = userjsfile.readlines()
+        prefs = [pref for pref in prefs if 'network.proxy' not in pref]
+        with open(userjspath, 'w') as userjsfile:
+            userjsfile.writelines(prefs)
 
     def _init_gecko_profiling(self, test):
         self.log.info("initializing gecko profiler")
         upload_dir = os.getenv('MOZ_UPLOAD_DIR')
         if not upload_dir:
             self.log.critical("Profiling ignored because MOZ_UPLOAD_DIR was not set")
         else:
             self.gecko_profiler = GeckoProfile(upload_dir,
                                                self.config,
                                                test)
 
+    def wait_for_test_finish(self, test, timeout):
+        # convert timeout to seconds and account for page cycles
+        timeout = int(timeout / 1000) * int(test['page_cycles'])
+        # account for the pause the raptor webext runner takes after browser startup
+        timeout += (int(self.post_startup_delay / 1000) + 3)
+
+        # if geckoProfile enabled, give browser more time for profiling
+        if self.config['gecko_profile'] is True:
+            timeout += 5 * 60
+
+        elapsed_time = 0
+        while not self.control_server._finished:
+            time.sleep(1)
+            # we only want to force browser-shutdown on timeout if not in debug mode;
+            # in debug-mode we leave the browser running (require manual shutdown)
+            if not self.debug_mode:
+                elapsed_time += 1
+                if elapsed_time > (timeout) - 5:  # stop 5 seconds early
+                    self.log.info("application timed out after {} seconds".format(timeout))
+                    self.control_server.wait_for_quit()
+                    break
+
     def process_results(self, test_names):
         # when running locally output results in build/raptor.json; when running
         # in production output to a local.json to be turned into tc job artifact
         if self.config.get('run_local', False):
             if 'MOZ_DEVELOPER_REPO_DIR' in os.environ:
                 raptor_json_path = os.path.join(os.environ['MOZ_DEVELOPER_REPO_DIR'],
                                                 'testing', 'mozharness', 'build', 'raptor.json')
             else:
@@ -478,110 +306,349 @@ class Raptor(object):
             raptor_json_path = os.path.join(os.getcwd(), 'local.json')
 
         self.config['raptor_json_path'] = raptor_json_path
         return self.results_handler.summarize_and_output(self.config, test_names)
 
     def get_page_timeout_list(self):
         return self.results_handler.page_timeout_list
 
-    def check_for_crashes(self):
-        if self.config['app'] in ["geckoview", "fennec"]:
-            # Turn off verbose to prevent logcat from being inserted into the main log.
-            verbose = self.device._verbose
-            self.device._verbose = False
-            logcat = self.device.get_logcat()
-            self.device._verbose = verbose
-            if logcat:
-                if mozcrash.check_for_java_exception(logcat, "raptor"):
-                    return
-            try:
-                dump_dir = tempfile.mkdtemp()
-                remote_dir = posixpath.join(self.device_profile, 'minidumps')
-                if not self.device.is_dir(remote_dir):
-                    self.log.error("No crash directory (%s) found on remote device" % remote_dir)
-                    return
-                self.device.pull(remote_dir, dump_dir)
-                mozcrash.log_crashes(self.log, dump_dir, self.config['symbols_path'])
-            finally:
-                try:
-                    shutil.rmtree(dump_dir)
-                except Exception:
-                    self.log.warning("unable to remove directory: %s" % dump_dir)
-        else:
-            try:
-                self.runner.check_for_crashes()
-            except NotImplementedError:  # not implemented for Chrome
-                pass
-
     def clean_up(self):
         self.control_server.stop()
         if self.config['app'] not in ['geckoview', 'fennec']:
             self.runner.stop()
         elif self.config['app'] in ['geckoview', 'fennec']:
             self.log.info('removing reverse socket connections')
             self.device.remove_socket_connections('reverse')
         else:
             pass
         self.log.info("finished")
 
 
-def view_gecko_profile(ffox_bin):
-    # automatically load the latest talos gecko-profile archive in perf-html.io
-    LOG = get_default_logger(component='raptor-view-gecko-profile')
+class RaptorDesktop(Raptor):
+    def __init__(self, app, binary, run_local=False, obj_path=None,
+                 gecko_profile=False, gecko_profile_interval=None, gecko_profile_entries=None,
+                 symbols_path=None, host=None, power_test=False, is_release_build=False,
+                 debug_mode=False):
+        Raptor.__init__(self, app, binary, run_local, obj_path, gecko_profile,
+                        gecko_profile_interval, gecko_profile_entries, symbols_path,
+                        host, power_test, is_release_build, debug_mode)
+
+    def create_browser_handler(self):
+        # create the desktop browser runner
+        self.log.info("creating browser runner using mozrunner")
+        self.output_handler = OutputHandler()
+        process_args = {
+            'processOutputLine': [self.output_handler],
+        }
+        runner_cls = runners[self.config['app']]
+        self.runner = runner_cls(
+            self.config['binary'], profile=self.profile, process_args=process_args,
+            symbols_path=self.config['symbols_path'])
+
+    def start_runner_proc(self):
+        # launch the browser via our previously-created runner
+        self.runner.start()
+        proc = self.runner.process_handler
+        self.output_handler.proc = proc
+
+        # give our control server the browser process so it can shut it down later
+        self.control_server.browser_proc = proc
+
+    def run_test(self, test, timeout=None):
+        self.run_test_setup(test)
+
+        if test.get('playback', None) is not None:
+            self.start_playback(test)
 
-    if sys.platform.startswith('win') and not ffox_bin.endswith(".exe"):
-        ffox_bin = ffox_bin + ".exe"
+        if self.config['host'] not in ('localhost', '127.0.0.1'):
+            self.delete_proxy_settings_from_profile()
+
+        # now start the browser/app under test
+        self.launch_desktop_browser(test)
+
+        # set our control server flag to indicate we are running the browser/app
+        self.control_server._finished = False
+
+        self.wait_for_test_finish(test, timeout)
+
+        self.run_test_teardown()
 
-    if not os.path.exists(ffox_bin):
-        LOG.info("unable to find Firefox bin, cannot launch view-gecko-profile")
-        return
+        # browser should be closed by now but this is a backup-shutdown (if not in debug-mode)
+        if not self.debug_mode:
+            if self.runner.is_running():
+                self.runner.stop()
+        else:
+            # in debug mode, and running locally, leave the browser running
+            if self.config['run_local']:
+                self.log.info("* debug-mode enabled - please shutdown the browser manually...")
+                self.runner.wait(timeout=None)
+
+    def check_for_crashes(self):
+        try:
+            self.runner.check_for_crashes()
+        except NotImplementedError:  # not implemented for Chrome
+            pass
+
+
+class RaptorDesktopFirefox(RaptorDesktop):
+    def __init__(self, app, binary, run_local=False, obj_path=None,
+                 gecko_profile=False, gecko_profile_interval=None, gecko_profile_entries=None,
+                 symbols_path=None, host=None, power_test=False, is_release_build=False,
+                 debug_mode=False):
+        RaptorDesktop.__init__(self, app, binary, run_local, obj_path, gecko_profile,
+                               gecko_profile_interval, gecko_profile_entries, symbols_path,
+                               host, power_test, is_release_build, debug_mode)
 
-    profile_zip = os.environ.get('RAPTOR_LATEST_GECKO_PROFILE_ARCHIVE', None)
-    if profile_zip is None or not os.path.exists(profile_zip):
-        LOG.info("No local talos gecko profiles were found so not launching perf-html.io")
-        return
+    def disable_non_local_connections(self):
+        # For Firefox we need to set MOZ_DISABLE_NONLOCAL_CONNECTIONS=1 env var before startup
+        # when testing release builds from mozilla-beta/release. This is because of restrictions
+        # on release builds that require webextensions to be signed unless this env var is set
+        self.log.info("setting MOZ_DISABLE_NONLOCAL_CONNECTIONS=1")
+        os.environ['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = "1"
+
+    def enable_non_local_connections(self):
+        # pageload tests need to be able to access non-local connections via mitmproxy
+        self.log.info("setting MOZ_DISABLE_NONLOCAL_CONNECTIONS=0")
+        os.environ['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = "0"
+
+    def launch_desktop_browser(self, test):
+        self.log.info("starting %s" % self.config['app'])
+        if self.config['is_release_build']:
+            self.disable_non_local_connections()
+
+        # if running debug-mode, tell Firefox to open the browser console on startup
+        if self.debug_mode:
+            self.runner.cmdargs.extend(['-jsconsole'])
+
+        self.start_runner_proc()
+
+        if self.config['is_release_build'] and test.get('playback', None) is not None:
+            self.enable_non_local_connections()
+
+        # if geckoProfile is enabled, initialize it
+        if self.config['gecko_profile'] is True:
+            self._init_gecko_profiling(test)
+            # tell the control server the gecko_profile dir; the control server will
+            # receive the actual gecko profiles from the web ext and will write them
+            # to disk; then profiles are picked up by gecko_profile.symbolicate
+            self.control_server.gecko_profile_dir = self.gecko_profiler.gecko_profile_dir
+
 
-    # need the view-gecko-profile tool, it's in repo/testing/tools
-    repo_dir = os.environ.get('MOZ_DEVELOPER_REPO_DIR', None)
-    if repo_dir is None:
-        LOG.info("unable to find MOZ_DEVELOPER_REPO_DIR, can't launch view-gecko-profile")
-        return
+class RaptorDesktopChrome(RaptorDesktop):
+    def __init__(self, app, binary, run_local=False, obj_path=None,
+                 gecko_profile=False, gecko_profile_interval=None, gecko_profile_entries=None,
+                 symbols_path=None, host=None, power_test=False, is_release_build=False,
+                 debug_mode=False):
+        RaptorDesktop.__init__(self, app, binary, run_local, obj_path, gecko_profile,
+                               gecko_profile_interval, gecko_profile_entries, symbols_path,
+                               host, power_test, is_release_build, debug_mode)
+
+    def setup_chrome_desktop_for_playback(self):
+        # if running a pageload test on google chrome, add the cmd line options
+        # to turn on the proxy and ignore security certificate errors
+        # if using host localhost, 127.0.0.1.
+        chrome_args = [
+            '--proxy-server=127.0.0.1:8080',
+            '--proxy-bypass-list=localhost;127.0.0.1',
+            '--ignore-certificate-errors',
+            '--no-default-browser-check',
+        ]
+        if self.config['host'] not in ('localhost', '127.0.0.1'):
+            chrome_args[0] = chrome_args[0].replace('127.0.0.1', self.config['host'])
+        if ' '.join(chrome_args) not in ' '.join(self.runner.cmdargs):
+            self.runner.cmdargs.extend(chrome_args)
+
+    def launch_desktop_browser(self, test):
+        self.log.info("starting %s" % self.config['app'])
+        # if running in debug-mode, open the devtools on the raptor test tab
+        if self.debug_mode:
+            self.runner.cmdargs.extend(['--auto-open-devtools-for-tabs'])
+
+        if test.get('playback', None) is not None:
+            self.setup_chrome_desktop_for_playback()
+
+        self.start_runner_proc()
+
+
+class RaptorAndroid(Raptor):
+    def __init__(self, app, binary, run_local=False, obj_path=None,
+                 gecko_profile=False, gecko_profile_interval=None, gecko_profile_entries=None,
+                 symbols_path=None, host=None, power_test=False, is_release_build=False,
+                 debug_mode=False):
+        Raptor.__init__(self, app, binary, run_local, obj_path, gecko_profile,
+                        gecko_profile_interval, gecko_profile_entries, symbols_path, host,
+                        power_test, is_release_build, debug_mode)
+
+        # on android, when creating the browser profile, we want to use a 'firefox' type profile
+        self.profile_class = "firefox"
 
-    view_gp = os.path.join(repo_dir, 'testing', 'tools',
-                           'view_gecko_profile', 'view_gecko_profile.py')
-    if not os.path.exists(view_gp):
-        LOG.info("unable to find the view-gecko-profile tool, cannot launch it")
-        return
+    def create_browser_handler(self):
+        # create the android device handler; it gets initiated and sets up adb etc
+        self.log.info("creating android device handler using mozdevice")
+        self.device = ADBDevice(verbose=True)
+        self.device.clear_logcat()
+        self.clear_app_data()
+
+    def clear_app_data(self):
+        self.log.info("clearing %s app data" % self.config['binary'])
+        self.device.shell("pm clear %s" % self.config['binary'])
+
+    def create_raptor_sdcard_folder(self):
+        # for android/geckoview, create a top-level raptor folder on the device
+        # sdcard; if it already exists remove it so we start fresh each time
+        self.device_raptor_dir = "/sdcard/raptor"
+        self.config['device_raptor_dir'] = self.device_raptor_dir
+        if self.device.is_dir(self.device_raptor_dir):
+            self.log.info("deleting existing device raptor dir: %s" % self.device_raptor_dir)
+            self.device.rm(self.device_raptor_dir, recursive=True)
+        self.log.info("creating raptor folder on sdcard: %s" % self.device_raptor_dir)
+        self.device.mkdir(self.device_raptor_dir)
+        self.device.chmod(self.device_raptor_dir, recursive=True)
+
+    def copy_profile_onto_device(self):
+        # for geckoview/fennec we must copy the profile onto the device and set perms
+        if not self.device.is_app_installed(self.config['binary']):
+            raise Exception('%s is not installed' % self.config['binary'])
+        self.device_profile = os.path.join(self.device_raptor_dir, "profile")
+
+        if self.device.is_dir(self.device_profile):
+            self.log.info("deleting existing device profile folder: %s" % self.device_profile)
+            self.device.rm(self.device_profile, recursive=True)
+        self.log.info("creating profile folder on device: %s" % self.device_profile)
+        self.device.mkdir(self.device_profile)
+
+        self.log.info("copying firefox profile onto the device")
+        self.log.info("note: the profile folder being copied is: %s" % self.profile.profile)
+        self.log.info('the adb push cmd copies that profile dir to a new temp dir before copy')
+        self.device.push(self.profile.profile, self.device_profile)
+        self.device.chmod(self.device_profile, recursive=True)
 
-    command = ['python',
-               view_gp,
-               '-b', ffox_bin,
-               '-p', profile_zip]
+    def turn_on_android_app_proxy(self):
+        # for geckoview/android pageload playback we can't use a policy to turn on the
+        # proxy; we need to set prefs instead; note that the 'host' may be different
+        # than '127.0.0.1' so we must set the prefs accordingly
+        self.log.info("setting profile prefs to turn on the android app proxy")
+        no_proxies_on = "localhost, 127.0.0.1, %s" % self.config['host']
+        proxy_prefs = {}
+        proxy_prefs["network.proxy.type"] = 1
+        proxy_prefs["network.proxy.http"] = self.config['host']
+        proxy_prefs["network.proxy.http_port"] = 8080
+        proxy_prefs["network.proxy.ssl"] = self.config['host']
+        proxy_prefs["network.proxy.ssl_port"] = 8080
+        proxy_prefs["network.proxy.no_proxies_on"] = no_proxies_on
+        self.profile.set_preferences(proxy_prefs)
 
-    LOG.info('Auto-loading this profile in perfhtml.io: %s' % profile_zip)
-    LOG.info(command)
+    def launch_firefox_android_app(self):
+        self.log.info("starting %s" % self.config['app'])
+        if self.config['app'] == "fennec":
+            self.launch_firefox_fennec()
+            return
+
+        extra_args = ["-profile", self.device_profile,
+                      "--es", "env0", "LOG_VERBOSE=1",
+                      "--es", "env1", "R_LOG_LEVEL=6"]
+
+        try:
+            # make sure the android app is not already running
+            self.device.stop_application(self.config['binary'])
+
+            self.device.launch_activity(self.config['binary'],
+                                        "GeckoViewActivity",
+                                        extra_args=extra_args,
+                                        url='about:blank',
+                                        e10s=True,
+                                        fail_if_running=False)
+        except Exception:
+            self.log.error("Exception launching %s" % self.config['binary'])
+            if self.config['power_test']:
+                finish_geckoview_power_test(self)
+            raise
 
-    # if the view-gecko-profile tool fails to launch for some reason, we don't
-    # want to crash talos! just dump error and finsh up talos as usual
-    try:
-        view_profile = subprocess.Popen(command,
-                                        stdout=subprocess.PIPE,
-                                        stderr=subprocess.PIPE)
-        # that will leave it running in own instance and let talos finish up
-    except Exception as e:
-        LOG.info("failed to launch view-gecko-profile tool, exeption: %s" % e)
-        return
+        # give our control server the device and app info
+        self.control_server.device = self.device
+        self.control_server.app_name = self.config['binary']
+
+    def launch_firefox_fennec(self):
+        self.log.info("starting %s" % self.config['app'])
+        extra_args = ["-profile", self.device_profile,
+                      "--es", "env0", "LOG_VERBOSE=1",
+                      "--es", "env1", "R_LOG_LEVEL=6"]
+
+        try:
+            # if fennec is already running, shut it down first
+            self.device.stop_application(self.config['binary'])
+            self.device.launch_fennec(self.config['binary'],
+                                      extra_args=extra_args,
+                                      url='about:blank',
+                                      fail_if_running=False)
+        except Exception:
+            self.log.error("Exception launching %s" % self.config['binary'])
+            if self.config['power_test']:
+                finish_geckoview_power_test(self)
+            raise
+
+    def run_test(self, test, timeout=None):
+        if self.config['power_test']:
+            init_geckoview_power_test(self)
+
+        self.run_test_setup(test)
+        self.create_raptor_sdcard_folder()
+
+        if test.get('playback', None) is not None:
+            self.start_playback(test)
+
+        if self.config['host'] not in ('localhost', '127.0.0.1'):
+            self.delete_proxy_settings_from_profile()
+
+        if test.get('playback', None) is not None:
+            self.turn_on_android_app_proxy()
+
+        self.copy_profile_onto_device()
 
-    time.sleep(5)
-    ret = view_profile.poll()
-    if ret is None:
-        LOG.info("view-gecko-profile successfully started as pid %d" % view_profile.pid)
-    else:
-        LOG.error('view-gecko-profile process failed to start, poll returned: %s' % ret)
+        # now start the browser/app under test
+        self.launch_firefox_android_app()
+
+        # set our control server flag to indicate we are running the browser/app
+        self.control_server._finished = False
+
+        self.wait_for_test_finish(test, timeout)
+
+        if self.config['power_test']:
+            finish_geckoview_power_test(self)
+
+        self.run_test_teardown()
+
+        # in debug mode, and running locally, leave the browser running
+        if self.debug_mode and self.config['run_local']:
+            self.log.info("* debug-mode enabled - please shutdown the browser manually...")
+            self.runner.wait(timeout=None)
+
+    def check_for_crashes(self):
+        # Turn off verbose to prevent logcat from being inserted into the main log.
+        verbose = self.device._verbose
+        self.device._verbose = False
+        logcat = self.device.get_logcat()
+        self.device._verbose = verbose
+        if logcat:
+            if mozcrash.check_for_java_exception(logcat, "raptor"):
+                return
+        try:
+            dump_dir = tempfile.mkdtemp()
+            remote_dir = posixpath.join(self.device_profile, 'minidumps')
+            if not self.device.is_dir(remote_dir):
+                self.log.error("No crash directory (%s) found on remote device" % remote_dir)
+                return
+            self.device.pull(remote_dir, dump_dir)
+            mozcrash.log_crashes(self.log, dump_dir, self.config['symbols_path'])
+        finally:
+            try:
+                shutil.rmtree(dump_dir)
+            except Exception:
+                self.log.warning("unable to remove directory: %s" % dump_dir)
 
 
 def main(args=sys.argv[1:]):
     args = parse_args()
     commandline.setup_logging('raptor', args, {'tbpl': sys.stdout})
     LOG = get_default_logger(component='raptor-main')
 
     LOG.info("raptor-start")
@@ -600,37 +667,41 @@ def main(args=sys.argv[1:]):
     if len(raptor_test_list) == 0:
         LOG.critical("abort: no tests found")
         sys.exit(1)
 
     LOG.info("raptor tests scheduled to run:")
     for next_test in raptor_test_list:
         LOG.info(next_test['name'])
 
-    raptor = Raptor(args.app,
-                    args.binary,
-                    run_local=args.run_local,
-                    obj_path=args.obj_path,
-                    gecko_profile=args.gecko_profile,
-                    gecko_profile_interval=args.gecko_profile_interval,
-                    gecko_profile_entries=args.gecko_profile_entries,
-                    symbols_path=args.symbols_path,
-                    host=args.host,
-                    power_test=args.power_test,
-                    is_release_build=args.is_release_build,
-                    debug_mode=args.debug_mode)
+    if args.app == "firefox":
+        raptor_class = RaptorDesktopFirefox
+    elif args.app == "chrome":
+        raptor_class = RaptorDesktopChrome
+    else:
+        raptor_class = RaptorAndroid
 
+    raptor = raptor_class(args.app,
+                          args.binary,
+                          run_local=args.run_local,
+                          obj_path=args.obj_path,
+                          gecko_profile=args.gecko_profile,
+                          gecko_profile_interval=args.gecko_profile_interval,
+                          gecko_profile_entries=args.gecko_profile_entries,
+                          symbols_path=args.symbols_path,
+                          host=args.host,
+                          power_test=args.power_test,
+                          is_release_build=args.is_release_build,
+                          debug_mode=args.debug_mode)
+
+    raptor.create_browser_profile()
+    raptor.create_browser_handler()
     raptor.start_control_server()
 
     for next_test in raptor_test_list:
-        if 'page_timeout' not in next_test.keys():
-            next_test['page_timeout'] = 120000
-        if 'page_cycles' not in next_test.keys():
-            next_test['page_cycles'] = 1
-
         raptor.run_test(next_test, timeout=int(next_test['page_timeout']))
 
     success = raptor.process_results(raptor_test_names)
     raptor.clean_up()
 
     if not success:
         # didn't get test results; test timed out or crashed, etc. we want job to fail
         LOG.critical("TEST-UNEXPECTED-FAIL: no raptor test results were found for %s" %
--- a/testing/raptor/raptor/utils.py
+++ b/testing/raptor/raptor/utils.py
@@ -2,20 +2,22 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import os
 import signal
+import subprocess
 import sys
+import time
 import urllib
 
-from mozlog import get_proxy_logger
+from mozlog import get_proxy_logger, get_default_logger
 from mozprocess import ProcessHandler
 
 LOG = get_proxy_logger(component="raptor-utils")
 here = os.path.dirname(os.path.realpath(__file__))
 
 if os.environ.get('SCRIPTSPATH', None) is not None:
     # in production it is env SCRIPTS_PATH
     mozharness_dir = os.environ['SCRIPTSPATH']
@@ -104,8 +106,63 @@ def download_file_from_url(url, local_de
     https://hg.mozilla.org/try/raw-file/acb5abf52c04da7d4548fa13bd6c6848a90c32b8/testing/
       config/tooltool-manifests/linux64/hostutils.manifest"""
     if os.path.exists(local_dest):
         LOG.info("file already exists at: %s" % local_dest)
         return True
     LOG.info("downloading: %s to %s" % (url, local_dest))
     _file, _headers = urllib.urlretrieve(url, local_dest)
     return os.path.exists(local_dest)
+
+
+def view_gecko_profile(ffox_bin):
+    # automatically load the latest talos gecko-profile archive in perf-html.io
+    LOG = get_default_logger(component='raptor-view-gecko-profile')
+
+    if sys.platform.startswith('win') and not ffox_bin.endswith(".exe"):
+        ffox_bin = ffox_bin + ".exe"
+
+    if not os.path.exists(ffox_bin):
+        LOG.info("unable to find Firefox bin, cannot launch view-gecko-profile")
+        return
+
+    profile_zip = os.environ.get('RAPTOR_LATEST_GECKO_PROFILE_ARCHIVE', None)
+    if profile_zip is None or not os.path.exists(profile_zip):
+        LOG.info("No local talos gecko profiles were found so not launching perf-html.io")
+        return
+
+    # need the view-gecko-profile tool, it's in repo/testing/tools
+    repo_dir = os.environ.get('MOZ_DEVELOPER_REPO_DIR', None)
+    if repo_dir is None:
+        LOG.info("unable to find MOZ_DEVELOPER_REPO_DIR, can't launch view-gecko-profile")
+        return
+
+    view_gp = os.path.join(repo_dir, 'testing', 'tools',
+                           'view_gecko_profile', 'view_gecko_profile.py')
+    if not os.path.exists(view_gp):
+        LOG.info("unable to find the view-gecko-profile tool, cannot launch it")
+        return
+
+    command = ['python',
+               view_gp,
+               '-b', ffox_bin,
+               '-p', profile_zip]
+
+    LOG.info('Auto-loading this profile in perfhtml.io: %s' % profile_zip)
+    LOG.info(command)
+
+    # if the view-gecko-profile tool fails to launch for some reason, we don't
+    # want to crash talos! just dump error and finsh up talos as usual
+    try:
+        view_profile = subprocess.Popen(command,
+                                        stdout=subprocess.PIPE,
+                                        stderr=subprocess.PIPE)
+        # that will leave it running in own instance and let talos finish up
+    except Exception as e:
+        LOG.info("failed to launch view-gecko-profile tool, exeption: %s" % e)
+        return
+
+    time.sleep(5)
+    ret = view_profile.poll()
+    if ret is None:
+        LOG.info("view-gecko-profile successfully started as pid %d" % view_profile.pid)
+    else:
+        LOG.error('view-gecko-profile process failed to start, poll returned: %s' % ret)
--- a/testing/raptor/test/conftest.py
+++ b/testing/raptor/test/conftest.py
@@ -13,34 +13,34 @@ here = os.path.abspath(os.path.dirname(_
 if os.environ.get('SCRIPTSPATH', None) is not None:
     # in production it is env SCRIPTS_PATH
     mozharness_dir = os.environ['SCRIPTSPATH']
 else:
     # locally it's in source tree
     mozharness_dir = os.path.join(here, '../../mozharness')
 sys.path.insert(0, mozharness_dir)
 
-from raptor.raptor import Raptor
+from raptor.raptor import RaptorDesktopFirefox
 
 
 @pytest.fixture(scope='function')
 def options(request):
     opts = {
         'app': 'firefox',
         'binary': 'path/to/dummy/browser',
     }
 
     if hasattr(request.module, 'OPTIONS'):
         opts.update(request.module.OPTIONS)
     return opts
 
 
 @pytest.fixture(scope='function')
 def raptor(options):
-    return Raptor(**options)
+    return RaptorDesktopFirefox(**options)
 
 
 @pytest.fixture(scope='session')
 def get_prefs():
     def _inner(browser):
         import raptor
         prefs_dir = os.path.join(raptor.__file__, 'preferences')
         with open(os.path.join(prefs_dir, '{}.json'.format(browser)), 'r') as fh:
--- a/testing/raptor/test/test_raptor.py
+++ b/testing/raptor/test/test_raptor.py
@@ -16,26 +16,35 @@ here = os.path.abspath(os.path.dirname(_
 if os.environ.get('SCRIPTSPATH', None) is not None:
     # in production it is env SCRIPTS_PATH
     mozharness_dir = os.environ['SCRIPTSPATH']
 else:
     # locally it's in source tree
     mozharness_dir = os.path.join(here, '../../mozharness')
 sys.path.insert(0, mozharness_dir)
 
-from raptor.raptor import Raptor
+from raptor.raptor import RaptorDesktopFirefox, RaptorDesktopChrome, RaptorAndroid
 
 
-@pytest.mark.parametrize('app', ['firefox', 'chrome'])
-def test_create_profile(options, app, get_prefs):
-    options['app'] = app
-    raptor = Raptor(**options)
+@pytest.mark.parametrize("raptor_class, app_name", [
+                         [RaptorDesktopFirefox, "firefox"],
+                         [RaptorDesktopChrome, "chrome"],
+                         [RaptorAndroid, "fennec"],
+                         [RaptorAndroid, "geckoview"],
+                         ])
+def test_create_profile(options, raptor_class, app_name, get_prefs):
+    options['app'] = app_name
+    raptor = raptor_class(**options)
+
+    if app_name in ["fennec", "geckoview"]:
+        raptor.profile_class = "firefox"
+    raptor.create_browser_profile()
 
     assert isinstance(raptor.profile, BaseProfile)
-    if app != 'firefox':
+    if app_name != 'firefox':
         return
 
     # These prefs are set in mozprofile
     firefox_prefs = [
         'user_pref("app.update.checkInstallTime", false);',
         'user_pref("app.update.disabledForTesting", true);',
         'user_pref("'
         'security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", true);'
@@ -49,16 +58,18 @@ def test_create_profile(options, app, ge
         for firefox_pref in firefox_prefs:
             assert firefox_pref in prefs
         assert raptor_pref in prefs
 
 
 def test_start_and_stop_server(raptor):
     assert raptor.control_server is None
 
+    raptor.create_browser_profile()
+    raptor.create_browser_handler()
     raptor.start_control_server()
 
     assert raptor.control_server._server_thread.is_alive()
     assert raptor.control_server.port is not None
     assert raptor.control_server.server is not None
 
     raptor.clean_up()
     assert not raptor.control_server._server_thread.is_alive()
@@ -67,17 +78,19 @@ def test_start_and_stop_server(raptor):
 @pytest.mark.parametrize('app', [
     'firefox',
     pytest.mark.xfail('chrome'),
 ])
 def test_start_browser(get_binary, app):
     binary = get_binary(app)
     assert binary
 
-    raptor = Raptor(app, binary)
+    raptor = RaptorDesktopFirefox(app, binary)
+    raptor.create_browser_profile()
+    raptor.create_browser_handler()
     raptor.start_control_server()
 
     test = {}
     test['name'] = 'raptor-{}-tp6'.format(app)
 
     thread = threading.Thread(target=raptor.run_test, args=(test,))
     thread.start()