Source code for mew.runner
"""Bridge between the Python registry and the C++ runner."""
from __future__ import annotations
import os
import sys
from collections.abc import Iterable, Iterator, Sequence
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any
import mew._core as _core
import mew.context as _context
from mew._registry import REGISTRY, Entry
from mew._session import new_session_id
from mew.reporter import Reporter
if TYPE_CHECKING:
from mew._typing import BenchmarkOptions
@contextmanager
def _silence_native_stderr() -> Iterator[None]:
"""Redirect OS-level fd 2 to /dev/null around the C++ call.
Google Benchmark's init writes platform diagnostics straight to fd 2, bypassing Python's ``sys.stderr``.
User-facing benchmark errors route through the reporter callback or as Python exceptions, not fd 2.
"""
sys.stderr.flush()
devnull = os.open(os.devnull, os.O_WRONLY)
saved = os.dup(2)
try:
os.dup2(devnull, 2)
yield
finally:
sys.stderr.flush()
os.dup2(saved, 2)
os.close(saved)
os.close(devnull)
def _apply_options(handle: _core.BenchmarkHandle, opts: BenchmarkOptions) -> None:
if (v := opts.get("min_time")) is not None:
handle.min_time(float(v))
if (v := opts.get("min_warmup_time")) is not None:
handle.min_warmup_time(float(v))
if (v := opts.get("iterations")) is not None:
handle.iterations(int(v))
if (v := opts.get("repetitions")) is not None:
handle.repetitions(int(v))
if v := opts.get("unit"):
handle.unit(v)
if opts.get("use_real_time"):
handle.use_real_time()
if opts.get("use_manual_time"):
handle.use_manual_time()
if opts.get("measure_process_cpu_time"):
handle.measure_process_cpu_time()
if opts.get("report_aggregates_only"):
handle.report_aggregates_only(True)
[docs]
def run(
entries: Sequence[Entry] | None = None,
*,
argv: Sequence[str] | None = None,
reporter: Reporter | Iterable[Reporter] | None = None,
filter: str | None = None,
session_tag: str | None = None,
) -> int:
"""Run benchmarks via the C++ Google Benchmark backend.
Each call is one *session*: a fresh time-ordered ``session_id`` is stamped
into the reporter context, so result files stay addressable when several
runs land in one archive.
Parameters
----------
entries : Sequence[Entry], optional
Benchmarks to run.
``None`` runs everything in the global registry; pass a filtered subset (e.g. from :meth:`Registry.filter`) to scope a run.
argv : Sequence[str], optional
Argv forwarded to Google Benchmark's ``Initialize``.
Defaults to ``["mew"]``.
reporter : Reporter, Iterable[Reporter], or None, optional
A single reporter, an iterable of reporters (multiplexed via :class:`Fanout`), or ``None`` for Google Benchmark's default console reporter.
filter : str, optional
Regex forwarded to Google Benchmark as ``--benchmark_filter=``.
session_tag : str, optional
Human label for this session (e.g. ``"before"``), persisted next to
``session_id`` in the reporter context.
Returns
-------
int
Number of benchmarks Google Benchmark executed.
``0`` if no entries were selected.
"""
selected = list(entries) if entries is not None else REGISTRY.all()
if not selected:
return 0
cli = list(argv) if argv is not None else ["mew"]
if filter:
cli.append(f"--benchmark_filter={filter}")
# Clear before registering so a second mew.run() in the same process
# doesn't double-register entries.
_core.clear_registered_benchmarks()
for entry in selected:
handle = _core.register_benchmark(entry.name, entry.fn)
_apply_options(handle, entry.options)
if entry.case_labels is not None:
if entry.cases is None:
handle.dense_range(0, len(entry.case_labels) - 1)
else:
# A name filter narrowed the family: register only those case
# indices. The arg value is the case index the trampoline reads
# via state.range(0), so the right kwargs/label still bind.
for i in entry.cases:
handle.arg(i)
handle.arg_name("case")
rep = _to_single_reporter(reporter)
# Provenance is overlaid onto the GB context by the binding (run_benchmarks
# merges extra_context before calling report_context), so every reporter —
# built-in or user-supplied — sees session identity without a wrapper.
extra_context: dict[str, Any] = {}
if rep is not None:
extra_context["session_id"] = new_session_id()
if session_tag:
extra_context["session_tag"] = session_tag
if custom := _context._snapshot():
extra_context["custom"] = custom
with _silence_native_stderr():
return _core.run_benchmarks(cli, rep, extra_context)
def _to_single_reporter(
reporter: Reporter | Iterable[Reporter] | None,
) -> Reporter | None:
"""Normalize the reporter argument to a single :class:`Reporter` or ``None``."""
if reporter is None:
return None
# Treat anything with the reporter callbacks as a single reporter, even if
# it happens to also be iterable.
if isinstance(reporter, Reporter):
return reporter
from mew.reporter import Fanout
reps = list(reporter)
if not reps:
return None
if len(reps) == 1:
return reps[0]
return Fanout(reps)