Skip to content

Commit 4b970f6

Browse files
committed
feat(a/b): add support scripts for A/B visualization
Add scripts for combining and plotting results of A/B runs. Move all A/B related scripts to the tools/ab Signed-off-by: Egor Lazarchuk <[email protected]>
1 parent bf5d9da commit 4b970f6

File tree

4 files changed

+183
-1
lines changed

4 files changed

+183
-1
lines changed
File renamed without changes.

tools/ab/combine.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
import json
3+
import argparse
4+
from pathlib import Path
5+
6+
parser = argparse.ArgumentParser(
7+
description="Combine A/B test fails into groups per test type"
8+
)
9+
parser.add_argument(
10+
"path",
11+
help="Path to the directory with failed A/B runs",
12+
type=Path,
13+
)
14+
args = parser.parse_args()
15+
16+
BLOCK = "test_block_performance"
17+
NET_THROUGHPUT = "test_network_throughput"
18+
NET_LATENCY = "test_network_latency"
19+
20+
block_data = []
21+
net_data = []
22+
net_lat_data = []
23+
for d in os.walk(args.path):
24+
if "ab.json" in d[-1]:
25+
path = d[0] + "/ab.json"
26+
print(path)
27+
with open(path, "r+") as f:
28+
lines = f.read()
29+
j = '{"data":' + lines + "}"
30+
data = json.loads(j)
31+
for e in data["data"]:
32+
match e["performance_test"]:
33+
case BLOCk:
34+
block_data.append(e)
35+
case NET_THROUGHPUT:
36+
net_data.append(e)
37+
case NET_LATENCY:
38+
net_lat_data.append(e)
39+
40+
with open(f"{NET_LATENCY}.json", "w") as f:
41+
json.dump({"results": net_lat_data}, f, indent=2, sort_keys=True)
42+
with open(f"{NET_THROUGHPUT}.json", "w") as f:
43+
json.dump({"results": net_data}, f, indent=2, sort_keys=True)
44+
with open(f"{BLOCK}.json", "w") as f:
45+
json.dump({"fails": block_data}, f, indent=2, sort_keys=True)

tools/ab/plot.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import os
2+
import json
3+
import argparse
4+
import numpy as np
5+
from enum import Enum
6+
import matplotlib.pyplot as plt
7+
8+
plt.style.use("dark_background")
9+
10+
11+
def clamp(min_v, max_v, v):
12+
return max(min_v, min(max_v, v))
13+
14+
15+
def lerp(color_a, color_b, t):
16+
return (
17+
clamp(0.0, 1.0, abs(color_a[0] * (1.0 - t) + color_b[0] * t)),
18+
clamp(0.0, 1.0, abs(color_a[1] * (1.0 - t) + color_b[1] * t)),
19+
clamp(0.0, 1.0, abs(color_a[2] * (1.0 - t) + color_b[2] * t)),
20+
)
21+
22+
23+
GREY = (0.5, 0.5, 0.5)
24+
GREEN = (0.1, 0.8, 0.1)
25+
RED = (0.8, 0.0, 0.1)
26+
27+
POSITIVE_COLOR = GREEN
28+
NEGATIVE_COLOR = RED
29+
30+
31+
class DataType(Enum):
32+
Block = "block"
33+
Net = "net"
34+
NetLatency = "net_latency"
35+
36+
37+
parser = argparse.ArgumentParser(description="Plot results of A/B test")
38+
parser.add_argument("path", type=str)
39+
args = parser.parse_args()
40+
41+
paths = [f"{args.path}/{f}" for f in os.listdir(args.path)]
42+
for path in paths:
43+
print(f"processing: {path}")
44+
with open(path) as f:
45+
fails = json.load(f)["fails"]
46+
47+
if not fails:
48+
print(f"skipping {path}. No data present")
49+
continue
50+
51+
instances = set()
52+
host_kernels = set()
53+
aggregated = {}
54+
55+
match fails[0]["performance_test"]:
56+
case "test_block_performance":
57+
data_type = DataType.Block
58+
case "test_network_tcp_throughput":
59+
data_type = DataType.Net
60+
case "test_network_latency":
61+
data_type = DataType.NetLatency
62+
case _:
63+
print("unknown data type. skipping")
64+
continue
65+
66+
for fail in fails:
67+
instances.add(fail["instance"])
68+
host_kernels.add(fail["host_kernel"])
69+
70+
if data_type == DataType.Block:
71+
tag = (
72+
fail["instance"],
73+
fail["host_kernel"],
74+
fail["guest_kernel"],
75+
fail["fio_mode"],
76+
fail["vcpus"],
77+
fail["io_engine"],
78+
)
79+
elif data_type == DataType.Net:
80+
tag = (
81+
fail["instance"],
82+
fail["host_kernel"],
83+
fail["guest_kernel"],
84+
fail["mode"],
85+
fail["vcpus"],
86+
)
87+
elif data_type == DataType.NetLatency:
88+
tag = (
89+
fail["instance"],
90+
fail["host_kernel"],
91+
fail["guest_kernel"],
92+
)
93+
POSITIVE_COLOR = RED
94+
NEGATIVE_COLOR = GREEN
95+
96+
if tag not in aggregated:
97+
aggregated[tag] = []
98+
aggregated[tag].append(fail["diff"])
99+
100+
for instance in sorted(instances):
101+
fig, ax = plt.subplots(len(host_kernels), figsize=(16, 11))
102+
if len(host_kernels) == 1:
103+
ax = [ax]
104+
fig.tight_layout(pad=8.0)
105+
106+
for i, host_kernel in enumerate(sorted(host_kernels)):
107+
data = []
108+
for key, value in aggregated.items():
109+
if key[0] == instance and key[1] == host_kernel:
110+
label = "\n".join(key[2:])
111+
values = np.array(value)
112+
mean = np.mean(values)
113+
std = np.std(values)
114+
data.append((label, mean, std))
115+
data.sort()
116+
labels = np.array([t[0] for t in data])
117+
means = np.array([t[1] for t in data])
118+
errors = np.array([t[2] for t in data])
119+
colors = [
120+
(
121+
lerp(GREY, POSITIVE_COLOR, t)
122+
if 0.0 < t
123+
else lerp(GREY, NEGATIVE_COLOR, -t)
124+
)
125+
for t in [m / 100.0 for m in means]
126+
]
127+
128+
bar = ax[i].bar(labels, means, yerr=errors, color=colors, ecolor="white")
129+
bar_labels = [f"{m:.2f} / {s:.2f}" for (m, s) in zip(means, errors)]
130+
ax[i].bar_label(bar, labels=bar_labels)
131+
ax[i].set_ylabel("Percentage of change: mean / std")
132+
ax[i].grid(color="grey", linestyle="-.", linewidth=0.5, alpha=0.5)
133+
ax[i].set_title(
134+
f"{data_type}\nInstance: {instance}\nHost kernel: {host_kernel}",
135+
)
136+
137+
plt.savefig(f"{args.path}/{data_type}_{instance}.png")

tools/devtool

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ cmd_test() {
760760
test_script="./tools/test.sh"
761761

762762
if [ $do_ab_test -eq 1 ]; then
763-
test_script="./tools/ab_test.py"
763+
test_script="./tools/ab/ab_test.py"
764764
fi
765765

766766
# Testing (running Firecracker via the jailer) needs root access,

0 commit comments

Comments
 (0)