Developing in mozperftest
=========================

Architecture overview
---------------------

`mozperftest` implements a mach command that is a thin wrapper on the
top of `runner.py`, which allows us to run the tool without having to go through
a mach call. Command arguments are prepared in `argparser.py` and then made
available for the runner.

The runner creates a `MachEnvironment` instance (see `environment.py`) and a
`Metadata` instance (see `metadata.py`). These two objects are shared during the
whole test and used to share data across all parts.

The runner then calls `MachEnvironment.run`,  which is in charge of running the test.
The `MachEnvironment` instance runs a sequence of **layers**.

Layers are classes responsible for one single aspect of a performance test. They
are organized in three categories:

- **system**: anything that sets up and tears down some resources or services
  on the system. Existing system layers: **android**, **proxy**
- **test**: layers that are in charge of running a test to collect metrics.
  Existing test layers: **browsertime** and **androidlog**
- **metrics**: all layers that process the metrics to turn them into usable
  metrics. Existing system layers: **perfherder** and **console**

The MachEnvironment instance collects a series of layers for each category and
runs them sequentially.

The goal of this organization is to allow adding new performance tests runners
that will be based on a specific combination of layers. To avoid messy code,
we need to make sure that each layer represents a single aspect of the process
and that is completely independent of other layers (besides sharing the data
through the common environment.)

For instance, we could use `perftest` to run a C++ benchmark by implementing a
new **test** layer.


Layer
-----

A layer is a class that inherits from `mozperftest.layers.Layer` and implements
a few methods and class variables.

List of methods and variables:

- `name`: name of the layer (class variable, mandatory)
- `activated`: boolean to activate by default the layer (class variable, False)
- `user_exception`: will trigger the `on_exception` hook when an exception occurs
- `arguments`: dict containing arguments. Each argument is following
  the `argparser` standard
- `run(self, metadata)`: called to execute the layer
- `setup(self)`: called when the layer is about to be executed
- `teardown(self)`: called when the layer is exiting

Example::

    class EmailSender(Layer):
        """Sends an email with the results.
        """
        name = "email"
        activated = False

        arguments = {
            "recipient": {
                "type": str,
                "default": "tarek@mozilla.com",
                "help": "Recipient",
            },
        }

        def setup(self):
            self.server = smtplib.SMTP(smtp_server,port)

        def teardown(self):
            self.server.quit()

        def __call__(self, metadata):
            self.server.send_email(self.get_arg("recipient"), metadata.results())


It can then be added to one of the top functions that are used to create a list
of layers for each category:

- **mozperftest.metrics.pick_metrics** for the metrics category
- **mozperftest.system.pick_system** for the system category
- **mozperftest.test.pick_browser** for the test category

And also added in each `get_layers` function in each of those categories.
The `get_layers` functions are invoked when building the argument parser.

In our example, adding the `EmailSender` layer will add two new options:

- **--email** a flag to activate the layer
- **--email-recipient**


Important layers
----------------

**mozperftest** can be used to run performance tests against browsers using the
**browsertime** test layer. It leverages the `browsertime.js
<https://www.sitespeed.io/documentation/browsertime/>`_ framework and provides
a full integration into Mozilla's build and CI systems.

Browsertime uses the Selenium Webdriver client to drive the browser, and
provides some metrics to measure performance during a user journey.


Coding style
------------

For the coding style, we want to:

- Follow `PEP 257 <https://www.python.org/dev/peps/pep-0257/>`_ for docstrings
- Avoid complexity as much as possible
- Use modern Python 3 code (for instance `pathlib` instead of `os.path`)
- Avoid dependencies on Mozilla build projects and frameworks as much as possible
  (mozharness, mozbuild, etc), or make sure they are isolated and documented


Landing patches
---------------

.. warning::

   It is mandatory for each patch to have a test. Any change without a test
   will be rejected.

Before landing a patch for mozperftest, make sure you run `perftest-test`::

    % ./mach perftest-test
    => black [OK]
    => flake8 [OK]
    => remove old coverage data [OK]
    => running tests [OK]
    => coverage
    Name                                             Stmts   Miss  Cover   Missing
    ------------------------------------------------------------------------------------------
    mozperftest/metrics/notebook/analyzer.py         29      20     31%    26-36, 39-42, 45-51
    ...
    mozperftest/system/proxy.py                      37      0     100%
    ------------------------------------------------------------------------------------------
    TOTAL                                            1614    240    85%

    [OK]

The command will run `black`, `flake8` and also make sure that the test coverage has not regressed.

You can use the `-s` option to bypass flake8/black to speed up your workflow, but make
sure you do a full tests run. You can also pass the name of one single test module.
