ommx.tracing#

Inspect the time spent inside an OMMX operation.

A span records the start and end of an instrumented operation. Nested spans show where time went within a parent operation. The parent includes its children's time. A single root span tells you only the total duration.

Capture one evaluation

>>> from ommx import Instance
>>> from ommx.tracing import capture_trace, render_text_tree, save_chrome_trace
>>> problem = Instance.minimize()
>>> x = problem.new_binary("x")
>>> problem.objective = x
>>> with capture_trace("evaluate candidate") as trace:
...     solution = problem.evaluate({x.id: 1})
>>> assert solution.objective == 1
>>> assert "evaluate candidate" in render_text_tree(trace)

Inspect the tree in a terminal or display the result in Jupyter. To view a timeline in Perfetto or another Chrome trace viewer, save the trace as JSON:

>>> from pathlib import Path
>>> from tempfile import TemporaryDirectory
>>> with TemporaryDirectory() as directory:
...     path = Path(directory) / "trace.json"
...     save_chrome_trace(trace, path)
...     assert path.stat().st_size > 0

To trace each call to a function, use traced(). The function still returns its normal result. On failure, the decorator attempts to save the trace and propagates the original exception.

>>> from ommx.tracing import traced
>>> with TemporaryDirectory() as directory:
...     path = Path(directory) / "evaluate.json"
...     @traced(output=path)
...     def evaluate(value):
...         return problem.evaluate({x.id: value})
...     assert evaluate(0).objective == 0 and path.is_file()

Trace one notebook cell

In a Jupyter kernel with the SDK installed, run this line in its own cell:

%load_ext ommx.tracing

Then start the cell you want to measure with the magic:

%%ommx_trace
from ommx import Instance
solution = Instance.minimize().evaluate({})

The output displays the span tree and a Chrome trace JSON link. Only instrumented operations produce child spans. If the cell raises an exception, the trace is displayed and the exception propagates.

For a solver breakdown, see ommx_highs_adapter.OMMXHighsAdapter. To keep a trace with an Experiment, see ommx.experiment.SealedRun.trace.

Export spans to an OpenTelemetry backend

Local capture works without an external exporter. To send the same spans to an OpenTelemetry collector, install opentelemetry-exporter-otlp-proto-grpc in the SDK environment. Set OTEL_EXPORTER_OTLP_ENDPOINT to the collector's gRPC endpoint and supply credentials if it requires them.

Configure the SDK provider once at the start of the process or notebook kernel, before importing OMMX. If the application already owns a provider, add the span processor to that provider instead. The following standalone script requires a running collector:

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)

from ommx import Instance
from ommx.tracing import capture_trace

with capture_trace("evaluate candidate") as captured:
    Instance.minimize().evaluate({})
print(captured)
provider.shutdown()

The local span collector attaches to the active provider, so the block appears in both the local tree and the external backend. In this standalone script, shutdown flushes the provider at process exit. In a service, leave shutdown to the owner of the provider.

Classes#

TraceResult

Populated result of a capture_trace block.

capture_trace

Context manager that captures every OTel span inside the block.

Functions#

chrome_trace_json(result) → str

load_ipython_extension(ipython) → None

Register the %%ommx_trace cell magic on ipython.

render_text_tree(result) → str

Render result as a nested ASCII tree, one root per top-level span.

save_chrome_trace(result, path) → None

Write result as Chrome Trace JSON to path.

traced(…)

Decorator that runs the wrapped function under capture_trace.

unload_ipython_extension(ipython) → None

IPython extension hook, kept as a no-op.

Package Contents#

class TraceResult#

Populated result of a capture_trace block.

Filled in by capture_trace on __exit__ (including the exception path, so the caller can always inspect the trace even when the block raised).

classmethod from_otlp_protobuf(payload: bytes) → TraceResult#

Build a trace result from an OMMX trace payload.

otlp_protobuf() → bytes#

Return OTLP protobuf bytes stored in Experiment traces.

request: opentelemetry.proto.collector.trace.v1.trace_service_pb2.ExportTraceServiceRequest#
property spans: list[opentelemetry.proto.trace.v1.trace_pb2.Span]#

Flattened OTLP protobuf spans exported in this trace result.

class capture_trace(name: str = _DEFAULT_ROOT_SPAN_NAME, tracer_name: str = _TRACER_NAME)#

Context manager that captures every OTel span inside the block.

The root span is started with an explicit empty OTel Context so each block gets its own fresh trace_id regardless of any ambient spans — the collector keys captures by trace_id, so without this guard sibling spans from unrelated instrumentation would bleed into the result.

chrome_trace_json(result: TraceResult) → str#
load_ipython_extension(ipython: InteractiveShell) → None#

Register the %%ommx_trace cell magic on ipython.

Invoked by IPython when the user runs %load_ext ommx.tracing. Safe to call more than once — later calls are no-ops since IPython dedupes magics by name.

render_text_tree(result: TraceResult) → str#

Render result as a nested ASCII tree, one root per top-level span.

The tree preserves parent→child relationships as recorded by OTel. Siblings are sorted by start time so the output reflects execution order.

save_chrome_trace(result: TraceResult, path: str | Path) → None#

Write result as Chrome Trace JSON to path.

Overwrites any existing file. The UTF-8 encoding matches the JSON spec and is what Perfetto / speedscope / chrome://tracing all accept.

traced(func: Callable[..., Any]) → Callable[..., Any]#
traced(*, name: str | None = ..., output: str | Path | None = ...) → Callable[[Callable[..., Any]], Callable[..., Any]]

Decorator that runs the wrapped function under capture_trace.

Supports all three call shapes:

@traced
def process(): ...

@traced()
def process(): ...

@traced(name="build_qubo", output="qubo.json")
def process(): ...

If output is given and a trace result is available, saving Chrome Trace JSON to that path is attempted after capture ends:

  • When the wrapped function returns normally, saving completes before its return value is passed to the caller. A save error propagates instead of returning that value.

  • When the wrapped function raises, saving is best-effort. Save errors derived from Exception are suppressed so that the original exception is re-raised unchanged. A trace file is not guaranteed to exist or be complete. If capture fails before a result is available, no save is attempted.

These rules apply to both synchronous and async def functions. For an async function, capture surrounds the awaited execution. With output=None, no trace file is saved automatically.

If name is omitted, the span is named after the function (fn.__qualname__) so traces from multiple decorated functions are easy to tell apart in the rendered tree.

unload_ipython_extension(ipython: InteractiveShell) → None#

IPython extension hook, kept as a no-op.

Removing a previously-registered magic leaves the shell in an awkward state (the name still resolves for tab completion), and there is no user-observable state to tear down — the collector sheds its entries on retrieval already.