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)