How Runtime Call Tracing Works Across Languages in Code‑Graph‑RAG
Code‑Graph‑RAG unifies runtime call tracing across Python, PHP, .NET, Go, Rust, and JVM languages by converting each platform's native profiling output into a shared JSON‑L interchange format containing caller‑callee edges, call counts, and workload attributions.
The open‑source repository vitali87/code‑graph‑rag implements a language‑agnostic architecture for capturing dynamic call graphs during program execution. Rather than forcing a single instrumentation approach, the project provides dedicated tracers for each supported runtime that normalize native profiling data into a common schema defined in codebase_rag/trace/records.py. This allows downstream graph construction and retrieval augmented generation (RAG) pipelines to consume execution traces uniformly regardless of source language.
The Trace Interchange Format
All tracers emit a line‑delimited JSON (JSONL) stream that conforms to the same record schema. This trace interchange format decouples data collection from consumption, enabling cross‑language analysis.
The schema defines two primary structures in codebase_rag/trace/records.py:
- TraceHeader – Metadata including version, language identifier, repository root path, tracer name (e.g.,
cgr-trace-xdebug), and a flag indicating whether the data is sampled. - CallRecord – Individual caller‑callee edges containing
FramePointobjects (path, qualname, line number), aggregated call counts, optional workload identifiers (test or script names), and sampled receiver types for dynamically dispatched methods.
Language‑Specific Tracer Implementations
Each supported language has a dedicated tracer that bridges native runtime profiling to the shared JSONL schema.
Python sys.monitoring Integration
The Python tracer (codebase_rag/trace/tracer.py) leverages PEP 669 sys.monitoring to register a low‑overhead profiler callback for the PY_START event. When the callback _on_py_start fires, it receives the CodeType of the callee and inspects sys._getframe(1) to locate the caller via f_back.
The implementation filters frames using _in_scope to include only files under the repository root while excluding directories like node_modules or venv. For each valid (caller_code, callee_code) pair, a _PairStats object aggregates call counts, workload IDs assigned via set_workload, and a sampled set of receiver types (collected only for the first few observations to minimize overhead).
PHP Xdebug Trace Conversion
The PHP tracer (codebase_rag/trace/xdebug.py) consumes Xdebug trace files generated with xdebug.mode=trace and format 1. It runs a two‑pass algorithm: first building a call tree to infer each function’s defining file and line from its children’s call sites, then emitting caller‑callee edges while preserving concrete receiver classes and ignoring internal glue functions. The output shares the JSONL schema with TraceHeader.tracer set to cgr-trace-xdebug.
.NET Speedscope Export
For .NET languages (C#, F#, VB), the tracer (codebase_rag/trace/speedscope.py) reads dotnet‑trace speedscope exports (JSON format). These exports contain frames and profiles that are either sampled (stack snapshots) or evented (open/close events). The converter walks each profile, filters frames by a user‑supplied include‑namespace list, and accumulates adjacent in‑scope frame pairs weighted by sample weight, producing TraceHeader.tracer = "cgr-trace-speedscope".
Go and Rust pprof Parsing
The Go tracer (codebase_rag/trace/pprof.py) parses pprof CPU or heap profiles, extracting sampled call stacks and resolving frame names to package‑qualified symbols. The Rust tracer (codebase_rag/trace/rust_pprof.py) handles output from the Rust pprof crate using identical stack‑walk and edge‑aggregation logic. Both emit JSONL with tracer identifiers cgr-trace-pprof and cgr-trace-rust-pprof respectively.
JVM Flight Recorder Support
For Java, Kotlin, and Scala, the tracer (codebase_rag/trace/jvm.py) reads JFR (Java Flight Recorder) or async‑profiler recordings, extracting method‑level call stacks to generate caller‑callee edges with TraceHeader.tracer = "cgr-trace-jvm".
The Aggregation Flow
While input sources vary by language, all tracers follow a consistent five‑stage aggregation pipeline:
- Scope Filtering – Ensure only repository code is processed (e.g.,
CallGraphTracer._in_scope). - Callback Handling – Capture callee and caller frames (e.g.,
_on_py_startusingsys._getframe). - Pair Statistics – Aggregate edges in a
_PairStatsobject tracking counts, workload IDs, and receiver type samples. - Workload Attribution – Map workload strings (test names) to integer IDs via
set_workloadfor subsequent attachment. - Record Materialization – Convert aggregated statistics into
CallRecordinstances withFramePointlocations, then serialize viawrite_trace_file.
Practical Usage Examples
Python Runtime Tracing
from pathlib import Path
from codebase_rag.trace.tracer import CallGraphTracer
repo_root = Path("/path/to/your/repo")
tracer = CallGraphTracer(repo_root)
tracer.set_workload("my_test::test_example") # optional workload attribution
tracer.start() # begin monitoring
# ---- run the code you want to trace ----
import mymodule
mymodule.do_something()
# ----------------------------------------
tracer.stop() # stop monitoring
output_path = Path("trace-output.jsonl")
record_count = tracer.write(output_path) # writes JSONL, returns #records
print(f"Wrote {record_count} call records")
PHP Xdebug Conversion
from pathlib import Path
from codebase_rag.trace.xdebug import convert_xdebug_trace
xdebug_file = Path("tmp/xdebug.trace")
out_file = Path("trace-output.jsonl")
record_cnt = convert_xdebug_trace(xdebug_file, out_file, workload="php_test")
print(f"Converted {record_cnt} Xdebug call edges")
.NET Speedscope Conversion
from pathlib import Path
from codebase_rag.trace.speedscope import convert_speedscope
profile = Path("dotnet-trace.speedscope.json")
out = Path("trace-output.jsonl")
include = ["MyApp.Services", "MyApp.Controllers"] # namespace filter
record_cnt = convert_speedscope(profile, out, include, workload="dotnet_test")
print(f"Created {record_cnt} sampled edges")
Summary
- Code‑Graph‑RAG achieves cross‑language runtime call tracing through a normalized JSONL interchange format rather than unified instrumentation.
- Each language has a dedicated tracer (Python
sys.monitoring, PHP Xdebug, .NET speedscope, Go/Rust pprof, JVM JFR) that converts native output toTraceHeaderandCallRecordschemas. - The Python implementation in
codebase_rag/trace/tracer.pyuses PEP 669 event monitoring with frame inspection and workload attribution. - All tracers perform scope filtering, edge aggregation, and workload tracking before materializing records.
- Downstream components consume traces uniformly via
codebase_rag/trace/records.py, enabling language‑agnostic graph construction and retrieval.
Frequently Asked Questions
How does Code‑Graph‑RAG handle dynamic dispatch across different languages?
The trace schema stores a receiver_types tuple within each CallRecord, allowing the system to capture concrete class information for polymorphic calls. In Python, the tracer samples receiver types during the first few observations of a bound method call. PHP’s Xdebug converter preserves concrete receiver classes when parsing trace files. This sampled type information attaches to the aggregated caller‑callee edge without requiring expensive full‑program instrumentation.
What is the performance overhead of the Python tracer in codebase_rag/trace/tracer.py?
The Python tracer uses sys.monitoring (PEP 669), which is designed for low‑overhead production profiling. It filters frames aggressively using _in_scope to ignore standard library and third‑party code, and it samples receiver types only for the initial observations of each call pair. The resulting overhead is typically suitable for test suite execution and scripted workloads, though high‑frequency loops may still exhibit measurable slowdown.
Can I trace specific namespaces or exclude internal functions in .NET or JVM languages?
Yes. The .NET tracer (codebase_rag/trace/speedscope.py) accepts an include parameter—a list of namespace prefixes—that filters frames before edge aggregation. Similarly, the PHP tracer automatically ignores internal "glue" functions during Xdebug trace conversion. For the JVM, you can configure async‑profiler or JFR to capture only specific packages, and the tracer respects those scope boundaries when generating CallRecord entries.
Where are the trace records ultimately stored and how are they consumed?
Tracers write JSONL files to a user‑specified path via write_trace_file in codebase_rag/trace/records.py. Downstream evaluation scripts (such as those in evals/calls_trace.py) and graph construction pipelines read these files to build queryable call graphs. Because all languages share the identical TraceHeader and CallRecord schema, the consumption logic remains language‑agnostic throughout the Code‑Graph‑RAG system.
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 →