summaryrefslogtreecommitdiffstats
path: root/netwerk/test/perf/hooks_throttling.py
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/test/perf/hooks_throttling.py')
-rw-r--r--netwerk/test/perf/hooks_throttling.py202
1 files changed, 202 insertions, 0 deletions
diff --git a/netwerk/test/perf/hooks_throttling.py b/netwerk/test/perf/hooks_throttling.py
new file mode 100644
index 0000000000..5f46b3f0d3
--- /dev/null
+++ b/netwerk/test/perf/hooks_throttling.py
@@ -0,0 +1,202 @@
+# 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/.
+"""
+Drives the throttling feature when the test calls our
+controlled server.
+"""
+import http.client
+import json
+import os
+import sys
+import time
+from urllib.parse import urlparse
+
+from mozperftest.test.browsertime import add_option
+from mozperftest.utils import get_tc_secret
+
+ENDPOINTS = {
+ "linux": "h3.dev.mozaws.net",
+ "darwin": "h3.mac.dev.mozaws.net",
+ "win32": "h3.win.dev.mozaws.net",
+}
+CTRL_SERVER = ENDPOINTS[sys.platform]
+TASK_CLUSTER = "TASK_ID" in os.environ.keys()
+_SECRET = {
+ "throttler_host": f"https://{CTRL_SERVER}/_throttler",
+ "throttler_key": os.environ.get("WEBNETEM_KEY", ""),
+}
+if TASK_CLUSTER:
+ _SECRET.update(get_tc_secret())
+
+if _SECRET["throttler_key"] == "":
+ if TASK_CLUSTER:
+ raise Exception("throttler_key not found in secret")
+ raise Exception("WEBNETEM_KEY not set")
+
+_TIMEOUT = 30
+WAIT_TIME = 60 * 10
+IDLE_TIME = 10
+BREATHE_TIME = 20
+
+
+class Throttler:
+ def __init__(self, env, host, key):
+ self.env = env
+ self.host = host
+ self.key = key
+ self.verbose = env.get_arg("verbose", False)
+ self.logger = self.verbose and self.env.info or self.env.debug
+
+ def log(self, msg):
+ self.logger("[throttler] " + msg)
+
+ def _request(self, action, data=None):
+ kw = {}
+ headers = {b"X-WEBNETEM-KEY": self.key}
+ verb = data is None and "GET" or "POST"
+ if data is not None:
+ data = json.dumps(data)
+ headers[b"Content-type"] = b"application/json"
+
+ parsed = urlparse(self.host)
+ server = parsed.netloc
+ path = parsed.path
+ if action != "status":
+ path += "/" + action
+
+ self.log(f"Calling {verb} {path}")
+ conn = http.client.HTTPSConnection(server, timeout=_TIMEOUT)
+ conn.request(verb, path, body=data, headers=headers, **kw)
+ resp = conn.getresponse()
+ res = resp.read()
+ if resp.status >= 400:
+ raise Exception(res)
+ res = json.loads(res)
+ return res
+
+ def start(self, data=None):
+ self.log("Starting")
+ now = time.time()
+ acquired = False
+
+ while time.time() - now < WAIT_TIME:
+ status = self._request("status")
+ if status.get("test_running"):
+ # a test is running
+ self.log("A test is already controlling the server")
+ self.log(f"Waiting {IDLE_TIME} seconds")
+ else:
+ try:
+ self._request("start_test")
+ acquired = True
+ break
+ except Exception:
+ # we got beat in the race
+ self.log("Someone else beat us")
+ time.sleep(IDLE_TIME)
+
+ if not acquired:
+ raise Exception("Could not acquire the test server")
+
+ if data is not None:
+ self._request("shape", data)
+
+ def stop(self):
+ self.log("Stopping")
+ try:
+ self._request("reset")
+ finally:
+ self._request("stop_test")
+
+
+def get_throttler(env):
+ host = _SECRET["throttler_host"]
+ key = _SECRET["throttler_key"].encode()
+ return Throttler(env, host, key)
+
+
+_PROTOCOL = "h2", "h3"
+_PAGE = "gallery", "news", "shopping", "photoblog"
+
+# set the network condition here.
+# each item has a name and some netem options:
+#
+# loss_ratio: specify percentage of packets that will be lost
+# loss_corr: specify a correlation factor for the random packet loss
+# dup_ratio: specify percentage of packets that will be duplicated
+# delay: specify an overall delay for each packet
+# jitter: specify amount of jitter in milliseconds
+# delay_jitter_corr: specify a correlation factor for the random jitter
+# reorder_ratio: specify percentage of packets that will be reordered
+# reorder_corr: specify a correlation factor for the random reordering
+#
+_THROTTLING = (
+ {"name": "full"}, # no throttling.
+ {"name": "one", "delay": "20"},
+ {"name": "two", "delay": "50"},
+ {"name": "three", "delay": "100"},
+ {"name": "four", "delay": "200"},
+ {"name": "five", "delay": "300"},
+)
+
+
+def get_test():
+ """Iterate on test conditions.
+
+ For each cycle, we return a combination of: protocol, page, throttling
+ settings. Each combination has a name, and that name will be used along with
+ the protocol as a prefix for each metrics.
+ """
+ for proto in _PROTOCOL:
+ for page in _PAGE:
+ url = f"https://{CTRL_SERVER}/{page}.html"
+ for throttler_settings in _THROTTLING:
+ yield proto, page, url, throttler_settings
+
+
+combo = get_test()
+
+
+def before_cycle(metadata, env, cycle, script):
+ global combo
+ if "throttlable" not in script["tags"]:
+ return
+ throttler = get_throttler(env)
+ try:
+ proto, page, url, throttler_settings = next(combo)
+ except StopIteration:
+ combo = get_test()
+ proto, page, url, throttler_settings = next(combo)
+
+ # setting the url for the browsertime script
+ add_option(env, "browsertime.url", url, overwrite=True)
+
+ # enabling http if needed
+ if proto == "h3":
+ add_option(env, "firefox.preference", "network.http.http3.enable:true")
+
+ # prefix used to differenciate metrics
+ name = throttler_settings["name"]
+ script["name"] = f"{name}_{proto}_{page}"
+
+ # throttling the controlled server if needed
+ if throttler_settings != {"name": "full"}:
+ env.info("Calling the controlled server")
+ throttler.start(throttler_settings)
+ else:
+ env.info("No throttling for this call")
+ throttler.start()
+
+
+def after_cycle(metadata, env, cycle, script):
+ if "throttlable" not in script["tags"]:
+ return
+ throttler = get_throttler(env)
+ try:
+ throttler.stop()
+ except Exception:
+ pass
+
+ # give a chance for a competitive job to take over
+ time.sleep(BREATHE_TIME)