202 lines
6 KiB
Python
202 lines
6 KiB
Python
# 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)
|