Skip to content

Commit eb2500d

Browse files
authored
[container_benchmark] Pre pull container images and rework reports (#854)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Pull and cache configured container images as tarballs and optionally clean them up. * New shared benchmark tasks for image pull/load, artifacts directory creation, benchmark run, and holistic container resource cleanup. * **Improvements** * Reports and plots now emphasize 95th‑percentile execution time and show jitter; plotting visuals and grouping improved. * Remote helpers made directory/file management more reliable across platforms. * **Refactor** * Consolidated cross‑platform command/remote execution flow and moved per-benchmark steps into shared task templates. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 538d3f6 + b3d1b97 commit eb2500d

File tree

38 files changed

+928
-797
lines changed

38 files changed

+928
-797
lines changed

projects/container_bench/testing/config.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ prepare:
100100
- glibc-static
101101
- golang
102102

103+
container_images:
104+
pull_images: true
105+
dir: "{@remote_host.base_work_dir}/images"
106+
images:
107+
- "quay.io/podman/hello:latest"
108+
- "registry.fedoraproject.org/fedora:latest"
109+
103110
podman:
104111
# Custom binary or repo version must be disabled for matbenchmarking
105112
custom_binary:
@@ -149,6 +156,7 @@ cleanup:
149156
podman: true
150157
exec_time: true
151158
venv: true
159+
container_images: false
152160

153161
podman_machine:
154162
delete: true
@@ -201,7 +209,6 @@ test:
201209
# - custom
202210
runtime: # Linux only
203211
- crun
204-
- krun
205212
- runc
206213
capture_metrics:
207214
enabled: true

projects/container_bench/testing/config_manager.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,17 @@ class ConfigKeys:
5757
PREPARE_DNF_INSTALL_DEPENDENCIES = "prepare.dnf.install_dependencies"
5858
PREPARE_DNF_ENABLE_DOCKER_REPO = "prepare.dnf.enable_docker_repo"
5959
PREPARE_DNF_DEPENDENCIES = "prepare.dnf.dependencies"
60+
PREPARE_CONTAINER_IMAGES_PULL_IMAGES = "prepare.container_images.pull_images"
61+
PREPARE_CONTAINER_IMAGES_IMAGES = "prepare.container_images.images"
62+
PREPARE_CONTAINER_IMAGES_IMAGES_DIR = "prepare.container_images.dir"
6063

6164
# Additional cleanup configuration
6265
CLEANUP_FILES_EXEC_TIME = "cleanup.files.exec_time"
6366
CLEANUP_FILES_VENV = "cleanup.files.venv"
6467
CLEANUP_PODMAN_MACHINE_DELETE = "cleanup.podman_machine.delete"
6568
CLEANUP_DOCKER_SERVICE_STOP = "cleanup.docker_service.stop"
6669
CLEANUP_DOCKER_DESKTOP_STOP = "cleanup.docker_desktop.stop"
70+
CLEANUP_CONTAINER_IMAGES = "cleanup.files.container_images"
6771

6872
# Additional remote host configuration
6973
REMOTE_HOST_DOCKER_ENABLED = "remote_host.docker.enabled"
@@ -193,6 +197,17 @@ def get_dnf_config():
193197
'dependencies': config.project.get_config(ConfigKeys.PREPARE_DNF_DEPENDENCIES, print=False),
194198
}
195199

200+
@staticmethod
201+
def get_container_images_config():
202+
return {
203+
'pull_images': config.project.get_config(
204+
ConfigKeys.PREPARE_CONTAINER_IMAGES_PULL_IMAGES, print=False),
205+
'images': config.project.get_config(
206+
ConfigKeys.PREPARE_CONTAINER_IMAGES_IMAGES, print=False),
207+
'dir': config.project.get_config(
208+
ConfigKeys.PREPARE_CONTAINER_IMAGES_IMAGES_DIR, print=False),
209+
}
210+
196211
@staticmethod
197212
def get_extended_cleanup_config():
198213
return {
@@ -205,6 +220,8 @@ def get_extended_cleanup_config():
205220
ConfigKeys.CLEANUP_DOCKER_SERVICE_STOP, print=False),
206221
'docker_desktop_stop': config.project.get_config(
207222
ConfigKeys.CLEANUP_DOCKER_DESKTOP_STOP, print=False),
223+
'container_images': config.project.get_config(
224+
ConfigKeys.CLEANUP_CONTAINER_IMAGES, print=False),
208225
}
209226

210227
@staticmethod

projects/container_bench/testing/container_engine.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from pathlib import Path
12
import remote_access
23
import json
34
import logging
@@ -75,6 +76,32 @@ def get_command(self):
7576

7677
return cmd
7778

79+
def store_container_images_as_tar(self):
80+
container_images_config = ConfigManager.get_container_images_config()
81+
pull_images = container_images_config['pull_images']
82+
images = container_images_config['images']
83+
images_dir = container_images_config['dir']
84+
dest = Path(images_dir)
85+
86+
if not pull_images:
87+
logging.info("Skipping pulling container images as per configuration.")
88+
return
89+
90+
if not remote_access.exists(dest):
91+
logging.info(f"Creating images directory at {dest} ...")
92+
remote_access.create_remote_directory(dest)
93+
94+
for image in images:
95+
logging.info(f"Pulling container image: {image} ...")
96+
image_filename = image.replace("/", "_").replace(":", "_").replace(".", "_") + ".tar"
97+
if remote_access.exists(dest / image_filename):
98+
continue
99+
cmd = f"{self.get_command()} pull {image}"
100+
remote_access.run_with_ansible_ssh_conf(self.base_work_dir, cmd)
101+
cmd = f"{self.get_command()} save -o {dest / image_filename} {image}"
102+
remote_access.run_with_ansible_ssh_conf(self.base_work_dir, cmd)
103+
self.rm_images(images)
104+
78105
def is_rootful(self):
79106
if ConfigManager.is_linux():
80107
return self.podman_config['linux_rootful']
@@ -100,10 +127,10 @@ def cleanup(self):
100127

101128
return ret.returncode == 0
102129

103-
def rm_image(self, image):
130+
def rm_images(self, images):
104131
ret = remote_access.run_with_ansible_ssh_conf(
105132
self.base_work_dir,
106-
f"{self.get_command()} image rm {image}",
133+
f"{self.get_command()} image rm {' '.join(images)}",
107134
check=False,
108135
capture_stdout=True,
109136
capture_stderr=True,

projects/container_bench/testing/platform_builders.py

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from abc import ABC, abstractmethod
22
from config_manager import ConfigManager
3+
import shlex
34

45

56
class PlatformCommandBuilder(ABC):
@@ -11,6 +12,38 @@ def build_env_command(self, env_dict):
1112
def build_service_start_script(self, service_name, start_command, binary_path):
1213
pass
1314

15+
@abstractmethod
16+
def build_chdir_command(self, chdir):
17+
pass
18+
19+
@abstractmethod
20+
def build_rm_command(self, file_path, recursive=False):
21+
pass
22+
23+
@abstractmethod
24+
def build_mkdir_command(self, path):
25+
pass
26+
27+
@abstractmethod
28+
def build_exists_command(self, path):
29+
pass
30+
31+
@abstractmethod
32+
def get_shell_command(self):
33+
pass
34+
35+
@abstractmethod
36+
def build_entrypoint_script(self, env_cmd, chdir_cmd, cmd, verbose):
37+
pass
38+
39+
@abstractmethod
40+
def check_exists_result(self, ret):
41+
pass
42+
43+
44+
def escape_powershell_single_quote(value):
45+
return str(value).replace("'", "''")
46+
1447

1548
class WindowsCommandBuilder(PlatformCommandBuilder):
1649
def build_env_command(self, env_dict):
@@ -21,8 +54,7 @@ def build_env_command(self, env_dict):
2154
for k, v in env_dict.items():
2255
if v is None or v == "":
2356
continue
24-
env_commands.append(f"$env:{k}='{v}'")
25-
57+
env_commands.append(f"$env:{escape_powershell_single_quote(k)}='{escape_powershell_single_quote(v)}'")
2658
return "; ".join(env_commands) + ";"
2759

2860
def build_service_start_script(self, service_name, start_command, binary_path):
@@ -66,18 +98,92 @@ def build_service_start_script(self, service_name, start_command, binary_path):
6698
Remove-Item "$env:USERPROFILE\\start_{service_name}.ps1" -Force -ErrorAction SilentlyContinue
6799
"""
68100

101+
def build_chdir_command(self, chdir):
102+
if chdir is None:
103+
return "Set-Location $env:USERPROFILE"
104+
return f"Set-Location '{escape_powershell_single_quote(chdir)}'"
105+
106+
def build_rm_command(self, file_path, recursive=False):
107+
flags = "-Force -ErrorAction SilentlyContinue"
108+
if recursive:
109+
flags += " -Recurse"
110+
return f"Remove-Item '{escape_powershell_single_quote(str(file_path))}' {flags}"
111+
112+
def build_mkdir_command(self, path):
113+
return f"New-Item -ItemType Directory -Path '{escape_powershell_single_quote(str(path))}' -Force"
114+
115+
def build_exists_command(self, path):
116+
return f"Test-Path '{escape_powershell_single_quote(str(path))}'"
117+
118+
def get_shell_command(self):
119+
return "powershell.exe -Command -"
120+
121+
def build_entrypoint_script(self, env_cmd, chdir_cmd, cmd, verbose):
122+
env_section = f"{env_cmd}\n" if env_cmd else ""
123+
script = f"""
124+
$ErrorActionPreference = "Stop"
125+
126+
{env_section}{chdir_cmd}
127+
128+
{cmd}
129+
"""
130+
if verbose:
131+
script = f"$VerbosePreference = 'Continue'\n{script}"
132+
return script
133+
134+
def check_exists_result(self, ret):
135+
return ret.stdout and ret.stdout.strip().lower() == "true"
136+
69137

70138
class UnixCommandBuilder(PlatformCommandBuilder):
71139
def build_env_command(self, env_dict):
72140
if not env_dict:
73141
return ""
74142

75-
env_values = " ".join(f"'{k}={v}'" for k, v in env_dict.items() if v is not None and v != "")
76-
return f"env {env_values}"
143+
env_values = " ".join(f"{k}={shlex.quote(str(v))}" for k, v in env_dict.items() if v is not None and v != "")
144+
return f"export {env_values}\n"
77145

78146
def build_service_start_script(self, service_name, start_command, binary_path) -> str:
79147
return start_command
80148

149+
def build_chdir_command(self, chdir):
150+
if chdir is None:
151+
return "cd $HOME"
152+
return f"cd '{shlex.quote(str(chdir))}'"
153+
154+
def build_rm_command(self, file_path, recursive=False):
155+
flag = "-rf" if recursive else "-f"
156+
return f"rm {flag} {shlex.quote(str(file_path))}"
157+
158+
def build_mkdir_command(self, path):
159+
return f"mkdir -p {shlex.quote(str(path))}"
160+
161+
def build_exists_command(self, path):
162+
return f"test -e {shlex.quote(str(path))}"
163+
164+
def get_shell_command(self):
165+
return "bash"
166+
167+
def build_entrypoint_script(self, env_cmd, chdir_cmd, cmd, verbose):
168+
script = f"""
169+
set -o pipefail
170+
set -o errexit
171+
set -o nounset
172+
set -o errtrace
173+
174+
{env_cmd}
175+
176+
{chdir_cmd}
177+
178+
{cmd}
179+
"""
180+
if verbose:
181+
script = f"set -x\n{script}"
182+
return script
183+
184+
def check_exists_result(self, ret):
185+
return ret.returncode == 0
186+
81187

82188
class PlatformFactory:
83189
@staticmethod

projects/container_bench/testing/prepare.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
import shlex
32

43
from container_engine import PodmanMachine, ContainerEngine, DockerDesktopMachine
54
from config_manager import ConfigManager
@@ -8,19 +7,6 @@
87
import utils
98

109

11-
def remove_remote_file(base_work_dir, file_path, recursive=False):
12-
if ConfigManager.is_windows():
13-
flags = "-Force -ErrorAction SilentlyContinue"
14-
if recursive:
15-
flags += " -Recurse"
16-
cmd = f"Remove-Item '{file_path}' {flags}"
17-
else:
18-
flag = "-rf" if recursive else "-f"
19-
cmd = f"rm {flag} {shlex.quote(str(file_path))}"
20-
21-
remote_access.run_with_ansible_ssh_conf(base_work_dir, cmd)
22-
23-
2410
def cleanup():
2511
cleanup_config = ConfigManager.get_extended_cleanup_config()
2612

@@ -30,15 +16,15 @@ def cleanup():
3016
exec_time_script = utils.get_benchmark_script_path(base_work_dir)
3117
if remote_access.exists(exec_time_script):
3218
logging.info(f"Removing {exec_time_script} ...")
33-
remove_remote_file(base_work_dir, exec_time_script)
19+
remote_access.remove_remote_file(base_work_dir, exec_time_script)
3420

3521
if cleanup_config['files_venv']:
3622
logging.info("Cleaning up virtual environment")
3723
base_work_dir = remote_access.prepare()
3824
venv_path = utils.get_benchmark_script_path(base_work_dir).parent / ".venv"
3925
if remote_access.exists(venv_path):
4026
logging.info(f"Removing {venv_path} ...")
41-
remove_remote_file(base_work_dir, venv_path, recursive=True)
27+
remote_access.remove_remote_file(base_work_dir, venv_path, recursive=True)
4228

4329
try:
4430
cleanup_podman_platform()
@@ -49,6 +35,10 @@ def cleanup():
4935
logging.info("Cleaning up Podman files")
5036
utils.cleanup_podman_files(remote_access.prepare())
5137

38+
if cleanup_config['container_images']:
39+
logging.info("Cleaning up container images")
40+
utils.cleanup_container_images(remote_access.prepare())
41+
5242
cleanup_docker_platform()
5343
return 0
5444

@@ -119,6 +109,7 @@ def prepare_docker_platform():
119109
docker_desktop.start()
120110

121111
docker = ContainerEngine("docker")
112+
docker.store_container_images_as_tar()
122113
docker.cleanup()
123114

124115

@@ -162,6 +153,7 @@ def prepare_podman_platform():
162153

163154
logging.info("cleaning up podman")
164155
podman = ContainerEngine("podman")
156+
podman.store_container_images_as_tar()
165157
podman.cleanup()
166158

167159
return 0

0 commit comments

Comments
 (0)