Call Graph Analysis - The Renacer Book - Pure Rust System Call Tracing
Description: A comprehensive guide to renacer: syscall tracing, DWARF correlation, and performance analysis

# 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:

1. Syscall entry - Program makes a syscall (e.g., `read()`)
2. Get registers - Read RIP (instruction pointer) and RBP (base pointer)
3. Walk frame pointer chain - Follow RBP links up the stack
4. Map to functions - Use DWARF debug info to resolve addresses to function names
5. 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 (`-g` flag 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 contents
- `parse_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_query` made 150 syscalls (total 234ms)
- `fetch_results` made 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_data` function
- 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:

1. Syscall trace (stdout) - Individual syscall events with source locations
2. Statistics summary (stderr) - Overall call counts and timing
3. 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 `write` syscalls
- 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