Repository navigation
Fix Rust support on macOS and Linux - #286
Merged
Merged
Conversation
Two independent bugs made the Rust bindings unusable.
macOS: every Rust binary crashed on startup with SIGBUS before reaching
main. `orig_signal_fn` was declared as a function-pointer *variable* bound
to the `_signal` symbol:
extern signal_fn_t orig_signal_fn __asm("_signal");
so its value was the first eight bytes of signal()'s machine code rather
than its address, and `orig_signal_fn(...)` branched into whatever those
instruction bytes decoded to. Rust's startup always calls
signal(SIGPIPE, SIG_IGN) from std::sys::pal::unix::init::reset_sigpipe,
so this fired for every Rust program (and any C program calling signal()).
Declare it as a function bound to `_signal`, matching the other originals.
Linux: `increment()` called `_coz_add_delays()` unconditionally. coz.h
gates the equivalent `_COZ_CHECK_DELAYS` to __APPLE__ only, because on
Linux delays are already applied in the SIGPROF handler via
process_samples(); calling add_delays() again at every progress point
double-applies them and collapses throughput under concurrency. The gate
landed in coz.h (8c07fc2) hours after the Rust call site (7cb3c86) and
the crate was never updated. Gate it behind target_os = "macos".
Verified on macOS arm64: rust/examples/toy now yields 146 experiments and
correctly attributes the bottleneck to toy.rs:12. The non-macOS path is
checked via `cargo check --target x86_64-unknown-linux-gnu`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a `go/` module exposing coz's progress points to Go, plus the one libcoz change needed to make Go programs safe to profile. The binding resolves `_coz_get_counter` and `_coz_add_delays` out of the injected libcoz with dlsym(RTLD_DEFAULT), the same way the Rust crate does, so it needs neither coz.h at build time nor a system coz-profiler package. The counter bump is a plain relaxed atomic on libcoz-owned memory performed from Go, so there is no cgo call on the hot path on Linux. `_coz_add_delays()` is gated to macOS, mirroring _COZ_CHECK_DELAYS in coz.h. API: Progress, ProgressNamed, Begin, End, Scope (defer-friendly latency pair), and NewThroughput/NewBegin/NewEnd for hoisting the lookup out of a hot loop. Progress names its point after the caller's file:line via runtime.Caller, so it behaves like COZ_PROGRESS rather than reporting a line inside the binding. With CGO_ENABLED=0 the package builds as no-ops instead of failing. libcoz: install the sampling and error signal handlers with SA_ONSTACK. Go's runtime creates its Ms with pthread_create (not clone) whenever cgo is in play, so coz interposes them, gives each a perf event, and delivers SIGPROF to threads that are running goroutine stacks. Those stacks are small and movable, and Go's cgo documentation requires foreign signal handlers to set SA_ONSTACK for exactly this reason. Without it the Go toy segfaults partway through every run; with it, three consecutive runs complete cleanly. Threads with no alternate stack ignore the flag, so this is inert for ordinary C/C++ programs (the C toy benchmark is unaffected: 11 experiments before and after). Verified end to end on macOS arm64 and on Linux arm64 (Ubuntu 24.04 container, Go 1.26.5). go/example/toy.go runs two goroutines per round, one doing twice the work of the other; coz attributes the speedup to the loop on the critical path (slope 0.57-1.08, R^2 0.87-0.99) and finds the other nearly flat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merged
Adds a lang-rust job that builds libcoz, builds and tests the crate, then runs rust/examples/toy under `coz run` on both platforms and asserts the resulting profile contains experiments attributed to toy.rs. This would have caught both bugs fixed in this branch: the macOS SIGBUS (no profile at all) and, indirectly, any regression that stops progress points from registering. Also adds .github/scripts/check_profile.py, shared by the per-language jobs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a lang-go job that gofmt/vet/race-tests the module, checks it still builds with CGO_ENABLED=0, then profiles go/example/toy.go under `coz run` on both platforms and asserts experiments are attributed to toy.go. This pins down the two things that were easy to get wrong: the SA_ONSTACK requirement (without it the Go toy segfaults mid-run) and the -ldflags=-compressdwarf=false requirement (without it coz finds no debug info). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Go bindings for coz
SA_ONSTACK is required for Go: with cgo the Go runtime creates its Ms with pthread_create, so coz interposes them and delivers SIGPROF to threads running goroutine stacks, which are small and movable. Without SA_ONSTACK the Go toy crashes (verified again here on x86_64: rc=2, runtime fatal error). But SA_ONSTACK means the handler runs on whatever alternate stack the program installed, and process_samples() needs more room than some runtimes hand out. Rust's std gives every thread exactly SIGSTKSZ = 8192 bytes. On x86_64 the signal frame plus the dynamic linker's xsavec trampoline eat much of that and the handler overruns the rest, so *every* Rust program died under coz: #0 _dl_lookup_symbol_x (undef_name="__fprintf_chk") dl-lookup.c:840 #1 _dl_fixup dl-runtime.c:95 #2 _dl_runtime_resolve_xsavec dl-trampoline.h:126 #3 fprintf (__fmt="Segmentation fault at %p\n") #4 profiler::on_error profiler.cpp:883 #5 <signal handler called> #6 profiler::process_samples profiler.cpp:719 Note the second fault: on_error could not even report the first one, because fprintf's lazy PLT resolution needed stack that was already gone. That is why CI showed a bare "Segmentation fault" with an empty profile and no message. aarch64 has a smaller signal frame and never tripped it, which is why this was invisible on the Apple-silicon containers used to develop the Go bindings, and only x86_64 CI caught it. Fix: interpose sigaltstack and, when a program asks for a stack smaller than the handler needs, install a 32 KiB one instead. Go already asks for 32 KiB -- at the floor, not below it -- so its stack is left exactly as it set it up, which matters because the Go runtime checks that a signal arrived on the gsignal stack it knows about. Measured on an x86_64 Ubuntu box, per language, with libcoz built three ways: no SA_ONSTACK SA_ONSTACK SA_ONSTACK + floor Go toy crash - 11 experiments Rust toy 33 experiments SIGSEGV 33 experiments (2/2 runs) C toy 12 experiments 12 12 experiments Also raises the Rust CI job's timeout from 180s to 300s. rust/examples/toy runs 4.4e9 loop iterations, and a 2-core runner does not finish enough experiments inside 180s -- which is what the *first* red run on this branch was, before SA_ONSTACK ever landed. Two different failures wore the same job name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
get_loaded_files() parsed /proc/self/maps and took `base - offset` of each
executable mapping as the object's load bias. That is only the bias when the
executable PT_LOAD segment satisfies p_vaddr == p_offset.
It usually does for a simple C program -- benchmarks/toy has off=0x1000 against
vaddr=0x1000 -- so the bug stayed hidden. rustc's output does not: the Rust toy
has off=0x14f80 against vaddr=0x15f80, and the computed bias came out 0x1000 too
large. Every line range was then shifted by a page.
The consequence is worse than a crash, because nothing looks wrong. Most samples
still land inside *some* line, so coz reports a confident, plausible profile
attributed to the wrong source lines. Measured on x86_64 with the Rust toy:
samples matched hottest line reported
before 39049 / 43506 (89.8%) rust/examples/toy.rs:5 (thread::spawn)
after 43492 / 43513 (99.95%) rust/src/lib.rs:164 (the atomic add)
Line 5 is where the toy spawns its threads; it burns no time there. Line 164 is
the counter increment the toy actually runs 4.4e9 times.
On some layouts the shift lands outside every range and nothing matches at all,
which is what CI hit: the Rust toy produced 10242 perf samples and matched zero
of them, while the C toy in the same job matched 23300 of 23300.
dl_iterate_phdr reports dlpi_addr, which *is* the bias, for the main executable
and every shared object. Use it. The main executable comes back with an empty
dlpi_name, so resolve it through /proc/self/exe; objects with no executable
PT_LOAD cannot contain a sampled PC and are skipped, as are synthetic ones like
the vDSO that have no file to read DWARF from.
benchmarks/toy is unchanged: 107488/107525 samples matched, toy.cpp:18 at slope
0.504. macOS is untouched -- it has its own get_loaded_files() using dyld.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit b050352)
The toy's loop body was nothing but coz::progress!(), so every sample landed in the inlined counter-increment code from rust/src/lib.rs and no experiment ever selected a line in toy.rs, failing the Linux CI check. Mirror benchmarks/toy/toy.cpp instead: two threads run unequal amounts of busy work with a progress point per work unit, keeping the hot lines in this file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent bugs made the Rust bindings unusable. Both are fixed here, with the macOS path verified end-to-end.
1. macOS: every Rust binary crashed on startup (SIGBUS)
libcoz/mac_interpose.cppdeclared the originalsignalas a function-pointer variable bound to the_signalsymbol:A variable bound to
_signalis the bytes at_signal— i.e.signal()'s machine code — soorig_signal_fn(...)branched to whatever those instruction bytes decoded to as an address. On arm64 the faulting PC was0x1_52800022, which is exactly the first two instruction words of libsystem'ssignal()read as a pointer (0x52800022=movz w2, #1).Rust's startup unconditionally calls
signal(SIGPIPE, SIG_IGN)fromstd::sys::pal::unix::init::reset_sigpipe, so every Rust program died beforemain. A barefn main() {}reproduces it. Any C program callingsignal()would hit it too.Fixed by declaring it as a function bound to
_signal, consistent withorig_sigaction,orig_pthread_create, and the rest.2. Linux:
_coz_add_delays()called on every progress pointinclude/coz.hgates_COZ_CHECK_DELAYSto__APPLE__only, and says why:The Rust crate called
coz_add_delays()unconditionally. The__APPLE__gate landed incoz.h(8c07fc2, 20:15) hours after the Rust call site was added (7cb3c86, 00:25) on the same day, and the crate was never updated. Now gated behindtarget_os = "macos", mirroring the header.Verification
coz run --- ./rust/target/release/examples/toyproduces 146 experiments and attributes the bottleneck totoy.rs:12(thebthread's progress point, which does 1.2x the work ofa) at ~+44% predicted speedup. Before this change: instant SIGBUS, no profile.cargo check --target x86_64-unknown-linux-gnucompiles clean with no dead-code warnings.cargo test --releasepasses.benchmarks/toy) still profiles correctly on macOS — 10 experiments — confirming no regression in the signal interposition.Also refreshes a stale
rust/README.mdcaveat claiming coz is Linux-only.🤖 Generated with Claude Code