Skip to content

Commit c6d921d

Browse files
committed
remove the allocation in signal handle of framehop
Signed-off-by: Yang Keao <[email protected]>
1 parent 0c52c5f commit c6d921d

File tree

4 files changed

+103
-2
lines changed

4 files changed

+103
-2
lines changed

.github/workflows/rust.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,9 @@ jobs:
159159
with:
160160
command: test
161161
args: --features flamegraph,protobuf-codec --target ${{ matrix.target }}
162+
163+
- name: Run cargo test framehop
164+
uses: actions-rs/[email protected]
165+
with:
166+
command: test
167+
args: --features flamegraph,protobuf-codec,framehop-unwinder --target ${{ matrix.target }}

src/backtrace/framehop_unwinder.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use framehop::{
33
};
44
use libc::{c_void, ucontext_t};
55
use once_cell::sync::Lazy;
6+
use spin::RwLock;
67
mod shlib;
78

89
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
@@ -123,7 +124,8 @@ fn read_stack(addr: u64) -> Result<u64, ()> {
123124
}
124125
}
125126

126-
static mut UNWINDER: Lazy<FramehopUnwinder> = Lazy::new(|| FramehopUnwinder::new());
127+
static UNWINDER: Lazy<RwLock<FramehopUnwinder>> =
128+
Lazy::new(|| RwLock::new(FramehopUnwinder::new()));
127129
#[derive(Clone, Debug)]
128130
pub struct Frame {
129131
pub ip: usize,
@@ -158,10 +160,22 @@ pub struct Trace;
158160
impl super::Trace for Trace {
159161
type Frame = Frame;
160162

163+
fn init() {
164+
let _ = UNWINDER.read();
165+
}
166+
161167
fn trace<F: FnMut(&Self::Frame) -> bool>(ctx: *mut c_void, cb: F)
162168
where
163169
Self: Sized,
164170
{
165-
unsafe { UNWINDER.iter_frames(ctx, cb) };
171+
// For Linux, this `try_write` should always succeed, because `SIGPROF` will never be delivered to
172+
// another thread while the signal handler is running. However, I'm not sure about other OSes, so
173+
// we use `try_write` to be safe instead of using `static mut` and `unsafe` directly.
174+
match UNWINDER.try_write() {
175+
None => return,
176+
Some(mut unwinder) => {
177+
unwinder.iter_frames(ctx, cb);
178+
}
179+
}
166180
}
167181
}

src/backtrace/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ pub trait Frame: Sized + Clone {
4343
pub trait Trace {
4444
type Frame;
4545

46+
// init will be called before running the first trace in signal handler
47+
fn init() {}
48+
4649
fn trace<F: FnMut(&Self::Frame) -> bool>(_: *mut libc::c_void, cb: F)
4750
where
4851
Self: Sized;

src/profiler.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ pub struct ProfilerGuard<'a> {
188188
fn trigger_lazy() {
189189
let _ = backtrace::Backtrace::new();
190190
let _profiler = PROFILER.read();
191+
TraceImpl::init();
191192
}
192193

193194
impl ProfilerGuard<'_> {
@@ -524,3 +525,80 @@ impl Profiler {
524525
if let Ok(()) = self.data.add(frames, 1) {}
525526
}
526527
}
528+
529+
#[cfg(test)]
530+
pub mod tests {
531+
use super::*;
532+
533+
struct AllocDetector {
534+
should_count_alloc: std::sync::atomic::AtomicBool,
535+
alloc_count: std::sync::atomic::AtomicUsize,
536+
}
537+
538+
unsafe impl std::alloc::GlobalAlloc for AllocDetector {
539+
unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
540+
if self
541+
.should_count_alloc
542+
.load(std::sync::atomic::Ordering::SeqCst)
543+
{
544+
if self
545+
.alloc_count
546+
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
547+
== 0
548+
{
549+
// it's not safe to print and unwind here, but it's fine for test purpose
550+
println!(
551+
"allocation happened during unwinding! {:?}",
552+
backtrace::Backtrace::new()
553+
);
554+
}
555+
}
556+
557+
unsafe { std::alloc::System.alloc(layout) }
558+
}
559+
560+
unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
561+
unsafe { std::alloc::System.dealloc(ptr, layout) }
562+
}
563+
}
564+
impl AllocDetector {
565+
fn enable_count_alloc(&self) {
566+
self.should_count_alloc
567+
.store(true, std::sync::atomic::Ordering::SeqCst);
568+
}
569+
570+
fn disable_count_alloc(&self) {
571+
self.should_count_alloc
572+
.store(false, std::sync::atomic::Ordering::SeqCst);
573+
}
574+
575+
fn alloc_count(&self) -> usize {
576+
self.alloc_count.load(std::sync::atomic::Ordering::SeqCst)
577+
}
578+
}
579+
580+
#[global_allocator]
581+
static ALLOC: AllocDetector = AllocDetector {
582+
should_count_alloc: std::sync::atomic::AtomicBool::new(false),
583+
alloc_count: std::sync::atomic::AtomicUsize::new(0),
584+
};
585+
586+
#[test]
587+
fn test_no_alloc_during_unwind() {
588+
trigger_lazy();
589+
PROFILER.write().as_mut().unwrap().start().unwrap();
590+
let timer = Timer::new(999);
591+
let start = std::time::Instant::now();
592+
ALLOC.enable_count_alloc();
593+
594+
// busy loop for a while to trigger some samples
595+
while start.elapsed().as_millis() < 500 {
596+
let _ = std::hint::black_box(());
597+
}
598+
assert_eq!(ALLOC.alloc_count(), 0);
599+
600+
ALLOC.disable_count_alloc();
601+
drop(timer);
602+
PROFILER.write().as_mut().unwrap().stop().unwrap();
603+
}
604+
}

0 commit comments

Comments
 (0)