Skip to content

BEX tracing: reconcile event identity (PR #3730 + review fixes) with the profiling ring (PR #3733) - #3740

Merged
rossirpaulo merged 27 commits into
canaryfrom
paulo/bex-prof-ring-reconcile
Jun 12, 2026
Merged

rossirpaulo merged 27 commits into
canaryfrom
paulo/bex-prof-ring-reconcile

Conversation

@rossirpaulo

@rossirpaulo rossirpaulo commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

BEX Identity × Profiling Ring — Reconciliation Handoff

Branch: paulo/bex-prof-ring-reconcile → targets canary.
What it contains: everything from both BEX tracing workstreams, reconciled:
Paulo's event identity + metadata + review-fix suite (paulo/bex-event-identity-metadata,
PR #3730 + commit 3c1929dfd) merged with Antonio's profiling ring stack
(antonio/bex-prof-ring, PR #3733, tip 27ec7c0b4), plus a post-merge
adversarial review pass. Landing this branch lands both workstreams.

Commit map:

Commit What
d318a6ceb Paulo M0/M1: identity, $id, metadata table, interim JSONL (PR #3730 — already inside Antonio's branch via his merge a7f247f79)
3c1929dfd Paulo's review fixes + contract test suite (T1–T34, see TASK/REVIEW.md / TASK/REVIEW-RESPONSE.md)
bb67b34a7..27ec7c0b4 Antonio PR1–PR5: ring, producer, consumer, .bamlprof, teardown, CI
128bb22bd The reconciliation merge (this document's subject; long commit message has the per-seam decisions)
80a2c5da6 Fixes from the post-merge adversarial review (one real $id regression + latent defects + doc truthfulness)

1. What Paulo's branch did (M0/M1 + review hardening)

Design doc: TASK/TICKET.md. Post-impl notes: TASK/POST-IMPLEMENTATION-NOTES.md.

  • ids.rs (M0) — the identity quad ProcessEuid / EngineId / BexThreadId / BexCallId
    plus FunctionId, CallRef/ThreadRef/RuntimeId with reversible string
    encodings (decode(encode(x)) == x), versioned base64url, hardened decode
    edges (malformed/truncated/cross-type all fail cleanly, unit-tested).
  • $id (M1) — language primitive: read returns the current call's
    identity; baml.id.new() mints an override UUID; baml.id.set(...) /
    $id = ... installs it. Compiler-side misuse diagnostics
    (RuntimeIdCompoundAssignment, RuntimeIdMemberAccess,
    RuntimeIdCallSiteArgument, reserved-name checks, throws-fact
    registration for the implicit id.set), all snapshot-pinned.
  • Function metadata table — per-program FunctionMetadata rows (fqn,
    display name, source span, kind, origin, owner type, lambda metadata,
    BEP-053 join fields), synthetic spawn-closure and unknown-function rows,
    carried in the artifact header.
  • Interim JSONL transport — engine-emitted DiskEventV1 records through
    EventSink (a stopgap until the ring landed; deleted in this merge).
  • Review-fix suite (3c1929dfd) — 30+ fixes pinned by tests T1–T34:
    caught-exception span desync (Unwound notification + engine depth
    self-healing), $id override persistence across nested calls, watch-filter
    crash fix + scoping, never-skip CallFunction + sentinel attribution,
    monotonic process-anchored timestamps, cancel/exit status mapping,
    multi-engine scoping, header delivery robustness, wire-shape pins.

2. What Antonio's branch did (M2–M4 ring stack)

Design docs (private BoundaryML/thoughts repo): antonio/bex-event-stream-design-v2.md
(the spec) and antonio/bex-event-stream-impl-plan.md (the locked decision
ledger D1–D7 + codebase map). Implemented in full as PR1–PR5:

  • Segmented SPSC ring per (engine, os-thread): lock-free push (memcpy +
    one Release store), lossless-by-growth, free-list recycling, append-only
    registry, orphan→pool→claim lifecycle. loom + miri gated
    (bex_events/src/prof/concurrency_tests.rs, cfg baml_loom).
  • Producer integration: VM mints call_id per call unconditionally
    (frames carry it), writes CallFunction/EndFunction raw records at
    call/return/unwind; engine emits StartThread/EndThread (+ sysop pairs)
    on cold paths; D5a ring-pointer refresh once per exec resume.
  • Background consumer: heap-permit-free std::thread, drains rings,
    transcodes raw POD → protobuf DiskEventV1, writes per-engine
    .bamlprof (prof/proto/bamlprof.proto), heartbeats, durable flush ack,
    torn-tail-tolerant reader, engine teardown closes the file.
  • His merge a7f247f79 pulled PR Implement BEX event identity and program metadata #3730 into "one id universe": he
    rewrote $id to be VM-sourced and lazy
    — which is exactly the §4.1
    (VM-owns-call_id, no per-call engine yield) and §4.2 (lazy CallRef
    minting) items Paulo's review response had deferred to him.
  • Measured: clock 8.5 ns/read; drain ~7.5M ev/s/core; call-pair ~63 ns on
    the pure-call microbench, ~0–4.4% realistic. Profiling ships
    default-off (BAML_PROFILE=1 opt-in) per PR5's rollout note.

3. Where the branches diverged

Antonio's merge predated Paulo's review fixes, so 3c1929dfd (written
against the engine-owned model) collided with his 12 ring commits on the
same seams — 54 conflict hunks across 7 files, plus semantic divergences no
textual conflict surfaced:

Seam Paulo (3c1929fd) Antonio (27ec7c0b4)
Identity owner Engine pushes CurrentBexIdentity into VM each step; engine mints call ids VM owns it: call_id_counter, current_call_id, bex_ref_seed; lazy CallRef encode on read
$id override Persisted on EngineSpan.id_override, written back at the SetId drain VM current_id_override, keyed by call id
Per-call lifecycle RuntimeCallNotification yield per call → engine emits Ring records pushed in-VM, no yield
Disk transport Interim JSONL via EventSink::send_disk_event (engine_id per line, header retry, T31–T34) Ring → per-engine .bamlprof; JSONL deleted
Unwind contract VM yields Unwound{frames_remaining}; engine closes spans by depth VM emits EndFunction{Error} per unwound frame into the ring
Function ids Pool-index scheme; engine ptr→id map; unknown sentinel attribution 1-based sequential stamped on Function objects; ids snapshot-pinned
Status mapping Cancel→Cancelled, exit(0)→Ok/Completed, exit(n)→Error (T16–T20) ThreadEndStatus{Completed,Cancelled,Errored}; all exits → Completed; no function-level Cancelled
Naming BexCallId (avoids sys_types::CallId clash) kept CallId, aliased in engine

4. What the reconciliation decided (merge 128bb22bd)

Principle: Antonio's architecture wins the seams he owns (it implements
the agreed deferral); Paulo's behavioral guarantees are ported onto it as
code where still applicable and as artifact-level tests everywhere.

  1. VM-owned identity stands. CurrentBexIdentity, pending_disk_events,
    the SetId drain, EngineSpan.id_override — deleted. $id reads VM state
    lazily; bex_engine re-exports our BexCallId naming and all
    Antonio-side ids::CallId references were renamed.
  2. JSONL deleted, pins ported. New artifact-level tests in
    bex_engine/tests/prof_gate.rs: caught_exception_keeps_ring_balance,
    call_callable_has_real_identity_and_balance,
    root_cancellation_ends_thread_cancelled,
    spawned_child_cancellation_ends_child_cancelled,
    spawned_child_error_ends_child_errored, sys_exit_status_mapping,
    sentinel_rows_present_in_header,
    same_display_name_functions_are_not_misattributed.
    bex_engine/tests/jsonl_artifact.rs deleted (T31 coverage is
    prof_gate's header/balance checks).
  3. Unwound ported onto SpanNotification. RuntimeCallNotification
    died with the per-call yield, but the traced (@trace) span stream still
    needs the §2.1 fix: the unwinder counts popped traced frames (plus one
    if it crosses the watch-filter interrupt boundary — that forced yield is
    what keeps an escaping filter exception loud instead of letting the
    program's completion be consumed as the filter verdict) and the engine
    closes spans by depth. bex_vm/tests/call_notifications.rs pins this on
    the SpanNotification stream.
  4. Function ids: sequential scheme kept, made coherent. The naive merge
    had the engine reading sequential ids as pool indices (would have
    misattributed every function in the ptr→id map) and the unknown sentinel
    at a pool-derived id. Fixed: the ptr→id map is built from the same
    stamping walk; sentinel rows sit at max+1 (spawn-closure) and max+2
    (unknown); per-call resolution stays pointer-based (no name scans on any
    per-call path).
  5. Status mapping ported where the ring supports it.
    run_thread_event_loop's EndThread mapping now distinguishes
    baml.sys.exit(0) (Completed) from exit(n≠0) (Errored) per T19's
    pinned policy; root/child cancellation reads Cancelled. The
    function-level Cancelled status does not exist in the proto — see
    pending items.
  6. Spawn-arm drains ported (§2.7): both abnormal spawn-task arms close
    open spans before the wrapper's EndThread.
  7. Tests as the contract: tracing.rs keeps every $id/RuntimeEvent
    assertion (22 tests), dropping only dead JSONL assertions, each with a
    pointer comment to its prof_gate replacement.

5. Post-merge adversarial review (commit 80a2c5da6)

A 30-agent verification pass (5 dimensions — lost fixes, merge junctions,
$id semantics, ring balance, doc consistency — every finding independently
re-verified) confirmed and fixed:

  • $id override nesting regression (the one real bug). The merged
    single-slot current_id_override meant a callee calling baml.id.set
    destroyed its caller's override (empirically reproduced: caller's $id
    reverted to the default CallRef after the callee returned). Replaced with
    a per-call override stack on the VM (id_overrides), popped with the
    exiting frame in prof_exit_call (>= guard self-heals). Pinned by
    id_override_survives_callee_override.
  • Doubled error-drain emit in the escaped-throw arm of
    run_thread_event_loop_inner (introduced by the ring branch's
    ChildSettleKind restructuring; currently a no-op only because the first
    drain empties the stack). Single emission restored.
  • Watch filter yielding a sys-op stranded the armed sysop CallFunction
    (its only close site — the engine's SysOp arm — is unreachable from
    interrupt()). The mini-runner now closes the pending pair with
    EndFunction{Error} before failing loudly.
  • ~10 comment/doc corrections so the tree tells the truth about the merged
    architecture (sentinel attribution, producer-less JSONL APIs, dangling
    cross-references, SetId vs SetFunctionId, stale M0 notes).

Refuted (checked, not real): prof-clock suspend-flake claim, dropped-header
claim, sysop early-return-leak claim.

6. Verification status

  • cargo test green across bex_vm, bex_events, bex_events_native,
    bex_engine (incl. prof_gate 14, tracing 23, call_notifications 7),
    bridge_ctypes, baml_cli (210), baml_tests (1,603 snapshots), and the
    rest of the workspace — 439/439 in the affected crates at tip.
  • cargo clippy --all-targets 0 warnings; cargo fmt --check clean.
  • One snapshot regenerated: baml_cli describe listing (line numbers
    shifted by ns_id/id.baml doc/throws changes — content-identical).
  • Not run here (sandbox lacks toolchains; need CI confirmation):
    bridge_python (pyo3 build), sdk_test_typescript_node (tsc/vitest),
    loom/miri jobs (--cfg baml_loom), wasm32 builds.

7. Pending items / things to check before & after landing

Decisions needed (Antonio + Paulo)

  1. Function-level Cancelled status. Ring proto FunctionEndStatus is
    {Ok, Error} — so cancellation cannot mark frames, and the old JSONL
    Cancelled mapping has no ring equivalent. Decide: add
    FUNCTION_END_STATUS_CANCELLED (additive, non-breaking) or document
    Error-on-cancel.
  2. Cancellation balance gap (// KNOWN GAP in
    prof_gate.rs::root_cancellation_ends_thread_cancelled): engine-side
    cancellation doesn't unwind the VM, so the frame open at cancel time gets
    no EndFunction at all (root and spawned children alike; the
    in-flight sysop pair does close). The thread closes Cancelled. The old
    JSONL stream drained every open span. Decide: engine-side drain of open
    call ids at cancel, or pin "cancelled threads may strand open calls" as
    renderer contract.
  3. exit(0) frame status (pinned-by-comment in
    prof_gate.rs::sys_exit_status_mapping): exit is a synthetic VM throw,
    so main ends EndFunction{Error} even for exit(0) (thread level is
    correct: Completed). Old contract said Ok. Needs either a
    VM-visible exit flag or an accepted-deviation note.
  4. Errored vs Error naming (REVIEW §6) — proto says ERRORED for
    threads, ERROR for functions; fine, but confirm and close the item.
  5. Unknown-function sentinel attribution. Unresolvable callees stamp
    function_id: 0 ("unassigned"); the header's baml.<unknown-function>
    row (max+2) is reserved but never referenced by records. Decide:
    re-point the id-0 paths (vm.rs prof_enter_call(0) for the $entry::
    wrapper, host-closure sysop pairs, interrupt callees) at the sentinel, or
    bless id-0 as the wire contract and keep the row as a display bucket.
  6. Watch-filter calls in the stream. Filter-internal calls now mint ids
    and emit balanced ring records attached to the interrupted call (old
    scoping decision said: no identity, no events). Balanced, but pollutes
    timelines. Decide whether to gate ring emission under
    interrupt_frame.is_some().
  7. Root-thread record order (pre-existing on the ring branch): root
    threads emit the entry CallFunction before their StartThread;
    children are the opposite. Renderer must not assume StartThread-first —
    either document, or move the root StartThread emission ahead of
    set_entry_point (uniform invariant; see review notes for the
    balance-safety argument).
  8. $id call-site form foo($id = ...) (§3.6): both design docs show
    it; implementation is callee-side assignment with a targeted diagnostic.
    Product decision — changes SetFunctionId adjacency.
  9. SetFunctionId dedup: emitted per set() call (review wanted
    once-per-override). Probably fine — consumer takes last-wins — but
    confirm renderer semantics.

Follow-up work (either of us, post-land)

  1. Delete the producer-less interim JSONL plumbing end-to-end:
    EventSink::send_disk_event/send_event_file_header (+ FanOut/native
    impls and BufferedEvent::Disk/Header), serialize::disk_event_to_jsonl
    • its shape-pin tests, bex_events::clock, bex_events::DiskEventV1
      (legacy Rust enum, distinct from the proto), and
      BexEngine::event_file_header_v1 if no host needs it. All are dead but
      tested; docs now say so explicitly.
  2. Spawned-task drop leak (abnormal host teardown only): a spawned task
    dropped at an await before its event loop runs leaks an unclosed
    StartThread/CallFunction. A small RAII closer in spawn_thread_inner's
    task body would close it; sketch is in the review notes.
  3. Adopt the ids.rs newtypes in RawRecord (fields are plain u64 —
    the remaining M0 unification step).
  4. Port the two JSONL-only tests: early-yield-resume-identical-stream
    and dropped-future truncation policy (T22), against .bamlprof.
  5. T27 call-heavy bench under tools/speedtest/workloads/ (Antonio's
    microbench exists; the speedtest-integrated workload doesn't).
  6. Serde-derive serializers (§7.4) — only relevant if any JSONL shape
    survives item 10; otherwise dies with it.
  7. Default-on flip for BAML_PROFILE once PR4/PR5 gates have CI
    history (his PR5 note), in its own one-line PR.

Things to double-check in review

  • The 54-hunk resolution in bex_engine/src/lib.rs / bex_vm/src/vm.rs
    (the merge commit message lists every decision; the adversarial pass
    specifically hunted junction errors and found two, both fixed).
  • vm.rs try_unwind_exception: the popped-traced-frame count + the
    interrupt-boundary crossing both force the SpanNotification::Unwound
    yield — confirm the crossing bump matches your mental model of the filter
    contract.
  • The override stack pop in prof_exit_call runs unconditionally
    (identity semantics, not profiling) — placed before the ring gate.
  • build_program_metadata + the stamping walk + function_ids_by_ptr must
    stay the same walk (comments now say so) — any future reorder breaks
    table↔record id agreement silently.
  • CI must run loom/miri + wasm + pyo3/TS-SDK jobs that this sandbox could
    not.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Runtime identity management via baml.id namespace: current() returns the active invocation ID, new() creates overrides, and set() installs them
    • Event profiling with automatic .bamlprof file generation for observability
    • Program and function metadata tracking in runtime events for call attribution
  • Documentation

    • Design documentation added for runtime identity model and BEX event profiling system

rossirpaulo and others added 17 commits June 10, 2026 02:34
…eam (PR1)

First slice of the BEX tracing M2-M4 plan: the prof module skeleton in
bex_events, environment knob parsing (BAML_PROFILE, BAML_RING_SEG_BYTES,
BAML_RING_MAX_OVERFLOW_BYTES, BAML_RING_FREELIST_CAP,
BAML_PROF_WAKE_INTERVAL_MS), the minstant-backed profiling clock with a
wasm32 stub, and the fixed-layout little-endian ring records (v2 spec
section 4.1) with panic-free decoding and round-trip tests.

New workspace deps: minstant (calibrated TSC clock), crossbeam-utils
(CachePadded, for the ring in the next PR), loom (cfg(loom)-gated
dev-only model checking, also next PR; cfg registered in check-cfg).

PR1 gate (clock micro-bench, 20M reads, this hardware):
  prof::clock::now_ns       8.52 ns/read
  minstant::Instant::now    8.05 ns/read
  std::time::Instant::now  20.10 ns/read
Under the <=10 ns/read assumption behind the ~25 ns/call-pair budget.
…m+miri gates (PR2)

The keystone of the BEX tracing M2-M4 plan: the lock-free profiling ring
in bex_events::prof, implementing the locked design decisions D1-D7.

- ring.rs: per-(engine, os-thread) segmented SPSC ring. Lock-free push
  (memcpy + one Release store per record), lossless-by-growth (linked
  segments; never drops, never blocks), D1 drain hand-off (re-load
  commit_len after observing the link), D2 producer-owned recycle reset,
  D3 CachePadded producer/consumer/shared field groups, D7 free-list cap
  with approximate length. Hitting BAML_RING_MAX_OVERFLOW_BYTES is a
  hard process error with a clear message (D6), never a silent drop.
- registry.rs: append-only push-only Treiber registry (no pop => no
  ABA), D5b orphan -> drain-to-empty -> pool -> CAS-claim lifecycle,
  TLS-destructor orphan trigger, and the D5a resume-site lookup
  (ring_for_engine) for the PR4 engine integration.
- wake.rs: D4 parked-flag + park_timeout protocol; the producer's wake
  path is a Relaxed load + lock-free OnceLock get + unpark, never a
  block. The residual lost-wakeup race is documented benign, bounded by
  the timeout.
- sync.rs: loom/std shims so the identical protocol code is model-checked
  and shipped. The cfg is baml_loom (not the conventional loom) because
  a global --cfg loom half-activates loom support in third-party deps
  (boxcar via salsa) that need their own loom feature to compile.

Verification (the PR2 gate, all green locally):
- loom: the 5-test checklist from plan section 3.7 (D1 hand-off, D2
  recycle, free-list SPSC, orphan/pool/claim incl. engine_id publication
  and double-claim exclusion, registry concurrent acquire + pooled
  reuse), preemption bound 3.
- miri (Stacked Borrows): same scenarios on real threads, plus the
  stress tests, with -Zmiri-ignore-leaks (rings are &'static by design)
  and -Zmiri-disable-isolation (park_timeout/sleep).
- stress: 2-producer flood of 400k real CallFunction records through
  256-byte segments (constant growth + recycling) with the real
  wake/park consumer loop; exact per-engine sequence equality; idle
  memory bound asserted. Orphan-churn and global-TLS reuse tests.
- miri runs use a clock stub: minstant's #[ctor] probes the TSC with
  _rdtsc, which miri cannot execute.

CI: new prof-concurrency job (loom + scoped miri) gated on bex_events
changes, wired into the failure alert; deliberately narrower than the
disabled whole-crate miri-tests job.

No PR4 (VM/engine) integration in this change, per the plan's gate
ordering.
Findings from a 33-agent adversarial review of the PR1+PR2 diff
(5 lenses: plan conformance, memory ordering, unsafe/aliasing,
lifecycle races, tests/CI; every finding cross-examined by 2
independent verifiers):

- RingHandle::push was a safe pub API whose record-length precondition
  was debug_assert-only: a >seg_bytes record in a release build would
  memcpy past the fresh segment's buffer (UB), reachable from safe
  downstream code (confirmed independently by all five lenses). Fixed
  twice over: Ring::push now enforces the bound with a release-mode
  assert placed in the slow path (an oversized record always takes the
  slow path, so the hot path stays check-free), and RingHandle::push is
  now an unsafe fn - the not-after-TLS-teardown half of its contract
  (stale Copy handle in a later-running TLS destructor could race a new
  claimant) is not type-enforceable, so it must be a stated obligation.
- orphan()/mark_pooled() gained debug prior-state asserts (Active ->
  Orphaned -> Pooled), cheap off-hot-path tripwires for the PR4
  integration.
- New loom+std scenario sweep_races_orphan: Registry::sweep racing the
  producer's orphan store (stale-Active and orphan-mid-sweep
  observations must be benign and self-correcting).
- Behavioral leak checks: live_bytes() == 0 asserted after Ring/Registry
  teardown - validates the segment-reclaim walks that miri cannot see
  under -Zmiri-ignore-leaks.
- New unpark_delivery test: a parked consumer must be woken by a
  producer segment fill well under a 10s timeout; the flood test alone
  (1 ms park) passes identically with a fully broken Wake.
- Knob parsing now saturates numeric overflow instead of silently
  falling back: an over-usize::MAX BAML_RING_MAX_OVERFLOW_BYTES
  previously shrank to the 1 GiB default - arming the D6 abort the user
  was raising the cap to avoid.
- CI: check_prof now also watches the workspace manifest (loom/minstant
  versions, check-cfg list), rust-toolchain.toml, and ci.yaml itself, so
  changes to the gate's own inputs re-run it. Cargo.lock deliberately
  excluded (churns on every bump; cargo-tests covers the std halves).

Gates re-run green: loom 9/9 scenarios, miri 29/29, cargo test 49/49,
clippy clean.
…les (PR3)

PR3 of the BEX tracing M2-M4 plan: the consumer half of the pipeline.

- prof/proto/bamlprof.proto + build.rs (vendored protoc, prost-build into
  OUT_DIR, bridge_ctypes pattern): DiskEventV1 oneof + EventFileHeaderV1
  with process_id, engine_id, program_id placeholder (M0 coordination),
  u128 wall anchor, and the per-run function metadata table.
- consumer.rs: the bex-prof-consumer std::thread, lazily spawned on first
  ring acquisition when BAML_PROFILE is on. Sweep loop per plan 3.4 + 3.6
  (park/recheck/timeout), per-engine BufWriter file writers, 1s process
  heartbeats, transcode (pure encoding change; 0=none ring conventions
  become optional fields). Never panics: IO failures disable that
  engine's file with a stderr report; a corrupt committed range reports
  once and drops the rest of the range. Never touches GC heap or permits.
- prof::flush_and_join(timeout): drain-everything-committed + file flush
  with ack (the bex_events_native flush pattern); consumer keeps running.
  Hooked at the four exit paths next to the legacy sink flushes: baml_cli
  run_command (both runtimes), baml_pack_host main, bridge_cffi
  flush_event_sink (reaches the C ABI flush_events), baml_lsp_server
  shutdown. No-op when profiling is off.
- prof::register_engine_metadata: engines register program/function-table
  header info (the 2.6 interim provider's output; PR4 calls it).
- file.rs: length-delimited framing writer + read_bamlprof reader (pub:
  used by the PR4 gates and ad-hoc tooling until the M5 renderer).
- New knob: BAML_PROFILE_DIR (default .baml/profiles).

PR3 gate:
- e2e fake-producer test: known record sequence through a real ring
  (tiny segments, forced growth) into a real consumer thread; the
  .bamlprof parses back to the exact event sequence with the registered
  header.
- Measured drain+transcode+write throughput: 7.5M events/s/core
  (prof_drain_throughput, release, this hardware) - recorded in the
  consumer module docs with the D6 capacity formula.
PR4 of the BEX tracing M2-M4 plan: the producer side. Per-call profiling
events now flow from the VM hot path and the engine arms into the ring.

VM (bex_vm):
- BexVm: prof_ring (D5a per-resume snapshot, never valid across .await),
  prof_thread_id, call_id_counter, current_call_id, pending_sysop_call_id.
  Call ids are minted unconditionally ($id semantics, M1 reads them);
  only ring writes are gated.
- BytecodeFrame: call_id + parent_call_id (Vec-element cost, not Object
  cost; parent stored in-frame so frameless inline natives in progress
  cannot be skipped by a frame-walk restore).
- CallFunction after every bytecode frame push (shared call path, entry
  points, trampolines, interrupt frames); EndFunction{Ok} at Return;
  EndFunction{Error} at the bytecode unwind pop AND at the terminal
  unwind (the entry frame is never popped on an unhandled throw - its
  pair is closed at the ThrownUnhandled return, keeping balance).
  Native/CPS frame pops emit nothing (nothing emitted on entry; plan
  invariant 3).
- PR4b: sys-op calls (incl. LLM calls and host-closure calls) emit
  CallFunction at the VmExecState::SysOp yield sites; the engine closes
  the pair on completion. Inline natives emit Call/End pairs only when
  they complete inline (Done/Error) with the true start timestamp;
  YieldToCall natives stay transparent (CPS tracking is a follow-up).

Engine (bex_engine):
- engine_id minted from a process-global AtomicU64; prof_enabled
  snapshot; per-engine logical-thread-id mint.
- Interim function-id provider (plan 2.6) riding the existing pre-heap
  compact-encode walk: per-run 1-based ids on Function.function_id
  (borsh(skip): runtime-only, pack format unchanged) + the header
  function table registered with the consumer. M0 replaces this seam.
- D5a refresh at the top of every exec resume (TLS lookup, never per
  push). Engine-side emissions (thread lifecycle, sys-op completion)
  always re-resolve the ring via TLS - post-await the task may sit on a
  different OS thread, so the VM snapshot must never be reused there.
- StartThread at root creation and at the Spawn arm (the one place the
  parent thread id, spawning call id, and child name are in hand);
  EndThread via a run_thread_event_loop wrapper covering every exit path
  (Completed/Cancelled/Errored; Exit counts as Completed).

Gates (bex_engine/tests/prof_gate.rs, all green):
- G3 lossless: spawn-heavy + call-heavy program at the smallest legal
  segment size (forced growth/recycling): exact per-function call counts
  (9 work / 2700 mid / 5400 leaf / 1 main), 9 logical threads, exact
  Call/End and StartThread/EndThread set balance.
- Reconstruction smoke (v2 7.2): per-thread stack discipline over
  ts-ordered events + spawn-edge validity (parent thread + call exist).
- Sys-op pair test (baml.sys.sleep, async + permit release) and the
  unwind test (Error ends for the whole unwound chain, Errored thread).
- G2 overhead bench (call-chain-100x10k, BAML_PROFILE=0 vs 1) measured
  separately; numbers recorded in the PR5 doc sync.
PR5 of the BEX tracing M2-M4 plan.

- Orphan-path soak: 64 rounds of short-lived producer threads (the
  blocking-pool churn pattern) against a LIVE consumer thread; asserts
  pooled-ring reuse keeps the registry bounded and every record reaches
  disk. (Back-to-back churn without idle gaps legally allocates more
  rings - bounded by peak concurrent un-pooled threads - covered by the
  no-gap variant in concurrency_tests.)
- Overflow-cap abort test: subprocess asserts the D6 hard error fires
  with the documented message (knob name + "cannot keep up" diagnosis),
  not a silent drop or an opaque OOM.
- Hot-path trims after the G2 measurement: exact-size record buffers
  (no 292-byte MAX_RECORD_LEN zeroing per emission), #[inline] on the
  encode path, and inline-native pairs written as a single push (one
  bounds check + one Release store for the pair).
- Freelist cap stays at 4 (idle cost 1 MiB/ring at defaults; no
  measurement motivated a change).

G2 numbers (call-chain-100x10k, 1M call pairs/run, medians; recorded in
the design doc's new section 0.1):
  pre-PR baseline 74.4 ms | profiling off 80.7 ms (+8.4 percent
  always-on: unconditional $id mint + 16-byte-larger frames) |
  profiling on 143.7 ms (~63 ns/pair vs the ~25 ns budget; 2 clock
  reads are ~17 ns of it). Realistic workloads: method-call ~0 percent,
  bubble-sort +4.4 percent. The microbench overshoot and follow-up
  candidates are documented in bex-event-stream-design-v2.md 0.1,
  which also gained the measured capacity formula (7.5M ev/s/core
  drain), the real knob list, and the implementation-status ledger.

Default stays OFF (BAML_PROFILE=1 opt-in); the default-on flip is its
own one-line PR after the gates have CI history, per plan section 5.
…und 2

A 69-agent adversarial review of the PR3-PR5 diff (5 lenses, every
finding cross-examined by 2 verifiers) confirmed one critical bug class
and several lifecycle holes. All fixed here.

CRITICAL - stale D5a ring snapshot crossing awaits (SPSC two-producer
race => UB + ring corruption), two manifestations:
- inject_sysop_throw runs the VM unwinder (which emits
  EndFunction{Error} through vm.prof_ring) after the sysop permit
  release/reacquire awaits - the snapshot belonged to the pre-await OS
  thread. Fired on every caught async sysop/host error.
- The root entry-frame CallFunction: new_root_thread snapshotted the
  ring, then awaited permit acquisition (a real suspension point under
  GC park contention) before set_entry_point pushed through it.
Fix: a single prof_refresh_vm_ring(vm) helper (TLS re-snapshot), called
at the loop head, after EVERY in-loop permit re-acquire (SysOp, Await,
AwaitAny), and immediately before set_entry_point in run_entry_point
and spawn_thread_inner. new_root_thread no longer snapshots at all.

Lifecycle balance:
- Root StartThread moved from new_root_thread into the
  run_thread_event_loop wrapper - the same function that emits
  EndThread - so early-error returns between root creation and the loop
  (arg conversion, stale handles, arity errors) can no longer leak a
  started-but-never-ended thread.
- Queued-then-cancelled spawns (TaskGroup ticket denied) never run the
  event loop: their task body now closes the already-emitted entry
  CallFunction (Error) and EndThread (Cancelled).
- ThreadOutcome::SettledChild now carries a ChildSettleKind
  (Fulfilled/Cancelled/Errored): spawned children previously always
  ended Completed - Cancelled/Errored were unreachable for non-roots.
- prof_emit_native_pair minted call ids only when profiling was on,
  violating plan invariant 5 ($id determinism); minting is now
  unconditional, only the ring write is gated.

Consumer robustness (uncertain-bucket findings adjudicated real):
- The flush drain loop is bounded (1024 sweeps): a producer outrunning
  the drain could previously starve the control channel - and every
  future flush ack - forever.
- report() no longer uses eprintln! (panics on closed stderr; the
  consumer must never die silently); metadata lock is poison-tolerant.
- Removed the dead corrupt-flag; honest durability wording
  (BufWriter::flush, not fsync); known-limitations note for the absent
  engine-teardown hooks (open writers + heartbeats for process life).

Also: proto header now documents that FILE ORDER IS NOT EVENT ORDER
(sort by timestamp_ns within thread_id); record.rs call_id doc fixed to
per-logical-thread scope; the interim provider's table row builder
extracted into the single seam fn the plan asked for
(prof_function_meta_entry); stale "Remove with PR3" blanket dead_code
allows removed (two targeted accessor allows remain); prof_gate's
profile dir gained a startup nonce against pid reuse.

Re-verified: loom 9/9, miri 32/32, bex_events 53/53, prof_gate 4/4,
full bex_engine suite green, clippy clean.
… ack

Closes the three documented limitations from review round 2.

- Engine teardown: BexEngine::drop sends a non-blocking
  EngineClosed(engine_id) to the consumer, which drains the engine's
  remaining events (every commit happened-before the last Arc release),
  fsyncs and closes its .bamlprof - freeing the fd and stopping its
  heartbeats - and frees its metadata entry. Closed ids are tombstoned
  so a stale record cannot silently resurrect the file (reported once,
  dropped). Engine-churning hosts (LSP recompiles) no longer accumulate
  open files or heartbeat work, clearing the blocker this had put on
  the default-on flip. Residual cost per closed engine: one 8-byte
  tombstone; rings claimed by still-live threads stay registered (idle)
  until those threads die and the rings pool - bounded by peak
  concurrency, per plan invariant 7.
- Torn tails: read_bamlprof now returns BamlprofContents { header,
  events, truncated } - the whole-message prefix instead of rejecting
  the entire file, honoring the design's partial-parseable bet (v2 core
  bet 3) for crashed writers and live heartbeat appends. The gate
  tests' retry-on-parse workaround is gone; unreadable headers (file
  mid-creation) are skipped by the demux loop.
- Durability: explicit flush requests and engine closes now sync_all;
  flush_and_join's ack means fsync-durable (survives power loss), while
  the consumer's idle-cadence flushes stay cheap (OS buffer only).

New gate (prof_gate 5/5): engine_teardown_closes_profile - drops an
engine, proves its file stops growing across two heartbeat intervals
after the close (FIFO control channel: the flush ack implies the close
already ran), and proves later engines still profile normally.

Verified before commit: bex_events 53/53, prof_gate 5/5, miri 32/32,
clippy clean. (Ring sync code untouched - the loom suite from the
previous commit still covers it.) Design doc 0.1 updated.
Reconciles paulo/bex-event-identity-metadata (M0 identity, program
metadata, the M1 $id surface) with the M2-M4 ring/consumer/producer
implementation. One id universe, one transport.

Taken wholesale from #3730:
- ids.rs (ProcessEuid/EngineId/ProgramId/BexThreadId/FunctionId/CallId,
  ThreadRef/CallRef wire encoding, RuntimeId) - the M0 contract.
- metadata.rs (rich FunctionMetadataTable + ProgramMetadata).
- The $id language surface (ns_id/id.baml, compiler lowering) and
  identity on host RuntimeEvents (CallRef on the bridge proto).
- EventSink::send_event_file_header (kept; fed at engine construction)
  and send_disk_event (kept as opt-in sink surface).

Unified (the divergence points):
- Call ids: ONE universe, minted VM-side per call (plan invariant 5).
  The span machinery's engine-side counter (SpanState.next_call_id) is
  gone; traced spans read the VM's current call id at FunctionEnter, so
  host-event identity, $id strings, and .bamlprof records carry
  byte-identical ids.
- $id is now VM-live (bex_ref_seed + current_call_id + per-call
  override) instead of engine-set per exec step from the traced-span
  stack: it works in every function, traced or not, profiling on or
  off, with CallRef encoding paid only when $id is actually read.
  baml.id.set() records SetFunctionId into the ring (stream clock).
- Function ids: 1-based sequential in pool order (0 = unassigned),
  stamped on Function pre-heap AND used by build_program_metadata - the
  metadata table replaces the interim plan-2.6 provider and now feeds
  the .bamlprof header (program_id = hex ProgramId).
- One engine-id mint (EngineId) and one logical-thread-id mint shared
  by profiling records and host-event identity.
- ThreadOutcome::SettledChild keeps the engine-internal ChildSettleKind;
  Paulo's span-close emissions for erroring/cancelled children are
  retained on top.

Dropped (interim scaffolding from #3730, superseded by the ring):
- The per-call RuntimeCallNotify VM yield + runtime_call_frames +
  pending_disk_events + the engine's shadow span stack for non-traced
  calls (a yield per call breaks the exec loop ~1M times/s on
  call-heavy code; the ring push is ~63ns inline).
- The DiskEventV1-via-EventSink per-call emission paths (StartThread /
  CallFunction / EndFunction / EndThread / SetId through the sink); the
  disk stream is the ring -> bex-prof-consumer -> .bamlprof.
- Per-step CurrentBexIdentity maintenance (incl. its per-step CallRef
  base64 encode).

Tests: tracing.rs identity tests pass against the unified VM ids
unchanged (thread 1 / call 1 / CallRef round-trip); its three
sink-stream lifecycle tests are superseded by prof_gate.rs and removed;
the $id tests keep their behavioral asserts and the SetFunctionId
record assertion moved to prof_gate::set_function_id_recorded (which
caught a real bug: the VM record buffer was sized for CallFunction and
panicked on the 41-byte SetFunctionId - fixed). New gate total 6/6;
engine suite 31 binaries green; bex_events 61; workspace clippy clean.
Boxed bex_events_native's BufferedEvent large variants (clippy).
The ring/producer/consumer contract this branch implements: the locked
concurrency decision ledger (D1-D7), the codebase map, the ring
specification with its memory-ordering table, the record/artifact
shapes, the phased PR gates, and the review invariants - plus the
coordination seams with the M0/M1 identity work (now merged here, see
PR #3733). Added verbatim from the planning doc with a status banner
recording implementation state and the documented deviations.
Internal planning docs live in boundaryml/thoughts (antonio/
bex-event-stream-impl-plan.md, with the implementation-status banner);
the in-repo copy was a sharing convenience that doesn't belong here.
…napshots

Three CI failures on PR #3733, all inherited or latent:

- prek's wasm clippy (-D warnings): the prof consumer half is compiled
  but unreachable on wasm32 (profiling forced off; the deferred
  cooperative drain would be its caller), so its API is dead code
  there. The blanket allows removed in the review-2 cleanup were
  validated native-only; restored as targeted
  cfg_attr(target_arch = "wasm32", allow(dead_code)) on the four prof
  files.
- test_vm_param_matches_vm_usage (all OSes): latent test weakness
  exposed by the first zero-param vm-less builtin (baml.id.new). The
  contains-pattern disambiguated same-name methods by params only, so
  zero-param methods aliased across traits
  (spawn.CancelToken.new(vm: &mut BexVm, ) matched the pattern built
  for baml.id.new()). Patterns now include the return type.
- snapshot tests: accepted the 8 snapshots changed by the new baml.id
  namespace (HIR/TIR/MIR/codegen/describe listings plus the
  global-index renumbering in the bytecode-format listings). Verified
  the diffs are only the id namespace + index shifts.

Verified locally with CI's own lines: the wasm clippy task config,
cargo insta test (no snapshots to review, no unreferenced), and
cargo nextest run --all-features -E 'not package(baml_tests) and not
package(/^sdk_test_/)' - 3433/3433 passed.
Fixes every pre-merge item from TASK/REVIEW.md §10:

- §2.1: unwind resync — the VM yields Unwound{frames_remaining} whenever an
  exception unwind pops notified frames; the engine closes spans by frame
  depth, self-heals on enter/exit depth mismatches, and the Complete arms
  (root and child) close stray spans loudly instead of blind-popping
- §2.2: $id overrides persist on EngineSpan (written back when the SetId
  event is drained), so they survive nested calls and spawns
- §2.3: interrupt() consumes notification states while the interrupt frame
  is alive; a filter exception escaping the boundary fails loudly
- §2.4/§4.3: CallFunction is never skipped (reserved unknown-function
  sentinel row); call notifications carry the resolved function pointer and
  the engine resolves identity via a prebuilt addr->FunctionId map — the
  per-call name clones, O(F) table scans, and display-name misattribution
  are gone; call_callable gets real callee identity
- §2.5: timestamp_ns is monotonic-since-process-start (bex_events::clock);
  the header's started_at_epoch_ns is the matching wall anchor so
  wall = started_at + ts composes
- §2.6/§2.7: root cancellations read Cancelled/Cancelled; baml.sys.exit(0)
  maps to Ok/Completed (non-zero to Error); abnormal spawn-task arms drain
  open spans before EndThread
- §3.x: compiler $id holes — compound assignment, non-string assignment,
  member access (incl. ?.), call-site labels, and $id bindings (let, param,
  destructure shorthand, function names) are targeted compile errors; the
  dead AST rewrite is removed; MIR's silent-temp lvalue fallback fails
  loudly; baml.id.set declares throws InvalidArgument and the $id = e sugar
  is held to the same throws contract
- §5.x: per-line engine_id on the interim JSONL (EventSink::send_disk_event
  carries EngineId), bounded-retry header sends, atomic per-line writes,
  documented lossiness

Contract test suite per §9: assert_balanced/assert_threads_closed helpers
used by every disk-event test (and retrofitted into the pre-existing loose
ones), T1-T26, T28, T31-T34, two-engine scoping, ids.rs decode edges, and an
end-to-end JSONL consumer test. Entropy pass per §7: emit_span_end
consolidation, single spawn EndThread exit point, BexCallId rename, doc
comments on the contract types, TODO(bep-053) markers on the heuristics.

Also fixes a pre-existing clippy 1.96 lint in baml_release (the pre-commit
hook denies warnings workspace-wide, so it gated this commit).

TASK/REVIEW-RESPONSE.md maps every finding to its fix and test (stable
T-numbers), records the design decisions, and lists the items deliberately
left for the cross-team sync (call-site $id form, yield gating /
VM-owns-call_id seam, lazy CallRef minting, status naming, call-heavy bench,
serde-derive migration).

Co-Authored-By: Claude Fable 5 <[email protected]>
Merges the profiling-ring stack (PR #3733) with the BEX identity
review-fix suite (3c1929d). Antonio's VM-owned identity model wins the
contested seams — it is exactly the §4.1/§4.2 refactor the review
response deferred to him: call ids minted by the VM, lazy CallRef
encoding on $id read, overrides keyed by call_id, per-call lifecycle in
the lock-free ring instead of per-call engine yields.

Resolution decisions:
- Engine-pushed identity (CurrentBexIdentity, pending_disk_events, the
  SetId drain, EngineSpan.id_override) is deleted; $id reads/overrides
  are VM-sourced.
- The interim JSONL disk-event stream is gone (its replacement — the
  ring + .bamlprof — landed). Its behavioral pins are ported to
  prof_gate.rs at the artifact level: caught-exception balance,
  call_callable identity, cancellation/exit/child-thread statuses,
  sentinel header rows, same-display-name attribution.
- RuntimeCallNotification is deleted with the per-call yield; the
  Unwound notification (review §2.1) is ported onto SpanNotification so
  the traced span stream keeps timely, self-healing closure of frames
  popped by caught exceptions, and so a watch-filter exception escaping
  the interrupt boundary still fails loudly (the unwinder counts the
  boundary crossing as a forced-yield trigger).
- Function ids: Antonio's 1-based sequential scheme is kept; the
  ptr->id map is built from the same stamping walk (the merge had left
  it reading sequential ids as pool indices), and the unknown-function
  sentinel row is re-seated at max+2. Per-call resolution stays
  pointer-based (review §4.3); the display-name scan is gone.
- run_thread_event_loop's ring EndThread mapping now distinguishes
  baml.sys.exit(0) (Completed) from exit(n != 0) (Errored), per the
  review's pinned §2.6 policy; the spawn task's abnormal arms drain
  open spans before the thread closes (§2.7).
- ids.rs keeps the review rename (CallId -> BexCallId); Antonio-side
  references updated.
- VM-level notification contracts (bex_vm/tests/call_notifications.rs)
  now pin the traced stream; engine tracing tests keep every $id and
  span assertion and drop only the dead JSONL assertions.

Known gaps recorded for the cross-team sync (pinned as KNOWN GAP
comments in prof_gate.rs): cancellation does not close the open frame
(no EndFunction; ring proto has no Cancelled function status), and
exit(0) frames end Error at function level (thread level reads
Completed) because exit is a synthetic VM throw.

Co-Authored-By: Claude Fable 5 <[email protected]>
A 30-agent verification pass over merge 128bb22 (5 dimensions, every
finding independently re-verified against the code) confirmed one
behavioral regression, two latent defects, and a set of stale
comments/docs from the resolution. Fixed here:

- $id override nesting (review §2.2, T4-T7): the merge had narrowed the
  override to a single VM slot, so a callee that also called
  `baml.id.set` silently destroyed its caller's override ($id reverted
  to the default CallRef after the callee returned; empirically
  reproduced). Replaced with a per-call override stack on the VM:
  set replaces-or-pushes the current call's entry, reads match only the
  top against current_call_id, and prof_exit_call pops the exiting
  call's entry (>= guard self-heals against silent pops). Pinned by the
  new tracing test id_override_survives_callee_override.
- Doubled error-drain emit in run_thread_event_loop_inner's
  escaped-throw arm (introduced by the ring branch's ChildSettleKind
  restructuring; currently a no-op only because the first drain empties
  the span stack). Single emission point restored.
- A watch filter yielding a sys-op stranded the armed sysop
  CallFunction (its only close site, the engine's SysOp arm, is
  unreachable from interrupt()): the mini-runner now closes the pending
  pair with EndFunction{Error} before failing loudly.

Comment/doc truthfulness (no behavior change): unknown-function
sentinel is header-only today (unresolvable callees stamp function_id 0
— re-pointing them at the sentinel row is an open cross-team item);
interrupt-internal calls DO mint call ids and ring records (only the
span stream is suppressed); EventSink::send_disk_event /
disk_event_to_jsonl / NativeEventSink disk paths marked producer-less
legacy; RuntimeId contract now names the live SetFunctionId record;
removed the orphaned pool-size comment, a dangling RuntimeCallNotify
cross-reference, a stale only_used_in_recursion allow, and wrong test
names in tracing.rs port notes; prof/mod.rs naming note updated for the
landed ids.rs.

Recorded for the cross-team sync (not changed here): root-thread
streams emit the entry CallFunction before StartThread (children are
the opposite — pre-existing ring-branch ordering), and a spawned task
dropped at an await before its event loop runs leaks an unclosed
StartThread/CallFunction (abnormal host teardown only).

Co-Authored-By: Claude Fable 5 <[email protected]>
TASK/RECONCILIATION.md: what each parent branch did, where they
diverged, every reconciliation decision, the post-merge review
findings, and the full pending-items list for landing into canary.

Co-Authored-By: Claude Fable 5 <[email protected]>
@vercel

vercel Bot commented Jun 11, 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 12, 2026 7:50am
promptfiddle2 Ready Ready Preview, Comment Jun 12, 2026 7:50am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
promptfiddle Skipped Skipped Jun 12, 2026 7:50am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds BEX runtime identity and profiling ring support across compiler contracts, VM/engine execution, on-disk .bamlprof output, CI gating, and validation tests.

Changes

BEX identity and profiling pipeline

Layer / File(s) Summary
Contracts and compiler $id surface
baml_language/..., .github/workflows/ci.yaml
CI now checks profiling-related paths separately, docs/whitelist entries expand, $id builtin and compiler lowering/type diagnostics are added, and MIR/type tests cover reserved/runtime-id behavior.
Profiling substrate and storage
baml_language/crates/bex_events/...
Adds the profiling clock, ring/registry/consumer/file/proto/record modules, config and concurrency support, and the supporting Cargo/build wiring for .bamlprof production and parsing.
Engine and VM profiling lifecycle
baml_language/crates/bex_engine/..., baml_language/crates/bex_vm/..., baml_language/crates/baml_pack_host/...
Threads engine identity, call ids, $id runtime behavior, profiling emission, spawn/sys-op/unwind handling, and pack-host flush/shutdown behavior through engine and VM execution.
Validation coverage and public API surface updates
baml_language/crates/bex_engine/tests/*, baml_language/crates/bex_events/src/lib.rs, baml_language/crates/bex_vm/tests/*, baml_language/crates/bridge_ctypes/...
Extends public profiling/metadata APIs and adds engine/VM/profiling tests for identity, balance, cancellation, truncation, and bridge event identity fields.

Sequence Diagram(s)

sequenceDiagram
  participant BexEngine
  participant RingRegistry
  participant BexVm
  participant ProfConsumer
  participant ProfileWriter
  BexEngine->>RingRegistry: register_engine_metadata
  BexEngine->>BexVm: attach prof_ring and call ids
  BexVm->>RingRegistry: ring_for_engine
  BexVm->>ProfConsumer: prof records via ring
  ProfConsumer->>ProfileWriter: append .bamlprof events
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

Suggested reviewers

  • antoniosarosi

Poem

🐇 I hopped through rings and ids so bright,
Through prof streams marching left to right.
The burrow hums with CallRef gleam,
And .bamlprof sings a balanced theme.
A carrot toast for $id today!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch paulo/bex-prof-ring-reconcile

@vercel
vercel Bot temporarily deployed to Preview – beps June 11, 2026 09:24 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 11, 2026 09:29 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 11, 2026 09:42 Inactive
# Conflicts:
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap
…my, cancel drain, StartThread-first

Decisions 1-9 (RECONCILIATION.md §7, resolved by Antonio 2026-06-11):

- FunctionEndStatus is now the closed frame-fate taxonomy {OK, ERRORED,
  CANCELLED, EXITED}: ERROR renamed ERRORED (tag unchanged), CANCELLED=2
  and EXITED=3 added. Statuses describe the frame's fate, not the
  program's outcome.
- The unwinder peeks the thrown value's panic class once per unwind
  (prof_unwind_status): Exit→Exited, Cancelled→Cancelled, else Errored.
  baml.sys.exit's own native pair closes Exited
  (prof_native_error_status); sysop pairs classify op errors the same way
  (prof_sysop_error_status — a host-abandoned CompletionHandle is
  cancel-classed without the thread's token firing).
- Terminated threads never strand open calls: prof_drain_open_calls
  closes every open frame innermost-first at the seven blocks that end a
  thread without unwinding the VM — six cancel blocks (Cancelled) plus
  the unobserved fire-and-forget child-error surfacing (Errored; found by
  the post-decision adversarial review). The weakened
  assert_balance_allowing_unended helper is deleted; cancel/exit tests
  assert full balance.
- Root StartThread moved ahead of set_entry_point: every thread's first
  record is its StartThread (wire invariant, pinned in
  reconstruction_smoke). The EndThread pairing now relies on the
  straight-line stretch into run_thread_event_loop — comments pin both
  ends.
- Doc-only rulings recorded in the proto header: function_id 0 blessed as
  the unattributable-call wire contract; watch-filter calls stay in the
  stream (code that runs); SetFunctionId is per-set, last-wins;
  foo($id = ...) deferred. EXITED's thread-level exit-code mapping is
  scoped to root threads (children settle Errored like any unhandled
  throw).

Gates: prof_gate 15/15 (new: unobserved_child_error_drains_parent_frames),
tracing 23, call_notifications 7, bex_events 68, loom 28/28, miri 32/32,
nextest 3468/3468, clippy clean.
@vercel
vercel Bot temporarily deployed to Preview – beps June 11, 2026 15:36 Inactive
@antoniosarosi
antoniosarosi marked this pull request as ready for review June 11, 2026 15:37
@github-actions

github-actions Bot commented Jun 11, 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.

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 11, 2026 15:42 Inactive
@github-actions

Copy link
Copy Markdown

No description provided.

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 11, 2026 15:55 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 11, 2026 19:16 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 11, 2026 19:29 Inactive
…-reconcile

# Conflicts:
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap
@vercel
vercel Bot temporarily deployed to Preview – beps June 11, 2026 23:53 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 11, 2026 23:59 Inactive
@vercel
vercel Bot temporarily deployed to Preview – beps June 12, 2026 00:01 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 12, 2026 00:06 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 12, 2026 00:18 Inactive
…-reconcile

# Conflicts:
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap
@vercel
vercel Bot temporarily deployed to Preview – beps June 12, 2026 07:13 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 12, 2026 07:19 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 12, 2026 07:31 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 12, 2026 07:44 Inactive
@vercel
vercel Bot temporarily deployed to Preview – beps June 12, 2026 07:45 Inactive
@rossirpaulo
rossirpaulo enabled auto-merge June 12, 2026 07:49
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 12, 2026 07:50 Inactive
@rossirpaulo
rossirpaulo added this pull request to the merge queue Jun 12, 2026
Merged via the queue into canary with commit dd3e0f6 Jun 12, 2026
54 checks passed
@rossirpaulo
rossirpaulo deleted the paulo/bex-prof-ring-reconcile branch June 12, 2026 08:02
meefs pushed a commit to meefs/baml that referenced this pull request Jun 12, 2026
…onsumer (BoundaryML#3759)

## Problem

`minstant-0.1.7` registers a `#[ctor]` static initializer that
busy-spins **≥20 ms calibrating the TSC before `main()` in every process
that links `bex_events`** — packed binaries, `baml-cli`, the LSP, and
every nextest test process — even with `BAML_PROFILE=0`. Its calibration
loop (`_cycles_per_sec`) requires ≥10 ms per sample window and ≥2
converging windows by construction.

Measured impact: an empty packed program (`function main() -> int {
return 0; }`) ran **10.8 ms pre-tracing → 31.3 ms** on canary. This
constant per-process tax was the dominant end-to-end regression from the
tracing merge (BoundaryML#3740) — it inflated every short-running benchmark by
+50–145% and is a large chunk of the test-suite wall-time increase.

Secondary problems: minstant only uses the TSC on Linux x86 (Macs
silently fall back to the OS clock), and the producer hot path paid a
tick→ns conversion on every clock read.

## Fix

- **Producers stamp raw ticks** (`clock::now_ticks()`): `rdtsc` on
x86_64 (gated on a CPUID invariant-TSC probe), `mrs CNTVCT_EL0` on
aarch64, monotonic-`Instant` fallback otherwise. No conversion, no
calibration, no ctor on any producer path. The anchor is forced inside
`now_ticks()`, so every stamped tick postdates the process base
regardless of init order.
- **The consumer owns tick→ns conversion** (`TickConverter`, built on
the consumer thread): exact rational rates where the platform states
them (`mach_timebase_info` on macOS — Apple Silicon's 125/3 handled
exactly; `CNTFRQ_EL0` on Linux ARM); for the x86 TSC, a ~2 ms two-point
estimate at consumer start, refined once across a ≥1 s window at
heartbeat cadence with piecewise-linear anchor continuity (per-thread
`timestamp_ns` stays monotone across the rate swap; late-draining ring
events convert through the previous segment).
- **Disk format unchanged**: `.bamlprof` timestamps remain nanoseconds;
`wall(event) = started_at_epoch_ns + timestamp_ns` holds; `prof_gate`
needed zero source changes. The header gains informational `clock_kind`
/ tick-rate rational / `clock_quality` fields (proto3-additive).
- `minstant` (and the `[email protected]` it pulled in) removed from the
workspace.

## Measured (Linux x86, 32-core dev box)

| Metric | before | after |
|---|---|---|
| Empty packed binary startup (profiling on) | 31.3 ms | **15.2 ms** |
| Same, `BAML_PROFILE=0`, vs pre-tracing baseline | +20.5 ms | **+1.1
ms** |
| `now_ticks` read (prof_clock bench) | 8.45 ns (minstant) | **8.33 ns**
|
| Tracing on-vs-off, divan pure-call-1m | ~42 ns/call-pair | **~24
ns/call-pair** |

The per-pair improvement comes from the two tick→ns conversions leaving
the VM hot path. End-to-end (speedtest corpus, packed binaries):
small/loop-heavy workloads return to ≈pre-tracing (+3–11%); remaining
overhead concentrates proportionally to traced call volume.

## Verification

- `cargo test -p bex_events` (51), `cargo test -p bex_engine --test
prof_gate` (17/17)
- Full CI mirror: `rustup run 1.93.0 cargo test -p bex_events -p bex_vm
-p bex_engine --all-features` — all green
- loom (`--cfg baml_loom`, 34) and miri (33) prof suites
- clippy `--all-targets` clean; wasm32 `cargo check` clean
- `.init_array` back to 53 entries (the ctor is gone); `cargo tree -i
minstant` / `-i [email protected]` → no matches
- New unit tests: cross-thread tick ordering, converter rate sanity
(catches a swapped mach timebase rational), refinement monotonicity
across the rate swap, pre-base clamp, identity passthrough

## Notes for reviewers

- ARM paths are validated by compilation + unit tests that will exercise
them on the macOS-ARM CI leg (`converter_rate_sane` fails loudly on a
wrong rational). Known monitor-don't-block risk: 24 MHz CNTVCT
granularity on M1–M3 slightly raises timestamp-tie probability in
prof_gate's large streams; ties are stable-sort-safe.
- The consumer blocks ~2 ms once at startup for the coarse TSC estimate
(x86 only, profiling on only); events transcoded in the first ~1 s carry
~0.1% rate error, recorded as `CLOCK_QUALITY_COARSE` in the header.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Profiling now stamps raw hardware ticks and converts them to
nanoseconds on the consumer side for improved consistency and
monotonicity.
* Trace file headers record clock diagnostics (clock kind, tick→ns rate,
and quality) so exported traces include clock metadata.

* **Chores**
* Removed an external timing dependency; benchmarks and tests updated to
measure ticks and conversion separately.
* Profiling is stubbed/disabled on unsupported targets (e.g.,
wasm/miri).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle2 — 88b7570f Deployed Jun 12, 2026 by vercel[bot]
Preview – beps — 88b7570f Deployed Jun 12, 2026 by vercel[bot]
Preview – promptfiddle — 88b7570f Deployed Jun 12, 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.

2 participants