Skip to content

fix: format Bytes-category custom metrics with byte units - #24218

Open
DevShiba wants to merge 14 commits into
apache:mainfrom
DevShiba:fix/bytes-category-metric-display
Open

fix: format Bytes-category custom metrics with byte units#24218
DevShiba wants to merge 14 commits into
apache:mainfrom
DevShiba:fix/bytes-category-metric-display

Conversation

@DevShiba

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

output_bytes=7.5 GB
bytes_scanned=1.26 B

bytes_scanned is actually 1.26 GB, but reads as "1.26 billion" because it's formatted with human_readable_count instead of human_readable_size.

Root cause: MetricValue::Count/Gauge are the generic variants used by operator-defined metrics with no dedicated variant of their own (bytes_scanned, stream_memory_usage, bytes_written). Unlike the metrics with dedicated variants (OutputBytes, SpilledBytes, CurrentMemoryUsage, PeakMemoryUsage), MetricValue itself does not carry the metric's category - only the wrapping Metric struct does. So Display for the generic Count/Gauge arms has no way to know it's holding a byte measurement and always falls back to human_readable_count's 1000-based K/M/B/T units, even when the metric was declared with .with_category(MetricCategory::Bytes).

This also affects stream_memory_usage (a Gauge) and bytes_written (a Count), which share the same root cause but weren't mentioned in the original report - found by grepping for every .with_category(MetricCategory::Bytes) call site paired with a generic .counter()/.gauge()/.global_counter() builder.

What changes are included in this PR?

Moves the format decision into Display for Metric, which does have both the value and the category, instead of Display for MetricValue, which doesn't. A generic Count/Gauge tagged Bytes now uses human_readable_size (1024-based KB/MB/GB/TB) like the dedicated byte variants already do. Rows/Timing-category and uncategorized generic metrics are untouched - human_readable_count was already correct for them.

Updated the 14 hardcoded bytes_scanned expected values across two sqllogictest files to match the corrected format. Did this by hand rather than via --complete, since --complete also baked in non-deterministic timing/path values that those tests intentionally wildcard with <slt:ignore> - --complete's output would have made the tests flaky.

Are these changes tested?

Yes. Added two unit tests (test_display_generic_count_respects_bytes_category, test_display_generic_gauge_respects_bytes_category) covering both the Count and Gauge cases, plus confirming a Rows-category generic counter is untouched. Ran the full sqllogictest suite (502/502 files) to confirm no other fixture was missed, and cargo check --workspace --all-targets.

Are there any user-facing changes?

Yes: EXPLAIN ANALYZE output for bytes_scanned, stream_memory_usage, and bytes_written now shows correct byte units (e.g. 1.26 GB) instead of count units (e.g. 1.26 B, misleadingly meaning "1.26 billion").

MetricValue::Count/Gauge are the generic variants used by
operator-defined metrics with no dedicated enum variant of their own
(e.g. bytes_scanned, stream_memory_usage, bytes_written). Unlike the
few metrics with dedicated variants (OutputBytes, SpilledBytes,
CurrentMemoryUsage, PeakMemoryUsage), MetricValue itself does not
carry the metric's category - only the wrapping Metric struct does -
so Display for MetricValue's generic Count/Gauge arms had no way to
know they held a byte measurement and always fell back to
human_readable_count's 1000-based K/M/B/T units.

Example from EXPLAIN ANALYZE before this change:

  output_bytes=7.5 GB
  bytes_scanned=1.26 B   <- actually 1.26 GB, using count units (billion)

Move the decision into Display for Metric, which does have both the
value and the category, so a Bytes-category Count/Gauge now uses
human_readable_size (1024-based KB/MB/GB/TB) like the dedicated byte
variants already do. Rows/Timing-category and uncategorized generic
metrics are untouched, since human_readable_count was already correct
for them.

Also fixes the same bug for stream_memory_usage and bytes_written,
which share the same root cause but weren't mentioned in the original
report.

Updated the 14 hardcoded bytes_scanned expected values across two
sqllogictest files to match the corrected format. Deliberately did
this by hand rather than via `--complete`, which would have also
baked in non-deterministic timing values that the tests intentionally
wildcard with <slt:ignore>.

Closes apache#24203
@github-actions github-actions Bot added physical-expr Changes to the physical-expr crates sqllogictest SQL Logic Tests (.slt) labels Aug 10, 2026

@getChan getChan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes: please consider fixing the metric registrations instead of changing the formatting of all generic Count/Gauge values based on MetricCategory::Bytes.

As discussed in issue #24203 (#24203 (comment)), bytes_scanned appears to be using the wrong metric type and should be represented by a dedicated byte-oriented metric. The same consideration likely applies to bytes_written and stream_memory_usage.

Changing Display for Metric to reinterpret generic Count/Gauge values based on the category broadens the behavior of operator-defined metrics and makes the metric type/category relationship less explicit. Could we instead add or use dedicated byte-oriented MetricValue/builder variants for these metrics, while leaving generic Count/Gauge formatting unchanged?

…cated byte-typed metrics

Address review feedback: instead of reinterpreting a Bytes-category
generic Count/Gauge at Display time, register the three affected
metrics as their own dedicated MetricValue variants, matching the
existing OutputBytes/SpilledBytes/PeakMemoryUsage precedent.

Adds two new MetricValue variants:
- BytesCount { name, count }: a named, byte-formatted Count, for
  bytes_scanned and bytes_written.
- BytesGauge { name, gauge }: a named, byte-formatted Gauge, for
  stream_memory_usage.

BytesGauge is deliberately not folded into the existing
PeakMemoryUsage variant even though they're structurally identical.
PeakMemoryUsage's existing call sites (peak_mem_used, max_mem_used,
build_mem_used) are all monotonically-increasing accumulators, so
"peak" is accurate for them. stream_memory_usage is `.set()` to a
capacity that can shrink as a sliding window prunes old rows - it is
not a peak, and mislabeling it as one would be misleading to anyone
reading EXPLAIN output or the enum variant itself.

Also updates the two places that previously matched on MetricValue
exhaustively and would otherwise have silently mis-handled the new
variants:
- MetricsSet::sum_by_name(), used by bytes_scanned's own tests via
  sum_by_name("bytes_scanned").
- display.rs's JSON EXPLAIN metric_value_to_json(): without an
  explicit arm, the new variants would have fallen through to the
  string-fallback branch, turning bytes_scanned's JSON output from a
  number into a string like "3.0 GB" - a real behavior change for any
  JSON EXPLAIN consumer.
- The FFI crate's FFI_MetricValue, which documents that new variants
  must be appended at the end since variant order is part of its
  stable ABI - done here, with matching conversions and round-trip
  test coverage in both directions.

Display for Metric goes back to the plain `write!(f, "{}", self.value)`
it had before the previous commit - MetricValue no longer needs help
from Metric's category to know it's holding bytes.

Rendered EXPLAIN ANALYZE output is byte-for-byte unchanged (reran the
full 502-file sqllogictest suite to confirm), so none of the
previously-updated .slt fixtures needed further changes.
@DevShiba

Copy link
Copy Markdown
Contributor Author

Good call, thanks for the pointer to #24203's discussion - pushed a rework.

bytes_scanned, bytes_written, and stream_memory_usage are now registered as two new dedicated MetricValue variants instead of being reinterpreted at Display time based on category:

  • BytesCount { name, count } - a named, byte-formatted Count (for bytes_scanned, bytes_written), used via a new MetricBuilder::bytes_counter/global_bytes_counter.
  • BytesGauge { name, gauge } - a named, byte-formatted Gauge (for stream_memory_usage), used via a new MetricBuilder::bytes_gauge.

This follows the exact shape OutputBytes/SpilledBytes/PeakMemoryUsage already use, and Display for Metric goes back to the plain write!(f, "{}", self.value) it had before - MetricValue no longer needs help from Metric's category to know it's holding bytes.

One design note: I didn't fold stream_memory_usage into the existing PeakMemoryUsage variant, even though they're structurally identical. PeakMemoryUsage's current call sites (peak_mem_used, max_mem_used, build_mem_used) are all monotonically-increasing accumulators, so "peak" is accurate for them. stream_memory_usage is .set() to a capacity that can shrink as a sliding window prunes rows - it's a live/current value, not a peak, so reusing that variant's name for it would be misleading to anyone reading the enum or debugging via EXPLAIN. Happy to fold them together if you'd rather keep the surface area smaller and treat "peak" loosely, but figured I'd flag the reasoning rather than silently pick one.

Also had to update two other places that matched on MetricValue exhaustively, since the compiler doesn't let a match silently ignore new variants:

  • MetricsSet::sum_by_name, which bytes_scanned's own tests rely on via sum_by_name("bytes_scanned").
  • display.rs's JSON EXPLAIN metric serialization - without an explicit arm there, the new variants would've fallen through to a string fallback, turning bytes_scanned's JSON output from a number into a string like "3.0 GB", which would've been a real regression for any JSON EXPLAIN consumer.
  • The datafusion-ffi crate's FFI_MetricValue also mirrors MetricValue across an ABI boundary and documents that new variants must be appended at the end (variant order is part of its stable ABI) - done that, with matching conversions and round-trip test coverage added in both directions.

Reran the full sqllogictest suite - the rendered EXPLAIN ANALYZE text is byte-for-byte unchanged, so none of the .slt fixture updates from the previous commit needed to change.

@github-actions github-actions Bot added datasource Changes to the datasource crate ffi Changes to the ffi crate physical-plan Changes to the physical-plan crate labels Aug 10, 2026
@kosiew

kosiew commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

hi @DevShiba
Can you resolve the merge conflicts?

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Aug 18, 2026
…metric-display

# Conflicts:
#	datafusion/sqllogictest/test_files/parquet_nested_schema_pruning.slt
@DevShiba

Copy link
Copy Markdown
Contributor Author

@kosiew Done - merged upstream/main in and resolved the one real conflict in parquet_nested_schema_pruning.slt. Main had landed a schema-pruning improvement in the meantime (unions the read leaves for disjoint cast targets instead of falling back to a full-column read), which changed bytes_scanned for one of the queries in that file (219 → 148 bytes) and added two new test cases. Kept main's new queries/values and applied this PR's byte-unit formatting on top of them.

Also picked up the 3 CI failures that showed up on the first run (cargo test (amd64), cargo test hash collisions (amd64), cargo test 'extended_tests' (amd64)) - those were the same conflict, since the branch was 114 commits behind and hadn't been tested against the current bytes_scanned value from main's pruning change.

Re-verified after merging: full cargo check --workspace --all-targets, cargo fmt --check, the two directly-touched sqllogictest files, and the metrics/FFI unit tests all pass clean.

datafusion/core/src/datasource/file_format/parquet.rs's own
assert_bytes_scanned test helper pattern-matched MetricValue::Count
directly (bypassing MetricsSet::sum_by_name, which was already updated
for the new BytesCount variant), so it stopped finding the
bytes_scanned metric once its registration moved to
MetricValue::BytesCount, failing capture_bytes_scanned_metric in
cargo test (amd64), cargo test hash collisions, and cargo test
extended_tests - all three were the same helper, just exercised under
different feature-flag runs.
@github-actions github-actions Bot added the core Core DataFusion crate label Aug 18, 2026
Two more test helpers pattern-matched MetricValue::Count directly
instead of going through the already-fixed MetricsSet::sum_by_name,
each in a different crate:

- datafusion/core/tests/parquet/page_pruning.rs's cast_count_metric,
  which caused without_pushdown_filter to panic on .unwrap() (the
  actual CI failure).
- datafusion/datasource-parquet/src/opener/mod.rs's
  counter_metric_value, currently only exercised with a plain Count
  metric so it wasn't failing, but would have silently returned 0
  instead of the real value the moment it's used for bytes_scanned.

Did a full repo-wide sweep this time, not just the one failing site:
grepped every MetricValue::Count and MetricValue::Gauge match plus
every "bytes_scanned"/"bytes_written"/"stream_memory_usage" string
reference, and manually vetted each one against the new BytesCount/
BytesGauge variants. These two were the only remaining gaps.

Before pushing, ran the exact three commands the failing CI jobs use
(from .github/workflows/rust.yml and extended.yml) locally, twice:
  cargo test --profile ci --exclude datafusion-examples \
    --exclude ffi_example_table_provider --exclude datafusion-cli \
    --workspace --lib --tests --bins \
    --features serde,avro,json,backtrace,integration-tests,parquet_encryption,substrait
  (cd datafusion && cargo test --profile ci \
    --exclude datafusion-examples --exclude datafusion-benchmarks \
    --exclude datafusion-sqllogictest --exclude datafusion-cli \
    --workspace --lib --tests --features=force_hash_collisions,avro)
  cargo test --profile ci --exclude datafusion-examples \
    --exclude datafusion-benchmarks --exclude datafusion-cli \
    --workspace --lib --tests --bins \
    --features avro,json,backtrace,extended_tests,recursive_protection,parquet_encryption
All three: zero failures. Also ran the exact CI clippy script
(ci/scripts/rust_clippy.sh, -D warnings) and cargo fmt --check clean,
and confirmed the working tree has no stray files after the test runs
(the same check CI itself runs).
@codecov-commenter

codecov-commenter commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.77419% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.61%. Comparing base (33028d5) to head (106da63).

Files with missing lines Patch % Lines
datafusion/physical-plan/src/display.rs 0.00% 2 Missing ⚠️
datafusion/physical-expr-common/src/metrics/mod.rs 98.11% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #24218   +/-   ##
=======================================
  Coverage   81.61%   81.61%           
=======================================
  Files        1124     1124           
  Lines      412077   412160   +83     
  Branches   412077   412160   +83     
=======================================
+ Hits       336318   336392   +74     
- Misses      55949    55952    +3     
- Partials    19810    19816    +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DevShiba,

Thanks for working on this. The byte-specific metric variants look like the right direction, and the changes cover the main metric formatting paths well.

I found one FFI coverage gap that I think we should address before merging. Since the new variants extend the stable ABI representation, it would be good to verify them through a real cross-library round trip, not only the in-process conversion tests.

I also left one small non-blocking suggestion to cover the global byte counter builder used by ParquetSink.

name: SString,
gauge: u64,
},
BytesCount {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BytesCount and BytesGauge extend FFI_MetricValue, whose layout is documented as a stable ABI. The existing conversion tests and mock-foreign ExecutionPlan::metrics() round trip are useful, but I think we should also cover this through a real cross-library test.

Could you add a cdylib integration test that gets an ExecutionPlan containing both byte metric variants from the test library, then calls metrics() and verifies the variant, metric name, and value?

The existing ffi_execution_plan integration tests do not currently call metrics(), so they would not catch an ABI layout or transport issue specifically affecting these new variants.

// human_readable_size's >= 2x-tier threshold for GB (below that it
// falls back to a large MB value).
let three_gib = 3 * 1024 * 1024 * 1024;
let bytes_scanned =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small suggestion: could we add global_bytes_counter("bytes_written") to this display regression test as well? ParquetSink uses the global builder, while the current coverage exercises the partitioned bytes_counter path. It would be nice to have both builders covered here.

DevShiba and others added 2 commits August 19, 2026 11:09
Addresses kosiew's review on PR apache#24218:

- Add a real cdylib integration test (test_ffi_execution_plan_byte_metrics_cross_library)
  that registers genuine BytesCount/BytesGauge metrics (via the same
  MetricBuilder::bytes_counter/bytes_gauge production code uses) on a
  plan served from a separately loaded copy of the datafusion-ffi
  cdylib, then calls .metrics() through the real ForeignExecutionPlan
  vtable to confirm both variants survive an actual FFI_MetricValue
  round trip across the dylib boundary - not just the in-process From
  conversions already covered by physical_expr::metrics's roundtrip
  tests.
- New create_exec_with_byte_metrics factory function and
  ForeignLibraryModule entry, following the existing
  create_exec_with_statistics pattern.
- Also cover global_bytes_counter("bytes_written") in
  test_bytes_counter_and_gauge_use_byte_units, since ParquetSink uses
  the global (non-partitioned) builder while the existing coverage
  only exercised the partitioned bytes_counter path.

Verified: the exact three commands the previously-failing CI jobs run
(cargo test (amd64), hash collisions, extended_tests), the exact CI
clippy script, cargo fmt --check, and the full datafusion-ffi
integration-tests suite - all clean.
@DevShiba

Copy link
Copy Markdown
Contributor Author

@kosiew Thanks for the detailed review — pushed both.

Cross-library FFI round trip: added test_ffi_execution_plan_byte_metrics_cross_library in datafusion/ffi/tests/ffi_execution_plan.rs, following the same pattern as the existing test_ffi_execution_plan_partition_statistics_cross_library. It registers real BytesCount/BytesGauge metrics (via MetricBuilder::bytes_counter/bytes_gauge, the same production constructors) on a plan served through a separately loaded copy of the datafusion_ffi cdylib (new create_exec_with_byte_metrics factory + ForeignLibraryModule entry), then calls .metrics() on the resulting ForeignExecutionPlan — which crosses the real FFI vtable into that other library image — and asserts the rendered strings are still byte-formatted (bytes_scanned{partition=0}=1536.0 B, stream_memory_usage{partition=0}=2.0 KB). This is a genuine ABI round trip, not the in-process From conversion the existing physical_expr::metrics tests cover.

bytes_written coverage: added a global_bytes_counter("bytes_written") case to test_bytes_counter_and_gauge_use_byte_units, asserting bytes_written=3.0 GB — covers the un-partitioned builder path ParquetSink actually uses, alongside the existing partitioned bytes_counter coverage.

Verified locally before pushing: the exact commands the three previously-failing CI jobs run, the exact ci/scripts/rust_clippy.sh script, cargo fmt --check, and the full datafusion-ffi --features integration-tests suite — all clean.

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DevShiba, thanks for working on this. The byte metric round-trip coverage is heading in the right direction, and the global bytes_written builder display case looks addressed.

I found two things that still need changes before this is ready. The new field on ForeignLibraryModule changes a public #[repr(C)] layout and matches the SemVer failure reported by the bot. Also, the cdylib test currently checks only rendered strings, so it does not verify that the metric variants themselves survive the ABI round trip.

I left inline comments with more detail.

Comment thread datafusion/ffi/src/tests/mod.rs Outdated

pub create_exec_with_statistics: extern "C" fn() -> FFI_ExecutionPlan,

pub create_exec_with_byte_metrics: extern "C" fn() -> FFI_ExecutionPlan,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ForeignLibraryModule is public and #[repr(C)], so adding a field here shifts every field that follows it. A consumer compiled against the previous layout could then call the wrong factory function pointers. This also appears to be exactly what the constructible_struct_adds_field SemVer check is reporting.

Even though this is test support, it is exposed through the public integration-tests feature and is the cross-library ABI surface these tests rely on. Could we avoid extending this struct in the current release API? A separate exported symbol or loading path for this test factory would keep the existing layout intact.

// that boundary - not just the in-process From conversions covered
// by physical_expr::metrics's roundtrip tests.
let metrics = plan.metrics().expect("plan should report metrics");
let rendered: Vec<String> = metrics.iter().map(|m| m.to_string()).collect();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we verify the metric variants explicitly here as well? Comparing the rendered strings confirms the display output, but it does not prove that the BytesCount and BytesGauge discriminants survived the ABI round trip.

I think this test should match metric.value() against MetricValue::BytesCount { name, count } and MetricValue::BytesGauge { name, gauge }, then assert both the names and numeric values. Keeping the display assertions too would still be useful.

Two follow-ups from review on PR apache#24218:

1. The previous commit added create_exec_with_byte_metrics as a new
   field on ForeignLibraryModule. That struct is public, #[repr(C)],
   and has no private/gated constructor, so every field is part of its
   exhaustive-construction ABI surface even though the struct only
   exists behind the integration-tests feature - exactly what
   cargo-semver-checks's constructible_struct_adds_field lint flagged.
   Reworked the new test factory as its own top-level exported symbol
   (datafusion_ffi_test_create_exec_with_byte_metrics, loaded via a new
   get_byte_metrics_exec() in tests/utils.rs, mirroring how
   get_module() already loads datafusion_ffi_get_module) instead of
   extending the struct. ForeignLibraryModule's fields are now
   byte-for-byte identical to the pre-fix commit.

2. test_ffi_execution_plan_byte_metrics_cross_library previously only
   compared the Display-rendered strings, which doesn't prove the
   BytesCount/BytesGauge discriminants themselves - as opposed to some
   other variant that happens to render the same text - survived the
   FFI round trip. Now matches metric.value() against
   MetricValue::BytesCount{name, count}/BytesGauge{name, gauge}
   explicitly, asserting both the name and the numeric value, in
   addition to (not instead of) the existing Display assertions.

Verified: full datafusion-ffi --features integration-tests suite, the
exact three commands the CI jobs run (cargo test (amd64), hash
collisions, extended_tests), the exact CI clippy script, and cargo fmt
--check - all clean. Confirmed via git diff against the pre-fix commit
that ForeignLibraryModule has zero field additions/removals.
@DevShiba

Copy link
Copy Markdown
Contributor Author

@kosiew Good catches on both — pushed fixes.

SemVer/ABI: reworked create_exec_with_byte_metrics as its own top-level exported symbol (datafusion_ffi_test_create_exec_with_byte_metrics), loaded via a new get_byte_metrics_exec() in tests/utils.rs that mirrors exactly how get_module() already loads datafusion_ffi_get_module (separate libloading::Library, lib.get(...), leak to keep it loaded). ForeignLibraryModule no longer has the new field — confirmed via git diff against the pre-fix commit that its fields are now byte-for-byte identical to before this PR touched it.

Discriminant coverage: the test now matches metric.value() against MetricValue::BytesCount { name, count } / MetricValue::BytesGauge { name, gauge } explicitly and asserts both the name and the numeric value, on top of (not instead of) the existing Display-string assertions.

Verified locally before pushing: full datafusion-ffi --features integration-tests suite, the exact three commands the previously-failing CI jobs run, the exact ci/scripts/rust_clippy.sh script, and cargo fmt --check — all clean.

@kosiew

kosiew commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

@DevShiba
Can you resolve the merge conflicts?

…metric-display

# Conflicts:
#	datafusion/sqllogictest/test_files/dynamic_filter_pushdown_config.slt
#	datafusion/sqllogictest/test_files/parquet_nested_schema_pruning.slt
…rge commit

The previous merge commit still had the literal PLACEHOLDER text in
place of the two empirically-confirmed values (bytes_processed=1147.0 B
and bytes_scanned=75.0 B) - staged before running sed to fill them in,
so the merge commit captured the stale index instead of the corrected
working tree. Caught by re-running git status/diff against HEAD before
pushing, not by CI.
@DevShiba

Copy link
Copy Markdown
Contributor Author

@kosiew Done — resolved. main had moved 59 commits since the last sync, with two real content conflicts in dynamic_filter_pushdown_config.slt and parquet_nested_schema_pruning.slt (both from further schema-pruning work landing upstream, same as last time).

Also found something worth flagging: main picked up a brand new bytes_processed metric (datafusion/datasource-parquet/src/metrics.rs) in the meantime, registered via the plain .counter(...) builder + MetricCategory::Bytes — the exact same bug this PR fixes, just introduced independently in a different PR after this one was opened. Switched it to .bytes_counter(...) and confirmed the corrected value (1147.0 B) against a real test run rather than computing it by hand.

Verified after merging (twice, since I initially staged the sqllogictest fixture fix before running the sed that filled in the confirmed values, so my first merge commit was wrong until I caught it against git diff HEAD): the exact three commands the CI jobs run, the exact clippy script, cargo fmt --check, the full datafusion-ffi --features integration-tests suite, and the two directly-touched sqllogictest files — all clean against the actual committed state, not just the working tree.

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DevShiba,

Thanks for working through the earlier feedback. The cross-library FFI coverage, explicit variant assertions, global_bytes_counter("bytes_written") coverage, and ForeignLibraryModule ABI layout concerns all look addressed now.

There is still one blocking issue around SemVer compatibility. MetricValue is a public exhaustive enum, so adding BytesCount and BytesGauge is a breaking change for downstream users that exhaustively match on it. cargo-semver-checks reports this as enum_variant_added.

Since this PR targets main, where the workspace version is still 55.0.0, and 55.0.0 has already been released from branch-55 in #24385, I think we need to avoid changing this public enum in the current release line.

A compatible approach would be to keep the existing MetricValue::Count and MetricValue::Gauge variants, keep MetricCategory::Bytes, and restore the category-aware display handling so byte metrics render through human_readable_size. The cdylib test can then assert the transported Bytes category, the generic variant/name/value, and the byte-formatted display output.

Requesting changes for that remaining issue. Thanks!

/// `bytes_scanned`, `bytes_written`). Like [`Self::Count`], but always
/// displayed with [`human_readable_size`]'s 1024-based byte units
/// (KB/MB/GB/TB) instead of [`human_readable_count`]'s 1000-based units.
BytesCount {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is still a SemVer-breaking change. MetricValue is public and exhaustive, so adding BytesCount and BytesGauge breaks downstream exhaustive matches. cargo-semver-checks reports this as enum_variant_added.

Because this PR targets main, where the workspace version is still 55.0.0, and 55.0.0 has already been released from branch-55 in #24385, we should avoid adding new variants to this enum here.

Could we keep MetricValue::Count and MetricValue::Gauge, retain MetricCategory::Bytes, and restore the category-aware display branch so generic byte metrics use human_readable_size? That should preserve the byte-unit behavior without changing the public Rust enum or the FFI_MetricValue ABI.

The cdylib test could then check the transported Bytes category, the generic variant/name/value, and the byte-formatted display output.

…splay

MetricValue is a public, already-released exhaustive enum (55.0.0), so
adding BytesCount/BytesGauge as new variants is a SemVer-breaking change
(cargo-semver-checks: enum_variant_added) for any downstream crate that
exhaustively matches on it.

Reverts to the original design: bytes_counter/bytes_gauge/
global_bytes_counter still tag their metric with MetricCategory::Bytes,
but construct generic Count/Gauge variants instead of dedicated ones.
Display for Metric is restored to check the category and render through
human_readable_size when it's Bytes, falling back to the plain
Display for MetricValue otherwise. FFI_MetricValue is reverted to its
exact pre-existing shape, and the cross-library FFI test now asserts the
transported Bytes category, the generic variant/name/value, and the
byte-formatted display output separately instead of matching on a
dedicated variant discriminant.
@DevShiba

Copy link
Copy Markdown
Contributor Author

@kosiew Done — reverted the BytesCount/BytesGauge enum-variant approach entirely and restored the original category-aware Display for Metric design, per your review:

  • MetricValue is back to its exact pre-PR shape (Count/Gauge/PeakMemoryUsage only, no new variants) — no enum_variant_added SemVer break.
  • bytes_counter/bytes_gauge/global_bytes_counter in MetricBuilder still tag the metric with MetricCategory::Bytes, but now construct generic Count/Gauge instead of dedicated variants.
  • Display for Metric checks metric_category == Some(MetricCategory::Bytes) and renders through human_readable_size for Count/Gauge in that case, falling back to Display for MetricValue otherwise (which is unchanged from before this PR touched it).
  • FFI_MetricValue in datafusion-ffi is reverted to its exact original shape — no new variants, no ABI change.
  • The cross-library FFI integration test (test_ffi_execution_plan_byte_metrics_cross_library) now asserts the transported Bytes category, the generic Count/Gauge variant/name/value, and the byte-formatted Display output separately, instead of matching on a dedicated variant discriminant — confirms the category survives the FFI round-trip correctly.
  • Updated test_bytes_counter_and_gauge_use_byte_units to assert the new (and correct) invariant: any Count/Gauge tagged MetricCategory::Bytes is byte-formatted, and one with no category (or a different category) is not.

Verified: cargo fmt --check, the full ci/scripts/rust_clippy.sh (-D warnings), the datafusion-ffi --features integration-tests suite (including the rewritten cross-library test), and all three CI test-job commands from rust.yml/extended.yml (workspace, force_hash_collisions, and extended_tests feature sets) all pass clean against the current committed state, merged up to date with main.

@github-actions github-actions Bot removed core Core DataFusion crate auto detected api change Auto detected API change labels Aug 24, 2026

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DevShiba,

Thanks for the follow-up. I reviewed the latest changes and all of my previous comments have been addressed.

The SemVer and ABI concern looks resolved. MetricValue and FFI_MetricValue keep their released enum shapes, and ForeignLibraryModule remains layout-compatible.

I also like the additional cross-library coverage. The cdylib test now checks the metric category, generic variant, name, value, and byte-formatted display after metrics() crosses the library boundary. The global bytes_written builder path is covered as well.

I did not find any new issues in the follow-up changes.

Validation reviewed:

  • cargo test -p datafusion-ffi --features integration-tests ffi_execution_plan_byte_metrics_cross_library
  • cargo test -p datafusion-physical-expr-common test_bytes_counter_and_gauge_use_byte_units

Thanks again for working through the feedback. This looks good to me.

…metric-display

# Conflicts:
#	datafusion/physical-expr-common/src/metrics/mod.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

datasource Changes to the datasource crate ffi Changes to the ffi crate physical-expr Changes to the physical-expr crates physical-plan Changes to the physical-plan crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistent Explain units

4 participants