v2.0.0
Microbench v2 is a significant upgrade with many new features versus v1.1.0.
Be sure to review the breaking changes before upgrading.
New features
-
Command-line interface (
microbench -- COMMAND): wrap any external
command and record host metadata alongside timing without writing Python
code. Useful for SLURM jobs, shell scripts, and compiled executables.- Records
command,returncode(list, one per timed iteration),
alongside the standard timing fields. Mixins are specified by short
kebab-case names without theMBprefix (e.g.host-info);
original MB-prefixed names are also accepted. - Use
--mixin MIXIN [MIXIN ...]to select metadata to capture (defaults to
host-info,slurm-info,loaded-modules,python-info
andworking-dir) - Use
--show-mixinsto
list all available mixins with descriptions; use--field KEY=VALUEto
attach extra labels - Use
--iterations Nand--warmup Nfor repeat
timing - Use
--stdout[=suppress]and--stderr[=suppress]to capture
subprocess output into the record (output is re-printed to the terminal
unless=suppressis given) - Use
--monitor-interval SECONDSto sample
child process CPU and memory over time. - Some mixins expose
their own configuration flags (shown in--show-mixinsand--help) - Capture failures are non-fatal by default
(capture_optional = True), making the CLI safe across heterogeneous
cluster nodes. - The process exits with the first non-zero returncode seen
across all timed iterations if present, or zero (success) otherwise.
- Records
-
summary(results)/bench.summary(): prints min / mean / median /
max / stdev ofcall.durationsacross all results. No dependencies required
beyond the Python standard library.bench.summary()is a one-liner
convenience that callsbench.get_results()internally. The module-level
summary(results)accepts any list of dicts and can be composed with other
results-processing steps.from microbench import MicroBench, summary bench = MicroBench() @bench def my_function(): ... for _ in range(10): my_function() bench.summary() # n=10 min=0.000031 mean=0.000038 median=0.000036 max=0.000059 stdev=0.000008 # or with explicit results list: summary(bench.get_results())
-
bench.time(name)sub-timing: [Python API] label phases inside a single benchmark
record with named timing sections. Sub-timings accumulate incall.timingsas
[{"name": ..., "duration": ...}, ...]in call order. Compatible with
bench.record(),bench.arecord(),@bench(sync and async), and
bench.record_on_exit(). Calling outside an active benchmark is a silent
no-op;call.timingsis absent whenbench.time()is never called.with bench.record('pipeline'): with bench.time('parse'): data = parse(raw) with bench.time('transform'): result = transform(data)
-
Async support: [Python API] the
@benchdecorator now detectsasync deffunctions
and returns anasync defwrapper that must be awaited. A new
bench.arecord(name)method provides the async counterpart of
bench.record()for use withasync with. All mixins, static fields,
output sinks,iterations, andwarmupwork identically to the sync path.
MBLineProfilerraisesNotImplementedErrorat decoration time when used
with an async function (line profiling of coroutines is not supported).@bench async def fetch(): await asyncio.sleep(0.01) asyncio.run(fetch()) async with bench.arecord('load'): await load_data()
Note: elapsed time includes event-loop interleaving from other concurrent
tasks; run in an otherwise-idle event loop for repeatable results. -
bench.record_on_exit(name, handle_sigterm=True): [Python API]
registers a
process-exit handler that writes one benchmark record when the script
terminates. Captures wall-clock duration from the call site to exit plus
all mixin fields. Designed for SLURM jobs and batch scripts where
restructuring code around a decorator is impractical. Key behaviours:- By default installs a SIGTERM handler (main thread only) that writes
the record, chains to any existing SIGTERM handler, then re-delivers
SIGTERM so the process exits with the conventional code 143 (128 + 15). - Wraps
sys.excepthookto capture unhandled exceptions into an
exceptionfield before the process exits. - Adds an
exit_signalfield when the exit was triggered by SIGTERM. - Falls back to writing the record to
sys.stderrif the primary output
sink raises (e.g. filesystem unmounted at exit time). - Calling a second time on the same instance replaces the first
registration and resets the start time.
- By default installs a SIGTERM handler (main thread only) that writes
-
bench.record(name)context manager: [Python API] times an arbitrary code block
and writes one record, without requiring the code to be in a named
function. All mixins, static fields, and output sinks behave identically
to the decorator form. -
Exception capture: [Python API] when a benchmarked block raises — via
bench.record()or a@bench-decorated function — the record is
written before the exception propagates. Anexceptionfield is added
containing{"type": ..., "message": ...}. The exception is always
re-raised. With--iterations N, timing stops at the first exception. -
MBPythonInfomixin replacesMBPythonVersion: records apythondict
withversion,prefix(sys.prefix), andexecutable(sys.executable),
giving a complete picture of the running interpreter in one field.MBPythonVersion
has been removed (see breaking changes above).MBPythonInfois included
inMicroBenchby default (Python API) and in the CLI default mixin set;
--no-mixinsuppresses it on the CLI as usual. -
MBLoadedModulesmixin: captures the loaded Lmod / Environment
Modules software stack into aloaded_modulesdict mapping module name
to version string (e.g.{"gcc": "12.2.0", "openmpi": "4.1.5"}). Reads
the standardLOADEDMODULESenvironment variable — no subprocess, no
extra dependencies. Empty dict when no modules are loaded. Included in
the CLI defaults alongsideMBHostInfo,MBSlurmInfo, andMBWorkingDir. -
MBWorkingDirmixin: captures the absolute path of the working
directory at benchmark time intocall.working_dir. No dependencies.
Included in the CLI defaults — useful for reproducibility when comparing
results across nodes or directories. -
MBGitInfomixin: captures the repository root path, current commit
hash, branch name, and dirty flag (uncommitted changes present) via
git≥ 2.11 on PATH. Stored ingit. Setgit_repoto inspect
a specific repository directory. -
MBPeakMemorymixin: captures peak Python memory allocation during the
benchmarked function ascall.peak_memory_bytes(bytes), using
tracemallocfrom the standard library. No extra dependencies required. -
MBSlurmInfomixin: captures allSLURM_*environment variables into
aslurmdict (keys lowercased,SLURM_prefix stripped). Empty dict
when running outside a SLURM job. Supersedes the manual
env_vars = ('SLURM_JOB_ID', ...)pattern. -
MBFileHashmixin: records a cryptographic checksum of specified files
in thefile_hashesfield (a dict mapping path to hex digest). Defaults to
hashingsys.argv[0]— the running script. Sethash_filesto an iterable
of paths to hash specific files instead. Sethash_algorithmto any
algorithm accepted byhashlib.new(default:'sha256'). -
MBCgroupLimitsmixin: captures the CPU quota and memory limit enforced by
the Linux cgroup filesystem. Works for SLURM jobs and Kubernetes pods (cgroup
v1 and v2). Fields incgroups:cpu_cores_limit(float — quota ÷ period,
ornullif unlimited),memory_bytes_limit(int ornullif unlimited),
version(1 or 2). Returns{}on non-Linux systems or when the
cgroup filesystem is unavailable.class Bench(MicroBench, MBSlurmInfo, MBCgroupLimits): pass
{ "cgroups": { "cpu_cores_limit": 4.0, "memory_bytes_limit": 17179869184, "version": 2 } } -
MBCondaPackagesimprovements:- Queries the environment identified by
CONDA_PREFIX(the shell's active conda
environment) rather thansys.prefix. Falls back tosys.prefixwhen
CONDA_PREFIXis not set. - Falls back to
CONDA_EXEifcondais not onPATH(common in non-interactive
SLURM batch scripts where conda is activated but itsbin/is not onPATH). - Replaces the separate
conda_versionsfield with a unifiedcondadict
containingname(CONDA_DEFAULT_ENV),path(CONDA_PREFIX), and
packages(the version dict). Either ofname/pathmay beNoneif
the corresponding variable is unset. Withget_results(flat=True)these
expand toconda.name,conda.path,conda.packages.<pkg>etc.
- Queries the environment identified by
-
MicroBenchBase: the core benchmarking machinery is now exposed as
MicroBenchBase(no default mixins).MicroBenchinherits from both
MicroBenchBaseandMBPythonInfo. SubclassMicroBenchBasedirectly when
you need a completely bare benchmark class with no automatic captures. -
warmupparameter: passwarmup=Nto run the functionNtimes
before timing begins, priming caches or JIT compilation without affecting
results. Warmup calls are unrecorded and do not interact with the monitor
thread or capture triggers. -
Multi-sink output architecture (#52): Results can now be written to
multiple destinations simultaneously by passing anoutputslist to
MicroBench. Three classes make up the new output API:Output— abstract base class; subclass this to implement custom sinks.FileOutput— writes JSONL to a file path or file-like object (wraps the
previous default behaviour).RedisOutput— writes to a Redis list.HttpOutput- New for v2 - POST each benchmark result to an HTTP/HTTPS endpoint.
The existing
outfileparameter and class-leveloutfileattribute continue
to work as shorthand for a singleFileOutput. Passing bothoutfileand
outputsraisesValueError.Example — write to a file and Redis simultaneously:
from microbench import MicroBench, FileOutput, RedisOutput bench = MicroBench(outputs=[ FileOutput('/home/user/results.jsonl'), RedisOutput('microbench:mykey', host='redis-host', port=6379), ])
get_results()delegates to the first sink that supports reading back
results (FileOutputandRedisOutputboth do). -
get_results(format=..., flat=...):get_resultsnow accepts two
keyword arguments.format='dict'(default) — returns a list of dicts; no pandas required.format='df'— returns a pandas DataFrame (previous default behaviour).flat=True— flattens nested dict fields (e.g.slurm,cgroups,
git) into dot-notation keys (slurm.job_id,call.name). Works for both
formats without requiring pandas.
-
capture_optionalclass attribute: setcapture_optional = Trueon
a benchmark class to catch exceptions fromcapture_andcapturepost_
methods instead of aborting the benchmark call. Failures are recorded in
call.capture_errors(a list of{"method": ..., "error": ...}dicts);
the field is absent when all captures succeed. Designed for production
jobs on heterogeneous cluster nodes where optional dependencies may not
be present on every node. -
python-dateutildependency removed fromLiveStream: timestamp
parsing now usesdatetime.fromisoformat()from the standard library.
Removepython-dateutilfrom your environment if it was
only installed for microbench.
Breaking changes (vs v1.1.0)
-
Removed deprecated mixins:
MBPythonVersion,MBHostCpuCores, and
MBHostRamTotalhave been removed.- Replace
MBPythonVersionwithMBPythonInfo(capturespython.version,
python.prefix,python.executable). - Replace
MBHostCpuCoresand/orMBHostRamTotalwithMBHostInfo, which
now captureshost.cpu_cores_logical,host.cpu_cores_physical, and
host.ram_totalautomatically when psutil is installed.
- Replace
-
Namespace restructuring of record fields: All benchmark record fields are
now grouped into top-level namespace dicts, making records self-documenting
and easier to query. The complete rename table is below.Core fields move into
mb(static config) andcall(per-call data):Old key New key mb_run_idmb.run_idmb_versionmb.versiontimestamp_tzmb.timezoneduration_countermb.duration_counterstart_timecall.start_timefinish_timecall.finish_timerun_durationscall.durationsfunction_namecall.nametelemetrycall.monitorenv_*(flat keys)env.*(dict)package_versions(fromcapture_versions)python.loaded_packagesMixin fields move to typed namespaces:
Old key New key hostnamehost.hostnameoperating_systemhost.oscpu_cores_physicalhost.cpu_cores_physicalram_totalhost.ram_totalargscall.argskwargscall.kwargsreturn_valuecall.return_valueline_profilercall.line_profilerpackage_versions(MBGlobalPackages)python.loaded_packagespackage_versions(MBInstalledPackages)python.installed_packagespackage_pathspython.installed_package_pathsnvidia_<attr>(multiple flat dicts)nvidia(list of per-GPU dicts withuuidkey)Unchanged namespaces:
conda.Migration: Use
get_results(flat=True)to access fields via dot-notation
keys (call.name,mb.run_id,host.hostname, etc.) in pandas or scripts
without rewriting nested dict access. Alternatively, update field access to
the new nested structure:result['call']['name'],result['mb']['run_id'],
result['host']['hostname'], etc. -
get_results()now returns a list of dicts by default: The default
format='dict'returns a list of plain Python dicts and requires no
dependencies. Passformat='df'to get a pandas DataFrame (previous
behaviour). Update existing callers:bench.get_results()→
bench.get_results(format='df'). -
telemetryrenamed tomonitor(#51): The background sampling thread
has been renamed throughout the API to better reflect its intent (continuous
monitoring, not data transmission).TelemetryThread→MonitorThread- Class variable
telemetry_interval→monitor_interval - Class variable
telemetry_timeout→monitor_timeout - Result field
bm_data['telemetry']→bm_data['monitor'] - Internal attribute
self._telemetry_thread→self._monitor_thread
-
MicroBenchRedisremoved (#52): Use
MicroBench(outputs=[RedisOutput(...)])instead.Before:
from microbench import MicroBenchRedis class RedisBench(MicroBenchRedis): redis_connection = {'host': 'localhost', 'port': 6379} redis_key = 'microbench:mykey' bench = RedisBench()
After:
from microbench import MicroBench, RedisOutput bench = MicroBench(outputs=[RedisOutput('microbench:mykey', host='localhost', port=6379)])
-
LiveStreamupdated for v2 record schema: field references updated from
the v1 flat schema (function_name,hostname,start_time,
finish_time) to the v2 nested schema (call.name,host.hostname,
call.start_time,call.finish_time). Records produced by microbench v1
are no longer parsed correctly byLiveStream; this is expected given the
v2 schema migration documented in the breaking changes section above.
Full list of changes
- feat!: rename telemetry → monitor by @alubbock in #51
- feat!: multi-sink output architecture (Output, FileOutput, RedisOutput) by @alubbock in #52
- docs: add MkDocs Material documentation site by @alubbock in #54
- chore(deps): update dependency python to 3.14 by @renovate[bot] in #55
- feat: add warmup=N parameter to MicroBench by @alubbock in #56
- feat: add MBSlurmInfo mixin by @alubbock in #57
- feat: add capture_optional class attribute by @alubbock in #58
- docs: restructure user guide and document LiveStream/envdiff by @alubbock in #59
- docs: add citation section to README and docs by @alubbock in #60
- feat: add MBPeakMemory mixin by @alubbock in #61
- docs: improve clarity and reduce jargon by @alubbock in #62
- feat: add MBFileHash mixin by @alubbock in #64
- feat: add MBGitInfo mixin by @alubbock in #63
- feat: add python -m microbench CLI by @alubbock in #65
- feat: add MBLoadedModules mixin for Lmod/Environment Modules capture by @alubbock in #66
- feat: stream --stdout/--stderr to terminal in real time by @alubbock in #67
- feat: add bench.record() context manager and transparent exception capture by @alubbock in #68
- feat: add bench.record_on_exit() for process-lifetime benchmarking by @alubbock in #69
- refactor: split init.py and test_base.py into focused submodules by @alubbock in #70
- feat: add async support for @bench decorator and bench.arecord() by @alubbock in #71
- fix: start monitor thread at record_on_exit() call time, not exit time by @alubbock in #73
- feat: add bench.time(name) sub-timing API by @alubbock in #72
- feat: add --monitor-interval for CLI subprocess CPU/RAM monitoring by @alubbock in #74
- feat: add MBCgroupLimits mixin by @alubbock in #75
- feat!: results API — get_results(format, flat) and summary() by @alubbock in #76
- feat: --mixin accepts multiple space-separated values by @alubbock in #77
- feat: kebab-case mixin names and --show-mixins by @alubbock in #78
- feat: autodiscover mixin CLI args (CLIArg); add --git-repo and --hash-file by @alubbock in #79
- feat: add MBWorkingDir mixin and include in CLI defaults by @alubbock in #80
- fix: make --all and --no-mixin mutually exclusive by @alubbock in #81
- feat: microbench entry point, MBPythonInfo, MicroBenchBase, conda improvements by @alubbock in #82
- feat!: restructure record fields into consistent namespaces (2.0.0) by @alubbock in #83
- feat: merge MBHostCpuCores and MBHostRamTotal into MBHostInfo by @alubbock in #84
- feat: add --version flag to CLI by @alubbock in #85
- feat: add --timeout and --timeout-grace-period to CLI by @alubbock in #86
- feat!: remove deprecated MBPythonVersion, MBHostCpuCores, MBHostRamTotal by @alubbock in #87
- feat: add --nvidia-attributes and --nvidia-gpus CLI flags by @alubbock in #88
- docs: document MBNvidiaSmi Python API attributes; add command-level tests by @alubbock in #89
- feat: add --dry-run flag to CLI by @alubbock in #90
- refactor: rename mixins.py to _mixins.py; export CLIArg from top-level package by @alubbock in #91
- feat: add HttpOutput and --http-output CLI flag by @alubbock in #92
- feat: add --redis-output CLI flag; group --help by category by @alubbock in #93
- refactor: split god-module into core, mixins, outputs, and cli subpackages by @alubbock in #94
- refactor: move tests to top-level tests/ package by @alubbock in #95
- fix: address all v2.0 review findings — code bugs, docs, config, README by @alubbock in #97
- docs: update README and docs by @alubbock in #98
- fix: cli return code is now first non-zero value by @alubbock in #99
- docs: further docs rewrites by @alubbock in #100
- docs: more CLI documentation, other tweaks by @alubbock in #101
- docs: fix minor issues by @alubbock in #102
- docs: rework long changelog item into list by @alubbock in #103
- docs: note about shell script usage with CLI by @alubbock in #104
- docs: changelog fixes by @alubbock #105 in #105
Full Changelog: v1.1.0...v2.0.0