Skip to content

Commit e3f01e6

Browse files
authored
Add benchmark scenario guidance (#3531)
1 parent b126855 commit e3f01e6

5 files changed

Lines changed: 220 additions & 0 deletions

File tree

docs/performance-benchmarks.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@ This page tracks datamodel-code-generator release and main-branch benchmark resu
66

77
datamodel-code-generator supports many schema styles and production use cases, so it includes a broad set of useful options. As releases add more capabilities, these benchmarks help keep the implementation measured, managed, and tuned so code generation stays fast in everyday use.
88

9+
## Scenario Guide
10+
11+
Each scenario combines an input type with a case size. This guide is generated from the scenario keys in the benchmark JSON and the collector case definitions, so it changes when the benchmark matrix changes.
12+
13+
| Scenario | Input fixture | Formatters | Represents |
14+
| --- | --- | --- | --- |
15+
| Small / JSON Schema | `tests/data/jsonschema/person.json` | `default` (black/isort default), `builtin` (Built-in), `ruff` (Ruff) | Compact fixture that emphasizes CLI startup, parsing, and formatter overhead for JSON Schema to Pydantic v2 model generation. |
16+
| Small / OpenAPI | `tests/data/openapi/api.yaml` | `default` (black/isort default), `builtin` (Built-in), `ruff` (Ruff) | Compact fixture that emphasizes CLI startup, parsing, and formatter overhead for OpenAPI component resolution and Pydantic v2 model generation. |
17+
| Large / JSON Schema | `tests/data/performance/large_models.json` | `default` (black/isort default), `builtin` (Built-in), `ruff` (Ruff) | Larger fixture that emphasizes parser and model graph throughput for JSON Schema to Pydantic v2 model generation. |
18+
| Large / OpenAPI | `tests/data/performance/openapi_large.yaml` | `default` (black/isort default), `builtin` (Built-in), `ruff` (Ruff) | Larger fixture that emphasizes parser and model graph throughput for OpenAPI component resolution and Pydantic v2 model generation. |
19+
20+
## Interpreting Metrics
21+
22+
- `median_ms` is the primary comparison value: it is the median generation duration after warmup runs, and lower is faster.
23+
- `min_ms`, `max_ms`, and `stdev_ms` describe the measured spread for the same row; wide ranges usually mean CI runner noise rather than a deliberate code change.
24+
- Formatter comparisons are scoped to the same scenario and version. `default` is the black/isort default baseline, while `builtin` and `ruff` ratios compare their medians to that baseline.
25+
- `ok` rows have timing data. `unsupported` means the formatter or option was unavailable in that release. `failed` means installation or command execution failed, so timing cells are intentionally empty.
26+
927
## Historical Trends
1028

1129
The benchmark data on this page is loaded from `docs/data/release-benchmarks.json` and rendered in the browser. Release runs update that JSON file directly, so the published page reflects new release measurements after the docs deployment without rewriting this Markdown file.

scripts/build_release_benchmark_docs.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
from typing import Any
1616

1717
try:
18+
from scripts.collect_release_benchmarks import BENCHMARK_CASES, DEFAULT_FORMATTERS
1819
from scripts.release_benchmark_safety import (
1920
SAFE_FIELD_PATTERN,
2021
safe_optional_field,
2122
safe_release_version,
2223
validated_benchmark_entry,
2324
)
2425
except ModuleNotFoundError: # pragma: no cover - direct script execution
26+
from collect_release_benchmarks import BENCHMARK_CASES, DEFAULT_FORMATTERS
2527
from release_benchmark_safety import (
2628
SAFE_FIELD_PATTERN,
2729
safe_optional_field,
@@ -33,6 +35,28 @@
3335
DATA_PATH = ROOT / "docs" / "data" / "release-benchmarks.json"
3436
NOTES_PATH = ROOT / "docs" / "data" / "release-benchmark-notes.json"
3537
DOCS_PATH = ROOT / "docs" / "performance-benchmarks.md"
38+
CASE_ORDER = ("small", "large")
39+
CASE_LABELS = {
40+
"small": "Small",
41+
"large": "Large",
42+
}
43+
CASE_GUIDANCE = {
44+
"small": "Compact fixture that emphasizes CLI startup, parsing, and formatter overhead",
45+
"large": "Larger fixture that emphasizes parser and model graph throughput",
46+
}
47+
INPUT_TYPE_LABELS = {
48+
"jsonschema": "JSON Schema",
49+
"openapi": "OpenAPI",
50+
}
51+
INPUT_TYPE_GUIDANCE = {
52+
"jsonschema": "JSON Schema to Pydantic v2 model generation",
53+
"openapi": "OpenAPI component resolution and Pydantic v2 model generation",
54+
}
55+
FORMATTER_LABELS = {
56+
"default": "black/isort default",
57+
"builtin": "Built-in",
58+
"ruff": "Ruff",
59+
}
3660

3761

3862
@dataclass(frozen=True, slots=True)
@@ -89,6 +113,40 @@ def _integer(value: object, *, default: int = 0) -> int:
89113
return default
90114

91115

116+
def _label(value: str) -> str:
117+
return " ".join(part.capitalize() for part in value.replace("_", "-").split("-") if part) or value
118+
119+
120+
def _case_label(case: str) -> str:
121+
return CASE_LABELS.get(case, _label(case))
122+
123+
124+
def _input_type_label(input_type: str) -> str:
125+
return INPUT_TYPE_LABELS.get(input_type, _label(input_type))
126+
127+
128+
def _formatter_label(formatter: str) -> str:
129+
return FORMATTER_LABELS.get(formatter, _label(formatter))
130+
131+
132+
def _order_key(value: str, order: tuple[str, ...]) -> tuple[int, str]:
133+
try:
134+
return order.index(value), value
135+
except ValueError:
136+
return len(order), value
137+
138+
139+
def _benchmark_case_paths() -> dict[tuple[str, str], str]:
140+
paths: dict[tuple[str, str], str] = {}
141+
for benchmark_case in BENCHMARK_CASES:
142+
try:
143+
input_path = benchmark_case.path.relative_to(ROOT)
144+
except ValueError:
145+
input_path = benchmark_case.path
146+
paths[benchmark_case.input_type, benchmark_case.name] = input_path.as_posix()
147+
return paths
148+
149+
92150
def _entry_from_raw(raw: object, *, path: Path, index: int) -> BenchmarkEntry:
93151
if not isinstance(raw, dict):
94152
msg = f"Benchmark entry #{index} must be an object"
@@ -156,6 +214,100 @@ def load_benchmark_data(path: Path = DATA_PATH, *, notes_path: Path | None = NOT
156214
)
157215

158216

217+
def _scenario_sort_key(scenario: tuple[str, str]) -> tuple[tuple[int, str], str]:
218+
input_type, case = scenario
219+
return _order_key(case, CASE_ORDER), input_type
220+
221+
222+
def _scenario_keys(entries: tuple[BenchmarkEntry, ...]) -> tuple[tuple[str, str], ...]:
223+
scenarios = {(entry.input_type, entry.case) for entry in entries}
224+
return tuple(sorted(scenarios, key=_scenario_sort_key))
225+
226+
227+
def _scenario_formatters(data: BenchmarkData, *, input_type: str, case: str) -> tuple[str, ...]:
228+
formatters = {entry.formatter for entry in data.entries if entry.input_type == input_type and entry.case == case}
229+
return tuple(sorted(formatters, key=lambda formatter: _order_key(formatter, DEFAULT_FORMATTERS)))
230+
231+
232+
def _scenario_description(input_type: str, case: str) -> str:
233+
case_guidance = CASE_GUIDANCE.get(case, f"{_case_label(case)} fixture")
234+
input_guidance = INPUT_TYPE_GUIDANCE.get(input_type, f"{_input_type_label(input_type)} generation")
235+
return f"{case_guidance} for {input_guidance}."
236+
237+
238+
def _scenario_fixture(input_type: str, case: str, benchmark_case_paths: dict[tuple[str, str], str]) -> str:
239+
if input_path := benchmark_case_paths.get((input_type, case)):
240+
return f"`{input_path}`"
241+
return "not declared by `scripts/collect_release_benchmarks.py`"
242+
243+
244+
def _markdown_table_cell(value: str) -> str:
245+
return value.replace("|", "\\|")
246+
247+
248+
def _scenario_table_lines(data: BenchmarkData) -> tuple[str, ...]:
249+
benchmark_case_paths = _benchmark_case_paths()
250+
lines = [
251+
"| Scenario | Input fixture | Formatters | Represents |",
252+
"| --- | --- | --- | --- |",
253+
]
254+
for input_type, case in _scenario_keys(data.entries):
255+
formatter_labels = ", ".join(
256+
f"`{formatter}` ({_formatter_label(formatter)})"
257+
for formatter in _scenario_formatters(data, input_type=input_type, case=case)
258+
)
259+
row = (
260+
f"{_case_label(case)} / {_input_type_label(input_type)}",
261+
_scenario_fixture(input_type, case, benchmark_case_paths),
262+
formatter_labels,
263+
_scenario_description(input_type, case),
264+
)
265+
lines.append("| " + " | ".join(_markdown_table_cell(cell) for cell in row) + " |")
266+
return tuple(lines)
267+
268+
269+
def render_scenario_guidance(data: BenchmarkData) -> tuple[str, ...]:
270+
"""Render static benchmark scenario and metric guidance from the loaded data."""
271+
lines = [
272+
"## Scenario Guide",
273+
"",
274+
(
275+
"Each scenario combines an input type with a case size. This guide is generated from the scenario keys "
276+
"in the benchmark JSON and the collector case definitions, so it changes when the benchmark matrix "
277+
"changes."
278+
),
279+
"",
280+
]
281+
if data.entries:
282+
lines.extend(_scenario_table_lines(data))
283+
else:
284+
lines.append("No benchmark scenarios have been collected yet.")
285+
lines.extend((
286+
"",
287+
"## Interpreting Metrics",
288+
"",
289+
(
290+
"- `median_ms` is the primary comparison value: it is the median generation duration after warmup runs, "
291+
"and lower is faster."
292+
),
293+
(
294+
"- `min_ms`, `max_ms`, and `stdev_ms` describe the measured spread for the same row; wide ranges usually "
295+
"mean CI runner noise rather than a deliberate code change."
296+
),
297+
(
298+
"- Formatter comparisons are scoped to the same scenario and version. `default` is the "
299+
"black/isort default baseline, while `builtin` and `ruff` ratios compare their medians to that baseline."
300+
),
301+
(
302+
"- `ok` rows have timing data. `unsupported` means the formatter or option was unavailable in that "
303+
"release. `failed` means installation or command execution failed, so timing cells are intentionally "
304+
"empty."
305+
),
306+
"",
307+
))
308+
return tuple(lines)
309+
310+
159311
def render_release_benchmark_markdown(data: BenchmarkData) -> str:
160312
"""Render the release benchmark documentation shell."""
161313
data_state = "available" if data.entries else "empty"
@@ -178,6 +330,7 @@ def render_release_benchmark_markdown(data: BenchmarkData) -> str:
178330
"measured, managed, and tuned so code generation stays fast in everyday use."
179331
),
180332
"",
333+
*render_scenario_guidance(data),
181334
"## Historical Trends",
182335
"",
183336
(

tests/data/expected/release_benchmark_docs/release_benchmark_cli_outputs.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@ This page tracks datamodel-code-generator release and main-branch benchmark resu
1010

1111
datamodel-code-generator supports many schema styles and production use cases, so it includes a broad set of useful options. As releases add more capabilities, these benchmarks help keep the implementation measured, managed, and tuned so code generation stays fast in everyday use.
1212

13+
## Scenario Guide
14+
15+
Each scenario combines an input type with a case size. This guide is generated from the scenario keys in the benchmark JSON and the collector case definitions, so it changes when the benchmark matrix changes.
16+
17+
| Scenario | Input fixture | Formatters | Represents |
18+
| --- | --- | --- | --- |
19+
| Small / JSON Schema | `tests/data/jsonschema/person.json` | `default` (black/isort default), `builtin` (Built-in), `ruff` (Ruff) | Compact fixture that emphasizes CLI startup, parsing, and formatter overhead for JSON Schema to Pydantic v2 model generation. |
20+
| Large / JSON Schema | `tests/data/performance/large_models.json` | `builtin` (Built-in) | Larger fixture that emphasizes parser and model graph throughput for JSON Schema to Pydantic v2 model generation. |
21+
| Large / OpenAPI | `tests/data/performance/openapi_large.yaml` | `builtin` (Built-in), `ruff` (Ruff) | Larger fixture that emphasizes parser and model graph throughput for OpenAPI component resolution and Pydantic v2 model generation. |
22+
23+
## Interpreting Metrics
24+
25+
- `median_ms` is the primary comparison value: it is the median generation duration after warmup runs, and lower is faster.
26+
- `min_ms`, `max_ms`, and `stdev_ms` describe the measured spread for the same row; wide ranges usually mean CI runner noise rather than a deliberate code change.
27+
- Formatter comparisons are scoped to the same scenario and version. `default` is the black/isort default baseline, while `builtin` and `ruff` ratios compare their medians to that baseline.
28+
- `ok` rows have timing data. `unsupported` means the formatter or option was unavailable in that release. `failed` means installation or command execution failed, so timing cells are intentionally empty.
29+
1330
## Historical Trends
1431

1532
The benchmark data on this page is loaded from `docs/data/release-benchmarks.json` and rendered in the browser. Release runs update that JSON file directly, so the published page reflects new release measurements after the docs deployment without rewriting this Markdown file.

tests/data/expected/release_benchmark_docs/release_benchmark_escaped_hostile_text.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,21 @@ This page tracks datamodel-code-generator release and main-branch benchmark resu
66

77
datamodel-code-generator supports many schema styles and production use cases, so it includes a broad set of useful options. As releases add more capabilities, these benchmarks help keep the implementation measured, managed, and tuned so code generation stays fast in everyday use.
88

9+
## Scenario Guide
10+
11+
Each scenario combines an input type with a case size. This guide is generated from the scenario keys in the benchmark JSON and the collector case definitions, so it changes when the benchmark matrix changes.
12+
13+
| Scenario | Input fixture | Formatters | Represents |
14+
| --- | --- | --- | --- |
15+
| Small / OpenAPI | `tests/data/openapi/api.yaml` | `default` (black/isort default) | Compact fixture that emphasizes CLI startup, parsing, and formatter overhead for OpenAPI component resolution and Pydantic v2 model generation. |
16+
17+
## Interpreting Metrics
18+
19+
- `median_ms` is the primary comparison value: it is the median generation duration after warmup runs, and lower is faster.
20+
- `min_ms`, `max_ms`, and `stdev_ms` describe the measured spread for the same row; wide ranges usually mean CI runner noise rather than a deliberate code change.
21+
- Formatter comparisons are scoped to the same scenario and version. `default` is the black/isort default baseline, while `builtin` and `ruff` ratios compare their medians to that baseline.
22+
- `ok` rows have timing data. `unsupported` means the formatter or option was unavailable in that release. `failed` means installation or command execution failed, so timing cells are intentionally empty.
23+
924
## Historical Trends
1025

1126
The benchmark data on this page is loaded from `docs/data/release-benchmarks.json` and rendered in the browser. Release runs update that JSON file directly, so the published page reflects new release measurements after the docs deployment without rewriting this Markdown file.

tests/data/expected/release_benchmark_docs/release_benchmark_rendered.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ This page tracks datamodel-code-generator release and main-branch benchmark resu
88

99
datamodel-code-generator supports many schema styles and production use cases, so it includes a broad set of useful options. As releases add more capabilities, these benchmarks help keep the implementation measured, managed, and tuned so code generation stays fast in everyday use.
1010

11+
## Scenario Guide
12+
13+
Each scenario combines an input type with a case size. This guide is generated from the scenario keys in the benchmark JSON and the collector case definitions, so it changes when the benchmark matrix changes.
14+
15+
| Scenario | Input fixture | Formatters | Represents |
16+
| --- | --- | --- | --- |
17+
| Small / JSON Schema | `tests/data/jsonschema/person.json` | `default` (black/isort default), `builtin` (Built-in), `ruff` (Ruff) | Compact fixture that emphasizes CLI startup, parsing, and formatter overhead for JSON Schema to Pydantic v2 model generation. |
18+
| Large / JSON Schema | `tests/data/performance/large_models.json` | `builtin` (Built-in) | Larger fixture that emphasizes parser and model graph throughput for JSON Schema to Pydantic v2 model generation. |
19+
| Large / OpenAPI | `tests/data/performance/openapi_large.yaml` | `builtin` (Built-in), `ruff` (Ruff) | Larger fixture that emphasizes parser and model graph throughput for OpenAPI component resolution and Pydantic v2 model generation. |
20+
21+
## Interpreting Metrics
22+
23+
- `median_ms` is the primary comparison value: it is the median generation duration after warmup runs, and lower is faster.
24+
- `min_ms`, `max_ms`, and `stdev_ms` describe the measured spread for the same row; wide ranges usually mean CI runner noise rather than a deliberate code change.
25+
- Formatter comparisons are scoped to the same scenario and version. `default` is the black/isort default baseline, while `builtin` and `ruff` ratios compare their medians to that baseline.
26+
- `ok` rows have timing data. `unsupported` means the formatter or option was unavailable in that release. `failed` means installation or command execution failed, so timing cells are intentionally empty.
27+
1128
## Historical Trends
1229

1330
The benchmark data on this page is loaded from `docs/data/release-benchmarks.json` and rendered in the browser. Release runs update that JSON file directly, so the published page reflects new release measurements after the docs deployment without rewriting this Markdown file.

0 commit comments

Comments
 (0)