Skip to content

Commit 1e4640d

Browse files
authored
Merge branch 'main' into pvtime
2 parents 7a0cadf + ae078ee commit 1e4640d

File tree

5 files changed

+98
-48
lines changed

5 files changed

+98
-48
lines changed

.buildkite/pipeline_perf.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,48 @@
1616
# the operating system sometimes uses it for book-keeping tasks. The memory node (-m parameter)
1717
# has to be the node associated with the NUMA node from which we picked CPUs.
1818
perf_test = {
19-
"virtio-block": {
20-
"label": "💿 Virtio Block Performance",
21-
"test_path": "integration_tests/performance/test_block_ab.py::test_block_performance",
19+
"virtio-block-sync": {
20+
"label": "💿 Virtio Sync Block Performance",
21+
"tests": "integration_tests/performance/test_block_ab.py::test_block_performance -k 'not Async'",
22+
"devtool_opts": "-c 1-10 -m 0",
23+
},
24+
"virtio-block-async": {
25+
"label": "💿 Virtio Async Block Performance",
26+
"tests": "integration_tests/performance/test_block_ab.py::test_block_performance -k Async",
2227
"devtool_opts": "-c 1-10 -m 0",
2328
},
2429
"vhost-user-block": {
2530
"label": "💿 vhost-user Block Performance",
26-
"test_path": "integration_tests/performance/test_block_ab.py::test_block_vhost_user_performance",
31+
"tests": "integration_tests/performance/test_block_ab.py::test_block_vhost_user_performance",
2732
"devtool_opts": "-c 1-10 -m 0",
2833
"ab_opts": "--noise-threshold 0.1",
2934
},
3035
"network": {
3136
"label": "📠 Network Latency and Throughput",
32-
"test_path": "integration_tests/performance/test_network_ab.py",
37+
"tests": "integration_tests/performance/test_network_ab.py",
3338
"devtool_opts": "-c 1-10 -m 0",
3439
# Triggers if delta is > 0.01ms (10µs) or default relative threshold (5%)
3540
# only relevant for latency test, throughput test will always be magnitudes above this anyway
3641
"ab_opts": "--absolute-strength 0.010",
3742
},
3843
"snapshot-latency": {
3944
"label": "📸 Snapshot Latency",
40-
"test_path": "integration_tests/performance/test_snapshot_ab.py::test_restore_latency integration_tests/performance/test_snapshot_ab.py::test_post_restore_latency",
45+
"tests": "integration_tests/performance/test_snapshot_ab.py::test_restore_latency integration_tests/performance/test_snapshot_ab.py::test_post_restore_latency",
4146
"devtool_opts": "-c 1-12 -m 0",
4247
},
4348
"population-latency": {
4449
"label": "📸 Memory Population Latency",
45-
"test_path": "integration_tests/performance/test_snapshot_ab.py::test_population_latency",
50+
"tests": "integration_tests/performance/test_snapshot_ab.py::test_population_latency",
4651
"devtool_opts": "-c 1-12 -m 0",
4752
},
4853
"vsock-throughput": {
4954
"label": "🧦 Vsock Throughput",
50-
"test_path": "integration_tests/performance/test_vsock_ab.py",
55+
"tests": "integration_tests/performance/test_vsock_ab.py",
5156
"devtool_opts": "-c 1-10 -m 0",
5257
},
5358
"memory-overhead": {
5459
"label": "💾 Memory Overhead and 👢 Boottime",
55-
"test_path": "integration_tests/performance/test_memory_overhead.py integration_tests/performance/test_boottime.py::test_boottime",
60+
"tests": "integration_tests/performance/test_memory_overhead.py integration_tests/performance/test_boottime.py::test_boottime",
5661
"devtool_opts": "-c 1-10 -m 0",
5762
},
5863
}
@@ -93,23 +98,21 @@
9398
tests = [perf_test[test] for test in pipeline.args.test or perf_test.keys()]
9499
for test in tests:
95100
devtool_opts = test.pop("devtool_opts")
96-
test_path = test.pop("test_path")
101+
test_selector = test.pop("tests")
97102
ab_opts = test.pop("ab_opts", "")
98103
devtool_opts += " --performance"
99-
pytest_opts = ""
104+
test_script_opts = ""
100105
if REVISION_A:
101106
devtool_opts += " --ab"
102-
pytest_opts = (
103-
f"{ab_opts} run build/{REVISION_A}/ build/{REVISION_B} --test {test_path}"
104-
)
107+
test_script_opts = f'{ab_opts} run build/{REVISION_A}/ build/{REVISION_B} --pytest-opts "{test_selector}"'
105108
else:
106109
# Passing `-m ''` below instructs pytest to collect tests regardless of
107110
# their markers (e.g. it will collect both tests marked as nonci, and
108111
# tests without any markers).
109-
pytest_opts += f" -m '' {test_path}"
112+
test_script_opts += f" -m '' {test_selector}"
110113

111114
pipeline.build_group(
112-
command=pipeline.devtool_test(devtool_opts, pytest_opts),
115+
command=pipeline.devtool_test(devtool_opts, test_script_opts),
113116
# and the rest can be command arguments
114117
**test,
115118
)

tests/framework/microvm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ def kill(self):
330330

331331
# filter ps results for the jailer's unique id
332332
_, stdout, stderr = utils.check_output(
333-
f"ps aux | grep {self.jailer.jailer_id}"
333+
f"ps ax -o cmd -ww | grep {self.jailer.jailer_id}"
334334
)
335335
# make sure firecracker was killed
336336
assert (

tests/integration_tests/performance/test_block_ab.py

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""Performance benchmark for block device emulation."""
44

55
import concurrent
6+
import glob
67
import os
78
import shutil
89
from pathlib import Path
@@ -44,7 +45,7 @@ def prepare_microvm_for_test(microvm):
4445
check_output("echo 3 > /proc/sys/vm/drop_caches")
4546

4647

47-
def run_fio(microvm, mode, block_size):
48+
def run_fio(microvm, mode, block_size, fio_engine="libaio"):
4849
"""Run a fio test in the specified mode with block size bs."""
4950
cmd = (
5051
CmdBuilder("fio")
@@ -59,7 +60,7 @@ def run_fio(microvm, mode, block_size):
5960
.with_arg("--randrepeat=0")
6061
.with_arg(f"--bs={block_size}")
6162
.with_arg(f"--size={BLOCK_DEVICE_SIZE_MB}M")
62-
.with_arg("--ioengine=libaio")
63+
.with_arg(f"--ioengine={fio_engine}")
6364
.with_arg("--iodepth=32")
6465
# Set affinity of the entire fio process to a set of vCPUs equal in size to number of workers
6566
.with_arg(
@@ -68,8 +69,8 @@ def run_fio(microvm, mode, block_size):
6869
# Instruct fio to pin one worker per vcpu
6970
.with_arg("--cpus_allowed_policy=split")
7071
.with_arg(f"--write_bw_log={mode}")
72+
.with_arg(f"--write_lat_log={mode}")
7173
.with_arg("--log_avg_msec=1000")
72-
.with_arg("--output-format=json+")
7374
.build()
7475
)
7576

@@ -102,51 +103,65 @@ def run_fio(microvm, mode, block_size):
102103
return logs_path, cpu_load_future.result()
103104

104105

105-
def process_fio_logs(vm, fio_mode, logs_dir, metrics):
106-
"""Parses the fio logs in `{logs_dir}/{fio_mode}_bw.*.log and emits their contents as CloudWatch metrics"""
107-
106+
def process_fio_log_files(logs_glob):
107+
"""Parses all fio log files matching the given glob and yields tuples of same-timestamp read and write metrics"""
108108
data = [
109-
Path(f"{logs_dir}/{fio_mode}_bw.{job_id + 1}.log")
110-
.read_text("UTF-8")
111-
.splitlines()
112-
for job_id in range(vm.vcpus_count)
109+
Path(pathname).read_text("UTF-8").splitlines()
110+
for pathname in glob.glob(logs_glob)
113111
]
114112

113+
assert data, "no log files found!"
114+
115115
for tup in zip(*data):
116-
bw_read = 0
117-
bw_write = 0
116+
read_values = []
117+
write_values = []
118118

119119
for line in tup:
120+
# See https://fio.readthedocs.io/en/latest/fio_doc.html#log-file-formats
120121
_, value, direction, _ = line.split(",", maxsplit=3)
121122
value = int(value.strip())
122123

123-
# See https://fio.readthedocs.io/en/latest/fio_doc.html#log-file-formats
124124
match direction.strip():
125125
case "0":
126-
bw_read += value
126+
read_values.append(value)
127127
case "1":
128-
bw_write += value
128+
write_values.append(value)
129129
case _:
130130
assert False
131131

132+
yield read_values, write_values
133+
134+
135+
def emit_fio_metrics(logs_dir, metrics):
136+
"""Parses the fio logs in `{logs_dir}/*_[clat|bw].*.log and emits their contents as CloudWatch metrics"""
137+
for bw_read, bw_write in process_fio_log_files(f"{logs_dir}/*_bw.*.log"):
132138
if bw_read:
133-
metrics.put_metric("bw_read", bw_read, "Kilobytes/Second")
139+
metrics.put_metric("bw_read", sum(bw_read), "Kilobytes/Second")
134140
if bw_write:
135-
metrics.put_metric("bw_write", bw_write, "Kilobytes/Second")
141+
metrics.put_metric("bw_write", sum(bw_write), "Kilobytes/Second")
142+
143+
for lat_read, lat_write in process_fio_log_files(f"{logs_dir}/*_clat.*.log"):
144+
# latency values in fio logs are in nanoseconds, but cloudwatch only supports
145+
# microseconds as the more granular unit, so need to divide by 1000.
146+
for value in lat_read:
147+
metrics.put_metric("clat_read", value / 1000, "Microseconds")
148+
for value in lat_write:
149+
metrics.put_metric("clat_write", value / 1000, "Microseconds")
136150

137151

138-
@pytest.mark.timeout(120)
139152
@pytest.mark.nonci
140153
@pytest.mark.parametrize("vcpus", [1, 2], ids=["1vcpu", "2vcpu"])
141154
@pytest.mark.parametrize("fio_mode", ["randread", "randwrite"])
142155
@pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"])
156+
@pytest.mark.parametrize("fio_engine", ["libaio", "psync"])
143157
def test_block_performance(
144158
microvm_factory,
145159
guest_kernel_acpi,
146160
rootfs,
147161
vcpus,
148162
fio_mode,
149163
fio_block_size,
164+
fio_engine,
150165
io_engine,
151166
metrics,
152167
):
@@ -170,15 +185,16 @@ def test_block_performance(
170185
"io_engine": io_engine,
171186
"fio_mode": fio_mode,
172187
"fio_block_size": str(fio_block_size),
188+
"fio_engine": fio_engine,
173189
**vm.dimensions,
174190
}
175191
)
176192

177193
vm.pin_threads(0)
178194

179-
logs_dir, cpu_util = run_fio(vm, fio_mode, fio_block_size)
195+
logs_dir, cpu_util = run_fio(vm, fio_mode, fio_block_size, fio_engine)
180196

181-
process_fio_logs(vm, fio_mode, logs_dir, metrics)
197+
emit_fio_metrics(logs_dir, metrics)
182198

183199
for thread_name, values in cpu_util.items():
184200
for value in values:
@@ -218,6 +234,7 @@ def test_block_vhost_user_performance(
218234
"io_engine": "vhost-user",
219235
"fio_mode": fio_mode,
220236
"fio_block_size": str(fio_block_size),
237+
"fio_engine": "libaio",
221238
**vm.dimensions,
222239
}
223240
)
@@ -227,7 +244,7 @@ def test_block_vhost_user_performance(
227244

228245
logs_dir, cpu_util = run_fio(vm, fio_mode, fio_block_size)
229246

230-
process_fio_logs(vm, fio_mode, logs_dir, metrics)
247+
emit_fio_metrics(logs_dir, metrics)
231248

232249
for thread_name, values in cpu_util.items():
233250
for value in values:

tests/integration_tests/performance/test_memory_overhead.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def test_memory_overhead(
3838
"""
3939

4040
for _ in range(5):
41-
microvm = microvm_factory.build(guest_kernel_acpi, rootfs)
41+
microvm = microvm_factory.build(guest_kernel_acpi, rootfs, monitor_memory=False)
4242
microvm.spawn(emit_metrics=True)
4343
microvm.basic_config(vcpu_count=vcpu_count, mem_size_mib=mem_size_mib)
4444
microvm.add_net_iface()

tools/ab_test.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,14 @@
4646
{"instance": "m6a.metal", "performance_test": "test_network_tcp_throughput"},
4747
# Network throughput on m7a.metal
4848
{"instance": "m7a.metal-48xl", "performance_test": "test_network_tcp_throughput"},
49+
# block latencies if guest uses async request submission
50+
{"fio_engine": "libaio", "metric": "clat_read"},
51+
{"fio_engine": "libaio", "metric": "clat_write"},
4952
]
5053

5154

5255
def is_ignored(dimensions) -> bool:
53-
"""Checks whether the given dimensions match a entry in the IGNORED dictionary above"""
56+
"""Checks whether the given dimensions match an entry in the IGNORED dictionary above"""
5457
for high_variance in IGNORED:
5558
matching = {key: dimensions[key] for key in high_variance if key in dimensions}
5659

@@ -114,6 +117,8 @@ def load_data_series(report_path: Path, tag=None, *, reemit: bool = False):
114117
# Dictionary mapping EMF dimensions to A/B-testable metrics/properties
115118
processed_emf = {}
116119

120+
distinct_values_per_dimenson = defaultdict(set)
121+
117122
report = json.loads(report_path.read_text("UTF-8"))
118123
for test in report["tests"]:
119124
for line in test["teardown"]["stdout"].splitlines():
@@ -133,6 +138,9 @@ def load_data_series(report_path: Path, tag=None, *, reemit: bool = False):
133138
if not dimensions:
134139
continue
135140

141+
for dimension, value in dimensions.items():
142+
distinct_values_per_dimenson[dimension].add(value)
143+
136144
dimension_set = frozenset(dimensions.items())
137145

138146
if dimension_set not in processed_emf:
@@ -149,22 +157,40 @@ def load_data_series(report_path: Path, tag=None, *, reemit: bool = False):
149157

150158
values.extend(result[metric][0])
151159

152-
return processed_emf
160+
irrelevant_dimensions = set()
161+
162+
for dimension, distinct_values in distinct_values_per_dimenson.items():
163+
if len(distinct_values) == 1:
164+
irrelevant_dimensions.add(dimension)
165+
166+
post_processed_emf = {}
167+
168+
for dimension_set, metrics in processed_emf.items():
169+
processed_key = frozenset(
170+
(dim, value)
171+
for (dim, value) in dimension_set
172+
if dim not in irrelevant_dimensions
173+
)
174+
175+
post_processed_emf[processed_key] = metrics
176+
177+
return post_processed_emf
153178

154179

155-
def collect_data(binary_dir: Path, tests: list[str]):
180+
def collect_data(binary_dir: Path, pytest_opts: str):
156181
"""Executes the specified test using the provided firecracker binaries"""
157182
binary_dir = binary_dir.resolve()
158183

159184
print(f"Collecting samples with {binary_dir}")
160185
subprocess.run(
161-
["./tools/test.sh", f"--binary-dir={binary_dir}", *tests, "-m", ""],
186+
f"./tools/test.sh --binary-dir={binary_dir} {pytest_opts} -m ''",
162187
env=os.environ
163188
| {
164189
"AWS_EMF_ENVIRONMENT": "local",
165190
"AWS_EMF_NAMESPACE": "local",
166191
},
167192
check=True,
193+
shell=True,
168194
)
169195
return load_data_series(
170196
Path("test_results/test-report.json"), binary_dir, reemit=True
@@ -258,7 +284,7 @@ def analyze_data(
258284

259285
failures = []
260286
for (dimension_set, metric), (result, unit) in results.items():
261-
if is_ignored(dict(dimension_set)):
287+
if is_ignored(dict(dimension_set) | {"metric": metric}):
262288
continue
263289

264290
print(f"Doing A/B-test for dimensions {dimension_set} and property {metric}")
@@ -308,15 +334,15 @@ def analyze_data(
308334
def ab_performance_test(
309335
a_revision: Path,
310336
b_revision: Path,
311-
tests,
337+
pytest_opts,
312338
p_thresh,
313339
strength_abs_thresh,
314340
noise_threshold,
315341
):
316342
"""Does an A/B-test of the specified test with the given firecracker/jailer binaries"""
317343

318344
return binary_ab_test(
319-
lambda bin_dir, _: collect_data(bin_dir, tests),
345+
lambda bin_dir, _: collect_data(bin_dir, pytest_opts),
320346
lambda ah, be: analyze_data(
321347
ah,
322348
be,
@@ -349,7 +375,11 @@ def ab_performance_test(
349375
help="Directory containing firecracker and jailer binaries whose performance we want to compare against the results from a_revision",
350376
type=Path,
351377
)
352-
run_parser.add_argument("--test", help="The test to run", nargs="+", required=True)
378+
run_parser.add_argument(
379+
"--pytest-opts",
380+
help="Parameters to pass through to pytest, for example for test selection",
381+
required=True,
382+
)
353383
analyze_parser = subparsers.add_parser(
354384
"analyze",
355385
help="Analyze the results of two manually ran tests based on their test-report.json files",
@@ -388,7 +418,7 @@ def ab_performance_test(
388418
ab_performance_test(
389419
args.a_revision,
390420
args.b_revision,
391-
args.test,
421+
args.pytest_opts,
392422
args.significance,
393423
args.absolute_strength,
394424
args.noise_threshold,

0 commit comments

Comments
 (0)