Skip to content

Commit

Permalink
Add support for tracking child processes (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kobzol authored Aug 16, 2022
1 parent 5c29584 commit 88897fc
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 45 deletions.
12 changes: 12 additions & 0 deletions docs/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,15 @@ Whenever to use a more intrusive, faster unwinding algorithm; enabled by default

Setting it to `0` will on average significantly slow down unwinding. This option
is provided only for debugging purposes.

### `MEMORY_PROFILER_TRACK_CHILD_PROCESSES`

*Default: `0`*

If set to `1`, bytehound will also track the memory allocations of child processes spawned by the
profiled process. This only applies to child processes spawned by the common `fork()` + `exec()`
combination.

Note that if you enable this, you should use a value with appropriates placeholders (like PID)
in `MEMORY_PROFILER_OUTPUT`, so that the output filenames for the parent and child processes are
different. Otherwise, they would overwrite each other's data.
117 changes: 76 additions & 41 deletions integration-tests/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,47 +469,7 @@ fn test_basic() {
]
).assert_success();

let analysis = analyze( "basic", cwd.join( "memory-profiling-basic.dat" ) );
let mut iter = analysis.allocations_from_source( "basic.c" );

let a0 = iter.next().unwrap(); // malloc, leaked
let a1 = iter.next().unwrap(); // malloc, freed
let a2 = iter.next().unwrap(); // malloc, freed through realloc
let a3 = iter.next().unwrap(); // realloc
let a4 = iter.next().unwrap(); // calloc, freed
let a5 = iter.next().unwrap(); // posix_memalign, leaked

assert!( a0.deallocation.is_none() );
assert!( a1.deallocation.is_some() );
assert!( a2.deallocation.is_some() );
assert!( a3.deallocation.is_none() );
assert!( a4.deallocation.is_none() );
assert!( a5.deallocation.is_none() );

assert_eq!( a5.address % 65536, 0 );

assert!( a0.size < a1.size );
assert!( a1.size < a2.size );
assert!( a2.size < a3.size );
assert!( a3.size < a4.size );
assert!( a4.size < a5.size );

assert_eq!( a0.thread, a1.thread );
assert_eq!( a1.thread, a2.thread );
assert_eq!( a2.thread, a3.thread );
assert_eq!( a3.thread, a4.thread );
assert_eq!( a4.thread, a5.thread );

assert_eq!( a0.backtrace.last().unwrap().line.unwrap() + 1, a1.backtrace.last().unwrap().line.unwrap() );

assert_eq!( a0.chain_length, 1 );
assert_eq!( a1.chain_length, 1 );
assert_eq!( a2.chain_length, 2 );
assert_eq!( a3.chain_length, 2 );
assert_eq!( a4.chain_length, 1 );
assert_eq!( a5.chain_length, 1 );

assert_eq!( iter.next(), None );
check_allocations_basic_program(&cwd.join( "memory-profiling-basic.dat" ));
}

#[cfg(test)]
Expand Down Expand Up @@ -1278,3 +1238,78 @@ fn test_cross_thread_alloc_non_culled() {

assert!( iter.next().is_none() );
}

#[test]
fn test_track_spawned_children() {
let cwd = workdir();

compile_with_flags( "basic.c", &["-o", "basic"] );
compile_with_flags( "spawn-child.c", &["-o", "spawn-child"] );

run_on_target(
&cwd,
"./spawn-child",
EMPTY_ARGS,
&[
("LD_PRELOAD", preload_path().into_os_string()),
("MEMORY_PROFILER_LOG", "debug".into()),
("MEMORY_PROFILER_TRACK_CHILD_PROCESSES", "1".into()),
("MEMORY_PROFILER_OUTPUT", "memory-profiling-%e.dat".into())
]
).assert_success();

check_allocations_basic_program(&cwd.join( "memory-profiling-basic.dat" ));

let analysis = analyze( "spawn-child", cwd.join( "memory-profiling-spawn-child.dat" ));
let mut iter = analysis.allocations_from_source( "spawn-child.c" );

let a0 = iter.next().unwrap();
assert_eq!(a0.size, 10001);

let a1 = iter.next().unwrap();
assert_eq!(a1.size, 10003);
}

fn check_allocations_basic_program(path: &Path) {
let analysis = analyze( "basic", path );
let mut iter = analysis.allocations_from_source( "basic.c" );

let a0 = iter.next().unwrap(); // malloc, leaked
let a1 = iter.next().unwrap(); // malloc, freed
let a2 = iter.next().unwrap(); // malloc, freed through realloc
let a3 = iter.next().unwrap(); // realloc
let a4 = iter.next().unwrap(); // calloc, freed
let a5 = iter.next().unwrap(); // posix_memalign, leaked

assert!( a0.deallocation.is_none() );
assert!( a1.deallocation.is_some() );
assert!( a2.deallocation.is_some() );
assert!( a3.deallocation.is_none() );
assert!( a4.deallocation.is_none() );
assert!( a5.deallocation.is_none() );

assert_eq!( a5.address % 65536, 0 );

assert!( a0.size < a1.size );
assert!( a1.size < a2.size );
assert!( a2.size < a3.size );
assert!( a3.size < a4.size );
assert!( a4.size < a5.size );

assert_eq!( a0.thread, a1.thread );
assert_eq!( a1.thread, a2.thread );
assert_eq!( a2.thread, a3.thread );
assert_eq!( a3.thread, a4.thread );
assert_eq!( a4.thread, a5.thread );

assert_eq!( a0.backtrace.last().unwrap().line.unwrap() + 1, a1.backtrace.last().unwrap().line.unwrap() );

assert_eq!( a0.chain_length, 1 );
assert_eq!( a1.chain_length, 1 );
assert_eq!( a2.chain_length, 2 );
assert_eq!( a3.chain_length, 2 );
assert_eq!( a4.chain_length, 1 );
assert_eq!( a5.chain_length, 1 );

assert_eq!( iter.next(), None );
}
22 changes: 22 additions & 0 deletions integration-tests/test-programs/spawn-child.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
usleep( 100000 );
malloc( 10001 );

pid_t pid = fork();
if( pid == 0 ) {
// Child
if (execl("./basic", "./basic", NULL) == -1) {
return 1;
}
return 0;
}

waitpid(pid, NULL, 0);
malloc( 10003 );

return 0;
}
6 changes: 4 additions & 2 deletions preload/src/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::thread;
use crate::arc_lite::ArcLite;
use crate::event::{InternalAllocationId, InternalEvent, send_event};
use crate::spin_lock::{SpinLock, SpinLockGuard};
use crate::syscall;
use crate::{opt, syscall};
use crate::unwind::{ThreadUnwindState, prepare_to_start_unwinding};
use crate::timestamp::Timestamp;
use crate::allocation_tracker::AllocationTracker;
Expand Down Expand Up @@ -553,7 +553,9 @@ fn initialize_stage_2() {
crate::init::initialize_atexit_hook();
crate::init::initialize_signal_handlers();

std::env::remove_var( "LD_PRELOAD" );
if !opt::get().track_child_processes {
std::env::remove_var( "LD_PRELOAD" );
}

info!( "Stage 2 initialization finished" );
}
Expand Down
8 changes: 6 additions & 2 deletions preload/src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ pub struct Opts {
pub backtrace_cache_size_level_2: usize,
pub cull_temporary_allocations: bool,
pub temporary_allocation_lifetime_threshold: u64,
pub temporary_allocation_pending_threshold: Option< usize >
pub temporary_allocation_pending_threshold: Option< usize >,
pub track_child_processes: bool
}

static mut OPTS: Opts = Opts {
Expand All @@ -48,6 +49,7 @@ static mut OPTS: Opts = Opts {
cull_temporary_allocations: false,
temporary_allocation_lifetime_threshold: 10000,
temporary_allocation_pending_threshold: None,
track_child_processes: false
};

trait ParseVar: Sized {
Expand Down Expand Up @@ -143,7 +145,9 @@ pub unsafe fn initialize() {
"MEMORY_PROFILER_TEMPORARY_ALLOCATION_LIFETIME_THRESHOLD"
=> &mut opts.temporary_allocation_lifetime_threshold,
"MEMORY_PROFILER_TEMPORARY_ALLOCATION_PENDING_THRESHOLD"
=> &mut opts.temporary_allocation_pending_threshold
=> &mut opts.temporary_allocation_pending_threshold,
"MEMORY_PROFILER_TRACK_CHILD_PROCESSES"
=> &mut opts.track_child_processes
}

opts.is_initialized = true;
Expand Down

0 comments on commit 88897fc

Please sign in to comment.