Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 291 additions & 137 deletions bench_vid_maker/README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions bench_vid_maker/configs/euler_lim.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ subtitle = "nested loop to i=1,000 -- same program, run as-is in each engine"

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/test_eulars_constant_lim_def.py"]
command = ["{python}", "test_codes_python/Loops/test_eulars_constant_lim_def.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/test_eulars_constant_lim_def.py"]
command = ["{lithon}", "test_codes_lithon/Loops/test_eulars_constant_lim_def.lio"]
10 changes: 10 additions & 0 deletions bench_vid_maker/configs/fib_30.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title = "Fibonacci(30): CPython vs Lithon"
subtitle = "same recursive program, run as-is in each engine"

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/Functions/fibb.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/Functions/fibb.lio"]
10 changes: 10 additions & 0 deletions bench_vid_maker/configs/find_e.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title = "Finding e by its series: CPython vs Lithon"
subtitle = "8,000 terms of 1/n!, computed with an explicit factorial loop"

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/Loops/find_e.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/Loops/find_e.lio"]
4 changes: 2 additions & 2 deletions bench_vid_maker/configs/fsum_1b.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ subtitle = "same program, run as-is in each engine"

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/test_1b_fsum.py"]
command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"]
command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"]
4 changes: 2 additions & 2 deletions bench_vid_maker/configs/fsum_1b_1core.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ cores = 1

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/test_1b_fsum.py"]
command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"]
command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"]
4 changes: 2 additions & 2 deletions bench_vid_maker/configs/fsum_1b_4core.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ cores = 4

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/test_1b_fsum.py"]
command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"]
command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"]
4 changes: 2 additions & 2 deletions bench_vid_maker/configs/fsum_1b_6core.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ cores = 6

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/test_1b_fsum.py"]
command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"]
command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"]
4 changes: 2 additions & 2 deletions bench_vid_maker/configs/fsum_1b_8core.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ cores = 8

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/test_1b_fsum.py"]
command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"]
command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"]
10 changes: 10 additions & 0 deletions bench_vid_maker/configs/fsum_1b_simd.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title = "Summing 1,000,000,000 half-increments: CPython vs Lithon"
subtitle = "float[64] accumulator -- the case Lithon's auto-vectorizer (AVX2) targets"

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/Loops/test_1b_fsum_simd.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum_simd.lio"]
4 changes: 2 additions & 2 deletions bench_vid_maker/configs/fsum_1k.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ subtitle = "tiny workload -- for quickly checking the bench_vid_maker UI/video,

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/test_1k_fsum.py"]
command = ["{python}", "test_codes_python/Loops/test_1k_fsum.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/test_1k_fsum.py"]
command = ["{lithon}", "test_codes_lithon/Loops/test_1k_fsum.lio"]
10 changes: 10 additions & 0 deletions bench_vid_maker/configs/multi_ops_1b.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title = "Mixed integer arithmetic, 1,000,000,000 iterations: CPython vs Lithon"
subtitle = "multiply, bitwise xor and right shift, accumulated -- pure integer ALU work"

[[tasks]]
name = "CPython"
command = ["{python}", "test_codes_python/Loops/test_multi_ops.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/Loops/test_multi_ops.lio"]
37 changes: 37 additions & 0 deletions bench_vid_maker/src/bench_vid_maker/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .config import PROJECT_ROOT, load_config
from .recorder import VideoRecorder
from .ui import run_with_ui
from .wsl import have_wsl, reexec_in_wsl

# Sized so the default rendered frame lands comfortably in 2K+ territory
# with the (larger, more legible) default font_size -- see VideoRecorder's
Expand All @@ -23,6 +24,26 @@
DEFAULT_FONT_SIZE = 34


def _wsl_preferred() -> bool:
"""Whether to prefer a WSL run over a Windows-native one by default.

The Lithon engine's fast tier is an x86-64 native binary; on Windows
it needs `build/tier_runner.exe`, and the repository is frequently
built on Linux/WSL only. Rather than failing with a confusing
"native runner not found" error, the tool measures on WSL when it can.
Set `BENCH_VID_MAKER_NO_WSL=1` to opt out.
"""
if os.environ.get("BENCH_VID_MAKER_NO_WSL"):
return False
from .wsl import wsl_executable

if wsl_executable() is None:
return False
# A Windows-native engine build is a perfectly good place to measure --
# only prefer WSL when the repo has no Windows runner at all.
return not (PROJECT_ROOT / "build" / "tier_runner.exe").exists()


def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(
prog="bench-vid-maker",
Expand Down Expand Up @@ -57,8 +78,24 @@ def main(argv: list[str] | None = None) -> int:
"--no-music", action="store_true",
help="don't mux the procedurally-generated (non-copyright) background music into the video",
)
ap.add_argument(
"--wsl", action="store_true",
help="re-execute the whole run inside WSL (both engines measured natively on Linux)",
)
ap.add_argument(
"--no-wsl", action="store_true",
help="never re-execute inside WSL, even when WSL is available",
)
args = ap.parse_args(argv)

# WSL re-exec: the honest place to measure is a Linux kernel with both
# engines running natively. The child re-enters this same entry point,
# so the guard below keeps it from re-execing twice.
if args.wsl and not args.no_wsl and have_wsl():
return reexec_in_wsl()
if not args.no_wsl and not args.wsl and have_wsl() and _wsl_preferred():
return reexec_in_wsl()

try:
cfg = load_config(args.config)
except (FileNotFoundError, ValueError) as e:
Expand Down
89 changes: 80 additions & 9 deletions bench_vid_maker/src/bench_vid_maker/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

[[tasks]]
name = "CPython"
command = ["{python}", "bench_vid_maker/tasks/python_sum.py"]
command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"]

[[tasks]]
name = "Lithon"
command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"]
command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"]
cwd = "." # optional, relative to repo root

All relative paths (command args and ``cwd``) are resolved against the repo
Expand All @@ -24,6 +24,7 @@
from __future__ import annotations

import dataclasses
import os
import pathlib
import shutil
import sys
Expand All @@ -44,18 +45,66 @@ def find_repo_root(start: pathlib.Path) -> pathlib.Path:
REPO_ROOT = find_repo_root(PROJECT_ROOT)


def _is_runnable(path: pathlib.Path) -> bool:
"""True when `path` exists *and* can actually be executed on this OS.

A Windows `.venv/Scripts/lithon.exe` sitting in the repo is a real trap
when the tool runs inside WSL: the file exists, so a plain `exists()`
check picks it, and it then fails with a baffling "native runner not
found at C:\\...\\tier_runner.exe" -- a Windows path, resolved from a
Linux process. The runner is only usable if this platform can run it.
"""
if not path.exists():
return False
if path.suffix.lower() == ".exe" and not sys.platform.startswith("win"):
return False # a Windows binary is useless on Linux/macOS
if not path.suffix and sys.platform.startswith("win"):
return False # a POSIX binary is useless on Windows
return os.access(path, os.X_OK) or path.suffix.lower() == ".exe"


def _find_lithon() -> str:
for rel in (".venv/Scripts/lithon.exe", ".venv/bin/lithon"):
"""Locate a `lithon` executable that can run *on this platform*.

Order:
1. the repo's own virtualenv (platform-correct subdirectory),
2. a sibling of the running interpreter (this is what makes
`~/.venvs/lithon-bench/bin/lithon` in WSL resolve correctly),
3. `lithon` on PATH,
4. the repo's `cli.py`, run through the current interpreter -- the
engine's own source entry point, so a repo that has been cloned
but not `pip install`ed still works.
"""
venv_names = (
(".venv/Scripts/lithon.exe", ".venv/bin/lithon")
if sys.platform.startswith("win")
else (".venv/bin/lithon", ".venv/Scripts/lithon.exe")
)
for rel in venv_names:
p = REPO_ROOT / rel
if p.exists():
if _is_runnable(p):
return str(p)

# A venv running bench-vid-maker itself (e.g. ~/.venvs/lithon-bench in
# WSL) that has the repo installed: lithon sits next to our interpreter.
for name in ("lithon.exe", "lithon"):
sibling = pathlib.Path(sys.executable).parent / name
if _is_runnable(sibling):
return str(sibling)

found = shutil.which("lithon")
if found:
if found and _is_runnable(pathlib.Path(found)):
return found

# Last resort: run the engine straight from its source entry point.
cli = REPO_ROOT / "cli.py"
if cli.exists():
return str(cli)

raise FileNotFoundError(
"could not find a built `lithon` executable (looked in "
f"{REPO_ROOT / '.venv/Scripts'} and {REPO_ROOT / '.venv/bin'}, and on PATH). "
"Build/install Lithon first."
"could not find a runnable `lithon` executable (looked in "
f"{REPO_ROOT / '.venv/bin'} and {REPO_ROOT / '.venv/Scripts'}, next to "
f"{sys.executable}, on PATH, and {cli}). Build/install Lithon first."
)


Expand Down Expand Up @@ -83,6 +132,28 @@ def _expand(token: str) -> str:
return token


def _expand_command(command: list[str]) -> list[str]:
"""Expand the placeholders in a whole command.

`{lithon}` may resolve to the engine's Python entry point (`cli.py`)
when no built executable is available -- in that case the command has
to be run *through* the interpreter, so the interpreter is prepended
and the marker is kept out of the final argv.
"""
expanded: list[str] = []
for tok in command:
if tok == "{lithon}":
lithon = _find_lithon()
if lithon.endswith(".py"):
expanded.append(sys.executable)
expanded.append(lithon)
else:
expanded.append(lithon)
else:
expanded.append(_expand(tok))
return expanded


def load_config(path: pathlib.Path) -> BenchConfig:
path = pathlib.Path(path).resolve()
data = tomllib.loads(path.read_text(encoding="utf-8"))
Expand All @@ -95,7 +166,7 @@ def load_config(path: pathlib.Path) -> BenchConfig:
for i, t in enumerate(raw_tasks):
if "name" not in t or "command" not in t:
raise ValueError(f"{path}: tasks[{i}] is missing 'name' or 'command'")
command = [_expand(tok) for tok in t["command"]]
command = _expand_command(t["command"])
cwd = REPO_ROOT / t.get("cwd", ".")
tasks.append(Task(name=t["name"], command=command, cwd=cwd.resolve()))

Expand Down
49 changes: 45 additions & 4 deletions bench_vid_maker/src/bench_vid_maker/recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"C:/Windows/Fonts/JetBrainsMonoNerdFontMono-Regular.ttf",
"C:/Windows/Fonts/JetBrainsMonoNerdFontMono-Bold.ttf",
), # Nerd Font first: the UI uses its icon glyphs (checked against its cmap, see ui.py)
(
str(pathlib.Path.home() / ".local/share/fonts/JetBrainsMonoNerdFontMono-Regular.ttf"),
str(pathlib.Path.home() / ".local/share/fonts/JetBrainsMonoNerdFontMono-Bold.ttf"),
), # WSL/Linux user-font install of the same Nerd Font
("C:/Windows/Fonts/consola.ttf", "C:/Windows/Fonts/consolab.ttf"),
("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"),
]
Expand Down Expand Up @@ -143,7 +147,15 @@ def __init__(
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-movflags", "+faststart",
str(self.out_path),
]
self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
# bufsize=0 hands us the raw pipe instead of a BufferedWriter. A
# single frame is ~35 MB at the default size, which is far larger
# than any pipe buffer: a buffered writer can hand a partial chunk
# to the OS and ffmpeg then reads a truncated frame and aborts with
# "Invalid buffer size, packet size N < expected frame_size M".
# Writing straight to the pipe keeps every frame's bytes contiguous.
self._proc = subprocess.Popen(
cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0
)
self._frame_count = 0

def _rasterize(self, renderable: RenderableType) -> bytes:
Expand Down Expand Up @@ -182,12 +194,41 @@ def capture(self, renderable: RenderableType, hold_frames: int = 1) -> None:
stdin = self._proc.stdin
assert stdin is not None
for _ in range(hold_frames):
stdin.write(data)
self._write_all(stdin, data)
self._frame_count += hold_frames

@staticmethod
def _write_all(stdin, data: bytes) -> None:
"""Write every byte of `data` to the pipe.

A raw pipe write is not guaranteed to transfer everything in one
call: it can return a short count when the pipe buffer fills up
while ffmpeg is busy. Writing the remainder is the only correct
behaviour -- a truncated frame makes ffmpeg abort the whole encode.
"""
view = memoryview(data)
while view:
written = stdin.write(view)
if written is None or written <= 0:
raise BrokenPipeError("ffmpeg closed the video pipe early")
view = view[written:]

def close(self) -> None:
assert self._proc.stdin is not None
self._proc.stdin.close()
try:
self._proc.stdin.close()
except BrokenPipeError:
# ffmpeg already died; its exit code and stderr say why
pass
self._proc.wait()
if self._proc.returncode != 0:
raise RuntimeError(f"ffmpeg exited {self._proc.returncode} while writing {self.out_path}")
detail = ""
if self._proc.stderr is not None:
try:
detail = self._proc.stderr.read().decode("utf-8", "replace").strip()
except (OSError, ValueError):
detail = ""
raise RuntimeError(
f"ffmpeg exited {self._proc.returncode} while writing {self.out_path}"
+ (f":\n{detail}" if detail else "")
)
Loading
Loading