tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

hooks_throttling.py (6109B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 """
      5 Drives the throttling feature when the test calls our
      6 controlled server.
      7 """
      8 
      9 import http.client
     10 import json
     11 import os
     12 import sys
     13 import time
     14 from urllib.parse import urlparse
     15 
     16 from mozperftest.test.browsertime import add_option
     17 from mozperftest.utils import get_tc_secret
     18 
     19 ENDPOINTS = {
     20    "linux": "h3.dev.mozaws.net",
     21    "darwin": "h3.mac.dev.mozaws.net",
     22    "win32": "h3.win.dev.mozaws.net",
     23 }
     24 CTRL_SERVER = ENDPOINTS[sys.platform]
     25 TASK_CLUSTER = "TASK_ID" in os.environ.keys()
     26 _SECRET = {
     27    "throttler_host": f"https://{CTRL_SERVER}/_throttler",
     28    "throttler_key": os.environ.get("WEBNETEM_KEY", ""),
     29 }
     30 if TASK_CLUSTER:
     31    _SECRET.update(get_tc_secret())
     32 
     33 if _SECRET["throttler_key"] == "":
     34    if TASK_CLUSTER:
     35        raise Exception("throttler_key not found in secret")
     36    raise Exception("WEBNETEM_KEY not set")
     37 
     38 _TIMEOUT = 30
     39 WAIT_TIME = 60 * 10
     40 IDLE_TIME = 10
     41 BREATHE_TIME = 20
     42 
     43 
     44 class Throttler:
     45    def __init__(self, env, host, key):
     46        self.env = env
     47        self.host = host
     48        self.key = key
     49        self.verbose = env.get_arg("verbose", False)
     50        self.logger = self.verbose and self.env.info or self.env.debug
     51 
     52    def log(self, msg):
     53        self.logger("[throttler] " + msg)
     54 
     55    def _request(self, action, data=None):
     56        kw = {}
     57        headers = {b"X-WEBNETEM-KEY": self.key}
     58        verb = data is None and "GET" or "POST"
     59        if data is not None:
     60            data = json.dumps(data)
     61            headers[b"Content-type"] = b"application/json"
     62 
     63        parsed = urlparse(self.host)
     64        server = parsed.netloc
     65        path = parsed.path
     66        if action != "status":
     67            path += "/" + action
     68 
     69        self.log(f"Calling {verb} {path}")
     70        conn = http.client.HTTPSConnection(server, timeout=_TIMEOUT)
     71        conn.request(verb, path, body=data, headers=headers, **kw)
     72        resp = conn.getresponse()
     73        res = resp.read()
     74        if resp.status >= 400:
     75            raise Exception(res)
     76        res = json.loads(res)
     77        return res
     78 
     79    def start(self, data=None):
     80        self.log("Starting")
     81        now = time.time()
     82        acquired = False
     83 
     84        while time.time() - now < WAIT_TIME:
     85            status = self._request("status")
     86            if status.get("test_running"):
     87                # a test is running
     88                self.log("A test is already controlling the server")
     89                self.log(f"Waiting {IDLE_TIME} seconds")
     90            else:
     91                try:
     92                    self._request("start_test")
     93                    acquired = True
     94                    break
     95                except Exception:
     96                    # we got beat in the race
     97                    self.log("Someone else beat us")
     98            time.sleep(IDLE_TIME)
     99 
    100        if not acquired:
    101            raise Exception("Could not acquire the test server")
    102 
    103        if data is not None:
    104            self._request("shape", data)
    105 
    106    def stop(self):
    107        self.log("Stopping")
    108        try:
    109            self._request("reset")
    110        finally:
    111            self._request("stop_test")
    112 
    113 
    114 def get_throttler(env):
    115    host = _SECRET["throttler_host"]
    116    key = _SECRET["throttler_key"].encode()
    117    return Throttler(env, host, key)
    118 
    119 
    120 _PROTOCOL = "h2", "h3"
    121 _PAGE = "gallery", "news", "shopping", "photoblog"
    122 
    123 # set the network condition here.
    124 # each item has a name and some netem options:
    125 #
    126 # loss_ratio: specify percentage of packets that will be lost
    127 # loss_corr: specify a correlation factor for the random packet loss
    128 # dup_ratio: specify percentage of packets that will be duplicated
    129 # delay: specify an overall delay for each packet
    130 # jitter: specify amount of jitter in milliseconds
    131 # delay_jitter_corr: specify a correlation factor for the random jitter
    132 # reorder_ratio: specify percentage of packets that will be reordered
    133 # reorder_corr: specify a correlation factor for the random reordering
    134 #
    135 _THROTTLING = (
    136    {"name": "full"},  # no throttling.
    137    {"name": "one", "delay": "20"},
    138    {"name": "two", "delay": "50"},
    139    {"name": "three", "delay": "100"},
    140    {"name": "four", "delay": "200"},
    141    {"name": "five", "delay": "300"},
    142 )
    143 
    144 
    145 def get_test():
    146    """Iterate on test conditions.
    147 
    148    For each cycle, we return a combination of: protocol, page, throttling
    149    settings. Each combination has a name, and that name will be used along with
    150    the protocol as a prefix for each metrics.
    151    """
    152    for proto in _PROTOCOL:
    153        for page in _PAGE:
    154            url = f"https://{CTRL_SERVER}/{page}.html"
    155            for throttler_settings in _THROTTLING:
    156                yield proto, page, url, throttler_settings
    157 
    158 
    159 combo = get_test()
    160 
    161 
    162 def before_cycle(metadata, env, cycle, script):
    163    global combo
    164    if "throttlable" not in script["tags"]:
    165        return
    166    throttler = get_throttler(env)
    167    try:
    168        proto, page, url, throttler_settings = next(combo)
    169    except StopIteration:
    170        combo = get_test()
    171        proto, page, url, throttler_settings = next(combo)
    172 
    173    # setting the url for the browsertime script
    174    add_option(env, "browsertime.url", url, overwrite=True)
    175 
    176    # enabling http if needed
    177    if proto == "h3":
    178        add_option(env, "firefox.preference", "network.http.http3.enable:true")
    179 
    180    # prefix used to differenciate metrics
    181    name = throttler_settings["name"]
    182    script["name"] = f"{name}_{proto}_{page}"
    183 
    184    # throttling the controlled server if needed
    185    if throttler_settings != {"name": "full"}:
    186        env.info("Calling the controlled server")
    187        throttler.start(throttler_settings)
    188    else:
    189        env.info("No throttling for this call")
    190        throttler.start()
    191 
    192 
    193 def after_cycle(metadata, env, cycle, script):
    194    if "throttlable" not in script["tags"]:
    195        return
    196    throttler = get_throttler(env)
    197    try:
    198        throttler.stop()
    199    except Exception:
    200        pass
    201 
    202    # give a chance for a competitive job to take over
    203    time.sleep(BREATHE_TIME)