Skip to content

feat: add .piot.probe() pass-through telemetry source - #34

Merged
ptomecek merged 4 commits into
mainfrom
feat/probe-source
Sep 1, 2026
Merged

feat: add .piot.probe() pass-through telemetry source#34
ptomecek merged 4 commits into
mainfrom
feat/probe-source

Conversation

@ptomecek

@ptomecek ptomecek commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

What

A result-preserving pass-through IO sourcelf.piot.probe(description="...") — that you can drop at any point in a lazy pipeline to get one io_source.execute[probe] OpenTelemetry span for the data flowing through that point:

  • polars_io_tools.next_elapsed_total_ms — pull latency of the sub-plan below the probe;
  • polars_io_tools.total_rows — rows through it;
  • polars_io_tools.explain_detail — the description (e.g. the stage being measured).

Both predicate and projection pushdown are forwarded to the input, so the probe is not an optimization barrier for them. It reuses the instrumentation added in #30, so the span is a no-op without an OpenTelemetry SDK and honours OTEL_PYTHON_INSTRUMENTATION_POLARS_IO_TOOLS_ENABLED=false.

Why not debug?

.piot.debug() is already an instrumented pass-through, but it (a) logs/prints on every execution and (b) computes self.explain() unconditionally — too heavy to leave in a pipeline. probe is debug minus that baggage.

Usage

lf = pl.scan_parquet(path).piot.probe(description="parquet:scan")
df = lf.select("id", "x").collect(engine="streaming")
# -> span io_source.execute[probe], next_elapsed_total_ms = read time, total_rows = rows read

Parquet profiling

Verified locally that pushdown reaches the parquet reader through the probe: a column projection prunes the columns actually read (the span's pull time drops accordingly), and a predicate is pushed into the scan (only matching rows flow through, reflected in total_rows). So it is genuinely useful for comparative questions — is pushdown working? did column pruning help? is the predicate reaching the reader?

Caveats. next_elapsed_total_ms is caller-observed pull latency, so it undercounts absolute read time when the reader decodes on background threads. And the probe inserts a PythonScan re-entry node, so it adds overhead — on the order of milliseconds, growing with the number of rows through it (a per-batch Rust↔Python round trip; the span emission itself is free). That is negligible relative to any non-trivial read but can dominate a small, fully-cached one. Prefer it for comparative profiling rather than absolute scan-time attribution.

Tests

tests/io_sources/test_lazy_probe.py: result-preserving pass-through, exactly one span with explain_name=probe + description, and predicate pushdown reaching the probe. Full io_sources suite: 1319 passed.

Add a result-preserving pass-through IO source, `.piot.probe(description=...)`,
that can be inserted at any point in a lazy pipeline to emit one
`io_source.execute[probe]` OpenTelemetry span for the data flowing through
that point. `next_elapsed_total_ms` is the pull latency of the sub-plan
below the probe and `total_rows` is the rows through it; both predicate and
projection pushdown are forwarded to the input, so the probe is not an
optimization barrier for them.

It is modelled on `debug` but omits the per-execution logging and the
unconditional `explain()` call, so it is cheap enough to leave in a
pipeline. Like every source registered via `register_io_source_with_is_pure`,
the span is a no-op unless an OpenTelemetry SDK is configured, and it can be
disabled with `OTEL_PYTHON_INSTRUMENTATION_POLARS_IO_TOOLS_ENABLED=false`.

Note that the probe inserts a `PythonScan` node that re-enters Polars via
`collect_lf_in_io_source`, so it is a measurement point rather than a
zero-cost tap.

Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Test Results

1 461 tests  +7   1 413 ✅ +7   36s ⏱️ -4s
    2 suites ±0      48 💤 ±0 
    2 files   ±0       0 ❌ ±0 

Results for commit 2f63d5f. ± Comparison against base commit 363a4f8.

♻️ This comment has been updated with latest results.

A pass-through cannot know whether its input is pure. Registering it as
`is_pure=True` (the default) let Polars deduplicate repeated uses of the
same probe and collapse executions, changing results for an impure input.
Register `is_pure=False` so every occurrence is measured and results are
preserved. Also pass the schema as a callable so it is resolved lazily
rather than forced at construction, and note the single-worker
full-materialization caveat in the docstring. Adds an impurity regression
test plus n_rows-pushdown and exact-order pass-through coverage.

Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com>
The probe's purity is its input's purity, which it cannot infer. A
hardcoded `is_pure=False` is safe but hides a pure input's dedup-ability:
Polars treats the probe as impure and re-executes the (pure) sub-plan for
every repeated use instead of collapsing it. Expose `is_pure` (keyword-only,
default `False`) so a caller with a known-pure input can opt into dedup
(one execution, one span). Adds a forwarding test.

Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com>
`debug` is side-effecting -- it logs (or prints) on every execution -- so
registering it as `is_pure=True` (the previous default) let Polars
deduplicate repeated uses and drop log output the caller asked for. Add
`is_pure` (keyword-only, default `False`), mirroring `probe`, so repeated
uses are not collapsed by default; a caller with a known-pure input can opt
into dedup. Adds a test that the log-side-effect runs per occurrence.

Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com>
@ptomecek
ptomecek marked this pull request as ready for review September 1, 2026 14:10
@ptomecek
ptomecek merged commit 61ecc4e into main Sep 1, 2026
6 checks passed
@ptomecek
ptomecek deleted the feat/probe-source branch September 1, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants