Skip to content

arrow: implement otel-arrow-dfe-pdata-views MetricsView over ArrowDecoder - #31

Open
bwplotka wants to merge 3 commits into
bwplotka/phase2-family-assemblyfrom
bwplotka/phase4-pdata-views
Open

bwplotka wants to merge 3 commits into
bwplotka/phase2-family-assemblyfrom
bwplotka/phase4-pdata-views

Conversation

@bwplotka

@bwplotka bwplotka commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Stacked on prometheus/scrape.rs#19.
Applies the benchfmt.rs and raw benchstat reporting rules from prometheus/scrape.rs#32.
Fixes prometheus/scrape.rs#28.

Implements the zero-copy otel-arrow-dfe-pdata-views trait hierarchy, MetricsView and its child views, over ArrowDecoder's staged columnar buffers and string interners so downstream OTAP consumers can pass DecodedMetricsView directly into otel_arrow_dfe_pdata::encode::encode_metrics_otap_batch or inspect decoded scrapes without materializing RecordBatches.

Commits in order:

  1. 945b9b2 adds otel-arrow-dfe-pdata-views = "0.54" under the arrow feature, MSRV 1.85 with zero mandatory transitive dependencies, and records contiguous data-point, attribute, and exemplar index ranges in MetricTable, AttrTable, and ExemplarTable.
  2. aa9a1b9 adds DecodedMetricsView and its child view and iterator types in src/arrow/views.rs plus ArrowDecoder::decode_view, ArrowDecoder::view, and DecodeStream::next_view.
  3. 022bf8f verifies OTLP equivalence between ArrowDecoder::decode and encode_metrics_otap_batch(&view) across all metric types, translation policies, upstream fixtures, and streaming chunks, migrates benches/decode.rs to benchfmt.rs, and adds tests/otap-compat/benches/otap_encode.rs.

benches/decode.rs benchstat comparison between before.txt at 54d4ff7 and after.txt at 022bf8f:

goos: linux
goarch: amd64
pkg: expfmt
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
                                                             │ before.txt  │             after.txt              │
                                                             │   sec/op    │    sec/op     vs base              │
Decode/case=alltypes.237mfs.nometa/mode=decode-2               933.3µ ± 2%   924.7µ ±  2%       ~ (p=0.310 n=6)
Decode/case=alltypes.237mfs.nometa/mode=stream_16k-2           931.8µ ± 3%   948.0µ ±  4%       ~ (p=0.485 n=6)
Decode/case=alltypes.237mfs.nometa/mode=stream_256-2           975.0µ ± 2%   963.7µ ±  2%       ~ (p=0.485 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=decode-2        320.7µ ± 2%   332.6µ ± 23%  +3.71% (p=0.026 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_16k-2    328.6µ ± 2%   333.2µ ±  5%       ~ (p=0.132 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256-2    335.3µ ± 6%   345.1µ ± 10%       ~ (p=0.065 n=6)
Decode/case=alltypes.237mfs/mode=decode-2                      1.116m ± 3%   1.126m ±  2%       ~ (p=0.485 n=6)
Decode/case=alltypes.237mfs/mode=stream_16k-2                  1.132m ± 3%   1.140m ± 17%       ~ (p=0.394 n=6)
Decode/case=alltypes.237mfs/mode=stream_256-2                  1.169m ± 1%   1.195m ± 12%  +2.26% (p=0.015 n=6)
Decode/case=alltypes.237mfs.nometa/mode=decode_view-2                        902.0µ ±  1%
Decode/case=alltypes.209mfs.counter_gauge/mode=decode_view-2                 310.6µ ±  2%
Decode/case=alltypes.237mfs/mode=decode_view-2                               1.088m ±  2%
geomean                                                        707.2µ        705.1µ        +1.26%

                                                             │  before.txt  │              after.txt              │
                                                             │     B/s      │      B/s       vs base              │
Decode/case=alltypes.237mfs.nometa/mode=decode-2               151.4Mi ± 2%   152.8Mi ±  2%       ~ (p=0.288 n=6)
Decode/case=alltypes.237mfs.nometa/mode=stream_16k-2           151.6Mi ± 2%   149.0Mi ±  4%       ~ (p=0.485 n=6)
Decode/case=alltypes.237mfs.nometa/mode=stream_256-2           144.9Mi ± 2%   146.6Mi ±  2%       ~ (p=0.485 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=decode-2        217.3Mi ± 2%   209.6Mi ± 18%  -3.56% (p=0.026 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_16k-2    212.1Mi ± 2%   209.1Mi ±  5%       ~ (p=0.132 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256-2    207.8Mi ± 6%   201.9Mi ±  9%       ~ (p=0.065 n=6)
Decode/case=alltypes.237mfs/mode=decode-2                      162.5Mi ± 3%   161.1Mi ±  2%       ~ (p=0.485 n=6)
Decode/case=alltypes.237mfs/mode=stream_16k-2                  160.2Mi ± 3%   159.2Mi ± 14%       ~ (p=0.394 n=6)
Decode/case=alltypes.237mfs/mode=stream_256-2                  155.2Mi ± 1%   151.8Mi ± 10%  -2.22% (p=0.015 n=6)
Decode/case=alltypes.237mfs.nometa/mode=decode_view-2                         156.6Mi ±  1%
Decode/case=alltypes.209mfs.counter_gauge/mode=decode_view-2                  224.4Mi ±  2%
Decode/case=alltypes.237mfs/mode=decode_view-2                                166.8Mi ±  2%
geomean                                                        171.5Mi        172.1Mi        -1.24%

                                                             │  before.txt  │              after.txt               │
                                                             │     B/op     │     B/op      vs base                │
Decode/case=alltypes.237mfs.nometa/mode=decode-2               100.6Ki ± 0%   100.6Ki ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs.nometa/mode=stream_16k-2           100.6Ki ± 0%   100.6Ki ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs.nometa/mode=stream_256-2           133.0Ki ± 0%   133.0Ki ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.209mfs.counter_gauge/mode=decode-2        49.83Ki ± 0%   49.83Ki ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_16k-2    49.83Ki ± 0%   49.83Ki ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256-2    55.33Ki ± 0%   55.33Ki ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs/mode=decode-2                      81.82Ki ± 0%   81.82Ki ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs/mode=stream_16k-2                  81.82Ki ± 0%   81.82Ki ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs/mode=stream_256-2                  104.2Ki ± 0%   104.2Ki ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs.nometa/mode=decode_view-2                           148.0 ± 0%
Decode/case=alltypes.209mfs.counter_gauge/mode=decode_view-2                    148.0 ± 0%
Decode/case=alltypes.237mfs/mode=decode_view-2                                  148.0 ± 0%
geomean                                                        79.64Ki        16.44Ki       +0.00%
¹ all samples are equal

                                                             │ before.txt │             after.txt              │
                                                             │ allocs/op  │ allocs/op   vs base                │
Decode/case=alltypes.237mfs.nometa/mode=decode-2               71.00 ± 0%   71.00 ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs.nometa/mode=stream_16k-2           71.00 ± 0%   71.00 ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs.nometa/mode=stream_256-2           561.0 ± 0%   561.0 ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.209mfs.counter_gauge/mode=decode-2        86.00 ± 0%   86.00 ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_16k-2    86.00 ± 0%   86.00 ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256-2    171.0 ± 0%   171.0 ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs/mode=decode-2                      177.0 ± 0%   177.0 ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs/mode=stream_16k-2                  177.0 ± 0%   177.0 ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs/mode=stream_256-2                  509.0 ± 0%   509.0 ± 0%       ~ (p=1.000 n=6) ¹
Decode/case=alltypes.237mfs.nometa/mode=decode_view-2                       1.000 ± 0%
Decode/case=alltypes.209mfs.counter_gauge/mode=decode_view-2                1.000 ± 0%
Decode/case=alltypes.237mfs/mode=decode_view-2                              1.000 ± 0%
geomean                                                        156.7        44.29       +0.00%
¹ all samples are equal

Heap bytes and allocation counts on all existing decode and decode_stream paths are unchanged, while the small +2.26% to +3.71% shifts on two rows reflect recording contiguous (start, end) row ranges on MetricTable, AttrTable, and ExemplarTable plus shared-VM variance (± 12% to ± 23%).

tests/otap-compat/benches/otap_encode.rs benchstat -col /mode comparison on 022bf8f:

goos: linux
goarch: amd64
pkg: expfmt_otap_compat
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
                                         │ direct_decode │            decode_view             │      encode_metrics_otap_batch       │
                                         │    sec/op     │   sec/op     vs base               │    sec/op     vs base                │
OtapEncode/case=alltypes.237mfs.nometa-2     904.5µ ± 1%   897.8µ ± 3%        ~ (p=0.485 n=6)   2124.0µ ± 3%  +134.82% (p=0.002 n=6)
OtapEncode/case=alltypes.237mfs-2            1.106m ± 7%   1.067m ± 3%   -3.55% (p=0.004 n=6)    1.619m ± 2%   +46.30% (p=0.002 n=6)
OtapEncode/case=alltypes.bench.om-2          186.6µ ± 1%   162.7µ ± 2%  -12.83% (p=0.002 n=6)    413.9µ ± 1%  +121.79% (p=0.002 n=6)
geomean                                      571.6µ        538.2µ        -5.85%                  1.125m        +96.78%

                                         │ direct_decode │             decode_view              │      encode_metrics_otap_batch      │
                                         │      B/s      │      B/s       vs base               │     B/s       vs base               │
OtapEncode/case=alltypes.237mfs.nometa-2   156.18Mi ± 1%   157.34Mi ± 3%        ~ (p=0.485 n=6)   66.51Mi ± 3%  -57.41% (p=0.002 n=6)
OtapEncode/case=alltypes.237mfs-2           163.9Mi ± 6%    170.0Mi ± 3%   +3.70% (p=0.004 n=6)   112.1Mi ± 2%  -31.65% (p=0.002 n=6)
OtapEncode/case=alltypes.bench.om-2         54.18Mi ± 1%    62.16Mi ± 2%  +14.72% (p=0.002 n=6)   24.43Mi ± 1%  -54.91% (p=0.002 n=6)
geomean                                     111.5Mi         118.5Mi        +6.22%                 56.68Mi       -49.18%

                                         │ direct_decode │            decode_view            │       encode_metrics_otap_batch        │
                                         │     B/op      │    B/op     vs base               │     B/op       vs base                 │
OtapEncode/case=alltypes.237mfs.nometa-2   102964.0 ± 0%   148.0 ± 0%  -99.86% (p=0.002 n=6)   800522.0 ± 0%   +677.48% (p=0.002 n=6)
OtapEncode/case=alltypes.237mfs-2           83780.0 ± 0%   148.0 ± 0%  -99.82% (p=0.002 n=6)   825920.0 ± 0%   +885.82% (p=0.002 n=6)
OtapEncode/case=alltypes.bench.om-2         19706.0 ± 0%   370.0 ± 0%  -98.12% (p=0.002 n=6)   784466.0 ± 0%  +3880.85% (p=0.002 n=6)
geomean                                     54.10Ki        200.9       -99.64%                  784.6Ki       +1350.40%

                                         │ direct_decode │            decode_view            │       encode_metrics_otap_batch        │
                                         │   allocs/op   │ allocs/op   vs base               │   allocs/op    vs base                 │
OtapEncode/case=alltypes.237mfs.nometa-2     71.000 ± 0%   1.000 ± 0%  -98.59% (p=0.002 n=6)    935.000 ± 0%  +1216.90% (p=0.002 n=6)
OtapEncode/case=alltypes.237mfs-2           177.000 ± 0%   1.000 ± 0%  -99.44% (p=0.002 n=6)   1121.000 ± 0%   +533.33% (p=0.002 n=6)
OtapEncode/case=alltypes.bench.om-2         219.000 ± 0%   5.000 ± 0%  -97.72% (p=0.002 n=6)   1166.000 ± 0%   +432.42% (p=0.002 n=6)
geomean                                       140.1        1.710       -98.78%                   1.069k        +662.92%

Direct ArrowDecoder::decode remains ~1.5x to 2.3x faster and allocates ~10x to 40x fewer bytes than encode_metrics_otap_batch(&view) because ArrowDecoder::decode builds dictionary-encoded Arrow columns directly from its arena interners without re-hashing string keys and values inside otel-arrow-dfe-pdata's generic AdaptiveArrayBuilders, while decode_view avoids RecordBatch materialization altogether and DecodedMetricsView provides drop-in compatibility with any MetricsView consumer.

Add otel-arrow-dfe-pdata-views 0.54 under the arrow feature and track contiguous data-point, attribute, and exemplar index ranges in MetricTable, AttrTable, and ExemplarTable so view iterators can slice rows in O(1) without scanning parent_id columns.

Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
…oder

Add DecodedMetricsView and associated zero-copy view types implementing the otel-arrow-dfe-pdata-views hierarchy over ArrowDecoder's staged metric, data-point, exemplar, and interned attribute buffers. Expose ArrowDecoder::decode_view, ArrowDecoder::view, and DecodeStream::next_view so OTAP encoders can traverse staged scrapes without materializing intermediate Arrow RecordBatches.

Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
@bwplotka
bwplotka marked this pull request as ready for review November 5, 2026 05:13
…ricsView

Verify in tests/otap-compat that passing DecodedMetricsView (from ArrowDecoder::decode_view and DecodeStream::next_view) to otel_arrow_dfe_pdata::encode::encode_metrics_otap_batch produces transport-optimizable OtapArrowRecords::Metrics batches and equivalent OTLP output across all metric types and upstream fixtures.

Migrate benches/decode.rs to benchfmt.rs (benchfmt::Suite and CountingAlloc) and add tests/otap-compat/benches/otap_encode.rs comparing direct ArrowDecoder::decode column builders, ArrowDecoder::decode_view, and encode_metrics_otap_batch(&view) in Go benchfmt format for benchstat.

Fixes #28

Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
@bwplotka
bwplotka force-pushed the bwplotka/phase4-pdata-views branch from db4a814 to 022bf8f Compare November 5, 2026 05:44
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.

1 participant