springdocker 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,343 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import csv
5
+ import socket
6
+ import subprocess
7
+ import time
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from springdocker.benchmarks.generate import EXPECTED_CSV_HEADER, default_scenarios
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class RunnerOptions:
17
+ profile: str
18
+ runs_override: int | None
19
+ skip_native: bool
20
+ native_duration: str | None
21
+ native_vus: int | None
22
+ native_cpu_work: int | None
23
+ java_version: int
24
+ regenerate_scenarios: bool
25
+
26
+
27
+ def parse_runner_args(profile: str, extra_args: list[str]) -> RunnerOptions:
28
+ parser = argparse.ArgumentParser(add_help=False)
29
+ parser.add_argument("--runs", type=int, default=None)
30
+ parser.add_argument("--skip-native", action="store_true")
31
+ parser.add_argument("--native-duration", default=None)
32
+ parser.add_argument("--native-vus", type=int, default=None)
33
+ parser.add_argument("--native-cpu-work", type=int, default=None)
34
+ parser.add_argument("--java-version", type=int, default=25)
35
+ parser.add_argument(
36
+ "--regenerate-scenarios",
37
+ action=argparse.BooleanOptionalAction,
38
+ default=True,
39
+ )
40
+ parsed, unknown = parser.parse_known_args(extra_args)
41
+ if unknown:
42
+ raise ValueError(f"unknown runner arguments: {' '.join(unknown)}")
43
+ return RunnerOptions(
44
+ profile=profile,
45
+ runs_override=parsed.runs,
46
+ skip_native=parsed.skip_native,
47
+ native_duration=parsed.native_duration,
48
+ native_vus=parsed.native_vus,
49
+ native_cpu_work=parsed.native_cpu_work,
50
+ java_version=parsed.java_version,
51
+ regenerate_scenarios=parsed.regenerate_scenarios,
52
+ )
53
+
54
+
55
+ def _docker_version() -> str:
56
+ try:
57
+ out = subprocess.run(
58
+ ["docker", "--version"],
59
+ check=False,
60
+ capture_output=True,
61
+ text=True,
62
+ ).stdout.strip()
63
+ if out:
64
+ return out.replace(",", "").split()[-1]
65
+ except OSError:
66
+ pass
67
+ return "unknown"
68
+
69
+
70
+ def _wait_readiness(base_url: str, timeout_seconds: float = 40.0) -> int:
71
+ start = time.time()
72
+ while time.time() - start < timeout_seconds:
73
+ probe = subprocess.run(
74
+ ["curl", "-fsS", base_url],
75
+ check=False,
76
+ stdout=subprocess.DEVNULL,
77
+ stderr=subprocess.DEVNULL,
78
+ )
79
+ if probe.returncode == 0:
80
+ return int((time.time() - start) * 1000)
81
+ time.sleep(0.25)
82
+ return -1
83
+
84
+
85
+ def _append_row(path: Path, row: list[str]) -> None:
86
+ with path.open("a", newline="", encoding="utf-8") as handle:
87
+ writer = csv.writer(handle)
88
+ writer.writerow(row)
89
+
90
+
91
+ def _ensure_csv(path: Path) -> None:
92
+ if path.exists():
93
+ return
94
+ path.parent.mkdir(parents=True, exist_ok=True)
95
+ path.write_text(EXPECTED_CSV_HEADER, encoding="utf-8")
96
+
97
+
98
+ def _tag_for(scenario_id: str, variant_name: str) -> str:
99
+ return f"bench-{scenario_id}:{variant_name}".replace("_", "-")
100
+
101
+
102
+ def _runtime_flags(cpuset_cpus: str | None, memory_limit: str | None, normalized_runtime: bool) -> list[str]:
103
+ flags: list[str] = []
104
+ if cpuset_cpus:
105
+ flags.extend(["--cpuset-cpus", cpuset_cpus])
106
+ if memory_limit:
107
+ flags.extend(["--memory", memory_limit])
108
+ if normalized_runtime:
109
+ flags.extend(["--read-only", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--tmpfs", "/tmp"])
110
+ return flags
111
+
112
+
113
+ def _default_runs_for(profile: str, scenario_id: str) -> int:
114
+ if scenario_id == "04-jep483-aot-cache":
115
+ return 8 if profile == "quick" else 15
116
+ return 3 if profile == "quick" else 10
117
+
118
+
119
+ def _run_standard_scenario(
120
+ project_root: Path,
121
+ scenario_dir: Path,
122
+ runs: int,
123
+ run_profile: str,
124
+ cpuset_cpus: str | None,
125
+ memory_limit: str | None,
126
+ warmup_runs: int,
127
+ port_seed: int,
128
+ normalized_runtime: bool,
129
+ ) -> None:
130
+ raw_csv = scenario_dir / "results" / "raw.csv"
131
+ _ensure_csv(raw_csv)
132
+ host = socket.gethostname()
133
+ docker_version = _docker_version()
134
+ scenario = scenario_dir.name
135
+ print(f"\n=== Scenario: {scenario} (runs={runs}, profile={run_profile}) ===")
136
+
137
+ for index, variant_dir in enumerate(sorted((scenario_dir / "variants").glob("*"))):
138
+ if not variant_dir.is_dir():
139
+ continue
140
+ dockerfile = variant_dir / "Dockerfile"
141
+ if not dockerfile.exists():
142
+ continue
143
+ variant = variant_dir.name
144
+ image_tag = _tag_for(scenario, variant)
145
+ host_port = str(19081 + (port_seed * 50) + index)
146
+ runtime_flags = _runtime_flags(cpuset_cpus=cpuset_cpus, memory_limit=memory_limit, normalized_runtime=normalized_runtime)
147
+ print(f"-- variant: {variant}")
148
+
149
+ for warmup_number in range(1, warmup_runs + 1):
150
+ warmup_container = f"{image_tag.replace(':', '-')}-warmup-{warmup_number}"
151
+ subprocess.run(
152
+ [
153
+ "docker",
154
+ "run",
155
+ "-d",
156
+ "--rm",
157
+ "--name",
158
+ warmup_container,
159
+ "-p",
160
+ f"{host_port}:8081",
161
+ *runtime_flags,
162
+ image_tag,
163
+ ],
164
+ check=False,
165
+ stdout=subprocess.DEVNULL,
166
+ stderr=subprocess.DEVNULL,
167
+ )
168
+ warmup_start = _wait_readiness("http://localhost:" + host_port + "/actuator/health/readiness")
169
+ subprocess.run(
170
+ ["docker", "stop", warmup_container],
171
+ check=False,
172
+ stdout=subprocess.DEVNULL,
173
+ stderr=subprocess.DEVNULL,
174
+ )
175
+ print(f"warmup {warmup_number}: startup={warmup_start}")
176
+
177
+ for run_number in range(1, runs + 1):
178
+ build_start = time.time()
179
+ build = subprocess.run(
180
+ ["docker", "build", "-q", "-f", str(dockerfile), "-t", image_tag, "."],
181
+ cwd=project_root,
182
+ check=False,
183
+ stdout=subprocess.DEVNULL,
184
+ stderr=subprocess.DEVNULL,
185
+ )
186
+ build_ms = int((time.time() - build_start) * 1000)
187
+ if build.returncode != 0:
188
+ _append_row(
189
+ raw_csv,
190
+ [
191
+ time.strftime("%Y-%m-%d"),
192
+ scenario,
193
+ variant,
194
+ str(run_number),
195
+ "-1",
196
+ "-1",
197
+ "-1",
198
+ "build_failed",
199
+ "docker build failed",
200
+ host,
201
+ docker_version,
202
+ run_profile,
203
+ "",
204
+ "",
205
+ "",
206
+ "",
207
+ "",
208
+ ],
209
+ )
210
+ print(f"run {run_number}: build failed")
211
+ continue
212
+
213
+ image_size = subprocess.run(
214
+ ["docker", "image", "inspect", image_tag, "--format", "{{.Size}}"],
215
+ check=False,
216
+ capture_output=True,
217
+ text=True,
218
+ ).stdout.strip() or "-1"
219
+
220
+ container_name = f"{image_tag.replace(':', '-')}-{run_number}"
221
+ subprocess.run(
222
+ [
223
+ "docker",
224
+ "run",
225
+ "-d",
226
+ "--rm",
227
+ "--name",
228
+ container_name,
229
+ "-p",
230
+ f"{host_port}:8081",
231
+ *runtime_flags,
232
+ image_tag,
233
+ ],
234
+ check=False,
235
+ stdout=subprocess.DEVNULL,
236
+ stderr=subprocess.DEVNULL,
237
+ )
238
+ startup_ms = _wait_readiness("http://localhost:" + host_port + "/actuator/health/readiness")
239
+ status = "ok" if startup_ms >= 0 else "readiness_failed"
240
+ notes = "" if status == "ok" else "readiness endpoint not reachable"
241
+ subprocess.run(["docker", "stop", container_name], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
242
+
243
+ _append_row(
244
+ raw_csv,
245
+ [
246
+ time.strftime("%Y-%m-%d"),
247
+ scenario,
248
+ variant,
249
+ str(run_number),
250
+ str(build_ms),
251
+ image_size,
252
+ str(startup_ms),
253
+ status,
254
+ notes,
255
+ host,
256
+ docker_version,
257
+ run_profile,
258
+ "",
259
+ "",
260
+ "",
261
+ "",
262
+ "",
263
+ ],
264
+ )
265
+ print(
266
+ f"run {run_number}: build={build_ms}ms size={image_size} "
267
+ f"startup={startup_ms} status={status}"
268
+ )
269
+
270
+
271
+ def run_benchmarks(
272
+ project_root: Path,
273
+ build_tool: str,
274
+ profile: str,
275
+ extra_args: list[str],
276
+ cpuset_cpus: str | None = None,
277
+ memory_limit: str | None = None,
278
+ warmup_runs: int = 0,
279
+ max_workers: int = 1,
280
+ normalized_runtime: bool = False,
281
+ ) -> int:
282
+ options = parse_runner_args(profile=profile, extra_args=extra_args)
283
+ print(f"Using profile: {options.profile}")
284
+ print(f"Project root: {project_root}")
285
+ print(f"Build tool: {build_tool}")
286
+ scenarios = default_scenarios(build_tool=build_tool, java_version=options.java_version)
287
+ print(f"Scenarios loaded: {len(scenarios)}")
288
+
289
+ work_items: list[tuple[int, Path, int]] = []
290
+ for scenario_index, scenario in enumerate(scenarios):
291
+ if scenario.scenario_type == "native":
292
+ if options.skip_native:
293
+ print(f"Skipping native scenario: {scenario.id}")
294
+ continue
295
+ print(f"Skipping native scenario in internal runner: {scenario.id}")
296
+ continue
297
+ scenario_dir = project_root / "benchmarks" / scenario.id
298
+ if not (scenario_dir / "variants").exists():
299
+ print(f"Skipping missing scenario directory: {scenario.id}")
300
+ continue
301
+ runs = options.runs_override or _default_runs_for(profile=options.profile, scenario_id=scenario.id)
302
+ work_items.append((scenario_index, scenario_dir, runs))
303
+
304
+ if max_workers <= 1 or len(work_items) <= 1:
305
+ for scenario_index, scenario_dir, runs in work_items:
306
+ _run_standard_scenario(
307
+ project_root=project_root,
308
+ scenario_dir=scenario_dir,
309
+ runs=runs,
310
+ run_profile=options.profile,
311
+ cpuset_cpus=cpuset_cpus,
312
+ memory_limit=memory_limit,
313
+ warmup_runs=warmup_runs,
314
+ port_seed=scenario_index,
315
+ normalized_runtime=normalized_runtime,
316
+ )
317
+ else:
318
+ print(f"Running standard scenarios concurrently (max_workers={max_workers})")
319
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
320
+ futures = {
321
+ executor.submit(
322
+ _run_standard_scenario,
323
+ project_root=project_root,
324
+ scenario_dir=scenario_dir,
325
+ runs=runs,
326
+ run_profile=options.profile,
327
+ cpuset_cpus=cpuset_cpus,
328
+ memory_limit=memory_limit,
329
+ warmup_runs=warmup_runs,
330
+ port_seed=scenario_index,
331
+ normalized_runtime=normalized_runtime,
332
+ ): scenario_dir.name
333
+ for scenario_index, scenario_dir, runs in work_items
334
+ }
335
+ for future in as_completed(futures):
336
+ scenario_name = futures[future]
337
+ try:
338
+ future.result()
339
+ except Exception as exc: # pragma: no cover - exercised via command-level integration flow
340
+ print(f"Scenario failed: {scenario_name}: {exc}")
341
+ return 1
342
+ print("\nAll done. CSV results were updated.")
343
+ return 0
springdocker/cli.py ADDED
@@ -0,0 +1,289 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from .commands import (
7
+ cmd_benchmark_analyze,
8
+ cmd_benchmark_compare,
9
+ cmd_benchmark_generate,
10
+ cmd_benchmark_run,
11
+ cmd_dockerfile_generate,
12
+ cmd_doctor,
13
+ cmd_explain,
14
+ cmd_init,
15
+ cmd_inspect,
16
+ )
17
+ from .config import (
18
+ load_config,
19
+ resolve_benchmark_generate_config,
20
+ resolve_benchmark_run_config,
21
+ resolve_dockerfile_generate_config,
22
+ resolve_doctor_config,
23
+ )
24
+
25
+
26
+ def build_parser() -> argparse.ArgumentParser:
27
+ parser = argparse.ArgumentParser(
28
+ prog="springdocker",
29
+ description="CLI for Dockerfile and benchmark workflows in Spring Boot Maven/Gradle projects.",
30
+ )
31
+ sub = parser.add_subparsers(dest="command", required=True)
32
+
33
+ def add_common_options(p: argparse.ArgumentParser, with_build_tool: bool = True) -> None:
34
+ p.add_argument("--project-root", default=".", help="Project root path (default: current directory)")
35
+ if with_build_tool:
36
+ p.add_argument("--build-tool", choices=["maven", "gradle"], default=None,
37
+ help="Override auto-detected build tool")
38
+
39
+ init = sub.add_parser("init", help="Generate starter .springdocker.toml for this project")
40
+ add_common_options(init)
41
+ init.add_argument("--config", default=".springdocker.toml", help="Config file path to create")
42
+ init.add_argument(
43
+ "--profile",
44
+ choices=["quick", "full"],
45
+ default="quick",
46
+ help="Default benchmark run profile to write in the generated config",
47
+ )
48
+ init.add_argument("--print", action="store_true", dest="print_only", help="Print config template to stdout")
49
+ init.add_argument("--force", action="store_true", help="Overwrite existing config file")
50
+
51
+ doctor = sub.add_parser("doctor", help="Detect project and validate basic prerequisites")
52
+ add_common_options(doctor)
53
+
54
+ inspect = sub.add_parser("inspect", help="Inspect project metadata and static compatibility signals")
55
+ add_common_options(inspect)
56
+ inspect.add_argument("--format", choices=["table", "json"], default="table")
57
+
58
+ explain = sub.add_parser("explain", help="Explain a springdocker-generated Dockerfile")
59
+ add_common_options(explain)
60
+ explain.add_argument("dockerfile", nargs="?", default="Dockerfile.generated")
61
+ explain.add_argument("--format", choices=["table", "json"], default="table")
62
+
63
+ dockerfile = sub.add_parser("dockerfile", help="Dockerfile operations")
64
+ dockerfile_sub = dockerfile.add_subparsers(dest="dockerfile_command", required=True)
65
+ gen = dockerfile_sub.add_parser("generate", help="Generate Dockerfile via existing wizard")
66
+ add_common_options(gen)
67
+ gen.add_argument("--output", default=None, help="Output Dockerfile path")
68
+ gen.add_argument("--java-version", type=int, default=None, help="Java major version for generated Dockerfile")
69
+ gen.add_argument(
70
+ "--wizard-arg",
71
+ action="append",
72
+ default=None,
73
+ help="Extra argument forwarded to tools/dockerfile_wizard.py; repeat for multiple args",
74
+ )
75
+ gen.add_argument(
76
+ "--use-legacy-scripts",
77
+ action=argparse.BooleanOptionalAction,
78
+ default=None,
79
+ help="Use project scripts instead of internal implementation (or set SPRINGDOCKER_LEGACY_SCRIPTS=1)",
80
+ )
81
+
82
+ bench = sub.add_parser("benchmark", help="Benchmark operations")
83
+ bench_sub = bench.add_subparsers(dest="benchmark_command", required=True)
84
+
85
+ bench_gen = bench_sub.add_parser("generate", help="Generate benchmark variants for selected build tool")
86
+ add_common_options(bench_gen)
87
+ bench_gen.add_argument("--java-version", type=int, default=None)
88
+ bench_gen.add_argument(
89
+ "--use-legacy-scripts",
90
+ action=argparse.BooleanOptionalAction,
91
+ default=None,
92
+ help="Use project scripts instead of internal implementation (or set SPRINGDOCKER_LEGACY_SCRIPTS=1)",
93
+ )
94
+
95
+ bench_run = bench_sub.add_parser("run", help="Run benchmark orchestration")
96
+ add_common_options(bench_run)
97
+ bench_run.add_argument("--profile", choices=["quick", "full"], default=None)
98
+ bench_run.add_argument(
99
+ "--config",
100
+ default=".springdocker.toml",
101
+ help="Path to TOML config file relative to project root (default: .springdocker.toml)",
102
+ )
103
+ bench_run.add_argument(
104
+ "--runner-arg",
105
+ action="append",
106
+ default=None,
107
+ help="Extra argument forwarded to benchmarks/common/run_all_benchmarks.py; repeat for multiple args",
108
+ )
109
+ bench_run.add_argument("--cpuset-cpus", default=None, help="Pin benchmark containers to a CPU set")
110
+ bench_run.add_argument("--memory", default=None, help="Limit benchmark containers to a memory amount")
111
+ bench_run.add_argument(
112
+ "--warmup-runs",
113
+ type=int,
114
+ default=None,
115
+ help="Run extra warmup iterations before recording benchmark rows",
116
+ )
117
+ bench_run.add_argument(
118
+ "--max-workers",
119
+ type=int,
120
+ default=None,
121
+ help="Run standard benchmark scenarios concurrently with up to this many workers",
122
+ )
123
+ bench_run.add_argument(
124
+ "--normalized-runtime",
125
+ action=argparse.BooleanOptionalAction,
126
+ default=None,
127
+ help="Apply normalized container runtime flags for reproducible benchmark runs",
128
+ )
129
+ bench_run.add_argument(
130
+ "--use-legacy-scripts",
131
+ action=argparse.BooleanOptionalAction,
132
+ default=None,
133
+ help="Use project scripts instead of internal implementation (or set SPRINGDOCKER_LEGACY_SCRIPTS=1)",
134
+ )
135
+
136
+ bench_analyze = bench_sub.add_parser("analyze", help="Analyze benchmark CSV")
137
+ add_common_options(bench_analyze, with_build_tool=False)
138
+ bench_analyze.add_argument("raw_csv", help="Path to results raw.csv")
139
+ bench_analyze.add_argument("--format", choices=["table", "json"], default="table")
140
+ bench_analyze.add_argument("--scenario", default=None, help="Filter by scenario id")
141
+ bench_analyze.add_argument("--variant", default=None, help="Filter by variant name")
142
+ bench_analyze.add_argument("--output", default=None, help="Write output to file instead of stdout")
143
+ bench_analyze.add_argument(
144
+ "--fail-on-success-rate-below",
145
+ type=float,
146
+ default=None,
147
+ help="Exit non-zero when any variant success rate is below this percentage (0-100)",
148
+ )
149
+ bench_analyze.add_argument("--baseline", default=None, help="Path to a baseline JSON report")
150
+ bench_analyze.add_argument(
151
+ "--fail-on-regression-above",
152
+ type=float,
153
+ default=None,
154
+ help="Exit non-zero when any tracked metric regresses above this percentage",
155
+ )
156
+
157
+ bench_compare = bench_sub.add_parser("compare", help="Compare benchmark variants against a baseline")
158
+ add_common_options(bench_compare, with_build_tool=False)
159
+ bench_compare.add_argument("raw_csv", help="Path to results raw.csv")
160
+ bench_compare.add_argument("--baseline-variant", required=True, help="Variant name to use as the baseline")
161
+ bench_compare.add_argument("--scenario", default=None, help="Filter by scenario id")
162
+ bench_compare.add_argument("--format", choices=["table", "json"], default="table")
163
+
164
+ return parser
165
+
166
+
167
+ def main(argv: list[str] | None = None) -> int:
168
+ parser = build_parser()
169
+ args = parser.parse_args(argv)
170
+ project_root = Path(args.project_root).resolve()
171
+
172
+ if args.command == "init":
173
+ config_path = Path(args.config)
174
+ if not config_path.is_absolute():
175
+ config_path = project_root / config_path
176
+ return cmd_init(
177
+ project_root=project_root,
178
+ build_tool=args.build_tool,
179
+ config_path=config_path,
180
+ profile=args.profile,
181
+ force=args.force,
182
+ print_only=args.print_only,
183
+ )
184
+
185
+ if args.command == "doctor":
186
+ config_path = project_root / ".springdocker.toml"
187
+ loaded = load_config(config_path)
188
+ resolved_doctor = resolve_doctor_config(cli_build_tool=args.build_tool, loaded_config=loaded)
189
+ return cmd_doctor(project_root, resolved_doctor.build_tool)
190
+
191
+ if args.command == "inspect":
192
+ return cmd_inspect(project_root=project_root, build_tool=args.build_tool, output_format=args.format)
193
+
194
+ if args.command == "explain":
195
+ return cmd_explain(project_root=project_root, dockerfile_path=args.dockerfile, output_format=args.format)
196
+
197
+ if args.command == "dockerfile" and args.dockerfile_command == "generate":
198
+ loaded = load_config(project_root / ".springdocker.toml")
199
+ resolved_dockerfile = resolve_dockerfile_generate_config(
200
+ cli_build_tool=args.build_tool,
201
+ cli_output=args.output,
202
+ cli_java_version=args.java_version,
203
+ cli_wizard_args=args.wizard_arg,
204
+ cli_use_legacy_scripts=args.use_legacy_scripts,
205
+ loaded_config=loaded,
206
+ )
207
+ return cmd_dockerfile_generate(
208
+ project_root=project_root,
209
+ build_tool=resolved_dockerfile.build_tool,
210
+ output=resolved_dockerfile.output,
211
+ java_version=resolved_dockerfile.java_version,
212
+ must_have_modules_file=resolved_dockerfile.must_have_modules_file,
213
+ extra_args=resolved_dockerfile.wizard_args,
214
+ use_legacy_scripts=resolved_dockerfile.use_legacy_scripts,
215
+ )
216
+
217
+ if args.command == "benchmark" and args.benchmark_command == "generate":
218
+ loaded = load_config(project_root / ".springdocker.toml")
219
+ resolved_generate = resolve_benchmark_generate_config(
220
+ cli_build_tool=args.build_tool,
221
+ cli_java_version=args.java_version,
222
+ cli_use_legacy_scripts=args.use_legacy_scripts,
223
+ loaded_config=loaded,
224
+ )
225
+ return cmd_benchmark_generate(
226
+ project_root=project_root,
227
+ build_tool=resolved_generate.build_tool,
228
+ java_version=resolved_generate.java_version,
229
+ use_legacy_scripts=resolved_generate.use_legacy_scripts,
230
+ )
231
+
232
+ if args.command == "benchmark" and args.benchmark_command == "run":
233
+ config_path = Path(args.config)
234
+ if not config_path.is_absolute():
235
+ config_path = project_root / config_path
236
+ loaded = load_config(config_path)
237
+ resolved_run = resolve_benchmark_run_config(
238
+ cli_build_tool=args.build_tool,
239
+ cli_profile=args.profile,
240
+ cli_runner_args=args.runner_arg,
241
+ cli_cpuset_cpus=args.cpuset_cpus,
242
+ cli_memory_limit=args.memory,
243
+ cli_warmup_runs=args.warmup_runs,
244
+ cli_max_workers=args.max_workers,
245
+ cli_normalized_runtime=args.normalized_runtime,
246
+ cli_use_legacy_scripts=args.use_legacy_scripts,
247
+ loaded_config=loaded,
248
+ )
249
+ return cmd_benchmark_run(
250
+ project_root=project_root,
251
+ build_tool=resolved_run.build_tool,
252
+ profile=resolved_run.profile,
253
+ extra_args=resolved_run.runner_args,
254
+ cpuset_cpus=resolved_run.cpuset_cpus,
255
+ memory_limit=resolved_run.memory_limit,
256
+ warmup_runs=resolved_run.warmup_runs,
257
+ max_workers=resolved_run.max_workers,
258
+ normalized_runtime=resolved_run.normalized_runtime,
259
+ use_legacy_scripts=resolved_run.use_legacy_scripts,
260
+ )
261
+
262
+ if args.command == "benchmark" and args.benchmark_command == "analyze":
263
+ return cmd_benchmark_analyze(
264
+ project_root=project_root,
265
+ raw_csv=args.raw_csv,
266
+ output_format=args.format,
267
+ scenario=args.scenario,
268
+ variant=args.variant,
269
+ output_path=args.output,
270
+ fail_on_success_rate_below=args.fail_on_success_rate_below,
271
+ baseline_path=args.baseline,
272
+ fail_on_regression_above=args.fail_on_regression_above,
273
+ )
274
+
275
+ if args.command == "benchmark" and args.benchmark_command == "compare":
276
+ return cmd_benchmark_compare(
277
+ project_root=project_root,
278
+ raw_csv=args.raw_csv,
279
+ baseline_variant=args.baseline_variant,
280
+ output_format=args.format,
281
+ scenario=args.scenario,
282
+ )
283
+
284
+ parser.error("unknown command")
285
+ return 2
286
+
287
+
288
+ if __name__ == "__main__":
289
+ raise SystemExit(main())