Use metadata effectively
Problem: You want context on every measurement (e.g. request id, stage, experiment id) without repeating it in every Timer() call.
Idea: Metadata is attached to each measurement. You can mutate measurement.metadata in on_start (or inside the block) to add or change keys for that run. Each measurement gets its own copy of the initial metadata when the run starts, so mutating it in on_start only affects that measurement.
Why this works
Each measurement gets its own deep copy of the metadata dict, so mutations in on_start or the block affect only that run. See Guide: Metadata for copy and isolation rules.
Example: add run context in on_start
Omit metadata (or pass a dict); an empty dict is the default when you pass None. Fill it per run in on_start from context vars or thread-local storage:
from contextvars import ContextVar
from timerun import Timer
request_id_ctx: ContextVar[str] = ContextVar("request_id", default="")
def add_request_id(m):
m.metadata["request_id"] = request_id_ctx.get()
with Timer(on_start=add_request_id) as m:
pass # your code
# m.metadata now includes "request_id" for this run
print(m.metadata) # e.g. {"request_id": "req-abc"}
Example: set tags inside the block
When context is fixed at the start of the run (request id, stage), on_start is often clearer than mutating inside the block. Mutating m.metadata in the block is still valid when values depend on work you do inside the timed region (outcome, branch taken, or a value known only after some steps):
with Timer(metadata={"stage": "ingest"}) as m:
do_work()
if some_condition:
m.metadata["tag"] = "slow_path"
# m.metadata is {"stage": "ingest", "tag": "slow_path"} when relevant
Example: invocation count with a closure
Use a factory that returns an on_start callback with its own counter so each measurement gets a monotonic call number (e.g. for a decorated hot path):
from timerun import Timer
def make_invocation_callback():
count = 0 # (1)!
def set_invocation(m):
nonlocal count # (2)!
count += 1
m.metadata["invocation"] = count
return set_invocation # (3)!
on_start = make_invocation_callback()
for _ in range(3):
with Timer(on_start=on_start) as m:
pass # your code
# After each block, invocation is 1, 2, 3, ...; last m.metadata["invocation"] is 3
- Counter lives in the closure; each factory call gets its own independent sequence.
nonlocalupdates the enclosingcountso every invocation ofset_invocationsees the same running total.- Return the inner function so
Timerreceives a stableon_startcallback with shared state.
See also: Guide: Metadata for passing metadata={...} and copy rules.