Skip to content

arrow: add cooperative incremental decoding and DecodeBudget - #30

Open
bwplotka wants to merge 10 commits into
bwplotka/phase4-parser-spansfrom
bwplotka/phase4-cooperative-decoding
Open

bwplotka wants to merge 10 commits into
bwplotka/phase4-parser-spansfrom
bwplotka/phase4-cooperative-decoding

Conversation

@bwplotka

@bwplotka bwplotka commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Stacked on prometheus/scrape.rs#29 and prometheus/scrape.rs#19, part 2 of 2 for prometheus/scrape.rs#27.
Applies the benchfmt.rs and raw benchstat reporting rules from prometheus/scrape.rs#32.
Closes prometheus/scrape.rs#27.

Adds cooperative incremental decoding to ArrowDecoder and DecodeStream so multi-megabyte scrapes can yield between bounded steps on single-threaded !Send LocalSet runtimes without starving timers or peer tasks:

  • Adds DecodeBudget, defaulting to 128 entries, 64 KiB, and 1ms duration while checking Instant::elapsed every 16 entries, along with DecodeStep::{Pending, Batch, Done}.
  • Adds DecodeStream::step(budget) and DecodeStream::pos() to pause and resume chunked decoding at Layer 0 Entry boundaries.
  • Adds lifetime-free DecodeCursor via ArrowDecoder::start_decode, start_decode_stream, step_decode, and finish_decode so owned PdataDecodeJob structs can hold an owned byte buffer and ArrowDecoder across suspension points under #![forbid(unsafe_code)].
  • Adds runtime-agnostic async helpers ArrowDecoder::decode_cooperative, with a single-pass synchronous fast path when input.len() <= budget.max_bytes, and DecodeStream::next_cooperative.
  • Adds LocalSet timer scheduling lateness and dhat bounded-memory tests in tests/otap-compat and benchfmt.rs benchmarks in benches/decode.rs.

Commits in order:

  1. 0ece0e0 arrow: add cooperative incremental decoding and DecodeBudget
  2. bf4b638 arrow: avoid private intra-doc links in public rustdoc comments
  3. f134292 arrow: make finish_decode and ActiveParserState::take_scratch_into idempotent
  4. 8807b39 arrow: implement Debug for DecodeCursor
  5. e6b3b82 arrow: reclaim parser scratch if decode_cooperative is dropped at yield
  6. f53fedd arrow: validate cursor scrape_id and input length across step_decode calls
  7. 6cd52c3 arrow: return DecodeStep::Done on empty scrapes in single-batch mode
  8. 1a8beb0 arrow: expand unit tests for cooperative error paths, duration budget, and cancellation
  9. bc4e9b2 tests: exercise DecodeStream::next_cooperative in dhat_bounded_mem
  10. 6aff3e6 bench: migrate benches/decode.rs to benchfmt.rs

benches/decode.rs benchstat comparison between before.txt at ef2ce5c and after.txt at 6aff3e6:

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                          918.5µ ±  1%   930.2µ ± 4%       ~ (p=0.093 n=6)
Decode/case=alltypes.237mfs.nometa/mode=stream_16k-2                      933.6µ ±  3%   935.2µ ± 1%       ~ (p=0.818 n=6)
Decode/case=alltypes.237mfs.nometa/mode=stream_256-2                      977.0µ ± 63%   933.3µ ± 4%  -4.47% (p=0.002 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=decode-2                   325.0µ ± 60%   322.9µ ± 3%       ~ (p=0.394 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_16k-2               329.2µ ± 63%   327.6µ ± 1%       ~ (p=0.394 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256-2               343.3µ ± 63%   335.5µ ± 3%  -2.28% (p=0.026 n=6)
Decode/case=alltypes.237mfs/mode=decode-2                                 1.109m ± 55%   1.112m ± 2%       ~ (p=0.818 n=6)
Decode/case=alltypes.237mfs/mode=stream_16k-2                             1.149m ± 55%   1.133m ± 2%       ~ (p=0.310 n=6)
Decode/case=alltypes.237mfs/mode=stream_256-2                             1.167m ±  4%   1.168m ± 3%       ~ (p=0.699 n=6)
Decode/case=alltypes.237mfs.nometa/mode=decode_cooperative-2                             930.8µ ± 4%
Decode/case=alltypes.237mfs.nometa/mode=stream_256_cooperative-2                         955.5µ ± 1%
Decode/case=alltypes.209mfs.counter_gauge/mode=decode_cooperative-2                      331.9µ ± 2%
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256_cooperative-2                  347.6µ ± 3%
Decode/case=alltypes.237mfs/mode=decode_cooperative-2                                    1.138m ± 9%
Decode/case=alltypes.237mfs/mode=stream_256_cooperative-2                                1.200m ± 1%
geomean                                                                   709.8µ         710.6µ       -0.83%

                                                                        │  before.txt   │             after.txt              │
                                                                        │      B/s      │     B/s       vs base              │
Decode/case=alltypes.237mfs.nometa/mode=decode-2                          153.8Mi ±  1%   151.9Mi ± 4%       ~ (p=0.093 n=6)
Decode/case=alltypes.237mfs.nometa/mode=stream_16k-2                      151.3Mi ±  3%   151.1Mi ± 1%       ~ (p=0.818 n=6)
Decode/case=alltypes.237mfs.nometa/mode=stream_256-2                      144.6Mi ± 39%   151.4Mi ± 4%  +4.68% (p=0.002 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=decode-2                   214.4Mi ± 37%   215.8Mi ± 3%       ~ (p=0.394 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_16k-2               211.7Mi ± 39%   212.8Mi ± 1%       ~ (p=0.394 n=6)
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256-2               203.0Mi ± 39%   207.7Mi ± 3%  +2.33% (p=0.026 n=6)
Decode/case=alltypes.237mfs/mode=decode-2                                 163.6Mi ± 36%   163.1Mi ± 2%       ~ (p=0.818 n=6)
Decode/case=alltypes.237mfs/mode=stream_16k-2                             158.0Mi ± 36%   160.0Mi ± 2%       ~ (p=0.310 n=6)
Decode/case=alltypes.237mfs/mode=stream_256-2                             155.4Mi ±  4%   155.3Mi ± 3%       ~ (p=0.699 n=6)
Decode/case=alltypes.237mfs.nometa/mode=decode_cooperative-2                              151.8Mi ± 4%
Decode/case=alltypes.237mfs.nometa/mode=stream_256_cooperative-2                          147.8Mi ± 1%
Decode/case=alltypes.209mfs.counter_gauge/mode=decode_cooperative-2                       210.0Mi ± 2%
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256_cooperative-2                   200.5Mi ± 3%
Decode/case=alltypes.237mfs/mode=decode_cooperative-2                                     159.3Mi ± 8%
Decode/case=alltypes.237mfs/mode=stream_256_cooperative-2                                 151.1Mi ± 1%
geomean                                                                   170.9Mi         170.7Mi       +0.84%

                                                                        │  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_cooperative-2                             100.6Ki ± 0%
Decode/case=alltypes.237mfs.nometa/mode=stream_256_cooperative-2                         133.0Ki ± 0%
Decode/case=alltypes.209mfs.counter_gauge/mode=decode_cooperative-2                      49.83Ki ± 0%
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256_cooperative-2                  55.33Ki ± 0%
Decode/case=alltypes.237mfs/mode=decode_cooperative-2                                    81.82Ki ± 0%
Decode/case=alltypes.237mfs/mode=stream_256_cooperative-2                                104.2Ki ± 0%
geomean                                                                   79.64Ki        80.75Ki       +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_cooperative-2                           71.00 ± 0%
Decode/case=alltypes.237mfs.nometa/mode=stream_256_cooperative-2                       561.0 ± 0%
Decode/case=alltypes.209mfs.counter_gauge/mode=decode_cooperative-2                    86.00 ± 0%
Decode/case=alltypes.209mfs.counter_gauge/mode=stream_256_cooperative-2                171.0 ± 0%
Decode/case=alltypes.237mfs/mode=decode_cooperative-2                                  177.0 ± 0%
Decode/case=alltypes.237mfs/mode=stream_256_cooperative-2                              509.0 ± 0%
geomean                                                                   156.7        170.6       +0.00%
¹ all samples are equal

benches/decode.rs benchstat -col /mode comparison on 6aff3e6:

goos: linux
goarch: amd64
pkg: expfmt
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
                                            │   decode    │        decode_cooperative         │            stream_16k             │            stream_256             │      stream_256_cooperative       │
                                            │   sec/op    │   sec/op     vs base              │   sec/op     vs base              │   sec/op     vs base              │   sec/op     vs base              │
Decode/case=alltypes.237mfs.nometa-2          930.2µ ± 4%   930.8µ ± 4%       ~ (p=0.937 n=6)   935.2µ ± 1%       ~ (p=0.240 n=6)   933.3µ ± 4%       ~ (p=0.485 n=6)   955.5µ ± 1%       ~ (p=0.065 n=6)
Decode/case=alltypes.209mfs.counter_gauge-2   322.9µ ± 3%   331.9µ ± 2%  +2.78% (p=0.004 n=6)   327.6µ ± 1%       ~ (p=0.065 n=6)   335.5µ ± 3%  +3.89% (p=0.004 n=6)   347.6µ ± 3%  +7.64% (p=0.002 n=6)
Decode/case=alltypes.237mfs-2                 1.112m ± 2%   1.138m ± 9%  +2.37% (p=0.002 n=6)   1.133m ± 2%  +1.93% (p=0.041 n=6)   1.168m ± 3%  +5.06% (p=0.002 n=6)   1.200m ± 1%  +7.93% (p=0.002 n=6)
geomean                                       693.8µ        705.8µ       +1.73%                 702.8µ       +1.30%                 715.1µ       +3.07%                 735.9µ       +6.07%

                                            │    decode    │         decode_cooperative         │             stream_16k             │             stream_256             │       stream_256_cooperative       │
                                            │     B/s      │     B/s       vs base              │     B/s       vs base              │     B/s       vs base              │     B/s       vs base              │
Decode/case=alltypes.237mfs.nometa-2          151.9Mi ± 4%   151.8Mi ± 4%       ~ (p=0.937 n=6)   151.1Mi ± 1%       ~ (p=0.240 n=6)   151.4Mi ± 4%       ~ (p=0.485 n=6)   147.8Mi ± 1%       ~ (p=0.065 n=6)
Decode/case=alltypes.209mfs.counter_gauge-2   215.8Mi ± 3%   210.0Mi ± 2%  -2.71% (p=0.004 n=6)   212.8Mi ± 1%       ~ (p=0.065 n=6)   207.7Mi ± 3%  -3.74% (p=0.004 n=6)   200.5Mi ± 3%  -7.10% (p=0.002 n=6)
Decode/case=alltypes.237mfs-2                 163.1Mi ± 2%   159.3Mi ± 8%  -2.31% (p=0.002 n=6)   160.0Mi ± 2%  -1.89% (p=0.041 n=6)   155.3Mi ± 3%  -4.81% (p=0.002 n=6)   151.1Mi ± 1%  -7.35% (p=0.002 n=6)
geomean                                       174.9Mi        171.9Mi       -1.70%                 172.6Mi       -1.28%                 169.6Mi       -2.98%                 164.8Mi       -5.72%

                                            │    decode    │          decode_cooperative          │              stream_16k              │              stream_256              │        stream_256_cooperative        │
                                            │     B/op     │     B/op      vs base                │     B/op      vs base                │     B/op       vs base               │     B/op       vs base               │
Decode/case=alltypes.237mfs.nometa-2          100.6Ki ± 0%   100.6Ki ± 0%       ~ (p=1.000 n=6) ¹   100.6Ki ± 0%       ~ (p=1.000 n=6) ¹    133.0Ki ± 0%  +32.23% (p=0.002 n=6)    133.0Ki ± 0%  +32.23% (p=0.002 n=6)
Decode/case=alltypes.209mfs.counter_gauge-2   49.83Ki ± 0%   49.83Ki ± 0%       ~ (p=1.000 n=6) ¹   49.83Ki ± 0%       ~ (p=1.000 n=6) ¹    55.33Ki ± 0%  +11.04% (p=0.002 n=6)    55.33Ki ± 0%  +11.04% (p=0.002 n=6)
Decode/case=alltypes.237mfs-2                 81.82Ki ± 0%   81.82Ki ± 0%       ~ (p=1.000 n=6) ¹   81.82Ki ± 0%       ~ (p=1.000 n=6) ¹   104.20Ki ± 0%  +27.36% (p=0.002 n=6)   104.20Ki ± 0%  +27.36% (p=0.002 n=6)
geomean                                       74.29Ki        74.29Ki       +0.00%                   74.29Ki       +0.00%                    91.52Ki       +23.20%                  91.52Ki       +23.20%
¹ all samples are equal

                                            │   decode   │         decode_cooperative         │             stream_16k             │             stream_256              │       stream_256_cooperative        │
                                            │ allocs/op  │ allocs/op   vs base                │ allocs/op   vs base                │  allocs/op   vs base                │  allocs/op   vs base                │
Decode/case=alltypes.237mfs.nometa-2          71.00 ± 0%   71.00 ± 0%       ~ (p=1.000 n=6) ¹   71.00 ± 0%       ~ (p=1.000 n=6) ¹   561.00 ± 0%  +690.14% (p=0.002 n=6)   561.00 ± 0%  +690.14% (p=0.002 n=6)
Decode/case=alltypes.209mfs.counter_gauge-2   86.00 ± 0%   86.00 ± 0%       ~ (p=1.000 n=6) ¹   86.00 ± 0%       ~ (p=1.000 n=6) ¹   171.00 ± 0%   +98.84% (p=0.002 n=6)   171.00 ± 0%   +98.84% (p=0.002 n=6)
Decode/case=alltypes.237mfs-2                 177.0 ± 0%   177.0 ± 0%       ~ (p=1.000 n=6) ¹   177.0 ± 0%       ~ (p=1.000 n=6) ¹    509.0 ± 0%  +187.57% (p=0.002 n=6)    509.0 ± 0%  +187.57% (p=0.002 n=6)
geomean                                       102.6        102.6       +0.00%                   102.6       +0.00%                    365.5       +256.16%                  365.5       +256.16%
¹ all samples are equal

Heap bytes and allocation counts on decode, stream_16k, and stream_256 are unchanged from ef2ce5c, and decode_cooperative and stream_256_cooperative match their synchronous decode and stream_256 counterparts in both B/op and allocs/op while adding +1.73% to +2.91% wall-time overhead from budget checks and ActiveParserState stashing across yields.

Add cooperative incremental decoding to ArrowDecoder and DecodeStream so multi-megabyte scrapes can yield between bounded steps on single-threaded !Send LocalSet runtimes without starving timers or peer tasks:

- Add DecodeBudget (defaulting to 128 entries, 64 KiB, and 1ms duration, checking Instant::elapsed every 16 entries) and DecodeStep::{Pending, Batch, Done}.
- Add DecodeStream::step(budget) and DecodeStream::pos() to pause and resume chunked decoding at Layer 0 Entry boundaries.
- Add lifetime-free DecodeCursor (ArrowDecoder::start_decode, start_decode_stream, step_decode, finish_decode) so owned PdataDecodeJob structs can hold an owned byte buffer and ArrowDecoder across suspension points under #![forbid(unsafe_code)].
- Add runtime-agnostic async helpers ArrowDecoder::decode_cooperative (with a single-pass synchronous fast path when input.len() <= budget.max_bytes) and DecodeStream::next_cooperative.
- Add LocalSet timer scheduling lateness and dhat bounded-memory tests in tests/otap-compat and criterion benchmarks in benches/decode.rs.

Closes #27

Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
@bwplotka
bwplotka force-pushed the bwplotka/phase4-cooperative-decoding branch from b8644c2 to 0ece0e0 Compare November 5, 2026 04:40
@bwplotka
bwplotka marked this pull request as ready for review November 5, 2026 05:10
Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
…empotent

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

Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
…, and cancellation

Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
Signed-off-by: Bartek Plotka <bwplotka@gmail.com>
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