44import subprocess
55import time
66from concurrent .futures import Future , ThreadPoolExecutor
7+ from pathlib import Path
78
89import docker
910import docker .errors
@@ -80,6 +81,79 @@ def clean_system_cache(self) -> None:
8081 self .log .error ("Failed to clean system cache" , error = e )
8182 raise e
8283
84+ def run_preflight_checks (self ) -> None :
85+ """Run preflight checks and log warnings for suboptimal system configuration."""
86+ self ._check_cpu_governor ()
87+ self ._check_transparent_huge_pages ()
88+ self ._check_noisy_timers ()
89+
90+ def _check_cpu_governor (self ) -> None :
91+ """Log a warning if any CPU is not using the 'performance' governor."""
92+ try :
93+ governor_path = Path ("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor" )
94+ if not governor_path .exists ():
95+ return
96+ governor = governor_path .read_text ().strip ()
97+ if governor != "performance" :
98+ self .log .warning (
99+ "CPU frequency governor is not set to 'performance', benchmark results may have higher variance. "
100+ "Fix: echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor" ,
101+ current_governor = governor ,
102+ )
103+ except Exception :
104+ pass
105+
106+ def _check_transparent_huge_pages (self ) -> None :
107+ """Log a warning if Transparent Huge Pages are enabled (causes latency spikes from compaction)."""
108+ try :
109+ thp_path = Path ("/sys/kernel/mm/transparent_hugepage/enabled" )
110+ if not thp_path .exists ():
111+ return
112+ content = thp_path .read_text ().strip ()
113+ # Format is like: "always [madvise] never" — bracketed value is active
114+ if "[always]" in content :
115+ self .log .warning (
116+ "Transparent Huge Pages are enabled, THP compaction can cause unpredictable latency spikes. "
117+ "Fix: echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled && "
118+ "echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/defrag" ,
119+ current = "always" ,
120+ )
121+ except Exception :
122+ pass
123+
124+ NOISY_TIMERS = [
125+ "sysstat-collect.timer" ,
126+ "apt-daily.timer" ,
127+ "apt-daily-upgrade.timer" ,
128+ "fwupd-refresh.timer" ,
129+ "fstrim.timer" ,
130+ ]
131+
132+ def _check_noisy_timers (self ) -> None :
133+ """Log a warning if systemd timers known to cause I/O or CPU spikes are active."""
134+ try :
135+ result = subprocess .run (
136+ ["systemctl" , "list-timers" , "--no-pager" , "--no-legend" ],
137+ capture_output = True ,
138+ text = True ,
139+ timeout = 5 ,
140+ )
141+ if result .returncode != 0 :
142+ return
143+ active_noisy = [
144+ timer
145+ for timer in self .NOISY_TIMERS
146+ if timer in result .stdout
147+ ]
148+ if active_noisy :
149+ self .log .warning (
150+ "Active systemd timers may cause benchmark variance. "
151+ f"Fix: systemctl stop { ' ' .join (active_noisy )} " ,
152+ active_timers = active_noisy ,
153+ )
154+ except Exception :
155+ pass
156+
83157 def pull_docker_images (self ) -> None :
84158 self .log .info ("updating docker images" )
85159 self .config .docker_client .images .pull (self .config .execution_client_image )
@@ -135,7 +209,7 @@ def start_execution_client(
135209 # Run execution container
136210 cpu_count = self .config .resources .cpu if self .config .resources else None
137211 mem_limit = self .config .resources .mem if self .config .resources else None
138- container = self . config . docker_client . containers . run (
212+ run_kwargs = dict (
139213 image = self .config .execution_client_image ,
140214 name = self .config .get_execution_client_container_name (),
141215 volumes = execution_container_volumes ,
@@ -152,6 +226,11 @@ def start_execution_client(
152226 group_add = self .config .docker_group_add ,
153227 stop_signal = stop_signal ,
154228 )
229+ if self .config .resources and self .config .resources .cpuset is not None :
230+ run_kwargs ["cpuset_cpus" ] = self .config .resources .cpuset
231+ if self .config .resources and self .config .resources .mem_swappiness is not None :
232+ run_kwargs ["mem_swappiness" ] = self .config .resources .mem_swappiness
233+ container = self .config .docker_client .containers .run (** run_kwargs )
155234 return container
156235
157236 def wait_for_client_json_rpc (
@@ -244,7 +323,7 @@ def start_alloy(
244323 self ,
245324 container_network : Network | None = None ,
246325 ) -> Container :
247- alloy_container = self . config . docker_client . containers . run (
326+ run_kwargs = dict (
248327 image = self .config .get_alloy_container_image (),
249328 name = self .config .get_alloy_container_name (),
250329 volumes = self .config .get_alloy_volumes (),
@@ -254,6 +333,9 @@ def start_alloy(
254333 restart_policy = {"Name" : "unless-stopped" },
255334 network = container_network .name if container_network else None ,
256335 )
336+ if self .config .resources and self .config .resources .infra_cpuset is not None :
337+ run_kwargs ["cpuset_cpus" ] = self .config .resources .infra_cpuset
338+ alloy_container = self .config .docker_client .containers .run (** run_kwargs )
257339 return alloy_container
258340
259341 # Payload Server Setup
@@ -269,7 +351,7 @@ def start_payload_server(
269351 self ,
270352 container_network : Network | None = None ,
271353 ) -> Container :
272- container = self . config . docker_client . containers . run (
354+ run_kwargs = dict (
273355 image = self .config .get_payload_server_container_image (),
274356 name = self .config .get_payload_server_container_name (),
275357 volumes = self .config .get_payload_server_volumes (),
@@ -279,6 +361,9 @@ def start_payload_server(
279361 restart_policy = {"Name" : "unless-stopped" },
280362 network = container_network .name if container_network else None ,
281363 )
364+ if self .config .resources and self .config .resources .infra_cpuset is not None :
365+ run_kwargs ["cpuset_cpus" ] = self .config .resources .infra_cpuset
366+ container = self .config .docker_client .containers .run (** run_kwargs )
282367 return container
283368
284369 def wait_for_payload_server (
@@ -352,7 +437,7 @@ def run_k6(
352437 k6_container_environment = self .config .get_k6_environment ()
353438
354439 # Execute k6 container
355- container = self . config . docker_client . containers . run (
440+ run_kwargs = dict (
356441 image = self .config .get_k6_container_image (),
357442 name = self .config .get_k6_container_name (),
358443 volumes = k6_container_volumes ,
@@ -365,6 +450,9 @@ def run_k6(
365450 group_add = self .config .docker_group_add ,
366451 stop_signal = "SIGINT" ,
367452 )
453+ if self .config .resources and self .config .resources .infra_cpuset is not None :
454+ run_kwargs ["cpuset_cpus" ] = self .config .resources .infra_cpuset
455+ container = self .config .docker_client .containers .run (** run_kwargs )
368456 return container
369457
370458 # Extra Commands Execution
@@ -636,6 +724,7 @@ def execute_scenario(
636724 scenario = self .config .executor_name ,
637725 execution_client = self .config .get_execution_client_name (),
638726 )
727+ self .run_preflight_checks ()
639728 self .clean_system_cache ()
640729 self .prepare_directories ()
641730 self .prepare_jwt_secret_file ()
@@ -681,9 +770,15 @@ def execute_scenario(
681770 docker_container_cpus = self .config .resources .cpu
682771 if self .config .resources
683772 else None ,
773+ docker_container_cpuset = self .config .resources .cpuset
774+ if self .config .resources
775+ else None ,
684776 docker_container_mem_limit = self .config .resources .mem
685777 if self .config .resources
686778 else None ,
779+ docker_container_mem_swappiness = self .config .resources .mem_swappiness
780+ if self .config .resources
781+ else None ,
687782 )
688783 stop_signal = (
689784 # If there are extra commands to execute, use SIGINT to stop the execution client
0 commit comments