Skip to content

prof: cut BEX tracing producer + consumer overhead - #3765

Merged
antoniosarosi merged 8 commits into
canaryfrom
bex-tracing-overhead
Jun 15, 2026
Merged

antoniosarosi merged 8 commits into
canaryfrom
bex-tracing-overhead

Conversation

@antoniosarosi

@antoniosarosi antoniosarosi commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Reduces the runtime overhead of the BEX tracing system on both sides of the ring: the producer (the VM encoding/pushing a raw record per call into a lock-free SPSC ring) and the consumer (the background thread transcoding to protobuf and writing per-engine .bamlprof). Profiling is default-on, so this is paid by every run.

Six commits, each measured and committed on its own merit. No .bamlprof format change; byte-identical wire output (guarded by a differential test). All bex_events unit tests, bex_engine's prof_gate, the loom/miri concurrency suites, and clippy --all-features are green.

Commits

Commit Side Change
e7daa17 consumer Reuse one encode buffer instead of a Vec::with_capacity per event (was a malloc + a redundant encoded_len walk per record).
85bf08b producer Encode records directly into the ring slot (Ring::push_with / Buf::with_slice_mut) — drops the intermediate stack buffer, the 41-byte zeroing, and one of two copies. Loom keeps per-byte tracked stores.
d8ce1f2 producer Encode fixed fields with ptr::write_unaligned instead of copy_from_slice — the latter did not inline at opt-level="s" (the shipped profile): ~78 instructions/record → ~15, no libcall.
d4eb542 consumer Convert ticks→ns with a precomputed reciprocal multiply-shift instead of a per-record u128 divide (~20-40 non-pipelined cycles). Exact for rate (1,1); ≤1 ns rounding for measured TSC rates, monotonic.
d55970 consumer Hand-roll the protobuf for the two hot events (CallFunction/EndFunction) in one pass with back-patched 1-byte length prefixes, instead of prost (which walks this nested-oneof message 3×). Cold records stay on prost. Same technique as Perfetto's protozero.
905d6a7 consumer One write per drained range instead of one per event: accumulate the whole range (one ~256 KiB segment slice) into the writer's buffer, then a single write straight to the File. Drops the 8 KiB BufWriter (~4,300 → ~135 write syscalls / 2M-event run).

Results (Linux x86 dev box)

Consumer — ~2.4× throughput

prof_drain_throughput: 7.7M → ~18M events/s/core. The reciprocal removes the per-record divide; the hand-rolled encoder removes ~2/3 of the prost cost (prost was size-walking the message twice and byte-walking once); the batched range write removes the per-event syscall churn.

End-to-end, single process (clean isolation)

Same binary, BAML_PROFILE toggled, real disk IO, profile dir wiped before each run so there is no cross-run accumulation. Medians, ms:

workload off (tracing disabled) discard (producer floor, no IO) full (real IO)
pure-call-1m (1M trivial calls) 111.8 131.5 (+18%) 169.3 (+51%)
call-chain-100x10k 82.3 101.7 (+24%) 125.9 (+53%)

Decomposition on these adversarial call-storm microbenches: producer ring-emit ~+18-24%, consumer transcode+write+fsync ~+30%. The producer cost is at the encode/push floor (the clock read itself is only ~1-6 ns/pair — verified by a stub experiment; the rest is encoding + the ring commit, both irreducible without dropping records). The consumer (~18M ev/s) nearly matches the producer's ~18-20M ev/s burst, leaving a small drain tail plus the one-file write/fsync.

Corpus medians

Across the full speedtest corpus, the same-binary tracing-on producer cost is +3.6% median; total full-pipeline +8.2% median. Real-shaped workloads (loops, strings, classes) are well under +10% — only the call-storm microbenches (millions of trivial calls, which no LLM-bound BAML program emits) are high.

Note on a measurement pitfall: a multi-leg speedtest run without cleaning .bamlprof between sample-processes accumulates GBs and triggers NVMe dirty-page writeback throttling, inflating the full leg to +200-500%. The single-process numbers above (dir cleaned per run) are the representative cost; the inflation is a harness artifact, not a per-run cost.

Follow-ups (not in this PR)

  • Per-segment compression (lz4). The remaining ~+30% consumer+IO on record-dense workloads scales with .bamlprof byte volume; per-segment lz4 (~1.8-3× smaller files) is the lever, viable now that the consumer has CPU headroom. It's a format-version bump, so it's kept out of this no-format-change PR (separate branch/PR). The batched-range write here (905d6a7) is its structural carrier.
  • Record-shrink / delta-ts — redundant with a block compressor on disk; revisit only for the ring overflow budget under sustained multi-second storms.
  • Leaf single-push fusion — evaluated and rejected (~3-4 ns/pair for a high-risk unwind/cancel/drain change).
  • opt-level=3 — evaluated and reverted (speeds the VM ~17% but not the tracing delta; risks the wasm size budget).
  • Fidelity levers (sampling / leaf-elision) are the only thing that bounds a pathological call-storm; out of scope here.

Verification

  • cargo test -p bex_events (53, incl. hand_encoder_matches_prost differential byte-equality and reciprocal_matches_exact_divide)
  • cargo test -p bex_engine --test prof_gate (decodes the written bytes unchanged through read_bamlprof)
  • loom (--cfg baml_loom) 34/34 + miri 33/33 on the ring/push_with change
  • clippy --all-targets --all-features clean; CI mirror under rustup run 1.93.0

Summary by CodeRabbit

  • Refactor
    • Improved profiling event recording and ring emission to encode directly into ring slots, reducing per-event overhead.
    • Streamlined profile writing by reusing a shared scratch buffer and optimizing the hottest event encoders.
    • Updated consumer processing to buffer and flush by committed ring ranges, including heartbeat persistence.
    • Accelerated tick-to-nanosecond conversion using a precomputed multiply-shift reciprocal scale.
  • Tests
    • Added/updated assertions to verify reciprocal-based tick conversion matches exact conversion within 1ns and updated refinement expectations.

ProfileWriter::write_message allocated a fresh Vec (and paid an extra
encoded_len walk to pre-size it) for every event the consumer transcoded —
a malloc/free per traced event on the consumer's hot path. Reuse one
scratch buffer on the writer: encode_length_delimited writes its own length
prefix and grows the buffer as needed, so after the first events capacity is
stable and no allocation happens. Bytes written are byte-identical.

Measured (speedtest, tracing on, tmpfs sink vs the same before this change):
record-dense workloads -13 to -16% end-to-end (fib32 -15.6, closure-apply
-14.6, pure-call -13.4, binary-tree -13.2, call-chain -12.6); loop-heavy
workloads ~0 (they emit few records). bex_events 51/51 green.
The VM producer encoded each raw record into a zeroed 41-byte stack buffer
and then memcpy'd that into the ring segment — two writes of the same bytes
plus a dead memset per traced event. Add Ring::push_with(len, |slot| ...)
which reserves the slot and lets the producer serialize straight into it
(Buf::with_slice_mut), so encode_to writes once, into the ring. push() now
delegates to push_with; the now-unused Buf::write is removed. The loom build
keeps per-byte tracked stores via a temp, so the data-race model is
unchanged.

Also folds in two producer micro-fixes: the clock anchor is split into an
inline OnceLock fast path + #[cold] init, and the per-emit ring gate is
deduped (the helper takes the resolved &Ring instead of re-checking Option).

Measured (divan, opt-level="s" as shipped, off-vs-discard ns/pair, fastest
sample): call-chain ~19.6 -> ~15.5 ns/pair (-21% of the producer overhead);
pure-call ~neutral within its run-to-run variance, no regression. Output is
byte-identical (record roundtrip + consumer e2e tests green); bex_events
51/51, bex_vm green.

NOTE: a stub-clock experiment showed the rdtsc reads cost only ~1-6 ns/pair
(they pipeline in-situ) -- the dominant producer cost is record encode + the
ring write, which this targets, not the clock.
…m_slice

The record Writer routed every u16/u32/u64 through bytes() -> copy_from_slice
with a runtime length, which does not inline at opt-level="s" (the shipped
profile) — ~78 instructions per 38-byte CallFunction. Write each fixed field
with a single ptr::write_unaligned (to_le for portability): one `mov` each,
~15 instructions, no out-of-line call. Byte-identical wire (record roundtrip
+ e2e consumer tests green, 51/51 bex_events).

This is the producer-encode lever from the consumer/producer research. An
earlier const-offset fixed-array attempt regressed (~6 ns) because it added a
zeroing + a final copy; a copy_nonoverlapping<N> form likely kept a memcpy
libcall at opt-s. write_unaligned is a direct store with neither. The ~5-8 ns/
pair it removes sits at the divan run-to-run noise floor on the call-storm
microbenches, so it is committed on codegen merit (provably fewer
instructions, no libcall), like the write-in-place change.
…rd divide

TickConverter::to_ns ran a u128 multiply then a u128 DIVIDE by denom on every
record the consumer transcodes — a variable 128-bit divide is ~20-40
non-pipelined cycles. The rate is fixed for a run (after the one-time TSC
refine), so precompute scale = numer<<48 / denom once at each rate install and
convert on the hot path via (dt * scale) >> 48 — a multiply-shift, no divide.
The cold prev-segment branch (late-draining pre-refine ticks) keeps the exact
divide.

Exact for rate (1,1): scale = 2^48 so ns == dt (the identity converter used by
the e2e oracle is bit-unchanged). For measured TSC/CNTVCT rates the rounding is
<=1 ns (below tracing-timestamp resolution) and monotonic, so per-thread
ordering is preserved; new test reciprocal_matches_exact_divide pins this for
125/3, 5/16, 1e9/24e6. bex_events 52/52.

Measured: prof_drain_throughput 4M events 518ms -> ~493ms median (~7.7M ->
~8.0-8.7M ev/s/core), consistent with removing the divide.
…tion)

The consumer transcoded every record by building a prost DiskEventV1 and
calling encode_length_delimited, which walks the nested-oneof message ~3x
(encoded_len, encode_raw, and the embedded-message path recomputes the inner
length again) before writing a byte — ~half the consumer's per-record cost.

Encode the two hot events (CallFunction/EndFunction = the per-call pair, ~all
event volume) directly in one pass with back-patched single-byte length
prefixes (both messages are < 128 B). Cold/rare records (header, StartThread,
SetFunctionId, EndThread, Heartbeat) stay on prost. This mirrors Perfetto's
"protozero" streaming protobuf encoder, used there for the same reason.

Measured: prof_drain_throughput 4M events ~493ms -> ~234ms (8.1M -> 17.0M
ev/s/core, 2.1x); with the reciprocal to_ns change the consumer is 7.7M ->
17.0M overall (2.2x). The consumer is no longer the per-record bottleneck for
record-dense workloads.

Correctness: to_disk_event still builds the prost structs, so a proto field
add/remove/rename/type-change is a COMPILE error (exhaustive struct literal +
the encoders' field reads). New differential test hand_encoder_matches_prost
asserts byte-for-byte equality with prost over a range of inputs, covering
even a field renumber. bex_events 53/53; e2e roundtrip + prof_gate decode the
hand-written bytes unchanged.
@vercel

vercel Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jun 15, 2026 5:24pm
promptfiddle Ready Ready Preview, Comment Jun 15, 2026 5:24pm
promptfiddle2 Ready Ready Preview, Comment Jun 15, 2026 5:24pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 39de2988-cc6a-4cdb-8184-615810fd1353

📥 Commits

Reviewing files that changed from the base of the PR and between 6906347 and 6b12247.

📒 Files selected for processing (1)
  • baml_language/crates/bex_events/src/prof/ring.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/crates/bex_events/src/prof/ring.rs

📝 Walkthrough

Walkthrough

This PR introduces a set of hot-path encoding optimizations in the bex_events and bex_vm profiling subsystems: Buf::write is replaced by with_slice_mut for direct in-place ring writes; TickConverter gains a precomputed fixed-point reciprocal (scale) for division-free tick→ns conversion; Writer switches u16/u32/u64 to unsafe unaligned pointer stores; and ProfileWriter adds a reusable scratch buffer plus hand-rolled protobuf encoders for the two hottest event variants. Consumer and VM paths integrate buffered encoding to reduce allocations.

Changes

Profiling hot-path encoding optimizations

Layer / File(s) Summary
Buf::with_slice_mut primitive and Ring push_with closure path
bex_events/src/prof/sync.rs, bex_events/src/prof/ring.rs
Buf::write is removed; with_slice_mut(offset, len, f) hands a &mut [u8] directly to the caller (std) or via staging (loom). Ring::push delegates to push_with with a byte-copy closure; push_with calls with_slice_mut before advancing head_pos.
ProfileWriter scratch buffer and hand-rolled protobuf encoders
bex_events/src/prof/file.rs
ProfileWriter gains a persistent scratch: Vec<u8>. encode_event routes CallFunction/EndFunction through new hand-rolled LEB128 + back-patched-length encoders; all other variants use prost into scratch. flush_buffered writes and clears scratch once. Unit tests assert byte-for-byte equality with prost output.
VM prof helpers: direct ring-slot encoding via push_with
bex_vm/src/vm.rs
prof_push_record is replaced by a helper that takes an explicit &Ring and writes RawRecords via encoded_len() + encode_to through push_with. All four call-site helpers switch to if let Some(ring) = self.prof_ring gating.
Consumer transcode and heartbeat buffering
bex_events/src/prof/consumer.rs
transcode accumulates events via writer.encode_event(...) for a whole drained range, then persists with a single flush_buffered() call. maybe_heartbeat similarly uses encode_event + flush_buffered(), marking engines as failed on buffered flush errors.
Writer unsafe unaligned LE stores for u16/u32/u64
bex_events/src/prof/record.rs
to_le_bytes() + bytes() copies are replaced with unsafe unaligned pointer writes guarded by debug_assert! bounds checks and #[expect(unsafe_code)] annotations. RawRecord::encode documentation adds the precondition that buf.len() >= Self::encoded_len() for release builds.
TickConverter fixed-point reciprocal and ANCHOR caching
bex_events/src/prof/clock.rs
ANCHOR becomes a module-level static with a OnceLock::get fast path. TickConverter gains scale: u128 (computed by scale_of with SCALE_SHIFT=48); to_ns() switches to (dt * scale) >> SCALE_SHIFT. Construction, maybe_refine(), and tests are all updated.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • BoundaryML/baml#3759: Introduces the raw-ticks→TickConverter conversion scheme that this PR directly extends with a fixed-point reciprocal scale field and multiply-shift to_ns hot path.

Poem

🐇 No more dividing on every tick,
A fixed-point shift does the math real quick!
Scratch buffers saved, no alloc in sight,
Unsafe pointer writes aligned just right.
The ring gets a closure, the prof stays lean—
The fastest profiler a bunny has seen! 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'prof: cut BEX tracing producer + consumer overhead' directly and accurately captures the main objective of the PR, which is to reduce runtime overhead across both producer and consumer sides of the BEX tracing system through multiple optimization techniques.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bex-tracing-overhead

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel
vercel Bot temporarily deployed to Preview – beps June 15, 2026 16:00 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 15, 2026 16:07 Inactive

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
baml_language/crates/bex_events/src/prof/record.rs (1)

197-200: 💤 Low value

Minor: Docstring claim about panic behavior is now approximate.

The doc says "Panics (slice indexing) if buf is shorter" but with the new u16/u32/u64 implementations, an undersized buffer could cause UB before hitting a safe slice index. In practice this is mitigated because:

  1. All records start with a u8(TAG_...) which panics on empty buffer
  2. The primary callers (encode() with MAX_RECORD_LEN, Ring::push_with with encoded_len()) always provide correctly-sized buffers

This is acceptable for internal hot-path code, just noting the doc is no longer strictly precise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_events/src/prof/record.rs` around lines 197 - 200,
Update the docstring for the encode method in RawRecord to be more precise about
the actual panic and undefined behavior. The current documentation states
"Panics (slice indexing) if `buf` is shorter" but with the new u16/u32/u64
implementations, an undersized buffer can trigger undefined behavior before
reaching safe slice indexing. Revise the docstring to clarify that undefined
behavior may occur with insufficient buffer size due to unsafe operations on
integral types, while noting that in practice this is mitigated by the initial
u8 TAG check and correct buffer sizing from the primary callers.
baml_language/crates/bex_vm/src/vm.rs (1)

2943-2947: ⚡ Quick win

Refresh this helper doc to match the new ring-slot path.

This comment still describes the removed stack-buffer implementation and says the helper no-ops when profiling is off, but the new helper requires callers to pass an already-unwrapped Ring and writes directly into the reserved slot.

Proposed doc update
-    /// Encodes and pushes one profiling record into the per-resume ring
-    /// snapshot. No-op when profiling is off. The buffer is sized by
-    /// `SET_FUNCTION_ID_LEN` (41 B — the largest fixed record the VM emits:
-    /// `CallFunction` 38, `EndFunction` 26, `SetFunctionId` 41) — the 292-byte
-    /// `MAX_RECORD_LEN` zeroing is measurable at per-call rates.
+    /// Encodes and pushes one profiling record into the supplied per-resume
+    /// ring snapshot. Callers perform the profiling-off gate before calling
+    /// this helper. The ring slot is sized from `RawRecord::encoded_len()` and
+    /// initialized in place via `RawRecord::encode_to()`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_vm/src/vm.rs` around lines 2943 - 2947, Update the
documentation comment for the helper function (around line 2943-2947) to reflect
the new implementation. Remove references to the removed stack-buffer
implementation and the stack-buffer sizing details (SET_FUNCTION_ID_LEN,
MAX_RECORD_LEN, CallFunction, EndFunction, SetFunctionId). Update the
documentation to accurately describe the new behavior: that the helper takes an
already-unwrapped Ring as a parameter from the caller, writes directly into a
reserved slot, and is no longer a no-op when profiling is off. Ensure the
updated comment clearly documents the new signature and behavior instead of the
old stack-buffer approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@baml_language/crates/bex_events/src/prof/record.rs`:
- Around line 197-200: Update the docstring for the encode method in RawRecord
to be more precise about the actual panic and undefined behavior. The current
documentation states "Panics (slice indexing) if `buf` is shorter" but with the
new u16/u32/u64 implementations, an undersized buffer can trigger undefined
behavior before reaching safe slice indexing. Revise the docstring to clarify
that undefined behavior may occur with insufficient buffer size due to unsafe
operations on integral types, while noting that in practice this is mitigated by
the initial u8 TAG check and correct buffer sizing from the primary callers.

In `@baml_language/crates/bex_vm/src/vm.rs`:
- Around line 2943-2947: Update the documentation comment for the helper
function (around line 2943-2947) to reflect the new implementation. Remove
references to the removed stack-buffer implementation and the stack-buffer
sizing details (SET_FUNCTION_ID_LEN, MAX_RECORD_LEN, CallFunction, EndFunction,
SetFunctionId). Update the documentation to accurately describe the new
behavior: that the helper takes an already-unwrapped Ring as a parameter from
the caller, writes directly into a reserved slot, and is no longer a no-op when
profiling is off. Ensure the updated comment clearly documents the new signature
and behavior instead of the old stack-buffer approach.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 28139e04-b8ef-4a44-ab74-b6b8a5739de8

📥 Commits

Reviewing files that changed from the base of the PR and between 430ddd1 and d559704.

📒 Files selected for processing (6)
  • baml_language/crates/bex_events/src/prof/clock.rs
  • baml_language/crates/bex_events/src/prof/file.rs
  • baml_language/crates/bex_events/src/prof/record.rs
  • baml_language/crates/bex_events/src/prof/ring.rs
  • baml_language/crates/bex_events/src/prof/sync.rs
  • baml_language/crates/bex_vm/src/vm.rs

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 15, 2026 16:19 Inactive
@github-actions

github-actions Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@github-actions

github-actions Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

No description provided.

The consumer transcoded the events of a drained range (one contiguous slice,
up to a 256 KiB segment) but issued a write_all per event into an 8 KiB
BufWriter — a syscall every ~460 events (~4,300 per 2M-event run). Accumulate
the whole range into the writer's reused buffer (encode_event appends; no
per-event clear), then a single flush_buffered per range. The BufWriter is
removed entirely: the range buffer is the buffer, so each range is one write
straight to the File (~135 writes per 2M-event run). flush()/sync() now flush
that buffer (sync still fsyncs).

No format change; bytes byte-identical (e2e_fake_producer_roundtrip + the
hand_encoder differential test green, bex_events 53/53). prof_drain_throughput
4M events ~234ms -> ~210-225ms (~17.0M -> ~17.7-19.0M ev/s/core).

This is also the structural carrier for optional per-segment compression
(lz4) — deferred; the binding constraint on record-dense full-IO is disk byte
volume, and compression is the lever, but it is a format-version bump kept
out of this no-format-change PR.
@vercel
vercel Bot temporarily deployed to Preview – beps June 15, 2026 16:37 Inactive

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
baml_language/crates/bex_events/src/prof/consumer.rs (1)

377-388: ⚡ Quick win

Preserve the heartbeat flush error.

flush_buffered() already returns the concrete io::Error; carrying it through keeps disk-full/permission/interruption diagnostics instead of reporting only "heartbeat write failed".

♻️ Proposed fix
-        let mut failed = Vec::new();
+        let mut failed: Vec<(u64, io::Error)> = Vec::new();
         for (&engine_id, slot) in &mut self.writers {
             if let Some(writer) = slot {
                 writer.encode_event(&event);
-                if writer.flush_buffered().is_err() {
-                    failed.push(engine_id);
+                if let Err(err) = writer.flush_buffered() {
+                    failed.push((engine_id, err));
                 }
             }
         }
-        for engine_id in failed {
-            self.fail_writer(engine_id, &io::Error::other("heartbeat write failed"));
+        for (engine_id, err) in failed {
+            self.fail_writer(engine_id, &err);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_events/src/prof/consumer.rs` around lines 377 - 388,
The code in the writers loop is discarding the actual error returned by
flush_buffered() and replacing it with a generic error message. Capture the
concrete io::Error from the flush_buffered() call instead of just checking
is_err(), and pass that actual error to self.fail_writer() to preserve
diagnostic details like disk-full, permission, or interruption errors instead of
reporting only the generic "heartbeat write failed" message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@baml_language/crates/bex_events/src/prof/consumer.rs`:
- Around line 377-388: The code in the writers loop is discarding the actual
error returned by flush_buffered() and replacing it with a generic error
message. Capture the concrete io::Error from the flush_buffered() call instead
of just checking is_err(), and pass that actual error to self.fail_writer() to
preserve diagnostic details like disk-full, permission, or interruption errors
instead of reporting only the generic "heartbeat write failed" message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7a6ae25d-6564-4ac0-858e-b7158dc389d9

📥 Commits

Reviewing files that changed from the base of the PR and between d559704 and 905d6a7.

📒 Files selected for processing (2)
  • baml_language/crates/bex_events/src/prof/consumer.rs
  • baml_language/crates/bex_events/src/prof/file.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/crates/bex_events/src/prof/file.rs

Two docstrings drifted from the code in this PR's earlier commits:
- RawRecord::encode_to said "Panics if buf is shorter", but the
  write_unaligned field writes (commit d8ce1f2) are unchecked, so an
  undersized buf is UB in release, not a panic. Restate it as a safety
  precondition (buf.len() >= encoded_len()), noting both callers satisfy it.
- prof_push_record still described the removed stack buffer + 41-byte zeroing
  + profiling-off no-op; it now takes an already-unwrapped &Ring and encodes
  into the reserved ring slot (commit 85bf08b). Doc-only.
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 15, 2026 16:43 Inactive
@antoniosarosi

Copy link
Copy Markdown
Contributor Author

Addressed both CodeRabbit nitpicks in 6906347 (doc-only): encode_to now states the buffer-size safety precondition (UB, not panic, on the unchecked write_unaligned path) with the satisfying callers; prof_push_record doc updated to the direct-ring-slot path (the stack-buffer/zeroing description was stale).

@vercel
vercel Bot temporarily deployed to Preview – beps June 15, 2026 16:44 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 15, 2026 16:50 Inactive
@antoniosarosi
antoniosarosi enabled auto-merge June 15, 2026 16:56
Bare [`push`] in push_with's doc resolves against module scope, where no
item named `push` exists — it is a method, so the link needs Self::push.
Caught only by CI's RUSTDOCFLAGS=-D warnings (rustdoc::broken_intra_doc_links);
local clippy/test passes do not run rustdoc.
@vercel
vercel Bot temporarily deployed to Preview – beps June 15, 2026 17:01 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 15, 2026 17:07 Inactive
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jun 15, 2026
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 15, 2026 17:24 Inactive
Merged via the queue into canary with commit 80981bc Jun 15, 2026
52 checks passed
@antoniosarosi
antoniosarosi deleted the bex-tracing-overhead branch June 15, 2026 17:29

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — 6b122476 Deployed Jun 15, 2026 by vercel[bot]
Preview – promptfiddle2 — 6b122476 Deployed Jun 15, 2026 by vercel[bot]
Preview – beps — 6b122476 Deployed Jun 15, 2026 by vercel[bot]
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