Skip to content

Commit 7682ec8

Browse files
committed
feat(A/B): add customizable artifacts
Update `ab_test.py` with ability to accept custom artifacts for A and B runs. Additionally update `pipeline_perf.py` as well. Now REVISION_A_ARTIFACTS and REVISION_B_ARTIFACTS environment variables specify custom artifacts which will be used in A/B test Signed-off-by: Egor Lazarchuk <yegorlz@amazon.co.uk>
1 parent 56abaa4 commit 7682ec8

File tree

6 files changed

+129
-69
lines changed

6 files changed

+129
-69
lines changed

.buildkite/common.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,10 +376,11 @@ def to_json(self):
376376
"""Serialize the pipeline to JSON"""
377377
return json.dumps(self.to_dict(), indent=4, sort_keys=True, ensure_ascii=False)
378378

379-
def devtool_download_artifacts(self, artifacts_s3_url):
379+
def devtool_download_artifacts(self, artifacts):
380380
"""Generate a `devtool download_ci_artifacts` command"""
381381
parts = ["./tools/devtool -y download_ci_artifacts"]
382-
382+
parts += artifacts
383+
return " ".join(parts)
383384

384385
def devtool_test(self, devtool_opts=None, pytest_opts=None):
385386
"""Generate a `devtool test` command"""

.buildkite/pipeline_perf.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,16 @@
9494

9595
REVISION_A = os.environ.get("REVISION_A")
9696
REVISION_B = os.environ.get("REVISION_B")
97+
REVISION_A_ARTIFACTS = os.environ.get("REVISION_A_ARTIFACTS")
98+
REVISION_B_ARTIFACTS = os.environ.get("REVISION_B_ARTIFACTS")
9799

98100
# Either both are specified or neither. Only doing either is a bug. If you want to
99101
# run performance tests _on_ a specific commit, specify neither and put your commit
100102
# into buildkite's "commit" field.
101103
assert (REVISION_A and REVISION_B) or (not REVISION_A and not REVISION_B)
104+
assert (REVISION_A_ARTIFACTS and REVISION_B_ARTIFACTS) or (
105+
not REVISION_A_ARTIFACTS and not REVISION_B_ARTIFACTS
106+
)
102107

103108
BKPipeline.parser.add_argument(
104109
"--test",
@@ -132,17 +137,28 @@
132137
ab_opts = test.pop("ab_opts", "")
133138
devtool_opts += " --performance"
134139
test_script_opts = ""
140+
artifacts = []
135141
if REVISION_A:
136142
devtool_opts += " --ab"
137-
test_script_opts = f'{ab_opts} run --binaries_a build/{REVISION_A}/ --binaries_b build/{REVISION_B} --pytest-opts "{test_selector}"'
143+
test_script_opts = f'{ab_opts} run --binaries-a build/{REVISION_A}/ --binaries-b build/{REVISION_B} --pytest-opts "{test_selector}"'
144+
if REVISION_A_ARTIFACTS:
145+
artifacts.append(REVISION_A_ARTIFACTS)
146+
artifacts.append(REVISION_B_ARTIFACTS)
147+
test_script_opts += f" --artifacts-a {REVISION_A_ARTIFACTS} --artifacts-b {REVISION_B_ARTIFACTS}"
138148
else:
139149
# Passing `-m ''` below instructs pytest to collect tests regardless of
140150
# their markers (e.g. it will collect both tests marked as nonci, and
141151
# tests without any markers).
142152
test_script_opts += f" -m '' {test_selector}"
143153

154+
command = []
155+
if artifacts:
156+
command.append(pipeline.devtool_download_artifacts(artifacts))
157+
# Hack because devtool_test already returns an array, just this array always
158+
# has just 1 element
159+
command.append(pipeline.devtool_test(devtool_opts, test_script_opts)[0])
144160
pipeline.build_group(
145-
command=pipeline.devtool_test(devtool_opts, test_script_opts),
161+
command=command,
146162
# and the rest can be command arguments
147163
**test,
148164
)

tests/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,13 +345,17 @@ def get(self, _netns_id):
345345
for netns in netns_fcty._all:
346346
netns._cleanup_orig()
347347

348+
348349
@pytest.fixture(scope="session")
349350
def current_artifacts_path():
350351
with open("/firecracker/build/current_artifacts", "r", encoding="utf-8") as ca:
351352
yield ca.read().strip()
352353

354+
353355
@pytest.fixture()
354-
def microvm_factory(request, record_property, results_dir, netns_factory, current_artifacts_path):
356+
def microvm_factory(
357+
request, record_property, results_dir, netns_factory, current_artifacts_path
358+
):
355359
"""Fixture to create microvms simply."""
356360

357361
binary_dir = request.config.getoption("--binary-dir") or DEFAULT_BINARY_DIR

tests/integration_tests/performance/test_block.py

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
BLOCK_DEVICE_SIZE_MB = 2048
1616

1717
# Time (in seconds) for which fio "warms up"
18-
WARMUP_SEC = 10
18+
WARMUP_SEC = 1
1919

2020
# Time (in seconds) for which fio runs after warmup is done
21-
RUNTIME_SEC = 30
21+
RUNTIME_SEC = 1
2222

2323
# VM guest memory size
2424
GUEST_MEM_MIB = 1024
@@ -100,10 +100,10 @@ def emit_fio_metrics(logs_dir, metrics):
100100

101101

102102
@pytest.mark.nonci
103-
@pytest.mark.parametrize("vcpus", [1, 2], ids=["1vcpu", "2vcpu"])
104-
@pytest.mark.parametrize("fio_mode", [fio.Mode.RANDREAD, fio.Mode.RANDWRITE])
103+
@pytest.mark.parametrize("vcpus", [1]) # , 2], ids=["1vcpu", "2vcpu"])
104+
@pytest.mark.parametrize("fio_mode", [fio.Mode.RANDREAD]) # , fio.Mode.RANDWRITE])
105105
@pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"])
106-
@pytest.mark.parametrize("fio_engine", [fio.Engine.LIBAIO, fio.Engine.PSYNC])
106+
@pytest.mark.parametrize("fio_engine", [fio.Engine.LIBAIO]) # , fio.Engine.PSYNC])
107107
def test_block_performance(
108108
uvm_plain_acpi,
109109
vcpus,
@@ -150,50 +150,50 @@ def test_block_performance(
150150
metrics.put_metric(f"cpu_utilization_{thread_name}", value, "Percent")
151151

152152

153-
@pytest.mark.nonci
154-
@pytest.mark.parametrize("vcpus", [1, 2], ids=["1vcpu", "2vcpu"])
155-
@pytest.mark.parametrize("fio_mode", [fio.Mode.RANDREAD])
156-
@pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"])
157-
def test_block_vhost_user_performance(
158-
uvm_plain_acpi,
159-
vcpus,
160-
fio_mode,
161-
fio_block_size,
162-
metrics,
163-
results_dir,
164-
):
165-
"""
166-
Execute block device emulation benchmarking scenarios.
167-
"""
168-
169-
vm = uvm_plain_acpi
170-
vm.spawn(log_level="Info", emit_metrics=True)
171-
vm.basic_config(vcpu_count=vcpus, mem_size_mib=GUEST_MEM_MIB)
172-
vm.add_net_iface()
173-
174-
# Add a secondary block device for benchmark tests.
175-
fs = drive_tools.FilesystemFile(size=BLOCK_DEVICE_SIZE_MB)
176-
vm.add_vhost_user_drive("scratch", fs.path)
177-
vm.start()
178-
179-
metrics.set_dimensions(
180-
{
181-
"performance_test": "test_block_performance",
182-
"io_engine": "vhost-user",
183-
"fio_mode": fio_mode,
184-
"fio_block_size": str(fio_block_size),
185-
"fio_engine": "libaio",
186-
**vm.dimensions,
187-
}
188-
)
189-
190-
next_cpu = vm.pin_threads(0)
191-
vm.disks_vhost_user["scratch"].pin(next_cpu)
192-
193-
cpu_util = run_fio(vm, fio_mode, fio_block_size, results_dir, fio.Engine.LIBAIO)
194-
195-
emit_fio_metrics(results_dir, metrics)
196-
197-
for thread_name, values in cpu_util.items():
198-
for value in values:
199-
metrics.put_metric(f"cpu_utilization_{thread_name}", value, "Percent")
153+
# @pytest.mark.nonci
154+
# @pytest.mark.parametrize("vcpus", [1, 2], ids=["1vcpu", "2vcpu"])
155+
# @pytest.mark.parametrize("fio_mode", [fio.Mode.RANDREAD])
156+
# @pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"])
157+
# def test_block_vhost_user_performance(
158+
# uvm_plain_acpi,
159+
# vcpus,
160+
# fio_mode,
161+
# fio_block_size,
162+
# metrics,
163+
# results_dir,
164+
# ):
165+
# """
166+
# Execute block device emulation benchmarking scenarios.
167+
# """
168+
#
169+
# vm = uvm_plain_acpi
170+
# vm.spawn(log_level="Info", emit_metrics=True)
171+
# vm.basic_config(vcpu_count=vcpus, mem_size_mib=GUEST_MEM_MIB)
172+
# vm.add_net_iface()
173+
#
174+
# # Add a secondary block device for benchmark tests.
175+
# fs = drive_tools.FilesystemFile(size=BLOCK_DEVICE_SIZE_MB)
176+
# vm.add_vhost_user_drive("scratch", fs.path)
177+
# vm.start()
178+
#
179+
# metrics.set_dimensions(
180+
# {
181+
# "performance_test": "test_block_performance",
182+
# "io_engine": "vhost-user",
183+
# "fio_mode": fio_mode,
184+
# "fio_block_size": str(fio_block_size),
185+
# "fio_engine": "libaio",
186+
# **vm.dimensions,
187+
# }
188+
# )
189+
#
190+
# next_cpu = vm.pin_threads(0)
191+
# vm.disks_vhost_user["scratch"].pin(next_cpu)
192+
#
193+
# cpu_util = run_fio(vm, fio_mode, fio_block_size, results_dir, fio.Engine.LIBAIO)
194+
#
195+
# emit_fio_metrics(results_dir, metrics)
196+
#
197+
# for thread_name, values in cpu_util.items():
198+
# for value in values:
199+
# metrics.put_metric(f"cpu_utilization_{thread_name}", value, "Percent")

tools/ab_test.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import subprocess
2525
from collections import defaultdict
2626
from pathlib import Path
27-
from typing import Callable, List, TypeVar
27+
from typing import Callable, List, TypeVar, Optional
2828

2929
import scipy
3030

@@ -194,16 +194,22 @@ def uninteresting_dimensions(data):
194194
return uninteresting
195195

196196

197-
def collect_data(tag: str, binary_dir: Path, pytest_opts: str):
197+
def collect_data(tag: str, binary_dir: Path, artifacts: Optional[Path], pytest_opts: str):
198198
"""
199199
Executes the specified test using the provided firecracker binaries and
200200
stores results into the `test_results/tag` directory
201201
"""
202202
binary_dir = binary_dir.resolve()
203203

204-
print(f"Collecting samples with {binary_dir}")
204+
print(f"Collecting samples | binaries path: {binary_dir}" + f" | artifacts path: {artifacts}" if artifacts else "")
205205
test_path = f"test_results/{tag}"
206206
test_report_path = f"{test_path}/test-report.json"
207+
208+
# It is not possible to just download them here this script is usually run inside docker
209+
# and artifacts downloading does not work inside it.
210+
if artifacts:
211+
subprocess.run(f"./tools/devtool set_current_artifacts {artifacts}", check=True, shell=True)
212+
207213
subprocess.run(
208214
f"./tools/test.sh --binary-dir={binary_dir} {pytest_opts} -m '' --json-report-file=../{test_report_path}",
209215
env=os.environ
@@ -371,25 +377,29 @@ def analyze_data(
371377

372378

373379
def binary_ab_test(
374-
test_runner: Callable[[Path, bool], T],
380+
test_runner: Callable[[Path, Optional[Path], bool], T],
375381
comparator: Callable[[T, T], U],
376382
*,
377383
a_directory: Path,
378384
b_directory: Path,
385+
a_artifacts: Optional[Path],
386+
b_artifacts: Optional[Path],
379387
):
380388
"""
381389
Similar to `git_ab_test`, but instead of locally checking out different revisions, it operates on
382390
directories containing firecracker/jailer binaries
383391
"""
384-
result_a = test_runner(a_directory, True)
385-
result_b = test_runner(b_directory, False)
392+
result_a = test_runner(a_directory, a_artifacts, True)
393+
result_b = test_runner(b_directory, b_artifacts, False)
386394

387395
return result_a, result_b, comparator(result_a, result_b)
388396

389397

390398
def ab_performance_test(
391-
a_revision: Path,
392-
b_revision: Path,
399+
a_directory: Path,
400+
b_directory: Path,
401+
a_artifacts: Optional[Path],
402+
b_artifacts: Optional[Path],
393403
pytest_opts,
394404
p_thresh,
395405
strength_abs_thresh,
@@ -398,7 +408,7 @@ def ab_performance_test(
398408
"""Does an A/B-test of the specified test with the given firecracker/jailer binaries"""
399409

400410
return binary_ab_test(
401-
lambda bin_dir, is_a: collect_data(is_a and "A" or "B", bin_dir, pytest_opts),
411+
lambda bin_dir, art_dir, is_a: collect_data(is_a and "A" or "B", bin_dir, art_dir, pytest_opts),
402412
lambda ah, be: analyze_data(
403413
ah,
404414
be,
@@ -407,8 +417,10 @@ def ab_performance_test(
407417
noise_threshold,
408418
n_resamples=int(100 / p_thresh),
409419
),
410-
a_directory=a_revision,
411-
b_directory=b_revision,
420+
a_directory=a_directory,
421+
b_directory=b_directory,
422+
a_artifacts=a_artifacts,
423+
b_artifacts=b_artifacts,
412424
)
413425

414426

@@ -433,6 +445,18 @@ def ab_performance_test(
433445
type=Path,
434446
required=True,
435447
)
448+
run_parser.add_argument(
449+
"--artifacts-a",
450+
help="Name of the artifacts directory in the build/artifacts to use for revision A test. If the directory does not exist, the name will be treated as S3 path and artifacts will be downloaded from there.",
451+
type=Path,
452+
required=False,
453+
)
454+
run_parser.add_argument(
455+
"--artifacts-b",
456+
help="Name of the artifacts directory in the build/artifacts to use for revision B test. If the directory does not exist, the name will be treated as S3 path and artifacts will be downloaded from there.",
457+
type=Path,
458+
required=False,
459+
)
436460
run_parser.add_argument(
437461
"--pytest-opts",
438462
help="Parameters to pass through to pytest, for example for test selection",
@@ -476,6 +500,8 @@ def ab_performance_test(
476500
ab_performance_test(
477501
args.binaries_a,
478502
args.binaries_b,
503+
args.artifacts_a,
504+
args.artifacts_b,
479505
args.pytest_opts,
480506
args.significance,
481507
args.absolute_strength,

tools/devtool

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,15 @@ cmd_download_ci_artifacts() {
606606
done
607607
}
608608

609+
cmd_set_current_artifacts() {
610+
local artifacts=$1
611+
if [ -z $artifacts ]; then
612+
say "No artifacts were specified"
613+
fi
614+
local artifacts_local_path=$(get_local_artifacts_path $artifacts)/$(uname -m)
615+
echo $artifacts_local_path > $LOCAL_ARTIFACTS_CURRENT_DIR_FILE
616+
}
617+
609618
ensure_ci_artifacts() {
610619
local artifacts=$1
611620

@@ -767,6 +776,10 @@ cmd_test() {
767776
"--no-ci-artifacts-check")
768777
do_ci_artifacts_check=0
769778
;;
779+
"--use-artifacts")
780+
shift
781+
local artifacts="$1"
782+
;;
770783
"--") { shift; break; } ;;
771784
*)
772785
die "Unknown argument: $1. Please use --help for help."
@@ -779,7 +792,7 @@ cmd_test() {
779792
[ $do_kvm_check != 0 ] && ensure_kvm
780793
ensure_devctr
781794
ensure_build_dir
782-
[ $do_ci_artifacts_check != 0 ] && ensure_ci_artifacts
795+
[ $do_ci_artifacts_check != 0 ] && ensure_ci_artifacts $artifacts
783796
if [ $do_build != 0 ]; then
784797
cmd_build --release
785798
if [ -n "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" ]; then

0 commit comments

Comments
 (0)