# How to Enable Dynamic Tracing for Java with Code-Graph-RAG

> Learn how to enable dynamic tracing for Java using Code-Graph-RAG. Enrich your knowledge graphs with runtime JVM execution data for deeper insights into your applications.

- Repository: [Vitali Avagyan/code-graph-rag](https://github.com/vitali87/code-graph-rag)
- Tags: how-to-guide
- Published: 2026-09-04

---

**Code-Graph-RAG enriches static knowledge graphs with runtime JVM execution data by converting async-profiler pprof outputs into graph edges using the `JvmFrameResolver`.**

The vitali87/code-graph-rag repository bridges static code analysis with dynamic execution traces, enabling AI-driven queries that reflect actual runtime behavior rather than just declared dependencies. Enabling dynamic tracing for Java with Code-Graph-RAG involves a three-stage pipeline that maps JVM stack frames to static graph nodes through profile collection, resolution, and ingestion into Memgraph.

## Prerequisites for JVM Runtime Tracing

Before capturing traces, compile your Java application with **debug symbols** enabled. The `JvmFrameResolver` requires line number information to disambiguate methods and match runtime frames to static source files.

Use the `-g` compiler flag or Maven's debug option:

```bash
mvn clean compile -DskipTests -Dmaven.compiler.debug=true

```

Without debug information, the resolver falls back to name-only matching, which increases ambiguity when handling overloaded methods or lambdas.

## The Three-Stage Dynamic Tracing Pipeline

### Stage 1: Collect Execution Profiles with async-profiler

Attach **async-profiler** to your running Java process to generate a pprof-compatible profile containing stack traces with file paths and line numbers. The profiler records each frame as a tuple of *(absolute path, qualified name, line)*.

```bash

# Start your Java application

java -jar target/myapp.jar &
PID=$!

# Record a 30-second CPU profile

~/async-profiler/profiler.sh -d 30 -f java_profile.pb.gz $PID

```

The output file `java_profile.pb.gz` contains the raw execution data required for graph enrichment.

### Stage 2: Convert pprof to Graph Records

Run the `cgr trace convert` command to resolve each frame to a graph node. The CLI invokes the `JvmFrameResolver` class (defined in [`codebase_rag/trace/resolution.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/trace/resolution.py) lines 93-115) to handle the conversion:

```bash
cgr trace convert \
    --format pprof \
    --language java \
    --repo-root $(pwd) \
    --input java_profile.pb.gz \
    --output java_trace.jsonl

```

The resolver performs **suffix matching** on file paths to reconcile JVM package-derived paths ([`com/example/Foo.java`](https://github.com/vitali87/code-graph-rag/blob/main/com/example/Foo.java)) with repository-relative paths ([`src/main/java/com/example/Foo.java`](https://github.com/vitali87/code-graph-rag/blob/main/src/main/java/com/example/Foo.java)). It then matches by **qualified name** (signature-less) and finally by **line span** to disambiguate overloads or lambda expressions.

### Stage 3: Ingest Dynamic Edges into Memgraph

Load the converted JSONL file into the graph database using the `cgr start` command with the `--update-graph` flag. This creates **CALL** edges representing actual execution flow:

```bash
cgr start \
    --repo-path $(pwd) \
    --update-graph \
    --dynamic-trace java_trace.jsonl

```

The ingestion process connects observed runtime call paths to the static code graph, enabling queries that span both declared and actual dependencies.

## How JvmFrameResolver Maps Runtime Frames to Static Nodes

The resolution logic in [`codebase_rag/trace/resolution.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/trace/resolution.py) handles the semantic gap between JVM runtime frames and static AST nodes:

- **Path Normalization**: The `_resolve_paths` method matches frames by path suffix, accounting for differences between JVM absolute paths and repository-relative source locations.
- **Qualified Name Matching**: Frames are matched to graph nodes using class and method names without full signatures to handle generic type erasure.
- **Line-Span Disambiguation**: For overloaded methods occupying the same class, the resolver uses line number ranges to select the correct node.
- **Statistics Tracking**: The `ResolutionStats` class (lines 71-78) counts unresolved frames, which the CLI reports to help diagnose coverage gaps.

## Complete Implementation Example

Combine the full workflow into a single automation script:

```bash
#!/bin/bash

# 1. Build with debug symbols

mvn clean compile -DskipTests -Dmaven.compiler.debug=true

# 2. Profile the application

java -jar target/myapp.jar & APP_PID=$!
sleep 5  # Allow JVM warmup

~/async-profiler/profiler.sh -d 30 -f java_profile.pb.gz $APP_PID

# 3. Convert profile to graph edges

cgr trace convert \
    --format pprof \
    --language java \
    --repo-root $(pwd) \
    --input java_profile.pb.gz \
    --output java_trace.jsonl

# 4. Merge into existing graph

cgr start \
    --repo-path $(pwd) \
    --update-graph \
    --dynamic-trace java_trace.jsonl

```

## Troubleshooting Path Resolution and Overloads

**Missing Line Numbers**: If the resolver reports high ambiguity rates, verify your build includes `-g` (source file, line number, and local variable debug information). Without line numbers, Code-Graph-RAG cannot distinguish between overloaded methods sharing the same name.

**Non-Standard Source Roots**: Projects using layouts other than `src/main/java` may fail suffix matching. Specify the source root explicitly using the `--source-root` flag in `cgr trace convert`, or adjust the resolver's path expectations in [`codebase_rag/trace/resolution.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/trace/resolution.py).

**Generated Code Collisions**: For frameworks generating code at the same line (such as Lombok or annotation processors), the resolver selects the first match when line spans overlap. Minimize collisions by ensuring processor-generated code carries distinct line number mappings via `javax.annotation.processing.Generated` annotations.

## Summary

- **Debug symbols are mandatory**: Compile Java sources with `-g` or `-Dmaven.compiler.debug=true` to enable line-number-based resolution in `JvmFrameResolver`.
- **Three-stage pipeline**: Collect profiles via async-profiler, convert using `cgr trace convert` with `--language java`, and ingest via `cgr start --update-graph`.
- **Intelligent frame resolution**: The resolver uses path suffix matching, qualified name comparison, and line-span analysis to map runtime frames to static nodes.
- **Graph enrichment**: Dynamic traces add **CALL** edges to the Memgraph database, bridging static dependency declarations with actual execution paths.

## Frequently Asked Questions

### Why does Code-Graph-RAG require debug symbols for Java tracing?

The `JvmFrameResolver` relies on line number information to disambiguate overloaded methods and match runtime stack frames to specific source code locations in the graph. Without debug symbols, the system can only perform name-based matching, which fails to distinguish between method overloads or accurately place lambdas.

### How does JvmFrameResolver handle different source directory layouts?

The resolver performs suffix matching on file paths to align JVM package paths (e.g., [`com/example/Service.java`](https://github.com/vitali87/code-graph-rag/blob/main/com/example/Service.java)) with repository structures (e.g., [`src/main/java/com/example/Service.java`](https://github.com/vitali87/code-graph-rag/blob/main/src/main/java/com/example/Service.java)). For non-standard layouts, use the `--source-root` parameter in the `cgr trace` command to specify the base directory, or modify the resolution logic in [`codebase_rag/trace/resolution.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/trace/resolution.py).

### Can I use profiling tools other than async-profiler with Code-Graph-RAG?

Yes, any profiler emitting **pprof-compatible** output will work. The conversion logic in [`codebase_rag/trace/ebpf_pprof.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/trace/ebpf_pprof.py) handles the generic pprof parsing, while `JvmFrameResolver` processes the language-specific frame data. Ensure the alternative profiler captures source file paths and line numbers in the pprof location lines.

### What happens to frames from external libraries not in my repository?

Frames referencing classes outside the `--repo-root` path fail suffix matching and increment the unresolved counter in `ResolutionStats`. These external calls are excluded from the graph to maintain focus on analyzable source code. You can monitor the CLI output to see the ratio of successfully resolved versus dropped frames.