Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2c96f8e

Browse files
committedAug 7, 2024
Initial (hacky) performance tests
Create initial performance tests measuring the time between hotplug API request and vCPUs being available to the guest. Note: These tests are NOT designed to be merged or used in CI, they are merely investigative tests for the latency of vCPU hotplugging Signed-off-by: James Curtis <jxcurtis@amazon.co.uk>
1 parent e6c2c93 commit 2c96f8e

File tree

9 files changed

+265
-4
lines changed

9 files changed

+265
-4
lines changed
 

‎src/vmm/src/devices/pseudo/boot_timer.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MAGIC_VALUE_SIGNAL_GUEST_BOOT_COMPLETE: u8 = 123;
1010
/// Pseudo device to record the kernel boot time.
1111
#[derive(Debug)]
1212
pub struct BootTimer {
13-
start_ts: TimestampUs,
13+
pub start_ts: TimestampUs,
1414
}
1515

1616
impl BootTimer {

‎src/vmm/src/lib.rs

+6
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,12 @@ impl Vmm {
688688
self.resume_vcpu_threads(start_idx.into())?;
689689

690690
self.acpi_device_manager.notify_cpu_container()?;
691+
if let Some(devices::BusDevice::BootTimer(timer)) =
692+
self.get_bus_device(DeviceType::BootTimer, "BootTimer")
693+
{
694+
let mut locked_timer = timer.lock().expect("Poisoned lock");
695+
locked_timer.start_ts = utils::time::TimestampUs::default();
696+
}
691697

692698
Ok(new_machine_config)
693699
}

‎tests/conftest.py

+12
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,9 @@ def rootfs_fxt(request, record_property):
361361
guest_kernel_linux_5_10 = pytest.fixture(
362362
guest_kernel_fxt, params=kernel_params("vmlinux-5.10*")
363363
)
364+
guest_kernel_linux_acpi_only = pytest.fixture(
365+
guest_kernel_fxt, params=kernel_params("vmlinux-5.10.221")
366+
)
364367
# Use the unfiltered selector, since we don't officially support 6.1 yet.
365368
# TODO: switch to default selector once we add full 6.1 support.
366369
guest_kernel_linux_6_1 = pytest.fixture(
@@ -394,6 +397,15 @@ def uvm_plain_rw(microvm_factory, guest_kernel_linux_5_10, rootfs_rw):
394397
return microvm_factory.build(guest_kernel_linux_5_10, rootfs_rw)
395398

396399

400+
@pytest.fixture
401+
def uvm_hotplug(microvm_factory, guest_kernel_linux_acpi_only, rootfs_rw):
402+
"""Create a VM with ACPI enabled kernels only.
403+
kernel: 5.10
404+
rootfs: Ubuntu 22.04
405+
"""
406+
return microvm_factory.build(guest_kernel_linux_acpi_only, rootfs_rw)
407+
408+
397409
@pytest.fixture
398410
def uvm_nano(uvm_plain):
399411
"""A preconfigured uvm with 2vCPUs and 256MiB of memory

‎tests/host_tools/1-cpu-hotplug.rules

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SUBSYSTEM=="cpu", ACTION=="add", ATTR{online}!="1", ATTR{online}="1"

‎tests/host_tools/hotplug.sh

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
# Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
while :; do
6+
[[ -d /sys/devices/system/cpu/cpu1 ]] && break
7+
done
8+
9+
readarray -t offline_cpus < <(lscpu -p=cpu --offline | sed '/^#/d')
10+
11+
for cpu_idx in ${offline_cpus[@]}; do
12+
echo 1 >/sys/devices/system/cpu/cpu$cpu_idx/online
13+
done
14+
15+
/home/hotplug_time.o

‎tests/host_tools/hotplug_time.c

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Init wrapper for boot timing. It points at /sbin/init.
5+
6+
#include <fcntl.h>
7+
#include <sys/mman.h>
8+
#include <sys/types.h>
9+
#include <unistd.h>
10+
11+
// Base address values are defined in arch/src/lib.rs as arch::MMIO_MEM_START.
12+
// Values are computed in arch/src/<arch>/mod.rs from the architecture layouts.
13+
// Position on the bus is defined by MMIO_LEN increments, where MMIO_LEN is
14+
// defined as 0x1000 in vmm/src/device_manager/mmio.rs.
15+
#ifdef __x86_64__
16+
#define MAGIC_MMIO_SIGNAL_GUEST_BOOT_COMPLETE 0xd0000000
17+
#endif
18+
#ifdef __aarch64__
19+
#define MAGIC_MMIO_SIGNAL_GUEST_BOOT_COMPLETE 0x40000000
20+
#endif
21+
22+
#define MAGIC_VALUE_SIGNAL_GUEST_BOOT_COMPLETE 123
23+
24+
int main() {
25+
int fd = open("/dev/mem", (O_RDWR | O_SYNC | O_CLOEXEC));
26+
int mapped_size = getpagesize();
27+
28+
char *map_base = mmap(NULL, mapped_size, PROT_WRITE, MAP_SHARED, fd,
29+
MAGIC_MMIO_SIGNAL_GUEST_BOOT_COMPLETE);
30+
31+
*map_base = MAGIC_VALUE_SIGNAL_GUEST_BOOT_COMPLETE;
32+
msync(map_base, mapped_size, MS_ASYNC);
33+
}

‎tests/host_tools/hotplug_time.o

879 KB
Binary file not shown.

‎tests/integration_tests/functional/test_vcpu_hotplug.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88

99
import pytest
1010

11-
from framework import microvm
1211
from framework.defs import MAX_SUPPORTED_VCPUS
13-
from framework.microvm import Serial
1412
from framework.utils_cpuid import check_guest_cpuid_output
1513

1614

@@ -63,7 +61,7 @@ def test_negative_hotplug_vcpus(uvm_plain, vcpu_count):
6361
with pytest.raises(
6462
RuntimeError,
6563
match=re.compile(
66-
f"An error occurred when deserializing the json body of a request: invalid value: integer `-\\d+`, expected u8+"
64+
"An error occurred when deserializing the json body of a request: invalid value: integer `-\\d+`, expected u8+"
6765
),
6866
):
6967
uvm_plain.api.hotplug.put(Vcpu={"add": vcpu_count})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Testing hotplug performance"""
5+
6+
import re
7+
import time
8+
from pathlib import Path
9+
10+
import pandas
11+
import pytest
12+
13+
from host_tools.cargo_build import gcc_compile
14+
15+
16+
@pytest.mark.parametrize(
17+
"vcpu_count", [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]
18+
)
19+
def test_custom_udev_rule_latency(
20+
microvm_factory, guest_kernel_linux_acpi_only, rootfs_rw, vcpu_count
21+
):
22+
"""Test the latency for hotplugging and booting CPUs in the guest"""
23+
api_durations = []
24+
onlining_durations = []
25+
print(f"Vcpu count: {vcpu_count}")
26+
for i in range(5):
27+
uvm_hotplug = microvm_factory.build(guest_kernel_linux_acpi_only, rootfs_rw)
28+
uvm_hotplug.jailer.extra_args.update({"no-seccomp": None})
29+
uvm_hotplug.help.enable_console()
30+
uvm_hotplug.spawn()
31+
uvm_hotplug.basic_config(vcpu_count=1, mem_size_mib=128)
32+
uvm_hotplug.add_net_iface()
33+
uvm_hotplug.start()
34+
uvm_hotplug.ssh.run("rm /usr/lib/udev/rules.d/40-vm-hotadd.rules")
35+
uvm_hotplug.ssh.scp_put(
36+
Path("./host_tools/1-cpu-hotplug.rules"),
37+
Path("/usr/lib/udev/rules.d/1-cpu-hotplug.rules"),
38+
)
39+
40+
time.sleep(0.25)
41+
42+
uvm_hotplug.api.hotplug.put(Vcpu={"add": vcpu_count})
43+
time.sleep(0.25)
44+
_, stdout, _ = uvm_hotplug.ssh.run("dmesg")
45+
46+
# Extract API call duration
47+
api_duration = (
48+
float(
49+
re.findall(
50+
r"Total previous API call duration: (\d+) us\.",
51+
uvm_hotplug.log_data,
52+
)[-1]
53+
)
54+
/ 1000
55+
)
56+
57+
# Extract onlining timings
58+
start = float(
59+
re.findall(r"\[\s+(\d+\.\d+)\] CPU1 has been hot-added\n", stdout)[0]
60+
)
61+
end = float(re.findall(r"\[\s+(\d+\.\d+)\] \w+", stdout)[-1])
62+
elapsed_time = (end - start) * 1000
63+
print(f"Api call duration: {api_duration} ms")
64+
print(f"Onlining duration: {elapsed_time} ms")
65+
api_durations.append(api_duration)
66+
onlining_durations.append(elapsed_time)
67+
uvm_hotplug.kill()
68+
time.sleep(1)
69+
70+
avg_api_duration = sum(api_durations) / 5
71+
avg_onlining_duration = sum(onlining_durations) / 5
72+
print(f"Averages for {vcpu_count} hotplugged vcpus:")
73+
print(f"\tAverage API call duration: {avg_api_duration} ms")
74+
print(f"\tAverage onliing duration: {avg_onlining_duration} ms")
75+
76+
77+
@pytest.mark.parametrize(
78+
"vcpu_count", [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]
79+
)
80+
def test_default_udev_rule_latency(
81+
microvm_factory, guest_kernel_linux_acpi_only, rootfs_rw, vcpu_count
82+
):
83+
"""Test the latency for hotplugging and booting CPUs in the guest"""
84+
api_durations = []
85+
onlining_durations = []
86+
print(f"Vcpu count: {vcpu_count}")
87+
for i in range(5):
88+
uvm_hotplug = microvm_factory.build(guest_kernel_linux_acpi_only, rootfs_rw)
89+
uvm_hotplug.jailer.extra_args.update({"no-seccomp": None})
90+
uvm_hotplug.help.enable_console()
91+
uvm_hotplug.spawn()
92+
uvm_hotplug.basic_config(vcpu_count=1, mem_size_mib=128)
93+
uvm_hotplug.add_net_iface()
94+
uvm_hotplug.start()
95+
96+
time.sleep(0.25)
97+
98+
_, stdout, _ = uvm_hotplug.ssh.run("ls /usr/lib/udev/rules.d")
99+
default_rule = re.search(r"40-vm-hotadd\.rules", stdout)
100+
assert default_rule is not None
101+
102+
uvm_hotplug.api.hotplug.put(Vcpu={"add": vcpu_count})
103+
time.sleep(0.25)
104+
_, stdout, _ = uvm_hotplug.ssh.run("dmesg")
105+
106+
# Extract API call duration
107+
api_duration = (
108+
float(
109+
re.findall(
110+
r"Total previous API call duration: (\d+) us\.",
111+
uvm_hotplug.log_data,
112+
)[-1]
113+
)
114+
/ 1000
115+
)
116+
117+
# Extract onlining timings
118+
start = float(
119+
re.findall(r"\[\s+(\d+\.\d+)\] CPU1 has been hot-added\n", stdout)[0]
120+
)
121+
end = float(re.findall(r"\[\s+(\d+\.\d+)\] \w+", stdout)[-1])
122+
elapsed_time = (end - start) * 1000
123+
print(f"Api call duration: {api_duration} ms")
124+
print(f"Onlining duration: {elapsed_time} ms")
125+
api_durations.append(api_duration)
126+
onlining_durations.append(elapsed_time)
127+
uvm_hotplug.kill()
128+
time.sleep(1)
129+
130+
avg_api_duration = sum(api_durations) / 5
131+
avg_onlining_duration = sum(onlining_durations) / 5
132+
print(f"Averages for {vcpu_count} hotplugged vcpus:")
133+
print(f"\tAverage API call duration: {avg_api_duration} ms")
134+
print(f"\tAverage onliing duration: {avg_onlining_duration} ms")
135+
136+
137+
@pytest.mark.parametrize(
138+
"vcpu_count", [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]
139+
)
140+
def test_manual_latency(
141+
microvm_factory, guest_kernel_linux_acpi_only, rootfs_rw, vcpu_count
142+
):
143+
"""Test the latency for hotplugging and booting CPUs in the guest"""
144+
gcc_compile(Path("./host_tools/hotplug_time.c"), Path("host_tools/hotplug_time.o"))
145+
data = []
146+
for _ in range(50):
147+
uvm_hotplug = microvm_factory.build(guest_kernel_linux_acpi_only, rootfs_rw)
148+
uvm_hotplug.jailer.extra_args.update({"boot-timer": None, "no-seccomp": None})
149+
uvm_hotplug.help.enable_console()
150+
uvm_hotplug.spawn()
151+
uvm_hotplug.basic_config(vcpu_count=1, mem_size_mib=128)
152+
uvm_hotplug.add_net_iface()
153+
uvm_hotplug.start()
154+
uvm_hotplug.ssh.scp_put(
155+
Path("./host_tools/hotplug.sh"), Path("/home/hotplug.sh")
156+
)
157+
uvm_hotplug.ssh.scp_put(
158+
Path("./host_tools//hotplug_time.o"), Path("/home/hotplug_time.o")
159+
)
160+
uvm_hotplug.ssh.run(
161+
"tmux new-session -d /bin/bash /home/hotplug.sh > /home/test 2>&1"
162+
)
163+
164+
uvm_hotplug.api.hotplug.put(Vcpu={"add": vcpu_count})
165+
166+
time.sleep(1.5)
167+
# Extract API call duration
168+
api_duration = (
169+
float(
170+
re.findall(
171+
r"Total previous API call duration: (\d+) us\.",
172+
uvm_hotplug.log_data,
173+
)[-1]
174+
)
175+
/ 1000
176+
)
177+
try:
178+
timestamp = (
179+
float(
180+
re.findall(
181+
r"Guest-boot-time\s+\=\s+(\d+)\s+us", uvm_hotplug.log_data
182+
)[0]
183+
)
184+
/ 1000
185+
)
186+
except IndexError:
187+
data.append({"vcpus": vcpu_count, "api": api_duration, "onlining": None})
188+
continue
189+
# Extract onlining timings
190+
data.append({"vcpus": vcpu_count, "api": api_duration, "onlining": timestamp})
191+
192+
df = pandas.DataFrame.from_dict(data).to_csv(
193+
f"../test_results/manual-hotplug_{vcpu_count}.csv",
194+
index=False,
195+
float_format="%.3f",
196+
)

0 commit comments

Comments
 (0)
Please sign in to comment.