How to Enable Dynamic Tracing for Java with Code-Graph-RAG
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:
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).
# 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 lines 93-115) to handle the conversion:
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) with repository-relative paths (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:
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 handles the semantic gap between JVM runtime frames and static AST nodes:
- Path Normalization: The
_resolve_pathsmethod 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
ResolutionStatsclass (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:
#!/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.
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
-gor-Dmaven.compiler.debug=trueto enable line-number-based resolution inJvmFrameResolver. - Three-stage pipeline: Collect profiles via async-profiler, convert using
cgr trace convertwith--language java, and ingest viacgr 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) with repository structures (e.g., 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.
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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →