Repository navigation
Conversation
…ng baseline - Add InstrumentedObjectStore that correlates requested byte ranges to Parquet file structures (footers, page indexes, bloom filters, data pages) - Add test_parquet_bloom_filter_io_baseline using public SessionContext SQL APIs - Use insta snapshot to capture and compare the storage I/O trace - Documents baseline on main where all candidate column Bloom filters are loaded upfront, establishing the target diff for incremental pruning (apache#25811)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split the Bloom filter I/O test into reusable setup code in tests/parquet/io/mod.rs and the test itself in tests/parquet/io/bloom_filter_io.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ushdown and TopK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The I/O snapshot tests only saw object store requests, so they could not say why a byte range was read. This adds tracing::debug! events with structured fields (offset_start, offset_end, column, row_group) at the parquet read sites, and a test-only tracing Layer that renders the events and the object store's request spans as a snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fixed set of fields limited what each call site could report. Call sites now record whatever fields fit, and the test layer prints every field. The scan emits spans for metadata load, page index load, Bloom filter load, row group and page pruning, column chunk fetches, and events for row filter evaluation and metadata cache hits. The test object store emits a span for every operation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI dropped spans and events at random when tests ran in parallel. tracing caches callsite interest across threads, so a test thread without a thread-local subscriber disabled callsites for the others. One global subscriber now writes into the capture that is active on the current thread. Also format the Cargo.toml files with taplo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alamb
left a comment
There was a problem hiding this comment.
Thank you @adriangb -- I really like this idea of instrumenting the parquet opener with key events to understand it better
However, I don't like the use of the tracing crate as I don't think everyone uses tracing (some use otel, for example, and we have our own thing at InfluxData)
I wonder if we could instead add our own tracing/span API in DataFusion , maybe something like
/// Receives scan events; default impl is a no-op
pub trait ParquetScanObserver: Debug + Send + Sync {
/// Called at a point of interest with structured fields
fn event(&self, event: ParquetScanEvent<'_>) {}
/// Begin a nested region; dropping the guard ends it
fn span(&self, span: ParquetScanSpan<'_>) -> Option<Box<dyn Any + Send>> { None }
}
pub enum ParquetScanEvent<'a> {
OpenFile { path: &'a str, size: u64 },
RowGroupsPruned { stage: PruneStage, before: usize, after: usize },
BloomFilterRead { column: &'a str, row_group: usize, range: Range<i64> },
RowFilterEvaluated { predicate: &'a dyn PhysicalExpr, rows: usize, matched: usize },
}| Load page index into cached metadata | ||
| Read Parquet metadata (range=192..213) | ||
| ObjectStore::get (location=table.parquet) | ||
| Read byte range (range=192..213, parquet=OffsetIndex for column 'a' (rg 0)) |
There was a problem hiding this comment.
A nit is I would probably combine this into one line
ObjectStore::get (location=table.parquet)
Read byte range (range=192..213, parquet=OffsetIndex for column 'a' (rg 0))
into
ObjectStore::get (location=table.parquet) (range=192..213, parquet=OffsetIndex for column 'a' (rg 0))
| Metadata cache hit (has_page_index=true) | ||
| Open Parquet file (file=table.parquet, size=1402) | ||
| Load Parquet metadata | ||
| Metadata cache hit (has_page_index=true) |
There was a problem hiding this comment.
this is cool to show the metadata cache hit
| env_logger = { workspace = true } | ||
| glob = { workspace = true } | ||
| insta = { workspace = true } | ||
| tracing = { workspace = true } |
There was a problem hiding this comment.
this is my biggest hesitation -- to add this new tracing dependency (I know it is just a dev-dependency, but still I feel like it would be easy to accidentally turn into a real dependnecy, and not everyone uses the tracing crate
| Ok(ParquetOpenState::LoadMetadata( | ||
| prepared | ||
| .load() | ||
| .instrument(tracing::debug_span!("Load Parquet metadata")) |
There was a problem hiding this comment.
I really like the idea of instrumenting the parquet opener, but I don't like tying the instrumentation to the tracing crate
| parking_lot = { workspace = true } | ||
| parquet = { workspace = true } | ||
| tokio = { workspace = true } | ||
| tracing = { workspace = true } |
There was a problem hiding this comment.
I think this is a bad idea to pick up the tracing dependency
|
As an alternative, we could maybe even add a more general API for tracing to cover other important events (like the sql planning, analyzer, optimizer passes, etc) Something like #[non_exhaustive]
pub enum TraceEvent<'a> {
// ---- planning ----
/// Span: parsing SQL text into a statement
ParseSql { sql: &'a str },
/// Event: one analyzer rule ran
AnalyzerRule { rule: &'a str, plan: &'a dyn Display },
...
// ---- parquet scan ----
/// Event: the scan started opening a file
ParquetOpenFile { path: &'a str, size: u64 },
/// Span: footer metadata load; `cached` says whether the metadata cache hit
ParquetLoadMetadata { path: &'a str, cached: bool },
...
}
/// Registered once on the session; every subsystem reports to it
pub trait TraceObserver: Debug + Send + Sync {
...
}
// ---- registration: one place ----
let ctx = SessionContext::new_with_state(
SessionStateBuilder::new()
.with_trace_observer(Arc::new(MyObserver)) // stored on SessionState
.build(),
);
// SessionState::task_ctx() copies it onto TaskContext, like the function registry.Then we could instrument call sites like Planner // EXPLAIN VERBOSE and tracing consume the same points.
self.optimizer().optimize(plan, self, |plan, rule| {
observer.event(&TraceEvent::OptimizerRule { rule: rule.name(), pass, transformed, plan });
})?;or in the Parquet opener observer.event(&TraceEvent::ParquetOpenFile { path: &file_name, size });
...
let _guard = observer.span(&TraceEvent::ParquetRowGroupsPruned { .. }); // sync regio |
Main has the merged version of apache#27022. It describes multi-structure reads, sets the metadata size hint to the footer size, and clears the metadata cache before each query. This merge keeps those changes and ports them to the tracing-based harness, then regenerates the snapshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I understand the hesitation to bring in tracing. As you already pointed our right now this is a dev only dep so there is zero impact / direct decision being made about what event / tracing API Datafusion exposes more generally. I think we both agree that it is somewhat of a slippery slope, but only in the sense that it's easy to say "hey all of these operations are instrumented, why not just expose them to users!". That said, if we do want to discuss what should Datafusion offer as it's event / tracing implementation I see 3 options:
Thus my vote would be to:
|
Note
Draft prototype stacked on #27022. Only the last two commits are new; review those.
Which issue does this PR close?
Rationale for this change
The I/O tests in #27022 only see object store requests. They cannot say which scan step caused a request. The review suggested
tracing::debug!events at important places, so tests can snapshot planning and scan operations as well as I/O.What changes are included in this PR?
datafusion-datasource-parquetdepends ontracing(no default features). The scan emits spans and events at these places:core/tests/parquet/io/trace_capture.rs) is atracing_subscriberlayer. It renders spans and events as an indented tree and prints every recorded field. The test object store emits a span for every operation and names the Parquet structure that each byte range covers.The tests look like this:
Two findings came out of this:
HEADrequest. The store reports aGETwithhead: trueas a read of the whole file.What is the testing strategy for this PR?
The 14 snapshot tests in
datafusion/core/tests/parquet/io/were converted to the new API and regenerated withcargo insta accept.The capture uses
tracing::subscriber::set_default, which only applies to the current thread. This works because#[tokio::test]runs every task on the test thread.Are there any user-facing changes?
There is a new
tracingdependency indatafusion-datasource-parquet. Events are atdebuglevel and cost almost nothing when no subscriber is installed. The API is unchanged.🤖 Generated with Claude Code