Bug 1533848 - Implement Android memory test using control server waits, r=rwood.
authorBob Clary <bclary@bclary.com>
Tue, 16 Apr 2019 18:55:34 +0000
changeset 469777 9ca0cca3edb0
parent 469776 dc74bceea27b
child 469778 ebcc8dd9c2bc
push id35882
push usercbrindusan@mozilla.com
push dateWed, 17 Apr 2019 15:54:01 +0000
treeherdermozilla-central@37185c0ae520 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrwood
bugs1533848
milestone68.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 1533848 - Implement Android memory test using control server waits, r=rwood. Depends on D26973 Differential Revision: https://phabricator.services.mozilla.com/D26974
testing/mozharness/mozharness/mozilla/testing/raptor.py
testing/raptor/mach_commands.py
testing/raptor/raptor/cmdline.py
testing/raptor/raptor/control_server.py
testing/raptor/raptor/memory.py
testing/raptor/raptor/raptor.py
testing/raptor/test/test_cmdline.py
testing/raptor/test/test_raptor.py
--- a/testing/mozharness/mozharness/mozilla/testing/raptor.py
+++ b/testing/mozharness/mozharness/mozilla/testing/raptor.py
@@ -135,20 +135,28 @@ class Raptor(TestingMixin, MercurialScri
         [["--host"], {
             "dest": "host",
             "help": "Hostname from which to serve urls (default: 127.0.0.1). "
                     "The value HOST_IP will cause the value of host to be "
                     "to be loaded from the environment variable HOST_IP.",
         }],
         [["--power-test"], {
             "dest": "power_test",
+            "action": "store_true",
+            "default": False,
             "help": "Use Raptor to measure power usage. Currently only supported for Geckoview. "
                     "The host ip address must be specified either via the --host command line "
                     "argument.",
         }],
+        [["--memory-test"], {
+            "dest": "memory_test",
+            "action": "store_true",
+            "default": False,
+            "help": "Use Raptor to measure memory usage.",
+        }],
         [["--debug-mode"], {
             "dest": "debug_mode",
             "action": "store_true",
             "default": False,
             "help": "Run Raptor in debug mode (open browser console, limited page-cycles, etc.)",
         }],
 
     ] + testing_config_options + copy.deepcopy(code_coverage_config_options)
@@ -220,16 +228,17 @@ class Raptor(TestingMixin, MercurialScri
             "--geckoProfile" in self.config.get("raptor_cmd_line_args", [])
         self.gecko_profile_interval = self.config.get('gecko_profile_interval')
         self.gecko_profile_entries = self.config.get('gecko_profile_entries')
         self.test_packages_url = self.config.get('test_packages_url')
         self.host = self.config.get('host')
         if self.host == 'HOST_IP':
             self.host = os.environ['HOST_IP']
         self.power_test = self.config.get('power_test')
+        self.memory_test = self.config.get('memory_test')
         self.is_release_build = self.config.get('is_release_build')
         self.debug_mode = self.config.get('debug_mode', False)
         self.firefox_android_browsers = ["fennec", "geckoview", "refbrow", "fenix"]
 
     # We accept some configuration options from the try commit message in the
     # format mozharness: <options>. Example try commit message: mozharness:
     # --geckoProfile try: <stuff>
     def query_gecko_profile_options(self):
@@ -357,16 +366,18 @@ class Raptor(TestingMixin, MercurialScri
         if 'raptor_cmd_line_args' in self.config:
             options += self.config['raptor_cmd_line_args']
         if self.config.get('code_coverage', False):
             options.extend(['--code-coverage'])
         if self.config.get('is_release_build', False):
             options.extend(['--is-release-build'])
         if self.config.get('power_test', False):
             options.extend(['--power-test'])
+        if self.config.get('memory_test', False):
+            options.extend(['--memory-test'])
         for key, value in kw_options.items():
             options.extend(['--%s' % key, value])
 
         return options
 
     def populate_webroot(self):
         """Populate the production test slaves' webroots"""
         self.raptor_path = os.path.join(
@@ -468,16 +479,18 @@ class Raptor(TestingMixin, MercurialScri
 
     def _validate_treeherder_data(self, parser):
         # late import is required, because install is done in create_virtualenv
         import jsonschema
 
         expected_perfherder = 1
         if self.config.get('power_test', None):
             expected_perfherder += 1
+        if self.config.get('memory_test', None):
+            expected_perfherder += 1
         if len(parser.found_perf_data) != expected_perfherder:
             self.critical("PERFHERDER_DATA was seen %d times, expected %d."
                           % (len(parser.found_perf_data), expected_perfherder))
             return
 
         schema_path = os.path.join(external_tools_path,
                                    'performance-artifact-schema.json')
         self.info("Validating PERFHERDER_DATA against %s" % schema_path)
@@ -603,16 +616,20 @@ class Raptor(TestingMixin, MercurialScri
                 dest = os.path.join(env['MOZ_UPLOAD_DIR'], 'perfherder-data.json')
                 self.info(str(dest))
                 self._artifact_perf_data(src, dest)
 
                 if self.power_test:
                     src = os.path.join(self.query_abs_dirs()['abs_work_dir'], 'raptor-power.json')
                     self._artifact_perf_data(src, dest)
 
+                if self.memory_test:
+                    src = os.path.join(self.query_abs_dirs()['abs_work_dir'], 'raptor-memory.json')
+                    self._artifact_perf_data(src, dest)
+
                 src = os.path.join(self.query_abs_dirs()['abs_work_dir'], 'screenshots.html')
                 if os.path.exists(src):
                     dest = os.path.join(env['MOZ_UPLOAD_DIR'], 'screenshots.html')
                     self.info(str(dest))
                     self._artifact_perf_data(src, dest)
 
 
 class RaptorOutputParser(OutputParser):
--- a/testing/raptor/mach_commands.py
+++ b/testing/raptor/mach_commands.py
@@ -55,16 +55,17 @@ class RaptorRunner(MozbuildObject):
         self.virtualenv_path = os.path.join(self._topobjdir, 'testing',
                                             'raptor-venv')
         self.python_interp = sys.executable
         self.raptor_args = raptor_args
         if kwargs.get('host', None) == 'HOST_IP':
             kwargs['host'] = os.environ['HOST_IP']
         self.host = kwargs['host']
         self.power_test = kwargs['power_test']
+        self.memory_test = kwargs['memory_test']
         self.is_release_build = kwargs['is_release_build']
 
     def setup_benchmarks(self):
         """Make sure benchmarks are linked to the proper location in the objdir.
 
         Benchmarks can either live in-tree or in an external repository. In the latter
         case also clone/update the repository if necessary.
         """
@@ -134,16 +135,17 @@ class RaptorRunner(MozbuildObject):
                 'python': self.python_interp,
                 'virtualenv': [self.python_interp, self.virtualenv_script],
             },
             'title': socket.gethostname(),
             'default_actions': default_actions,
             'raptor_cmd_line_args': self.raptor_args,
             'host': self.host,
             'power_test': self.power_test,
+            'memory_test': self.memory_test,
             'is_release_build': self.is_release_build,
         }
 
     def make_args(self):
         self.args = {
             'config': {},
             'initial_config_file': self.config_file_path,
         }
--- a/testing/raptor/raptor/cmdline.py
+++ b/testing/raptor/raptor/cmdline.py
@@ -64,16 +64,18 @@ def create_parser(mach_interface=False):
     add_arg('--host', dest='host',
             help="Hostname from which to serve urls, defaults to 127.0.0.1. "
             "The value HOST_IP will cause the value of host to be "
             "loaded from the environment variable HOST_IP.",
             default='127.0.0.1')
     add_arg('--power-test', dest="power_test", action="store_true",
             help="Use Raptor to measure power usage. Currently supported for Geckoview. "
             "The host ip address must be specified via the --host command line argument.")
+    add_arg('--memory-test', dest="memory_test", action="store_true",
+            help="Use Raptor to measure memory usage.")
     add_arg('--is-release-build', dest="is_release_build", default=False,
             action='store_true',
             help="Whether the build is a release build which requires work arounds "
             "using MOZ_DISABLE_NONLOCAL_CONNECTIONS to support installing unsigned "
             "webextensions. Defaults to False.")
     add_arg('--geckoProfile', action="store_true", dest="gecko_profile",
             help=argparse.SUPPRESS)
     add_arg('--geckoProfileInterval', dest='gecko_profile_interval', type=float,
--- a/testing/raptor/raptor/control_server.py
+++ b/testing/raptor/raptor/control_server.py
@@ -2,31 +2,108 @@
 # 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/.
 
 # control server for raptor performance framework
 # communicates with the raptor browser webextension
 from __future__ import absolute_import
 
 import BaseHTTPServer
+import datetime
 import json
 import os
 import socket
 import threading
+import time
 
 from mozlog import get_proxy_logger
 
 LOG = get_proxy_logger(component='raptor-control-server')
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
 def MakeCustomHandlerClass(results_handler, shutdown_browser, write_raw_gecko_profile):
 
     class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
+        """
+        Control server expects messages of the form
+        {'type': 'messagetype', 'data':...}
+
+        Each message is given a key which is calculated as
+
+           If type is 'webext_status', then
+              the key is data['type']/data['data']
+           otherwise
+              the key is data['type'].
+
+        The contol server can be forced to wait before performing an
+        action requested via POST by sending a special message
+
+        {'type': 'wait-set', 'data': key}
+
+        where key is the key of the message control server should
+        perform a wait before processing. The handler will store
+        this key in the wait_after_messages dict as a True value.
+
+        wait_after_messages[key] = True
+
+        For subsequent requests the handler will check the key of
+        the incoming message against wait_for_messages and if it is
+        found and its value is True, the handler will assign the key
+        to waiting_in_state and will loop until the key is removed
+        or until its value is changed to False.
+
+        Control server will stop waiting for a state to be continued
+        or cleared after wait_timeout seconds after which the wait
+        will be removed and the control server will finish processing
+        the current POST request. wait_timeout defaults to 60 seconds
+        but can be set globally for all wait states by sending the
+        message
+
+        {'type': 'wait-timeout', 'data': timeout}
+
+        The value of waiting_in_state can be retrieved by sending the
+        message
+
+        {'type': 'wait-get', 'data': ''}
+
+        which will return the value of waiting_in_state in the
+        content of the response. If the value returned is not
+        'None', then the control server has received a message whose
+        key is recorded in wait_after_messages and is waiting before
+        completing the request.
+
+        The control server can be told to stop waiting and to finish
+        processing the current request while keeping the wait for
+        subsequent requests by sending
+
+        {'type': 'wait-continue', 'data': ''}
+
+        The control server can be told to stop waiting and to finish
+        processing the current request while removing the wait for
+        subsequent requests by sending
+
+        {'type': 'wait-clear', 'data': key}
+
+            if key is the empty string ''
+                the key in waiting_in_state is removed from wait_after_messages
+                waiting_in_state is set to None
+            else if key is 'all'
+                 all keys in wait_after_messages are removed
+            else key is not in wait_after_messages
+                 the message is ignored
+            else
+                 the key is removed from wait_after messages
+                 if the key matches the value in waiting_in_state,
+                 then waiting_in_state is set to None
+        """
+        wait_after_messages = {}
+        waiting_in_state = None
+        wait_timeout = 60
 
         def __init__(self, *args, **kwargs):
             self.results_handler = results_handler
             self.shutdown_browser = shutdown_browser
             self.write_raw_gecko_profile = write_raw_gecko_profile
             super(MyHandler, self).__init__(*args, **kwargs)
 
         def log_request(self, code='-', size='-'):
@@ -60,16 +137,42 @@ def MakeCustomHandlerClass(results_handl
             self.send_header('Access-Control-Allow-Origin', '*')
             self.send_header('Content-type', 'text/html')
             self.end_headers()
             content_len = int(self.headers.getheader('content-length'))
             post_body = self.rfile.read(content_len)
             # could have received a status update or test results
             data = json.loads(post_body)
 
+            if data['type'] == 'webext_status':
+                wait_key = "%s/%s" % (data['type'], data['data'])
+            else:
+                wait_key = data['type']
+
+            if MyHandler.wait_after_messages.get(wait_key, None):
+                LOG.info("Waiting in %s" % wait_key)
+                MyHandler.waiting_in_state = wait_key
+                start_time = datetime.datetime.now()
+
+            while MyHandler.wait_after_messages.get(wait_key, None):
+                time.sleep(1)
+                elapsed_time = datetime.datetime.now() - start_time
+                if elapsed_time > datetime.timedelta(seconds=MyHandler.wait_timeout):
+                    del MyHandler.wait_after_messages[wait_key]
+                    MyHandler.waiting_in_state = None
+                    LOG.error("TEST-UNEXPECTED-ERROR | "
+                              "ControlServer wait %s exceeded %s seconds" %
+                              (wait_key, MyHandler.wait_timeout))
+
+            if MyHandler.wait_after_messages.get(wait_key, None) is not None:
+                # If the wait is False, it was continued and we just set it back
+                # to True for the next time. If it was removed by clear, we
+                # leave it alone so it will not cause a wait any more.
+                MyHandler.wait_after_messages[wait_key] = True
+
             if data['type'] == "webext_gecko_profile":
                 # received gecko profiling results
                 _test = str(data['data'][0])
                 _pagecycle = str(data['data'][1])
                 _raw_profile = data['data'][2]
                 LOG.info("received gecko profile for test %s pagecycle %s" % (_test, _pagecycle))
                 self.write_raw_gecko_profile(_test, _pagecycle, _raw_profile)
             elif data['type'] == 'webext_results':
@@ -79,25 +182,58 @@ def MakeCustomHandlerClass(results_handl
                 LOG.info("received " + data['type'] + ": " + str(data['data']))
 
                 if len(data['data']) == 2:
                     data['data'].append("")
                 # pageload test has timed out; record it as a failure
                 self.results_handler.add_page_timeout(str(data['data'][0]),
                                                       str(data['data'][1]),
                                                       dict(data['data'][2]))
-            elif data['data'] == "__raptor_shutdownBrowser":
+            elif data['type'] == 'webext_status' and data['data'] == "__raptor_shutdownBrowser":
                 LOG.info("received " + data['type'] + ": " + str(data['data']))
                 # webext is telling us it's done, and time to shutdown the browser
                 self.shutdown_browser()
             elif data['type'] == 'webext_screenshot':
                 LOG.info("received " + data['type'])
                 self.results_handler.add_image(str(data['data'][0]),
                                                str(data['data'][1]),
                                                str(data['data'][2]))
+            elif data['type'] == 'webext_status':
+                LOG.info("received " + data['type'] + ": " + str(data['data']))
+            elif data['type'] == 'wait-set':
+                LOG.info("received " + data['type'] + ": " + str(data['data']))
+                MyHandler.wait_after_messages[str(data['data'])] = True
+            elif data['type'] == 'wait-timeout':
+                LOG.info("received " + data['type'] + ": " + str(data['data']))
+                MyHandler.wait_timeout = data['data']
+            elif data['type'] == 'wait-get':
+                self.wfile.write(MyHandler.waiting_in_state)
+            elif data['type'] == 'wait-continue':
+                LOG.info("received " + data['type'] + ": " + str(data['data']))
+                if MyHandler.waiting_in_state:
+                    MyHandler.wait_after_messages[MyHandler.waiting_in_state] = False
+                    MyHandler.waiting_in_state = None
+            elif data['type'] == 'wait-clear':
+                LOG.info("received " + data['type'] + ": " + str(data['data']))
+                clear_key = str(data['data'])
+                if clear_key == '':
+                    if MyHandler.waiting_in_state:
+                        del MyHandler.wait_after_messages[MyHandler.waiting_in_state]
+                        MyHandler.waiting_in_state = None
+                    else:
+                        pass
+                elif clear_key == 'all':
+                    MyHandler.wait_after_messages = {}
+                    MyHandler.waiting_in_state = None
+                elif clear_key not in MyHandler.wait_after_messages:
+                    pass
+                else:
+                    del MyHandler.wait_after_messages[clear_key]
+                    if MyHandler.waiting_in_state == clear_key:
+                        MyHandler.waiting_in_state = None
             else:
                 LOG.info("received " + data['type'] + ": " + str(data['data']))
 
         def do_OPTIONS(self):
             self.send_response(200, "ok")
             self.send_header('Access-Control-Allow-Origin', '*')
             self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
             self.send_header("Access-Control-Allow-Headers", "X-Requested-With")
new file mode 100644
--- /dev/null
+++ b/testing/raptor/raptor/memory.py
@@ -0,0 +1,43 @@
+# 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 re
+
+
+def get_app_memory_usage(raptor):
+    app_name = raptor.config['binary']
+    total = 0
+    re_total_memory = re.compile(r'TOTAL:\s+(\d+)')
+    verbose = raptor.device._verbose
+    raptor.device._verbose = False
+    meminfo = raptor.device.shell_output("dumpsys meminfo %s" % app_name).split('\n')
+    raptor.device._verbose = verbose
+    for line in meminfo:
+        match = re_total_memory.search(line)
+        if match:
+            total = int(match.group(1))
+            break
+    return total
+
+
+def generate_android_memory_profile(raptor, test_name):
+    if not raptor.device or not raptor.config['memory_test']:
+        return
+    foreground = get_app_memory_usage(raptor)
+    # put app into background
+    verbose = raptor.device._verbose
+    raptor.device._verbose = False
+    raptor.device.shell_output("am start -a android.intent.action.MAIN "
+                               "-c android.intent.category.HOME")
+    raptor.device._verbose = verbose
+    background = get_app_memory_usage(raptor)
+    meminfo_data = {'type': 'memory',
+                    'test': test_name,
+                    'unit': 'KB',
+                    'values': {
+                        'foreground': foreground,
+                        'background': background
+                    }}
+    raptor.control_server.submit_supporting_data(meminfo_data)
--- a/testing/raptor/raptor/raptor.py
+++ b/testing/raptor/raptor/raptor.py
@@ -8,16 +8,18 @@ from __future__ import absolute_import
 import json
 import os
 import posixpath
 import shutil
 import sys
 import tempfile
 import time
 
+import requests
+
 import mozcrash
 import mozinfo
 from mozdevice import ADBDevice
 from mozlog import commandline, get_default_logger
 from mozprofile import create_profile
 from mozrunner import runners
 
 # need this so raptor imports work both from /raptor and via mach
@@ -52,26 +54,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 mozproxy import get_playback
 from results import RaptorResultsHandler
 from gecko_profile import GeckoProfile
 from power import init_android_power_test, finish_android_power_test
+from memory import generate_android_memory_profile
 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, post_startup_delay=None, activity=None):
+                 symbols_path=None, host=None, power_test=False, memory_test=False,
+                 is_release_build=False, debug_mode=False, post_startup_delay=None, activity=None):
 
         # 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
@@ -79,17 +82,19 @@ class Raptor(object):
         self.config['run_local'] = run_local
         self.config['obj_path'] = obj_path
         self.config['gecko_profile'] = gecko_profile
         self.config['gecko_profile_interval'] = gecko_profile_interval
         self.config['gecko_profile_entries'] = gecko_profile_entries
         self.config['symbols_path'] = symbols_path
         self.config['host'] = host
         self.config['power_test'] = power_test
+        self.config['memory_test'] = memory_test
         self.config['is_release_build'] = is_release_build
+        self.config['enable_control_server_wait'] = memory_test
         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 = post_startup_delay
@@ -182,16 +187,19 @@ class Raptor(object):
 
         # add profile dir to our config
         self.config['local_profile_dir'] = self.profile.profile
 
     def start_control_server(self):
         self.control_server = RaptorControlServer(self.results_handler, self.debug_mode)
         self.control_server.start()
 
+        if self.config['enable_control_server_wait']:
+            self.control_server_wait_set('webext_status/__raptor_shutdownBrowser')
+
         # for android we must make the control server available to the device
         if self.config['app'] in self.firefox_android_apps and \
                 self.config['host'] in ('localhost', '127.0.0.1'):
             self.log.info("making the raptor control server port available to device")
             _tcp_port = "tcp:%s" % self.control_server.port
             self.device.create_socket_connection('reverse', _tcp_port, _tcp_port)
 
     def get_playback_config(self, test):
@@ -331,16 +339,22 @@ class Raptor(object):
         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:
+            if self.config['enable_control_server_wait']:
+                response = self.control_server_wait_get()
+                if response == 'webext_status/__raptor_shutdownBrowser':
+                    if self.config['memory_test']:
+                        generate_android_memory_profile(self, test['name'])
+                    self.control_server_wait_continue()
             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()
@@ -360,35 +374,63 @@ class Raptor(object):
 
         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 clean_up(self):
+        if self.config['enable_control_server_wait']:
+            self.control_server_wait_clear('all')
+
         self.control_server.stop()
         if self.config['app'] not in self.firefox_android_apps:
             self.runner.stop()
         elif self.config['app'] in self.firefox_android_apps:
             self.log.info('removing reverse socket connections')
             self.device.remove_socket_connections('reverse')
         else:
             pass
         self.log.info("finished")
 
+    def control_server_wait_set(self, state):
+        response = requests.post("http://127.0.0.1:%s/" % self.control_server.port,
+                                 json={"type": "wait-set", "data": state})
+        return response.content
+
+    def control_server_wait_timeout(self, timeout):
+        response = requests.post("http://127.0.0.1:%s/" % self.control_server.port,
+                                 json={"type": "wait-timeout", "data": timeout})
+        return response.content
+
+    def control_server_wait_get(self):
+        response = requests.post("http://127.0.0.1:%s/" % self.control_server.port,
+                                 json={"type": "wait-get", "data": ""})
+        return response.content
+
+    def control_server_wait_continue(self):
+        response = requests.post("http://127.0.0.1:%s/" % self.control_server.port,
+                                 json={"type": "wait-continue", "data": ""})
+        return response.content
+
+    def control_server_wait_clear(self, state):
+        response = requests.post("http://127.0.0.1:%s/" % self.control_server.port,
+                                 json={"type": "wait-clear", "data": state})
+        return response.content
+
 
 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, post_startup_delay=None, activity=None):
+                 symbols_path=None, host=None, power_test=False, memory_test=False,
+                 is_release_build=False, debug_mode=False, post_startup_delay=None, activity=None):
         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,
+                        host, power_test, memory_test, is_release_build, debug_mode,
                         post_startup_delay)
 
     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],
@@ -441,21 +483,21 @@ class RaptorDesktop(Raptor):
             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, post_startup_delay=None, activity=None):
+                 symbols_path=None, host=None, power_test=False, memory_test=False,
+                 is_release_build=False, debug_mode=False, post_startup_delay=None, activity=None):
         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,
+                               host, power_test, memory_test, is_release_build, debug_mode,
                                post_startup_delay)
 
     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"
@@ -486,21 +528,21 @@ class RaptorDesktopFirefox(RaptorDesktop
             # 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
 
 
 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, post_startup_delay=None, activity=None):
+                 symbols_path=None, host=None, power_test=False, memory_test=False,
+                 is_release_build=False, debug_mode=False, post_startup_delay=None, activity=None):
         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,
+                               host, power_test, memory_test, is_release_build, debug_mode,
                                post_startup_delay)
 
     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',
@@ -525,21 +567,21 @@ class RaptorDesktopChrome(RaptorDesktop)
             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, post_startup_delay=None, activity=None):
+                 symbols_path=None, host=None, power_test=False, memory_test=False,
+                 is_release_build=False, debug_mode=False, post_startup_delay=None, activity=None):
         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, post_startup_delay)
+                        power_test, memory_test, is_release_build, debug_mode, post_startup_delay)
 
         # on android, when creating the browser profile, we want to use a 'firefox' type profile
         self.profile_class = "firefox"
         self.config['activity'] = activity
 
     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")
@@ -842,16 +884,17 @@ def main(args=sys.argv[1:]):
                           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,
+                          memory_test=args.memory_test,
                           is_release_build=args.is_release_build,
                           debug_mode=args.debug_mode,
                           post_startup_delay=args.post_startup_delay,
                           activity=args.activity)
 
     raptor.create_browser_profile()
     raptor.create_browser_handler()
     raptor.start_control_server()
--- a/testing/raptor/test/test_cmdline.py
+++ b/testing/raptor/test/test_cmdline.py
@@ -11,58 +11,63 @@ from raptor.cmdline import verify_option
 
 def test_verify_options(filedir):
     args = Namespace(app='firefox',
                      binary='invalid/path',
                      gecko_profile='False',
                      page_cycles=1,
                      page_timeout=60000,
                      debug='True',
-                     power_test=False)
+                     power_test=False,
+                     memory_test=False)
     parser = ArgumentParser()
 
     with pytest.raises(SystemExit):
         verify_options(parser, args)
 
     args.binary = os.path.join(filedir, 'fake_binary.exe')
     verify_options(parser, args)  # assert no exception
 
     args = Namespace(app='geckoview',
                      binary='org.mozilla.geckoview_example',
                      activity='GeckoViewActivity',
                      gecko_profile='False',
                      is_release_build=False,
                      host='sophie',
-                     power_test=False)
+                     power_test=False,
+                     memory_test=False)
     verify_options(parser, args)  # assert no exception
 
     args = Namespace(app='refbrow',
                      binary='org.mozilla.reference.browser',
                      activity='BrowserTestActivity',
                      gecko_profile='False',
                      is_release_build=False,
                      host='sophie',
-                     power_test=False)
+                     power_test=False,
+                     memory_test=False)
     verify_options(parser, args)  # assert no exception
 
     args = Namespace(app='fenix',
                      binary='org.mozilla.fenix.browser',
                      activity='BrowserTestActivity',
                      gecko_profile='False',
                      is_release_build=False,
                      host='sophie',
-                     power_test=False)
+                     power_test=False,
+                     memory_test=False)
     verify_options(parser, args)  # assert no exception
 
     args = Namespace(app='refbrow',
                      binary='org.mozilla.reference.browser',
                      activity=None,
                      gecko_profile='False',
                      is_release_build=False,
                      host='sophie',
-                     power_test=False)
+                     power_test=False,
+                     memory_test=False)
     parser = ArgumentParser()
 
     verify_options(parser, args)  # also will work as uses default activity
 
 
 if __name__ == '__main__':
     mozunit.main()
--- a/testing/raptor/test/test_raptor.py
+++ b/testing/raptor/test/test_raptor.py
@@ -86,16 +86,77 @@ def test_start_and_stop_server(raptor):
     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()
 
 
+def test_server_wait_states(raptor):
+    import datetime
+
+    import requests
+
+    def post_state():
+        requests.post("http://127.0.0.1:%s/" % raptor.control_server.port,
+                      json={"type": "webext_status",
+                            "data": "test status"})
+
+    assert raptor.control_server is None
+
+    raptor.create_browser_profile()
+    raptor.create_browser_handler()
+    raptor.start_control_server()
+
+    wait_time = 5
+    message_state = 'webext_status/test status'
+    rhc = raptor.control_server.server.RequestHandlerClass
+
+    # Test initial state
+    assert rhc.wait_after_messages == {}
+    assert rhc.waiting_in_state is None
+    assert rhc.wait_timeout == 60
+    assert raptor.control_server_wait_get() == 'None'
+
+    # Test setting a state
+    assert raptor.control_server_wait_set(message_state) == ''
+    assert message_state in rhc.wait_after_messages
+    assert rhc.wait_after_messages[message_state]
+
+    # Test clearing a non-existent state
+    assert raptor.control_server_wait_clear('nothing') == ''
+    assert message_state in rhc.wait_after_messages
+
+    # Test clearing a state
+    assert raptor.control_server_wait_clear(message_state) == ''
+    assert message_state not in rhc.wait_after_messages
+
+    # Test clearing all states
+    assert raptor.control_server_wait_set(message_state) == ''
+    assert message_state in rhc.wait_after_messages
+    assert raptor.control_server_wait_clear('all') == ''
+    assert rhc.wait_after_messages == {}
+
+    # Test wait timeout
+    # Block on post request
+    assert raptor.control_server_wait_set(message_state) == ''
+    assert rhc.wait_after_messages[message_state]
+    assert raptor.control_server_wait_timeout(wait_time) == ''
+    assert rhc.wait_timeout == wait_time
+    start = datetime.datetime.now()
+    post_state()
+    assert datetime.datetime.now() - start < datetime.timedelta(seconds=wait_time+2)
+    assert raptor.control_server_wait_get() == 'None'
+    assert message_state not in rhc.wait_after_messages
+
+    raptor.clean_up()
+    assert not raptor.control_server._server_thread.is_alive()
+
+
 @pytest.mark.parametrize('app', [
     'firefox',
     pytest.mark.xfail('chrome'),
     pytest.mark.xfail('chromium'),
 ])
 def test_start_browser(get_binary, app):
     binary = get_binary(app)
     assert binary