755 lines
26 KiB
Python
755 lines
26 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/.
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import tempfile
|
|
import threading
|
|
from pathlib import Path
|
|
from subprocess import PIPE, Popen
|
|
|
|
import filters
|
|
from base_python_support import BasePythonSupport
|
|
from logger.logger import RaptorLogger
|
|
|
|
LOG = RaptorLogger(component="raptor-browsertime")
|
|
|
|
|
|
class NetworkBench(BasePythonSupport):
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self._is_chrome = False
|
|
self.browsertime_node = None
|
|
self.backend_server = None
|
|
self.backend_port = None
|
|
self.caddy_port = None
|
|
self.caddy_server = None
|
|
self.caddy_stdout = None
|
|
self.caddy_stderr = None
|
|
self.http_version = "h1"
|
|
self.transfer_type = "download"
|
|
# loopback
|
|
self.interface = "lo"
|
|
self.network_type = "unthrottled"
|
|
self.packet_loss_rate = None
|
|
self.cleanup = []
|
|
|
|
def setup_test(self, test, args):
|
|
from cmdline import CHROME_ANDROID_APPS, CHROMIUM_DISTROS
|
|
|
|
LOG.info(f"setup_test: '{test}'")
|
|
|
|
self._is_chrome = (
|
|
args.app in CHROMIUM_DISTROS or args.app in CHROME_ANDROID_APPS
|
|
)
|
|
|
|
test_name = test.get("name").split("-", 2)
|
|
self.http_version = test_name[0] if test_name[0] in ["h3", "h2"] else "unknown"
|
|
self.transfer_type = (
|
|
test_name[1] if test_name[1] in ["download", "upload"] else "unknown"
|
|
)
|
|
LOG.info(f"http_version: '{self.http_version}', type: '{self.transfer_type}'")
|
|
|
|
if self.http_version == "unknown" or self.transfer_type == "unknown":
|
|
raise Exception("Unsupported test")
|
|
|
|
def check_caddy_installed(self):
|
|
try:
|
|
result = subprocess.run(
|
|
["caddy", "version"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 0:
|
|
LOG.info(f"Caddy is installed. Version: {result.stdout.strip()}")
|
|
return True
|
|
else:
|
|
LOG.error("Caddy is not installed.")
|
|
except FileNotFoundError:
|
|
LOG.error("Caddy is not installed.")
|
|
return False
|
|
|
|
def start_backend_server(self, path):
|
|
if self.browsertime_node is None or not self.browsertime_node.exists():
|
|
return None
|
|
|
|
LOG.info(f"node bin: {self.browsertime_node}")
|
|
|
|
server_path = (
|
|
Path(__file__).parent / ".." / ".." / "browsertime" / "utils" / path
|
|
)
|
|
LOG.info(f"server_path: {server_path}")
|
|
|
|
if not server_path.exists():
|
|
return None
|
|
|
|
process = Popen(
|
|
[self.browsertime_node, server_path],
|
|
stdin=PIPE,
|
|
stdout=PIPE,
|
|
stderr=PIPE,
|
|
universal_newlines=True,
|
|
start_new_session=True,
|
|
)
|
|
msg = process.stdout.readline()
|
|
LOG.info(f"server msg: {msg}")
|
|
match = re.search(r"Server is running on http://[^:]+:(\d+)", msg)
|
|
if match:
|
|
self.backend_port = match.group(1)
|
|
LOG.info(f"backend port: {self.backend_port}")
|
|
return process
|
|
return None
|
|
|
|
def find_free_port(self, socket_type):
|
|
with socket.socket(socket.AF_INET, socket_type) as s:
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
s.bind(("localhost", 0))
|
|
return s.getsockname()[1]
|
|
|
|
def start_caddy(self, download_html, test_file_path):
|
|
if not self.check_caddy_installed():
|
|
return None
|
|
if self.caddy_port is None or not (1 <= self.caddy_port <= 65535):
|
|
return None
|
|
|
|
utils_path = Path(__file__).parent / ".." / ".." / "browsertime" / "utils"
|
|
|
|
if not utils_path.exists():
|
|
return None
|
|
|
|
key_path = utils_path / "http2-cert.key"
|
|
LOG.info(f"key_path: {key_path}")
|
|
if not key_path.exists():
|
|
return None
|
|
|
|
pem_path = utils_path / "http2-cert.pem"
|
|
LOG.info(f"pem_path: {pem_path}")
|
|
if not pem_path.exists():
|
|
return None
|
|
|
|
port_str = f":{self.caddy_port}"
|
|
if self.transfer_type == "upload":
|
|
upstream = f"localhost:{self.backend_port}"
|
|
routes = [
|
|
{
|
|
"handle": [
|
|
{
|
|
"handler": "reverse_proxy",
|
|
"upstreams": [{"dial": upstream}],
|
|
}
|
|
]
|
|
}
|
|
]
|
|
elif self.transfer_type == "download":
|
|
routes = [
|
|
{
|
|
"match": [{"path": [f"/{download_html.name}"]}],
|
|
"handle": [
|
|
{
|
|
"handler": "file_server",
|
|
"root": str(tempfile.gettempdir()),
|
|
"browse": {},
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"match": [{"path": [f"/{test_file_path.name}"]}],
|
|
"handle": [
|
|
{
|
|
"handler": "headers",
|
|
"response": {
|
|
"set": {
|
|
"Cache-Control": [
|
|
"no-store",
|
|
"no-cache",
|
|
"must-revalidate",
|
|
"proxy-revalidate",
|
|
"max-age=0",
|
|
],
|
|
"Pragma": ["no-cache"],
|
|
"Expires": ["0"],
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"handler": "file_server",
|
|
"root": str(tempfile.gettempdir()),
|
|
"browse": {},
|
|
},
|
|
],
|
|
},
|
|
]
|
|
protocols = ["h3"] if self.http_version == "h3" else ["h1", "h2"]
|
|
caddyfile_content = {
|
|
"admin": {"disabled": True},
|
|
"apps": {
|
|
"http": {
|
|
"servers": {
|
|
"server1": {
|
|
"listen": [port_str],
|
|
"protocols": protocols,
|
|
"routes": routes,
|
|
"tls_connection_policies": [
|
|
{"certificate_selection": {"any_tag": ["cert1"]}}
|
|
],
|
|
"automatic_https": {"disable": True},
|
|
}
|
|
},
|
|
},
|
|
"tls": {
|
|
# Disable 0RTT for now. Can be reverted once
|
|
# https://github.com/quic-go/quic-go/issues/5001 and
|
|
# https://github.com/mozilla/neqo/issues/2476 are fixed.
|
|
"session_tickets": {"disabled": True},
|
|
"certificates": {
|
|
"load_files": [
|
|
{
|
|
"certificate": str(pem_path),
|
|
"key": str(key_path),
|
|
"tags": ["cert1"],
|
|
}
|
|
]
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
LOG.info(f"caddyfile_content: {caddyfile_content}")
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", delete=False, suffix=".json"
|
|
) as temp_json_file:
|
|
json.dump(caddyfile_content, temp_json_file, indent=2)
|
|
temp_json_file_path = temp_json_file.name
|
|
|
|
LOG.info(f"temp_json_file_path: {temp_json_file_path}")
|
|
command = ["caddy", "run", "--config", temp_json_file_path]
|
|
|
|
def read_output(pipe, log_func):
|
|
for line in iter(pipe.readline, ""):
|
|
log_func(line)
|
|
|
|
process = Popen(
|
|
command,
|
|
stdin=PIPE,
|
|
stdout=PIPE,
|
|
stderr=PIPE,
|
|
universal_newlines=True,
|
|
start_new_session=True,
|
|
)
|
|
self.caddy_stdout = threading.Thread(
|
|
target=read_output, args=(process.stdout, LOG.info)
|
|
)
|
|
self.caddy_stderr = threading.Thread(
|
|
target=read_output, args=(process.stderr, LOG.info)
|
|
)
|
|
self.caddy_stdout.start()
|
|
self.caddy_stderr.start()
|
|
return process
|
|
|
|
def check_tc_command(self):
|
|
try:
|
|
result = subprocess.run(
|
|
["sudo", "tc", "-help"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 0:
|
|
LOG.info("tc can be executed as root")
|
|
return True
|
|
else:
|
|
LOG.error("tc is not available")
|
|
except Exception as e:
|
|
LOG.error(f"Error executing tc: {str(e)}")
|
|
return False
|
|
|
|
def run_command(self, command):
|
|
try:
|
|
result = subprocess.run(
|
|
command,
|
|
shell=True,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
LOG.info(command)
|
|
LOG.info(f"Output: {result.stdout.decode().strip()}")
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
LOG.info(f"Error executing command: {command}")
|
|
LOG.info(f"Error: {e.stderr.decode()}")
|
|
return False
|
|
|
|
def network_type_to_bandwidth_rtt(self, network_type):
|
|
# Define the mapping of network types
|
|
network_mapping = {
|
|
"1M_400ms": {"bandwidth": "1Mbit", "rtt_ms": 400},
|
|
"300M_40ms": {"bandwidth": "300Mbit", "rtt_ms": 40},
|
|
"300M_80ms": {"bandwidth": "300Mbit", "rtt_ms": 80},
|
|
"10M_40ms": {"bandwidth": "10Mbit", "rtt_ms": 40},
|
|
"100M_40ms": {"bandwidth": "100Mbit", "rtt_ms": 40},
|
|
}
|
|
|
|
# Look up the network type in the mapping
|
|
result = network_mapping.get(network_type)
|
|
|
|
if result is None:
|
|
raise Exception(f"Unknown network type: {network_type}")
|
|
return result["bandwidth"], result["rtt_ms"]
|
|
|
|
def apply_network_throttling(
|
|
self, interface, network_type, loss, protocol_and_port
|
|
):
|
|
def calculate_bdp(bandwidth_mbit, rtt_ms):
|
|
bandwidth_kbps = bandwidth_mbit * 1_000
|
|
bdp_bits = bandwidth_kbps * rtt_ms
|
|
bdp_bytes = bdp_bits / 8
|
|
bdp_bytes = max(bdp_bytes, 1500)
|
|
return int(bdp_bytes)
|
|
|
|
bandwidth_str, rtt_ms = self.network_type_to_bandwidth_rtt(network_type)
|
|
# The delay used in netem is applied before sending,
|
|
# so the delay value should be rtt_ms / 2.
|
|
delay_ms = rtt_ms / 2
|
|
bandwidth_mbit = float(bandwidth_str.replace("Mbit", ""))
|
|
bdp_bytes = calculate_bdp(bandwidth_mbit, rtt_ms)
|
|
|
|
LOG.info(
|
|
f"apply_network_throttling: bandwidth={bandwidth_str} delay={delay_ms}ms loss={loss}"
|
|
)
|
|
|
|
self.run_command(f"sudo tc qdisc del dev {interface} root")
|
|
if not self.run_command(
|
|
f"sudo tc qdisc add dev {interface} root handle 1: htb default 12"
|
|
):
|
|
return False
|
|
else:
|
|
LOG.info("Register cleanup function")
|
|
self.cleanup.append(
|
|
lambda: self.run_command(f"sudo tc qdisc del dev {self.interface} root")
|
|
)
|
|
|
|
if not self.run_command(
|
|
f"sudo tc class add dev {interface} parent 1: classid 1:1 htb rate {bandwidth_str}"
|
|
):
|
|
return False
|
|
|
|
delay_str = f"{delay_ms}ms"
|
|
if not loss or loss == "0":
|
|
if not self.run_command(
|
|
f"sudo tc qdisc add dev {interface} parent 1:1 handle 10: netem delay {delay_str} limit {bdp_bytes}"
|
|
):
|
|
return False
|
|
elif not self.run_command(
|
|
f"sudo tc qdisc add dev {interface} parent 1:1 handle 10: netem delay {delay_str} loss {loss}% limit {bdp_bytes}"
|
|
):
|
|
return False
|
|
|
|
protocol = 6 if protocol_and_port[0] == "tcp" else 17
|
|
port = protocol_and_port[1]
|
|
# Add a filter to match TCP/UDP traffic on the specified port for IPv4
|
|
if not self.run_command(
|
|
f"sudo tc filter add dev {interface} protocol ip parent 1:0 u32 "
|
|
f"match ip protocol {protocol} 0xff "
|
|
f"match ip dport {port} 0xffff "
|
|
f"flowid 1:1"
|
|
):
|
|
return False
|
|
if not self.run_command(
|
|
f"sudo tc filter add dev {interface} protocol ip parent 1:0 u32 "
|
|
f"match ip protocol {protocol} 0xff "
|
|
f"match ip sport {port} 0xffff "
|
|
f"flowid 1:1"
|
|
):
|
|
return False
|
|
# Add a filter to match TCP/UDP traffic on the specified port for IPv6
|
|
if not self.run_command(
|
|
f"sudo tc filter add dev {interface} parent 1:0 protocol ipv6 u32 "
|
|
f"match ip6 protocol {protocol} 0xff "
|
|
f"match ip6 dport {port} 0xffff "
|
|
f"flowid 1:1"
|
|
):
|
|
return False
|
|
if not self.run_command(
|
|
f"sudo tc filter add dev {interface} parent 1:0 protocol ipv6 u32 "
|
|
f"match ip6 protocol {protocol} 0xff "
|
|
f"match ip6 sport {port} 0xffff "
|
|
f"flowid 1:1"
|
|
):
|
|
return False
|
|
return True
|
|
|
|
def get_network_conditions(self, cmd):
|
|
try:
|
|
i = 0
|
|
while i < len(cmd):
|
|
if cmd[i] == "--network_type":
|
|
self.network_type = cmd[i + 1]
|
|
i += 2
|
|
elif cmd[i] == "--pkt_loss_rate":
|
|
self.packet_loss_rate = cmd[i + 1]
|
|
i += 2
|
|
else:
|
|
i += 1
|
|
except Exception:
|
|
raise Exception("failed to get network condition")
|
|
|
|
def network_type_to_temp_file(self, network_type):
|
|
def calculate_file_size(bandwidth_mbps):
|
|
time_seconds = 60
|
|
# Calculate transfer size in bits
|
|
transfer_size_bits = bandwidth_mbps * 1e6 * time_seconds
|
|
transfer_size_bytes = int(transfer_size_bits / 8)
|
|
return transfer_size_bytes
|
|
|
|
def generate_temp_file(file_size_in_bytes, target_dir):
|
|
prefix = f"temp_file_{file_size_in_bytes}B_"
|
|
suffix = ".bin"
|
|
try:
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="wb",
|
|
prefix=prefix,
|
|
suffix=suffix,
|
|
dir=target_dir,
|
|
delete=False,
|
|
) as temp_file:
|
|
# Write random bytes to the file
|
|
temp_file.write(os.urandom(file_size_in_bytes))
|
|
temp_file_path = Path(temp_file.name)
|
|
LOG.info(f"Temporary file created in: {temp_file_path}")
|
|
return temp_file_path
|
|
except Exception as e:
|
|
LOG.error(f"Failed to create temporary file: {e}")
|
|
return None
|
|
|
|
if network_type == "unthrottled":
|
|
bandwidth_str = "1000Mbit"
|
|
else:
|
|
bandwidth_str, _ = self.network_type_to_bandwidth_rtt(network_type)
|
|
bandwidth_mbit = float(bandwidth_str.replace("Mbit", ""))
|
|
|
|
file_size = calculate_file_size(bandwidth_mbit)
|
|
temp_file_path = generate_temp_file(file_size, tempfile.gettempdir())
|
|
return temp_file_path, file_size
|
|
|
|
def generate_download_test_html(self, temp_path, test_file_name):
|
|
html_content = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Download test</title>
|
|
</head>
|
|
<body>
|
|
<section>Download test</section>
|
|
<button id='downloadBtn'>Download Test</button>
|
|
<p id='download_status'></p>
|
|
<p id="progress"> </p>
|
|
<script>
|
|
let download_status = '';
|
|
|
|
function set_status(status) {{
|
|
download_status = status;
|
|
console.log('download_status:' + status);
|
|
document.getElementById('download_status').textContent = status;
|
|
}}
|
|
|
|
set_status('not_started');
|
|
|
|
const handleDownloadTest = () => {{
|
|
set_status('started');
|
|
const startTime = performance.now();
|
|
console.log('start');
|
|
fetch('/{test_file_name}')
|
|
.then((response) => response.blob())
|
|
.then((_) => {{
|
|
console.log('done');
|
|
const endTime = performance.now();
|
|
const downloadTime = endTime - startTime;
|
|
set_status('success time:' + downloadTime);
|
|
}})
|
|
.catch((error) => {{
|
|
console.error(error);
|
|
set_status('error');
|
|
}});
|
|
}};
|
|
document
|
|
.querySelector('#downloadBtn')
|
|
.addEventListener('click', handleDownloadTest);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
# Write the HTML content to the file
|
|
prefix = "download_test_"
|
|
suffix = ".html"
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w",
|
|
encoding="utf-8",
|
|
prefix=prefix,
|
|
suffix=suffix,
|
|
dir=temp_path,
|
|
delete=False,
|
|
) as html_file:
|
|
html_file.write(html_content)
|
|
return Path(html_file.name)
|
|
return None
|
|
|
|
def start_iperf3(self, port):
|
|
command = ["iperf3", "--server", "--interval", "10", "--port", str(port)]
|
|
server_process = Popen(
|
|
command,
|
|
stdin=PIPE,
|
|
stdout=PIPE,
|
|
stderr=PIPE,
|
|
universal_newlines=True,
|
|
start_new_session=True,
|
|
)
|
|
|
|
def read_output(pipe, log_func):
|
|
for line in iter(pipe.readline, ""):
|
|
log_func(line)
|
|
|
|
server_stdout = threading.Thread(
|
|
target=read_output, args=(server_process.stdout, LOG.info)
|
|
)
|
|
server_stderr = threading.Thread(
|
|
target=read_output, args=(server_process.stderr, LOG.info)
|
|
)
|
|
server_stdout.start()
|
|
server_stderr.start()
|
|
|
|
# Configuring MSS to 1240 and buffer length to 1200 leads to an
|
|
# IP payload of 1280 bytes for iperf3.
|
|
# This is consistent with the IP payload size observed in Firefox.
|
|
command = [
|
|
"iperf3",
|
|
"--client",
|
|
"127.0.0.1",
|
|
"-p",
|
|
str(port),
|
|
"-M",
|
|
"1240", # MSS
|
|
"-l",
|
|
"1200", # The length of buffer to write
|
|
"--time",
|
|
"30", # The time in seconds to transmit for
|
|
"--interval",
|
|
"10", # Reporting interval in seconds
|
|
]
|
|
|
|
client_process = Popen(
|
|
command,
|
|
stdin=PIPE,
|
|
stdout=PIPE,
|
|
stderr=PIPE,
|
|
universal_newlines=True,
|
|
start_new_session=True,
|
|
)
|
|
|
|
client_process.wait()
|
|
|
|
self.shutdown_process("iperf3_client", client_process)
|
|
self.shutdown_process("iperf3_server", server_process)
|
|
server_stdout.join()
|
|
server_stderr.join()
|
|
|
|
def iperf3_baseline(self):
|
|
self.run_command("sysctl net.ipv4.tcp_rmem")
|
|
self.run_command("sysctl net.ipv4.tcp_wmem")
|
|
tcp_port = self.find_free_port(socket.SOCK_STREAM)
|
|
LOG.info(f"iperf3_baseline on port:{tcp_port}")
|
|
|
|
if not self.check_tc_command():
|
|
raise Exception("tc is not available")
|
|
|
|
if not self.apply_network_throttling(
|
|
self.interface,
|
|
self.network_type,
|
|
self.packet_loss_rate,
|
|
("tcp", tcp_port),
|
|
):
|
|
raise Exception("apply_network_throttling failed")
|
|
|
|
self.start_iperf3(tcp_port)
|
|
|
|
def modify_command(self, cmd, test):
|
|
if not self._is_chrome:
|
|
cmd += [
|
|
"--firefox.acceptInsecureCerts",
|
|
"true",
|
|
]
|
|
protocol = "tcp"
|
|
if self.http_version == "h3":
|
|
protocol = "udp"
|
|
self.caddy_port = self.find_free_port(socket.SOCK_DGRAM)
|
|
if not self._is_chrome:
|
|
cmd += [
|
|
"--firefox.preference",
|
|
f"network.http.http3.alt-svc-mapping-for-testing:localhost;h3=:{self.caddy_port}",
|
|
"--firefox.preference",
|
|
"network.http.http3.force-use-alt-svc-mapping-for-testing:true",
|
|
"--firefox.preference",
|
|
"network.http.http3.disable_when_third_party_roots_found:false",
|
|
]
|
|
else:
|
|
spki = "VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8="
|
|
cmd += [
|
|
f"--chrome.args=--origin-to-force-quic-on=localhost:{self.caddy_port}",
|
|
f"--chrome.args=--ignore-certificate-errors-spki-list={spki}",
|
|
]
|
|
elif self.http_version == "h2":
|
|
self.caddy_port = self.find_free_port(socket.SOCK_STREAM)
|
|
if self._is_chrome:
|
|
spki = "VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8="
|
|
cmd += [
|
|
f"--chrome.args=--ignore-certificate-errors-spki-list={spki}",
|
|
]
|
|
else:
|
|
raise Exception("Unsupported HTTP version")
|
|
|
|
self.get_network_conditions(cmd)
|
|
temp_file_path = None
|
|
download_html = None
|
|
|
|
if self.network_type != "unthrottled":
|
|
self.iperf3_baseline()
|
|
|
|
if self.transfer_type == "upload":
|
|
cmd += [
|
|
"--browsertime.server_url",
|
|
f"https://localhost:{self.caddy_port}",
|
|
]
|
|
elif self.transfer_type == "download":
|
|
temp_file_path, file_size = self.network_type_to_temp_file(
|
|
self.network_type
|
|
)
|
|
if not temp_file_path:
|
|
raise Exception("Failed to generate temporary file")
|
|
self.cleanup.append(temp_file_path.unlink)
|
|
|
|
download_html = self.generate_download_test_html(
|
|
tempfile.gettempdir(), temp_file_path.name
|
|
)
|
|
if not download_html:
|
|
raise Exception("Failed to generate file for download test")
|
|
self.cleanup.append(download_html.unlink)
|
|
cmd += [
|
|
"--browsertime.server_url",
|
|
f"https://localhost:{self.caddy_port}/{download_html.name}",
|
|
"--browsertime.test_file_size",
|
|
str(file_size),
|
|
]
|
|
|
|
LOG.info(f"modify_command: {cmd}")
|
|
|
|
# We know that cmd[0] is the path to nodejs.
|
|
self.browsertime_node = Path(cmd[0])
|
|
self.backend_server = self.start_backend_server("benchmark_backend_server.js")
|
|
if self.backend_server:
|
|
self.caddy_server = self.start_caddy(download_html, temp_file_path)
|
|
if self.caddy_server is None:
|
|
raise Exception("Failed to start test servers")
|
|
|
|
if self.network_type != "unthrottled":
|
|
if not self.check_tc_command():
|
|
raise Exception("tc is not available")
|
|
if not self.apply_network_throttling(
|
|
self.interface,
|
|
self.network_type,
|
|
self.packet_loss_rate,
|
|
(protocol, self.caddy_port),
|
|
):
|
|
raise Exception("apply_network_throttling failed")
|
|
|
|
def handle_result(self, gt_result, raw_result, last_result=False, **kwargs):
|
|
goodput_key = (
|
|
"upload-goodput" if self.transfer_type == "upload" else "download-goodput"
|
|
)
|
|
|
|
def get_goodput(data):
|
|
try:
|
|
extras = data.get("extras", [])
|
|
if extras and isinstance(extras, list):
|
|
custom_data = extras[0].get("custom_data", {})
|
|
if goodput_key in custom_data:
|
|
return custom_data[goodput_key]
|
|
return None # Return None if any key or index is missing
|
|
except Exception:
|
|
return None
|
|
|
|
goodput = get_goodput(raw_result)
|
|
if not goodput:
|
|
return
|
|
|
|
LOG.info(f"Goodput: {goodput}")
|
|
for g in goodput:
|
|
gt_result["measurements"].setdefault(goodput_key, []).append(g)
|
|
|
|
def _build_subtest(self, measurement_name, replicates, test):
|
|
unit = test.get("unit", "Mbit/s")
|
|
if test.get("subtest_unit"):
|
|
unit = test.get("subtest_unit")
|
|
|
|
return {
|
|
"name": measurement_name,
|
|
"lowerIsBetter": test.get("lower_is_better", False),
|
|
"alertThreshold": float(test.get("alert_threshold", 2.0)),
|
|
"unit": unit,
|
|
"replicates": replicates,
|
|
"shouldAlert": True,
|
|
"value": round(filters.mean(replicates), 3),
|
|
}
|
|
|
|
def summarize_test(self, test, suite, **kwargs):
|
|
suite["type"] = "benchmark"
|
|
if suite["subtests"] == {}:
|
|
suite["subtests"] = []
|
|
for measurement_name, replicates in test["measurements"].items():
|
|
if not replicates:
|
|
continue
|
|
suite["subtests"].append(
|
|
self._build_subtest(measurement_name, replicates, test)
|
|
)
|
|
suite["subtests"].sort(key=lambda subtest: subtest["name"])
|
|
|
|
def summarize_suites(self, suites):
|
|
for index, item in enumerate(suites):
|
|
if "extraOptions" in item:
|
|
item["extraOptions"].append(self.network_type)
|
|
loss_str = (
|
|
f"loss-{self.packet_loss_rate}"
|
|
if self.packet_loss_rate
|
|
else "loss-0"
|
|
)
|
|
item["extraOptions"].append(loss_str)
|
|
|
|
def shutdown_process(self, name, proc):
|
|
LOG.info(f"{name} server shutting down ...")
|
|
if proc.poll() is not None:
|
|
LOG.info(f"server already dead {proc.poll()}")
|
|
else:
|
|
LOG.info(f"server pid is {proc.pid}")
|
|
try:
|
|
os.killpg(proc.pid, signal.SIGTERM)
|
|
except Exception as e:
|
|
LOG.error("Failed during kill: " + str(e))
|
|
|
|
def clean_up(self):
|
|
if self.caddy_server:
|
|
self.shutdown_process("Caddy", self.caddy_server)
|
|
if self.backend_server:
|
|
self.shutdown_process("Backend", self.backend_server)
|
|
if self.caddy_stdout:
|
|
self.caddy_stdout.join()
|
|
if self.caddy_stderr:
|
|
self.caddy_stderr.join()
|
|
while self.cleanup:
|
|
func = self.cleanup.pop()
|
|
func()
|