33"""Performance benchmark for block device emulation."""
44
55import concurrent
6+ import glob
67import os
78import shutil
89from 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,6 +69,7 @@ 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" )
7274 .build ()
7375 )
@@ -101,40 +103,55 @@ def run_fio(microvm, mode, block_size):
101103 return logs_path , cpu_load_future .result ()
102104
103105
104- def process_fio_logs (vm , fio_mode , logs_dir , metrics ):
105- """Parses the fio logs in `{logs_dir}/{fio_mode}_bw.*.log and emits their contents as CloudWatch metrics"""
106-
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"""
107108 data = [
108- Path (f"{ logs_dir } /{ fio_mode } _bw.{ job_id + 1 } .log" )
109- .read_text ("UTF-8" )
110- .splitlines ()
111- for job_id in range (vm .vcpus_count )
109+ Path (pathname ).read_text ("UTF-8" ).splitlines ()
110+ for pathname in glob .glob (logs_glob )
112111 ]
113112
113+ assert data , "no log files found!"
114+
114115 for tup in zip (* data ):
115- bw_read = 0
116- bw_write = 0
116+ read_values = []
117+ write_values = []
117118
118119 for line in tup :
120+ # See https://fio.readthedocs.io/en/latest/fio_doc.html#log-file-formats
119121 _ , value , direction , _ = line .split ("," , maxsplit = 3 )
120122 value = int (value .strip ())
121123
122- # See https://fio.readthedocs.io/en/latest/fio_doc.html#log-file-formats
123124 match direction .strip ():
124125 case "0" :
125- bw_read += value
126+ read_values . append ( value )
126127 case "1" :
127- bw_write += value
128+ write_values . append ( value )
128129 case _:
129130 assert False
130131
132+ yield read_values , write_values
133+
134+
135+ def emit_fio_throughput_metrics (logs_dir , metrics ):
136+ """Parses the fio logs in `{logs_dir}/*_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" ):
131138 if bw_read :
132- metrics .put_metric ("bw_read" , bw_read , "Kilobytes/Second" )
139+ metrics .put_metric ("bw_read" , sum ( bw_read ) , "Kilobytes/Second" )
133140 if bw_write :
134- metrics .put_metric ("bw_write" , bw_write , "Kilobytes/Second" )
141+ metrics .put_metric ("bw_write" , sum (bw_write ), "Kilobytes/Second" )
142+
143+
144+ def emit_fio_latency_metrics (logs_dir , metrics ):
145+ """Parses the fio logs in `{logs_dir}/*_clat.*.log and emits their contents as CloudWatch metrics"""
146+ for lat_read , lat_write in process_fio_log_files (f"{ logs_dir } /*_clat.*.log" ):
147+ # latency values in fio logs are in nanosecons, but cloudwatch only supports
148+ # microseconds as the more granular unit, so need to divide by 1000.
149+ for value in lat_read :
150+ metrics .put_metric ("clat_read" , value / 1000 , "Microseconds" )
151+ for value in lat_write :
152+ metrics .put_metric ("clat_write" , value / 1000 , "Microseconds" )
135153
136154
137- @pytest .mark .timeout (120 )
138155@pytest .mark .nonci
139156@pytest .mark .parametrize ("vcpus" , [1 , 2 ], ids = ["1vcpu" , "2vcpu" ])
140157@pytest .mark .parametrize ("fio_mode" , ["randread" , "randwrite" ])
@@ -175,15 +192,61 @@ def test_block_performance(
175192
176193 vm .pin_threads (0 )
177194
178- logs_dir , cpu_util = run_fio (vm , fio_mode , fio_block_size )
195+ # latency metrics with async engine are very volatile, so use fio's default sync engine.
196+ logs_dir , cpu_util = run_fio (vm , fio_mode , fio_block_size , fio_engine = "psync" )
179197
180- process_fio_logs ( vm , fio_mode , logs_dir , metrics )
198+ emit_fio_throughput_metrics ( logs_dir , metrics )
181199
182200 for thread_name , values in cpu_util .items ():
183201 for value in values :
184202 metrics .put_metric (f"cpu_utilization_{ thread_name } " , value , "Percent" )
185203
186204
205+ @pytest .mark .nonci
206+ @pytest .mark .parametrize ("vcpus" , [1 , 2 ], ids = ["1vcpu" , "2vcpu" ])
207+ @pytest .mark .parametrize ("fio_mode" , ["randrw" ])
208+ @pytest .mark .parametrize ("fio_block_size" , [4096 ], ids = ["bs4096" ])
209+ def test_block_latency (
210+ microvm_factory ,
211+ guest_kernel_acpi ,
212+ rootfs ,
213+ vcpus ,
214+ fio_mode ,
215+ fio_block_size ,
216+ io_engine ,
217+ metrics ,
218+ ):
219+ """
220+ Execute block device emulation benchmarking scenarios.
221+ """
222+ vm = microvm_factory .build (guest_kernel_acpi , rootfs , monitor_memory = False )
223+ vm .spawn (log_level = "Info" , emit_metrics = True )
224+ vm .basic_config (vcpu_count = vcpus , mem_size_mib = GUEST_MEM_MIB )
225+ vm .add_net_iface ()
226+ # Add a secondary block device for benchmark tests.
227+ fs = drive_tools .FilesystemFile (
228+ os .path .join (vm .fsfiles , "scratch" ), BLOCK_DEVICE_SIZE_MB
229+ )
230+ vm .add_drive ("scratch" , fs .path , io_engine = io_engine )
231+ vm .start ()
232+
233+ metrics .set_dimensions (
234+ {
235+ "performance_test" : "test_block_latency" ,
236+ "io_engine" : io_engine ,
237+ "fio_mode" : fio_mode ,
238+ "fio_block_size" : str (fio_block_size ),
239+ ** vm .dimensions ,
240+ }
241+ )
242+
243+ vm .pin_threads (0 )
244+
245+ logs_dir , _ = run_fio (vm , fio_mode , fio_block_size )
246+
247+ emit_fio_latency_metrics (logs_dir , metrics )
248+
249+
187250@pytest .mark .nonci
188251@pytest .mark .parametrize ("vcpus" , [1 , 2 ], ids = ["1vcpu" , "2vcpu" ])
189252@pytest .mark .parametrize ("fio_mode" , ["randread" ])
@@ -226,7 +289,7 @@ def test_block_vhost_user_performance(
226289
227290 logs_dir , cpu_util = run_fio (vm , fio_mode , fio_block_size )
228291
229- process_fio_logs ( vm , fio_mode , logs_dir , metrics )
292+ emit_fio_throughput_metrics ( logs_dir , metrics )
230293
231294 for thread_name , values in cpu_util .items ():
232295 for value in values :
0 commit comments