|
| 1 | +# Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | +"""Performance benchmark for pmem device""" |
| 4 | + |
| 5 | +import concurrent |
| 6 | +import os |
| 7 | +from pathlib import Path |
| 8 | + |
| 9 | +import pytest |
| 10 | + |
| 11 | +import framework.utils_fio as fio |
| 12 | +import host_tools.drive as drive_tools |
| 13 | +from framework.utils import track_cpu_utilization |
| 14 | + |
| 15 | +PMEM_DEVICE_SIZE_MB = 2048 |
| 16 | +PMEM_DEVICE_SIZE_SINGLE_READ_MB = 512 |
| 17 | +WARMUP_SEC = 10 |
| 18 | +RUNTIME_SEC = 30 |
| 19 | +GUEST_MEM_MIB = 1024 |
| 20 | + |
| 21 | + |
| 22 | +def run_fio( |
| 23 | + microvm, test_output_dir, mode: fio.Mode, block_size: int, fio_engine: fio.Engine |
| 24 | +): |
| 25 | + """Run a normal fio test""" |
| 26 | + cmd = fio.build_cmd( |
| 27 | + "/dev/pmem0", |
| 28 | + PMEM_DEVICE_SIZE_MB, |
| 29 | + block_size, |
| 30 | + mode, |
| 31 | + microvm.vcpus_count, |
| 32 | + fio_engine, |
| 33 | + RUNTIME_SEC, |
| 34 | + WARMUP_SEC, |
| 35 | + ) |
| 36 | + |
| 37 | + with concurrent.futures.ThreadPoolExecutor() as executor: |
| 38 | + cpu_load_future = executor.submit( |
| 39 | + track_cpu_utilization, |
| 40 | + microvm.firecracker_pid, |
| 41 | + RUNTIME_SEC, |
| 42 | + omit=WARMUP_SEC, |
| 43 | + ) |
| 44 | + |
| 45 | + rc, _, stderr = microvm.ssh.run(f"cd /tmp; {cmd}") |
| 46 | + assert rc == 0, stderr |
| 47 | + assert stderr == "" |
| 48 | + |
| 49 | + microvm.ssh.scp_get("/tmp/fio.json", test_output_dir) |
| 50 | + microvm.ssh.scp_get("/tmp/*.log", test_output_dir) |
| 51 | + |
| 52 | + return cpu_load_future.result() |
| 53 | + |
| 54 | + |
| 55 | +def emit_fio_metrics(logs_dir, metrics): |
| 56 | + """Parses the fio logs and emits bandwidth as metrics""" |
| 57 | + bw_reads, bw_writes = fio.process_log_files(logs_dir, fio.LogType.BW) |
| 58 | + for tup in zip(*bw_reads): |
| 59 | + metrics.put_metric("bw_read", sum(tup), "Kilobytes/Second") |
| 60 | + for tup in zip(*bw_writes): |
| 61 | + metrics.put_metric("bw_write", sum(tup), "Kilobytes/Second") |
| 62 | + |
| 63 | + clat_reads, clat_writes = fio.process_log_files(logs_dir, fio.LogType.CLAT) |
| 64 | + # latency values in fio logs are in nanoseconds, but cloudwatch only supports |
| 65 | + # microseconds as the more granular unit, so need to divide by 1000. |
| 66 | + for tup in zip(*clat_reads): |
| 67 | + for value in tup: |
| 68 | + metrics.put_metric("clat_read", value / 1000, "Microseconds") |
| 69 | + for tup in zip(*clat_writes): |
| 70 | + for value in tup: |
| 71 | + metrics.put_metric("clat_write", value / 1000, "Microseconds") |
| 72 | + |
| 73 | + |
| 74 | +@pytest.mark.nonci |
| 75 | +@pytest.mark.parametrize("vcpus", [1, 2], ids=["1vcpu", "2vcpu"]) |
| 76 | +@pytest.mark.parametrize("fio_mode", [fio.Mode.RANDREAD, fio.Mode.RANDWRITE]) |
| 77 | +@pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"]) |
| 78 | +@pytest.mark.parametrize("fio_engine", [fio.Engine.LIBAIO, fio.Engine.PSYNC]) |
| 79 | +def test_pmem_performance( |
| 80 | + uvm_plain_acpi, |
| 81 | + vcpus, |
| 82 | + fio_mode, |
| 83 | + fio_block_size, |
| 84 | + fio_engine, |
| 85 | + metrics, |
| 86 | + results_dir, |
| 87 | +): |
| 88 | + """ |
| 89 | + Measure performance of pmem device |
| 90 | + """ |
| 91 | + vm = uvm_plain_acpi |
| 92 | + vm.memory_monitor = None |
| 93 | + vm.spawn() |
| 94 | + vm.basic_config(vcpu_count=vcpus, mem_size_mib=GUEST_MEM_MIB) |
| 95 | + vm.add_net_iface() |
| 96 | + # Add a secondary block device for benchmark tests. |
| 97 | + fs = drive_tools.FilesystemFile( |
| 98 | + os.path.join(vm.fsfiles, "scratch"), PMEM_DEVICE_SIZE_MB |
| 99 | + ) |
| 100 | + vm.add_pmem("scratch", fs.path, False, False) |
| 101 | + vm.start() |
| 102 | + vm.pin_threads(0) |
| 103 | + |
| 104 | + metrics.set_dimensions( |
| 105 | + { |
| 106 | + "performance_test": "test_pmem_performance", |
| 107 | + "fio_mode": fio_mode, |
| 108 | + "fio_block_size": str(fio_block_size), |
| 109 | + "fio_engine": fio_engine, |
| 110 | + **vm.dimensions, |
| 111 | + } |
| 112 | + ) |
| 113 | + |
| 114 | + # Do a full read run before benchmarking to deal with shadow page faults. |
| 115 | + # The impact of shadow page faults is tested in another test. |
| 116 | + run_fio_single_read(vm, 0, results_dir, fio_block_size) |
| 117 | + |
| 118 | + cpu_util = run_fio(vm, results_dir, fio_mode, fio_block_size, fio_engine) |
| 119 | + emit_fio_metrics(results_dir, metrics) |
| 120 | + for thread_name, values in cpu_util.items(): |
| 121 | + for value in values: |
| 122 | + metrics.put_metric(f"cpu_utilization_{thread_name}", value, "Percent") |
| 123 | + |
| 124 | + |
| 125 | +def run_fio_single_read(microvm, run_index, test_output_dir, block_size: int): |
| 126 | + """ |
| 127 | + Run a single full read test with fio. |
| 128 | + The test is single threaded and uses only `libaio` since we just need |
| 129 | + to test a sequential |
| 130 | + """ |
| 131 | + cmd = fio.build_cmd( |
| 132 | + "/dev/pmem0", |
| 133 | + None, |
| 134 | + block_size, |
| 135 | + fio.Mode.READ, |
| 136 | + 1, |
| 137 | + fio.Engine.LIBAIO, |
| 138 | + None, |
| 139 | + None, |
| 140 | + False, |
| 141 | + ) |
| 142 | + |
| 143 | + rc, _, stderr = microvm.ssh.run(f"cd /tmp; {cmd}") |
| 144 | + assert rc == 0, stderr |
| 145 | + assert stderr == "" |
| 146 | + |
| 147 | + log_path = Path(test_output_dir) / f"fio_{run_index}.json" |
| 148 | + microvm.ssh.scp_get("/tmp/fio.json", log_path) |
| 149 | + |
| 150 | + |
| 151 | +def emit_fio_single_read_metrics(logs_dir, metrics): |
| 152 | + """Process json output of the fio command and emmit `read` metrics""" |
| 153 | + bw_reads, _ = fio.process_json_files(logs_dir) |
| 154 | + for reads in bw_reads: |
| 155 | + metrics.put_metric("bw_read", sum(reads) / 1000, "Kilobytes/Second") |
| 156 | + |
| 157 | + |
| 158 | +@pytest.mark.nonci |
| 159 | +@pytest.mark.parametrize("fio_block_size", [4096], ids=["bs4096"]) |
| 160 | +def test_pmem_first_read( |
| 161 | + microvm_factory, |
| 162 | + guest_kernel_acpi, |
| 163 | + rootfs, |
| 164 | + fio_block_size, |
| 165 | + metrics, |
| 166 | + results_dir, |
| 167 | +): |
| 168 | + """ |
| 169 | + Measure performance of a first full read from the pmem device. |
| 170 | + Values should be lower than in normal perf test since the first |
| 171 | + read of each page should also trigger a KVM internal page fault |
| 172 | + which should slow things down. |
| 173 | + """ |
| 174 | + |
| 175 | + for i in range(10): |
| 176 | + vm = microvm_factory.build( |
| 177 | + guest_kernel_acpi, rootfs, pci=True, monitor_memory=False |
| 178 | + ) |
| 179 | + vm.spawn() |
| 180 | + vm.basic_config(mem_size_mib=GUEST_MEM_MIB) |
| 181 | + vm.add_net_iface() |
| 182 | + |
| 183 | + fs = drive_tools.FilesystemFile( |
| 184 | + os.path.join(vm.fsfiles, "scratch"), |
| 185 | + PMEM_DEVICE_SIZE_SINGLE_READ_MB, |
| 186 | + ) |
| 187 | + vm.add_pmem("scratch", fs.path, False, False) |
| 188 | + |
| 189 | + vm.start() |
| 190 | + vm.pin_threads(0) |
| 191 | + |
| 192 | + metrics.set_dimensions( |
| 193 | + { |
| 194 | + "performance_test": "test_pmem_first_read", |
| 195 | + "fio_block_size": str(fio_block_size), |
| 196 | + **vm.dimensions, |
| 197 | + } |
| 198 | + ) |
| 199 | + run_fio_single_read(vm, i, results_dir, fio_block_size) |
| 200 | + |
| 201 | + emit_fio_single_read_metrics(results_dir, metrics) |
0 commit comments