Skip to content

prof: replace minstant with raw-tick clock; convert ticks→ns in the consumer - #3759

Merged
antoniosarosi merged 4 commits into
canaryfrom
bex-clock-raw-ticks
Jun 12, 2026
Merged

antoniosarosi merged 4 commits into
canaryfrom
bex-clock-raw-ticks

Conversation

@antoniosarosi

@antoniosarosi antoniosarosi commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

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 (#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.

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).

…consumer

minstant-0.1.7 registers a #[ctor] that busy-spins >=20ms calibrating the
TSC before main() in EVERY process linking bex_events — packed binaries,
baml-cli, the LSP, and each nextest test process — even with BAML_PROFILE=0
(empty packed binary: 31.3ms -> 15.2ms median after this change; with
BAML_PROFILE=0 the startup delta vs pre-tracing is now +1.1ms, was +20.5ms).

Producers now stamp records with clock::now_ticks() — raw rdtsc on x86_64
(gated on CPUID invariant-TSC), CNTVCT_EL0 on aarch64 (exact rate from
mach_timebase_info on macOS / CNTFRQ_EL0 elsewhere), std::time::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): exact rational rates
where the platform states them; for the x86 TSC a ~2ms two-point estimate
at consumer start, refined once across a >=1s window at heartbeat cadence
with piecewise-linear anchor continuity (per-thread timestamp_ns stays
monotone across the rate swap; pre-switch ticks keep converting through the
previous segment). Disk format stays nanoseconds — wall(event) =
started_at_epoch_ns + timestamp_ns is unchanged and prof_gate needed no
source changes. The header gains informational clock_kind / tick rational /
clock_quality fields (proto3-additive).

Measured (Linux x86, 32-core dev box):
- now_ticks 8.33 ns/read (minstant was 8.45); consumer-side to_ns ~free.
- Tracing on-vs-off on pure-call-1m: ~24 ns/call-pair, down from ~42
  (two tick->ns conversions left the VM hot path).
- ARM reads go from OS-clock fallback to a raw counter read; rates are
  exact with zero calibration.

Gates: bex_events 51 tests; prof_gate 17/17; loom (--cfg baml_loom) 34;
miri 33; clippy --all-targets clean; wasm32 check clean; .init_array back
to 53 entries; cargo tree shows minstant and [email protected] fully pruned.
@vercel

vercel Bot commented Jun 12, 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:08pm
promptfiddle Ready Ready Preview, Comment Jun 12, 2026 7:08pm
promptfiddle2 Ready Ready Preview, Comment Jun 12, 2026 7:08pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 12, 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: 9cdaf84d-9673-44ac-9975-a508a3506054

📥 Commits

Reviewing files that changed from the base of the PR and between 1d1f016 and 6bb1da7.

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

📝 Walkthrough

Walkthrough

Producer-side timestamps changed from nanoseconds to raw hardware ticks (now_ticks); a TickConverter converts ticks→ns in the consumer. RawRecord variants, consumer transcoding, file headers/protobuf, engine/VM emission sites, tests, benches, and workspace dependency were updated accordingly.

Changes

Profiling Clock Architecture Refactoring

Layer / File(s) Summary
Clock producer/consumer
baml_language/crates/bex_events/src/prof/clock.rs
Replaces producer-side now_ns with now_ticks, adds ClockKind/ClockMeta/ClockQuality, implements source detection and init(), and provides TickConverter with refinement and tests.
Ring record schema: ts_ns → ts_ticks
baml_language/crates/bex_events/src/prof/record.rs
Renames timestamp fields on RawRecord variants to ts_ticks, updates encode/decode paths and all module tests.
Consumer transcoding & wiring
baml_language/crates/bex_events/src/prof/consumer.rs, baml_language/crates/bex_events/benches/prof_clock.rs
Adds ClockMode (Detect/Fixed), extends ConsumerEnv/ConsumerState to hold a TickConverter, transcodes ts_ticks→protobuf timestamp_ns via conv.to_ns(...), uses conv for heartbeats and headers, and updates tests/benches to use identity converters where appropriate.
File format: header + protobuf metadata
baml_language/crates/bex_events/src/prof/file.rs, baml_language/crates/bex_events/src/prof/proto/bamlprof.proto
Adds clock_kind, tick_ns_numer/tick_ns_denom, and clock_quality to EventFileHeaderV1; updates build_header to accept TickConverter and populate clock diagnostics; adds ClockKind/ClockQuality enums.
Engine/VM emission and registry init
baml_language/crates/bex_engine/src/lib.rs, baml_language/crates/bex_vm/src/vm.rs, baml_language/crates/bex_events/src/prof/registry.rs
All profiling emitters now call now_ticks() and populate ts_ticks; prof_emit_native_pair signature updated to accept tick start; ring_for_engine now calls clock::init() before ring allocation.
Dependency, docs, and benches
baml_language/Cargo.toml, baml_language/crates/bex_events/Cargo.toml, baml_language/crates/bex_events/src/prof/config.rs, baml_language/crates/bex_events/src/prof/mod.rs
Removes workspace minstant, updates Cargo comments and docs to describe in-tree tick producer with Instant fallback and wasm32/miri stub, updates benches to measure now_ticks and conversion separately, and replaces "TSC clock" wording with "tick clock".

Sequence Diagram

sequenceDiagram
  participant Engine as BEX Engine
  participant Clock as prof::clock
  participant Ring as Ring Buffer
  participant Consumer as prof Consumer
  participant Conv as TickConverter
  Engine->>Clock: init() once
  Clock->>Clock: detect_source (CPUID/EL0/fallback), record base_ticks & epoch
  Engine->>Clock: now_ticks() per event
  Engine->>Ring: emit RawRecord(ts_ticks)
  Consumer->>Clock: ClockMode::Detect → TickConverter::from_clock()
  Consumer->>Ring: read RawRecord(ts_ticks)
  Consumer->>Conv: to_ns(ts_ticks)
  Conv->>Conv: maybe_refine()
  Conv-->>Consumer: timestamp_ns
  Consumer->>Consumer: write protobuf Event + header metadata
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • BoundaryML/baml#3740: Previous changes touching the BEX profiling clock/record pipeline; directly related to the shift from now_ns-based profiling to a tick-based design.

Poem

🐰 I counted ticks beneath the core,
I hopped from tick to tick and more,
A converter bridge from raw to fine,
It maps each hop to tidy time,
My whiskers hum — profiling's chore.

🚥 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 accurately summarizes the main change: removing minstant dependency and shifting from producer-side nanosecond timestamps to consumer-side tick-to-nanosecond conversion.
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-clock-raw-ticks

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 12, 2026 18:09 Inactive
@github-actions

github-actions Bot commented Jun 12, 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 12, 2026 18:14 Inactive
@github-actions

Copy link
Copy Markdown

No description provided.

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

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@baml_language/crates/bex_events/benches/prof_clock.rs`:
- Around line 3-6: The TickConverter::to_ns benchmark currently calls
now_ticks() inside the timed closure, so it measures the producer tick read plus
conversion; to fix, either hoist a tick sample out of the timed closure (call
let t = now_ticks() before the bench iteration and pass t into
TickConverter::to_ns inside the closure) to measure only the consumer tick→ns
cost, or explicitly relabel the benchmark row to indicate it is an end-to-end
measurement; locate uses of TickConverter::to_ns and now_ticks() in
prof_clock.rs and apply one of these changes consistently for the converter
benchmark (also update the similar cases around lines 35–39).

In `@baml_language/crates/bex_events/src/prof/clock.rs`:
- Around line 110-143: The detect() logic currently treats CPUID invariant-TSC
(has_invariant_tsc()) as sufficient to choose Source::Tsc, but that doesn't
guarantee cross-core synchronized rdtsc; change detect() to perform a runtime
cross-core monotonicity/synchronization probe before returning Source::Tsc: if
has_invariant_tsc() is true then run a short OS-backed multi-core probe (e.g.,
schedule/affine two threads on different logical CPUs and compare rdtsc reads or
validate monotonic increments across cores) and only return (Source::Tsc, None)
when that probe confirms cross-core ordering; otherwise return (Source::Instant,
Some((1,1))). Update has_invariant_tsc() to remain as a quick CPUID check but do
not by itself drive the Source choice—use it only as a prerequisite for running
the runtime probe. Ensure the probe code is referenced/triggered from detect()
and documented so now_ticks()/Source consumers rely on safe fallback to Instant
when synchronization cannot be verified.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3dc6557a-1221-4b3f-8d38-5e75198ff8cf

📥 Commits

Reviewing files that changed from the base of the PR and between bdab837 and 88a8dc5.

⛔ Files ignored due to path filters (1)
  • baml_language/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • baml_language/Cargo.toml
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bex_events/Cargo.toml
  • baml_language/crates/bex_events/benches/prof_clock.rs
  • baml_language/crates/bex_events/src/prof/clock.rs
  • baml_language/crates/bex_events/src/prof/concurrency_tests.rs
  • baml_language/crates/bex_events/src/prof/config.rs
  • baml_language/crates/bex_events/src/prof/consumer.rs
  • baml_language/crates/bex_events/src/prof/file.rs
  • baml_language/crates/bex_events/src/prof/mod.rs
  • baml_language/crates/bex_events/src/prof/proto/bamlprof.proto
  • baml_language/crates/bex_events/src/prof/record.rs
  • baml_language/crates/bex_events/src/prof/registry.rs
  • baml_language/crates/bex_vm/src/vm.rs
💤 Files with no reviewable changes (1)
  • baml_language/Cargo.toml

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 12, 2026 18:28 Inactive
We stamp ticks with CNTVCT_EL0 but converted using mach_timebase_info's
rational, which describes mach_absolute_time. On M1-M3 both counters run
at 24 MHz so they coincided; on M4/M5 CNTVCT_EL0 runs at 1 GHz while
mach_absolute_time stays at 24 MHz, so applying the mach rational (125/3)
to raw CNTVCT_EL0 reads inflated every timestamp ~41.7x.

Read CNTFRQ_EL0 (the architectural rate of CNTVCT_EL0) on every aarch64
OS, macOS included. Correct on both M1-M3 (24 MHz) and M4/M5 (1 GHz);
converter_rate_sane now passes on Apple Silicon M5.
@vercel
vercel Bot temporarily deployed to Preview – beps June 12, 2026 18:33 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 12, 2026 18:39 Inactive
@vercel
vercel Bot temporarily deployed to Preview – beps June 12, 2026 18:42 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 12, 2026 18:47 Inactive
The CPUID invariant-TSC bit guarantees constant-rate/never-stop, not
cross-core synchronization. On Linux, additionally require the kernel's
clocksource verdict (it measures cross-CPU TSC sync at boot) — the same
check minstant relied on. Relabel the prof_clock converter row as the
end-to-end read+convert measurement it is.
@vercel
vercel Bot temporarily deployed to Preview – beps June 12, 2026 18:49 Inactive
@antoniosarosi
antoniosarosi enabled auto-merge June 12, 2026 18:53
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 12, 2026 18:55 Inactive
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jun 12, 2026
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 12, 2026 19:08 Inactive
Merged via the queue into canary with commit 9dc0910 Jun 12, 2026
52 checks passed
@antoniosarosi
antoniosarosi deleted the bex-clock-raw-ticks branch June 12, 2026 19:10

This branch was previously deployed

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

1 participant