Call Graph Analysis
Call graph analysis reveals the parent-child relationships between functions, helping you understand which code paths lead to syscalls and I/O operations.
TDD-Verified: Call graph features tested in
tests/sprint13_function_profiling_tests.rs
Parent Chapter: See Function Profiling for overview and basic usage.
Overview
A call graph shows the hierarchical relationships between functions in your program:
main
├─ setup_logging
│ └─ open_log_file → openat() syscall
├─ load_config
│ ├─ read_config_file → read() syscall
│ └─ parse_yaml
└─ process_data
├─ read_input → read() syscall
└─ write_output → write() syscall
Why call graphs matter:
- Root cause analysis - Trace syscalls back to high-level application logic
- Responsibility attribution - Know which top-level function caused I/O
- Optimization guidance - Identify call chains to refactor
- Debugging - Understand execution flow without stepping through code
How Renacer Builds Call Graphs
Renacer uses stack unwinding to reconstruct the call stack when each syscall happens:
- Syscall entry - Program makes a syscall (e.g.,
read()) - Get registers - Read RIP (instruction pointer) and RBP (base pointer)
- Walk frame pointer chain - Follow RBP links up the stack
- Map to functions - Use DWARF debug info to resolve addresses to function names
- Build call graph - Record parent-child relationships
Algorithm: See Function Profiling - Stack Unwinding for technical details.
Tested by: test_stack_unwinding_with_simple_program in Sprint 13 tests
Enabling Call Graph Analysis
Call graph tracking requires both --function-time and --source:
renacer --function-time --source -- ./my-app
Why both flags?
--function-time- Enables function profiling and stack unwinding--source- Enables DWARF debug info lookup for function names
Tested by: test_function_time_flag_accepted, test_function_time_output_format
Without --source:
$ renacer --function-time -- ./my-app
Output shows: Instruction addresses instead of function names (not very useful!)
Requirements:
- Debug symbols (
-gflag during compilation) - Frame pointers (enabled by default in most builds)
See Function Profiling - Requirements for details.
Reading Call Graph Output
Basic Call Stack Display
When --source is enabled, each syscall shows the function that made it:
$ renacer --function-time --source -- cargo build
Example Output:
openat(AT_FDCWD, "Cargo.toml", O_RDONLY) = 3 [src/main.rs:42 in load_manifest]
read(3, "package]\\nname = \\"renacer\\"\\n...", 832) = 832 [src/config.rs:78 in parse_toml]
close(3) = 0 [src/config.rs:95 in parse_toml]
=== Function Profiling Summary ===
Function Calls Total Time Avg Time Slow I/O
──────────────────────────────────────────────────────────────────────────
src/main.rs:42 1 234 μs 234 μs 0
src/config.rs:78 1 156 μs 156 μs 0
src/config.rs:95 1 12 μs 12 μs 0
Interpretation:
load_manifest(line 42) opened "Cargo.toml"parse_toml(line 78) read the file contentsparse_toml(line 95) closed the file
Call chain: main → load_manifest → openat() and main → load_manifest → parse_toml → read()
Function Attribution
The function profiling summary groups syscalls by the function that made them:
Function Calls Total Time Avg Time Slow I/O
──────────────────────────────────────────────────────────────────────────
src/db.rs:execute_query 150 234567 μs 1563 μs 148 ⚠️
src/db.rs:fetch_results 150 345678 μs 2304 μs 150 ⚠️
This shows:
execute_querymade 150 syscalls (total 234ms)fetch_resultsmade 150 syscalls (total 345ms)- Both have slow I/O (database latency)
Tested by: test_function_time_with_statistics_mode
Practical Examples
Example 1: Understanding I/O Attribution
Scenario: Identify which functions are causing file I/O
$ renacer --function-time --source -e trace=file -- ./myapp
Output:
openat(AT_FDCWD, "/tmp/data.txt", O_RDONLY) = 3 [src/main.rs:15 in load_data]
read(3, "test data\\n", 4096) = 10 [src/main.rs:20 in load_data]
close(3) = 0 [src/main.rs:23 in load_data]
=== Function Profiling Summary ===
Function Calls Total Time Avg Time Slow I/O
──────────────────────────────────────────────────────────────────────────
src/main.rs:15 1 120 μs 120 μs 0
src/main.rs:20 1 45 μs 45 μs 0
src/main.rs:23 1 8 μs 8 μs 0
Analysis:
- All file I/O comes from
load_datafunction - Three syscalls: open, read, close
- Total time: 173μs (fast, no bottleneck)
Tested by: test_function_time_output_format
Example 2: Combining with Statistics
Scenario: See both call graphs and aggregate statistics
$ renacer --function-time --source -c -- ./myapp
Output includes:
- Syscall trace (stdout) - Individual syscall events with source locations
- Statistics summary (stderr) - Overall call counts and timing
- Function profiling (stderr) - Per-function attribution
Benefit: See both high-level statistics and detailed function-level breakdown.
Tested by: test_function_time_with_statistics_mode
Example 3: Filtering Specific Syscalls
Scenario: Focus on specific I/O operations
$ renacer --function-time --source -e trace=write -- ./loggy-app
Shows:
- Only
writesyscalls - Functions that perform writes
- Easier to analyze logging or output-heavy code
Tested by: test_function_time_with_filter
Advanced Usage
With Filtering (-e)
Focus call graph analysis on specific syscall types:
$ renacer --function-time --source -e trace=network -- ./app
Shows:
- Only network syscalls (
sendto,recvfrom, etc.) - Functions that make network calls
- Easier to analyze network-specific call chains
Tested by: test_profile_self_with_filtering
With Multi-Process Tracing (-f)
Track call graphs across parent and child processes:
$ renacer -f --function-time --source -- make -j8
Aggregates:
- Function profiling for parent process (e.g.,
make) - Function profiling for child processes (e.g.,
gcc,ld) - Combined call graph understanding across process tree
Use case: Build system analysis, multi-process applications
With I/O Bottleneck Detection
See I/O Bottleneck Detection for detailed slow I/O analysis.
Combination:
$ renacer --function-time --source -c -- ./database-app
Output shows:
- Call graphs (which functions make syscalls)
- Slow I/O counts (which functions have bottlenecks)
- Statistics (overall performance impact)
Powerful combo: Identify which call chains are causing which bottlenecks.
Troubleshooting
Missing Function Names (Addresses Instead)
Problem: Profiling shows addresses like 0x557a3f4b1234 instead of function names.
Cause: Missing --source flag or DWARF debug info.
Solution:
# Ensure both flags are used
$ renacer --function-time --source -- ./my-app
# Verify debug symbols
$ file ./my-app # Should show "with debug_info, not stripped"
No Function Profiling Data
Problem: "No function profiling data collected"
Cause: Binary lacks DWARF debug information.
Solution: See Function Profiling - Troubleshooting
Tested by: Verified in test_stack_frame_struct
Frame Pointer Omission
Problem: Call graph shows incorrect or missing functions.
Cause: Binary compiled with -fomit-frame-pointer.
Solution:
# Ensure frame pointers are enabled (default for most builds)
$ gcc -g my_program.c -o my_program # Frame pointers enabled
# Avoid
$ gcc -g -fomit-frame-pointer my_program.c -o my_program # ❌ Breaks stack unwinding
Tested by: Stack unwinding safety verified in test_stack_unwinding_max_depth_protection, test_stack_unwinding_does_not_crash
Stack Unwinding Errors
Problem: "Stack unwinding failed" or truncated call graphs.
Cause:
- Corrupted stack (e.g., buffer overflow)
- Invalid RBP chain
- Stack depth >64 frames (max depth protection)
Check:
# Verify program doesn't crash
$ ./my-app # Should run without segfaults
# Check stack depth
$ renacer --function-time --source -- ./my-app
Tested by: test_stack_unwinding_max_depth_protection, test_stack_unwinding_does_not_crash
Inlined Functions
Problem: Profiling attributes syscalls to caller instead of inlined function.
Cause: Compiler inlining (function body copied to call site).
Example:
#[inline(always)]
fn log_debug(msg: &str) {
write(fd, msg.as_bytes()); // Inlined - won't appear in call graph
}
fn main() {
log_debug("test"); // Syscall attributed to main(), not log_debug()
}
Workaround: Disable inlining for profiling:
#[inline(never)] // Force function to appear in call graph
fn log_debug(msg: &str) {
write(fd, msg.as_bytes());
}
Or build without optimizations:
$ cargo build # Dev build (inlining disabled)
$ cargo build --release # Release build (inlining enabled - harder to profile)
Performance Impact
Overhead:
- Stack unwinding: ~50-100μs per syscall (RBP chain walk + DWARF lookups)
- Function profiling: ~10-30% total overhead (depends on syscall frequency)
Tested by: test_profile_self_flag_outputs_summary, test_profile_self_reports_nonzero_syscalls
Mitigation:
- Use filtering (
-e trace=write) to reduce syscall count - Disable when not needed (zero overhead when not enabled)
- Use in development/profiling, not production
Tested by: test_function_time_without_flag_no_profiling, test_profile_self_without_flag_no_output
Summary
Call graph analysis provides:
- ✅ Parent-child relationships - Understand which functions call which syscalls
- ✅ Root cause attribution - Trace I/O back to high-level application logic
- ✅ Optimization guidance - Identify call chains to refactor
- ✅ Integration with filtering, statistics, bottleneck detection
- ✅ Source correlation - Line numbers + function names via DWARF
Limitations:
- Only immediate caller shown (not full stack trace)
- Requires debug symbols + frame pointers
- Compiler inlining can hide functions
All examples tested in: tests/sprint13_function_profiling_tests.rs
Related
- Function Profiling - Parent chapter with basic usage
- I/O Bottleneck Detection - Identify slow I/O in call chains
- Flamegraph Export - Visualize call graphs as flamegraphs
- DWARF Source Correlation - How function names are resolved