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.
- springdocker/__init__.py +9 -0
- springdocker/analyze.py +284 -0
- springdocker/benchmarks/__init__.py +2 -0
- springdocker/benchmarks/generate.py +104 -0
- springdocker/benchmarks/runner.py +343 -0
- springdocker/cli.py +289 -0
- springdocker/commands.py +388 -0
- springdocker/compare.py +138 -0
- springdocker/config.py +365 -0
- springdocker/dockerfile.py +332 -0
- springdocker/errors.py +16 -0
- springdocker/plugins.py +75 -0
- springdocker/project_detect.py +256 -0
- springdocker/regression.py +151 -0
- springdocker/services/__init__.py +2 -0
- springdocker/services/benchmark_service.py +124 -0
- springdocker/services/dockerfile_service.py +76 -0
- springdocker/services/project_service.py +37 -0
- springdocker-1.0.1.dist-info/METADATA +189 -0
- springdocker-1.0.1.dist-info/RECORD +23 -0
- springdocker-1.0.1.dist-info/WHEEL +5 -0
- springdocker-1.0.1.dist-info/entry_points.txt +2 -0
- springdocker-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -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())
|