Generator + in-process harness timing the real loader's three stages and template/YAML call counts, across tunable profiles. cases/ git-ignored; see test/benchmark/README.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
180 lines
5.9 KiB
Python
Executable File
180 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Generate synthetic ``.tum`` test trees to benchmark *load* time.
|
|
|
|
The generated trees are deliberately cheap to *build* (only ``let`` leaves and
|
|
``group`` containers — no subprocess, no runtime side effect) so the load
|
|
benchmark measures the parse / template / tree-build pipeline and nothing else.
|
|
|
|
Profiles, each targeting a specific cost in the loader:
|
|
|
|
flat one main file, N inline ``let`` steps, no include, no jinja.
|
|
Baseline: YAML parse of a big document + linear object build.
|
|
|
|
includes main ``!include``s N *distinct* sub-files (a few steps each).
|
|
Stresses the per-include template+YAML+tempfile round-trip and the
|
|
``sequence`` splice in test_set.load_test_recursively.
|
|
|
|
repeat main ``!include``s the *same* parametrised leaf file N times.
|
|
Stresses jinja *recompilation*: the compiled template is identical
|
|
every time, only the render params (idx) differ -> the case a
|
|
template cache collapses.
|
|
|
|
jinja one main file whose ``{% for %}`` loop emits N steps.
|
|
Stresses a single large jinja render + a single large YAML parse.
|
|
|
|
deep nested includes, depth N (main -> d0 -> d1 -> ...).
|
|
Stresses include recursion and per-level template+YAML.
|
|
|
|
mix a realistic blend: groups, a jinja loop, distinct includes and a
|
|
repeated parametrised include.
|
|
|
|
Usage:
|
|
gen_bench_test.py --profile repeat --size 1000 --out cases/repeat_1000
|
|
-> writes <out>/main.tum (+ includes, + param.yaml) and prints the path.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
|
|
|
|
def _let(indent, i, name=None):
|
|
name = name if name is not None else f"s{i}"
|
|
pad = " " * indent
|
|
return (
|
|
f"{pad}- let:\n"
|
|
f"{pad} name: {name}\n"
|
|
f"{pad} values:\n"
|
|
f"{pad} - k{i}: {i}\n"
|
|
)
|
|
|
|
|
|
def gen_flat(out, n):
|
|
body = "".join(_let(8, i) for i in range(n))
|
|
main = f"main:\n name: bench flat {n}\n steps:\n{body}"
|
|
_write(out, "main.tum", main)
|
|
|
|
|
|
def gen_includes(out, n):
|
|
steps = "".join(f" - !include inc_{i}.tum\n" for i in range(n))
|
|
main = f"main:\n name: bench includes {n}\n steps:\n{steps}"
|
|
_write(out, "main.tum", main)
|
|
for i in range(n):
|
|
# each include is a YAML *sequence* (list of steps)
|
|
seq = "".join(_let(0, i * 3 + j, name=f"inc{i}_{j}") for j in range(3))
|
|
_write(out, f"inc_{i}.tum", seq)
|
|
|
|
|
|
def gen_repeat(out, n):
|
|
steps = "".join(
|
|
f" - !include {{file: leaf.tum, idx: {i}}}\n" for i in range(n)
|
|
)
|
|
main = f"main:\n name: bench repeat {n}\n steps:\n{steps}"
|
|
_write(out, "main.tum", main)
|
|
leaf = (
|
|
"- let:\n"
|
|
" name: leaf_{{ idx }}\n"
|
|
" values:\n"
|
|
" - leaf_{{ idx }}: {{ idx }}\n"
|
|
)
|
|
_write(out, "leaf.tum", leaf)
|
|
|
|
|
|
def gen_jinja(out, n):
|
|
main = (
|
|
f"main:\n name: bench jinja {n}\n steps:\n"
|
|
"{% for i in range(" + str(n) + ") %}\n"
|
|
" - let:\n"
|
|
" name: j{{ i }}\n"
|
|
" values:\n"
|
|
" - k{{ i }}: {{ i }}\n"
|
|
"{% endfor %}\n"
|
|
)
|
|
_write(out, "main.tum", main)
|
|
|
|
|
|
def gen_deep(out, n):
|
|
main = (
|
|
f"main:\n name: bench deep {n}\n steps:\n"
|
|
" - let:\n name: top\n values:\n - a: 0\n"
|
|
" - !include d_0.tum\n"
|
|
)
|
|
_write(out, "main.tum", main)
|
|
for i in range(n):
|
|
seq = _let(0, i, name=f"d{i}")
|
|
if i < n - 1:
|
|
seq += f"- !include d_{i + 1}.tum\n"
|
|
_write(out, f"d_{i}.tum", seq)
|
|
|
|
|
|
def gen_mix(out, n):
|
|
# n groups, each: 2 inline lets, one distinct include, one repeated include,
|
|
# plus a small jinja loop. Roughly ~6*n steps.
|
|
per = max(1, n)
|
|
parts = [f"main:\n name: bench mix {n}\n steps:\n"]
|
|
for g in range(per):
|
|
parts.append(
|
|
f" - group:\n"
|
|
f" name: grp{g}\n"
|
|
f" steps:\n"
|
|
)
|
|
parts.append(_let(16, g * 2, name=f"g{g}_a"))
|
|
parts.append(_let(16, g * 2 + 1, name=f"g{g}_b"))
|
|
parts.append(f" - !include inc_{g}.tum\n")
|
|
parts.append(f" - !include {{file: leaf.tum, idx: {g}}}\n")
|
|
parts.append(
|
|
"{% for i in range(3) %}\n"
|
|
f" - let:\n"
|
|
f" name: g{g}_j{{{{ i }}}}\n"
|
|
f" values:\n"
|
|
f" - g{g}_k{{{{ i }}}}: {{{{ i }}}}\n"
|
|
"{% endfor %}\n"
|
|
)
|
|
_write(out, "main.tum", "".join(parts))
|
|
for g in range(per):
|
|
_write(out, f"inc_{g}.tum", _let(0, g, name=f"mixinc{g}"))
|
|
_write(
|
|
out,
|
|
"leaf.tum",
|
|
"- let:\n name: mixleaf_{{ idx }}\n values:\n - mixleaf_{{ idx }}: {{ idx }}\n",
|
|
)
|
|
|
|
|
|
PROFILES = {
|
|
"flat": gen_flat,
|
|
"includes": gen_includes,
|
|
"repeat": gen_repeat,
|
|
"jinja": gen_jinja,
|
|
"deep": gen_deep,
|
|
"mix": gen_mix,
|
|
}
|
|
|
|
|
|
def _write(out, name, content):
|
|
with open(os.path.join(out, name), "w") as f:
|
|
f.write(content)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--profile", required=True, choices=sorted(PROFILES))
|
|
ap.add_argument("--size", type=int, default=1000,
|
|
help="profile-specific count (steps / includes / depth)")
|
|
ap.add_argument("--out", required=True, help="output directory (recreated)")
|
|
args = ap.parse_args()
|
|
|
|
out = os.path.abspath(args.out)
|
|
if os.path.isdir(out):
|
|
shutil.rmtree(out)
|
|
os.makedirs(out)
|
|
|
|
# minimal config file so the loader does not emit "no param file" noise
|
|
_write(out, "param.yaml", "bench_dummy: 1\n")
|
|
|
|
PROFILES[args.profile](out, args.size)
|
|
print(os.path.join(out, "main.tum"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|