From 533fa57340bf233c0871dcb9048b98e42d54fbbe Mon Sep 17 00:00:00 2001 From: WizardBornov Date: Mon, 21 Sep 2026 13:25:18 +0530 Subject: [PATCH 1/3] Add callsite-level, source-located call graph export example PhASAR's existing exportICFGAsJson() aggregates function-to-function edges and builds one in-memory JSON object before writing anything to disk. Correlating a static call graph against runtime instrumentation data needs callsite-level resolution, and on FFmpeg-scale IR (~1.2M+ edges) the in-memory approach hits an unbounded memory ceiling before writing a single byte. This adds a streaming CSV export built on the same getCallsFromWithin/getCalleesOfCallAt primitives, plus a resolveLocation() helper that fixes a File/Line pairing bug in getSrcCodeInfoFromIR (it composes File and Line from separate helper calls with different fallback branches, which can mismatch a File from one instruction with a Line from another). Invited by Fabian Schiebel to submit this as a PR. Co-Authored-By: Claude Sonnet 5 --- .../09-export-callsite-cg/CMakeLists.txt | 10 + .../how-to/09-export-callsite-cg/README.md | 54 +++++ .../export_callsite_cg_streaming.cpp | 186 ++++++++++++++++++ examples/how-to/README.md | 1 + 4 files changed, 251 insertions(+) create mode 100644 examples/how-to/09-export-callsite-cg/CMakeLists.txt create mode 100644 examples/how-to/09-export-callsite-cg/README.md create mode 100644 examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp diff --git a/examples/how-to/09-export-callsite-cg/CMakeLists.txt b/examples/how-to/09-export-callsite-cg/CMakeLists.txt new file mode 100644 index 0000000000..2a55249f35 --- /dev/null +++ b/examples/how-to/09-export-callsite-cg/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.14...3.28) + +project(export-callsite-cg) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +find_package(phasar REQUIRED CONFIG) + +add_executable(export-callsite-cg-streaming export_callsite_cg_streaming.cpp) +target_link_libraries(export-callsite-cg-streaming PRIVATE phasar::phasar) diff --git a/examples/how-to/09-export-callsite-cg/README.md b/examples/how-to/09-export-callsite-cg/README.md new file mode 100644 index 0000000000..d140eef158 --- /dev/null +++ b/examples/how-to/09-export-callsite-cg/README.md @@ -0,0 +1,54 @@ +# Export a Callsite-Level Call Graph + +Exports PhASAR's call graph as callsite-resolved, source-located CSV: one row per +resolved edge, with caller/callee function name plus file/line/column for each +side, written incrementally as edges are discovered. + +This differs from `CallGraph::printAsDot()` / `printAsJson()` in two ways: + +- **Callsite-level, not function-level.** Each row is a single call instruction + resolved to its callee(s), rather than an aggregated function-to-function edge. + This is what correlating a static call graph against runtime instrumentation + data needs — the runtime side reports individual call sites, not just "A calls + B somewhere". +- **Streaming output.** `exportICFGAsJson()` builds one in-memory `nlohmann::json` + object holding every edge before writing anything to disk. On a large enough + input (validated against a real ~1.2M-edge FFmpeg call graph) this grows + unbounded — 11GB resident plus climbing swap, with zero bytes written the whole + time. This driver writes each edge to disk the moment it's produced and + discards it, so memory stays roughly constant regardless of total edge count. + +It also documents a real source-location bug it works around: PhASAR's +`getSrcCodeInfoFromIR` resolves `File` and `Line` through separate helper calls, +each with its own fallback logic for instructions lacking direct `!dbg` +metadata — which can pair a `File` from one resolution path with a `Line` from +an unrelated one. A concrete case from the FFmpeg run: a callsite reported line +2776 inside a 63-line header. `resolveLocation()` here instead resolves a single +`DILocation` per instruction and reads `File`/`Line`/`Column` off that same +object, marking (rather than silently guessing at) instructions with no direct +location at all. + +## Build + +```bash +# Invoked from the 09-export-callsite-cg root folder: +$ mkdir -p build && cd build +$ cmake .. +$ cmake --build . +``` + +## Usage + +```bash +./export-callsite-cg-streaming [cha|rta|vta|otf] +``` + +Output is CSV with one row per resolved call edge: + +``` +caller_function,caller_file,caller_line,caller_column,callee_function,callee_file,callee_line,callee_column,caller_loc_approximate +``` + +`caller_loc_approximate` is `true` when the call instruction had no direct debug +location and the row fell back to the enclosing function's declaration site +instead of the real call site. diff --git a/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp b/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp new file mode 100644 index 0000000000..f01f2604a6 --- /dev/null +++ b/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp @@ -0,0 +1,186 @@ +// export_callsite_cg_streaming.cpp +// +// export_callsite_cg.cpp (the first version) calls PhASAR's +// exportICFGAsJson(), which builds ONE nlohmann::json object holding +// every edge in memory, and only writes to disk at the very end. On +// FFmpeg's real edge count (~1.2M+, per the CHA/RTA baseline), this grew +// unbounded -- confirmed against a real container run: 11GB resident, +// then 2.8GB of swap and climbing, zero bytes written the entire time, +// no realistic path to finishing. +// +// This version uses the same underlying PhASAR primitives +// (getCallsFromWithin, getCalleesOfCallAt, getSrcCodeInfoFromIR) that +// exportICFGAsJson itself is built on, but writes each edge to disk as +// CSV the moment it's produced, then discards it. Memory stays roughly +// constant regardless of total edge count -- the file also grows +// visibly during the run, so progress is actually observable instead of +// a black box until either completion or an OOM kill. +// +// Usage: +// export_callsite_cg_streaming [cha|rta|vta|otf] + +#include "phasar/ControlFlow/CallGraphAnalysisType.h" +#include "phasar/PhasarLLVM/ControlFlow/LLVMBasedICFG.h" +#include "phasar/PhasarLLVM/HelperAnalyses.h" +#include "phasar/PhasarLLVM/HelperAnalysisConfig.h" +#include "phasar/PhasarLLVM/Utils/LLVMIRToSrc.h" + +#include "llvm/IR/DebugInfoMetadata.h" +#include "llvm/IR/Function.h" +#include "llvm/IR/Instructions.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include + +using namespace psr; + +// Deliberately bypasses PhASAR's getSrcCodeInfoFromIR: it composes File +// from getFilePathFromIR/getDIFileFromIR and Line/Column from +// getLineFromIR/getDILocation -- two functions with DIFFERENT fallback +// branches for instructions lacking direct !dbg metadata (inlined or +// macro-expanded calls). This can silently pair a File from one +// resolution path with a Line from another, unrelated one -- confirmed +// against real FFmpeg data: a "task_wrapper" callsite reported line 2776 +// in a 63-line header. +// +// This resolves ONE DILocation per instruction, reads File/Line/Column +// off that SAME object, and explicitly marks (rather than silently +// guesses at) cases with no direct location at all. +struct ResolvedLoc { + std::string File; + unsigned Line = 0; + unsigned Column = 0; + bool Approximate = false; // true = fell back to enclosing function's + // declaration site, not the real call site +}; + +static ResolvedLoc resolveLocation(const llvm::Instruction *I) { + if (const llvm::DebugLoc &DL = I->getDebugLoc()) { + const llvm::DILocation *Loc = DL.get(); + return {Loc->getFilename().str(), Loc->getLine(), Loc->getColumn(), + false}; + } + // No direct location on this instruction -- fall back to the + // enclosing function's declared file, but SAY SO explicitly rather + // than silently blending it with an unrelated line number the way + // PhASAR's composed helper does. + if (const llvm::DISubprogram *SP = I->getFunction()->getSubprogram()) { + return {SP->getFilename().str(), SP->getLine(), 0, true}; + } + return {"", 0, 0, true}; +} + +static ResolvedLoc resolveLocation(const llvm::Function *F) { + if (const llvm::DISubprogram *SP = F->getSubprogram()) { + return {SP->getFilename().str(), SP->getLine(), 0, false}; + } + return {"", 0, 0, true}; +} + +static CallGraphAnalysisType parseCGType(const std::string &S) { + if (S == "cha") return CallGraphAnalysisType::CHA; + if (S == "rta") return CallGraphAnalysisType::RTA; + if (S == "vta") return CallGraphAnalysisType::VTA; + if (S == "otf") return CallGraphAnalysisType::OTF; + llvm::errs() << "Unknown call-graph analysis type '" << S + << "', defaulting to OTF\n"; + return CallGraphAnalysisType::OTF; +} + +// Minimal CSV field escaping -- source lines can contain commas/quotes. +static std::string csvField(const std::string &S) { + bool needsQuote = S.find(',') != std::string::npos || + S.find('"') != std::string::npos || + S.find('\n') != std::string::npos; + if (!needsQuote) + return S; + std::string Out = "\""; + for (char C : S) { + if (C == '"') + Out += "\"\""; + else + Out += C; + } + Out += "\""; + return Out; +} + +int main(int argc, char **argv) { + if (argc < 5) { + llvm::errs() << "usage: " << argv[0] + << " [cha|rta|vta|otf] \n"; + return 1; + } + + std::string BitcodeFile = argv[1]; + std::vector EntryPoints = {argv[2]}; + CallGraphAnalysisType CGTy = parseCGType(argv[3]); + std::string OutPath = argv[4]; + + FILE *Out = std::fopen(OutPath.c_str(), "w"); + if (!Out) { + llvm::errs() << "could not open " << OutPath << " for writing\n"; + return 1; + } + std::fprintf(Out, "caller_function,caller_file,caller_line,caller_column," + "callee_function,callee_file,callee_line,callee_column," + "caller_loc_approximate\n"); + std::fflush(Out); + + HelperAnalyses HA(BitcodeFile, EntryPoints, + HelperAnalysisConfig{}.withCGType(CGTy)); + LLVMBasedICFG &ICF = HA.getICFG(); + + uint64_t EdgeCount = 0; + uint64_t FnCount = 0; + + // Walk every function PhASAR knows about, every call instruction + // inside it, and every resolved callee at that specific call site -- + // exactly the same traversal exportICFGAsJson does internally, just + // writing (and forgetting) each result immediately instead of + // accumulating all of them. + for (const llvm::Function *Fun : ICF.getAllFunctions()) { + if (Fun->isDeclaration()) + continue; + ++FnCount; + + for (const llvm::Instruction *CS : ICF.getCallsFromWithin(Fun)) { + ResolvedLoc CallerLoc = resolveLocation(CS); + + for (const llvm::Function *Callee : ICF.getCalleesOfCallAt(CS)) { + if (!Callee) + continue; + ResolvedLoc CalleeLoc = resolveLocation(Callee); + + std::fprintf( + Out, "%s,%s,%u,%u,%s,%s,%u,%u,%s\n", + csvField(Fun->getName().str()).c_str(), + csvField(CallerLoc.File).c_str(), + CallerLoc.Line, CallerLoc.Column, + csvField(Callee->getName().str()).c_str(), + csvField(CalleeLoc.File).c_str(), + CalleeLoc.Line, CalleeLoc.Column, + CallerLoc.Approximate ? "true" : "false"); + + ++EdgeCount; + if (EdgeCount % 50000 == 0) { + std::fflush(Out); // periodic flush -- makes progress visible on + // disk instead of buffered invisibly + llvm::errs() << "[export_callsite_cg_streaming] " << EdgeCount + << " edges written, " << FnCount + << " functions processed so far\n"; + } + } + } + } + + std::fflush(Out); + std::fclose(Out); + + llvm::errs() << "[export_callsite_cg_streaming] DONE: " << EdgeCount + << " total edges across " << FnCount << " functions -> " + << OutPath << "\n"; + return 0; +} diff --git a/examples/how-to/README.md b/examples/how-to/README.md index b84875a609..10c601fc45 100644 --- a/examples/how-to/README.md +++ b/examples/how-to/README.md @@ -11,3 +11,4 @@ Currently supporting: - [x] Run an IDE analysis ([here](./05-run-ide-analysis/README.md)) - [x] Write an IFDS analysis ([here](./07-write-ifds-analysis/README.md)) - [x] Write an IDE analysis ([here](./08-write-ide-analysis/)) +- [x] Export a callsite-level call graph ([here](./09-export-callsite-cg/README.md)) From 7df5c4a7bad5f640887bf17b556e553d76c4af77 Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Mon, 21 Sep 2026 16:32:17 +0200 Subject: [PATCH 2/3] LLVM'ify the cexport_callsite_cg_streaming tool --- .../export_callsite_cg_streaming.cpp | 282 +++++++++--------- 1 file changed, 140 insertions(+), 142 deletions(-) diff --git a/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp b/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp index f01f2604a6..cfa848fe5c 100644 --- a/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp +++ b/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp @@ -1,186 +1,184 @@ -// export_callsite_cg_streaming.cpp -// -// export_callsite_cg.cpp (the first version) calls PhASAR's -// exportICFGAsJson(), which builds ONE nlohmann::json object holding -// every edge in memory, and only writes to disk at the very end. On -// FFmpeg's real edge count (~1.2M+, per the CHA/RTA baseline), this grew -// unbounded -- confirmed against a real container run: 11GB resident, -// then 2.8GB of swap and climbing, zero bytes written the entire time, -// no realistic path to finishing. -// -// This version uses the same underlying PhASAR primitives -// (getCallsFromWithin, getCalleesOfCallAt, getSrcCodeInfoFromIR) that -// exportICFGAsJson itself is built on, but writes each edge to disk as -// CSV the moment it's produced, then discards it. Memory stays roughly -// constant regardless of total edge count -- the file also grows -// visibly during the run, so progress is actually observable instead of -// a black box until either completion or an OOM kill. -// -// Usage: -// export_callsite_cg_streaming [cha|rta|vta|otf] - #include "phasar/ControlFlow/CallGraphAnalysisType.h" #include "phasar/PhasarLLVM/ControlFlow/LLVMBasedICFG.h" +#include "phasar/PhasarLLVM/DB/LLVMProjectIRDB.h" #include "phasar/PhasarLLVM/HelperAnalyses.h" #include "phasar/PhasarLLVM/HelperAnalysisConfig.h" #include "phasar/PhasarLLVM/Utils/LLVMIRToSrc.h" +#include "phasar/Pointer/AliasAnalysisType.h" #include "llvm/IR/DebugInfoMetadata.h" #include "llvm/IR/Function.h" #include "llvm/IR/Instructions.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/WithColor.h" #include "llvm/Support/raw_ostream.h" #include +#include #include +#include #include -using namespace psr; - -// Deliberately bypasses PhASAR's getSrcCodeInfoFromIR: it composes File -// from getFilePathFromIR/getDIFileFromIR and Line/Column from -// getLineFromIR/getDILocation -- two functions with DIFFERENT fallback -// branches for instructions lacking direct !dbg metadata (inlined or -// macro-expanded calls). This can silently pair a File from one -// resolution path with a Line from another, unrelated one -- confirmed -// against real FFmpeg data: a "task_wrapper" callsite reported line 2776 -// in a 63-line header. -// -// This resolves ONE DILocation per instruction, reads File/Line/Column -// off that SAME object, and explicitly marks (rather than silently -// guesses at) cases with no direct location at all. +namespace cl = llvm::cl; + +static cl::OptionCategory Cat("CallGraphCSV"); + +static cl::opt IRFile(cl::Positional, cl::Required, + cl::desc("The LLVM IR file to analyze"), + cl::cat(Cat)); + +static cl::opt + CGTy("call-graph-analysis", cl::init(psr::CallGraphAnalysisType::VTA), + cl::cat(Cat), + cl::ValuesClass{ +#define CALL_GRAPH_ANALYSIS_TYPE(NAME, CMDFLAG, DESC) \ + clEnumValN(psr::CallGraphAnalysisType::NAME, CMDFLAG, DESC), +#include "phasar/ControlFlow/CallGraphAnalysisType.def" + }); + +static cl::opt AATy( + "alias-analysis", cl::init(psr::AliasAnalysisType::AndersenOTF), + cl::cat(Cat), + cl::desc("The alias analysis to be used by VTA or OTF call-graph analysis. " + "Note that CFLAnders/CFLSteens should only be used with " + "call-graph-analysis=otf"), + cl::ValuesClass{ +#define ALIAS_ANALYSIS_TYPE(NAME, CMDFLAG, DESC) \ + clEnumValN(psr::AliasAnalysisType::NAME, CMDFLAG, DESC), +#include "phasar/Pointer/AliasAnalysisType.def" + }); + +static cl::list EntryPointsOpt( + "entry-points", cl::OneOrMore, cl::cat(Cat), + cl::desc("The functions from which the analysis should start. " + "For executables, usually 'main'; use '__ALL__' for all " + "externally visible functions")); + +static cl::opt + OutFile("o", cl::init("-"), cl::cat(Cat), + cl::desc("The CSV output file path. Stdout by default")); + struct ResolvedLoc { - std::string File; - unsigned Line = 0; - unsigned Column = 0; - bool Approximate = false; // true = fell back to enclosing function's - // declaration site, not the real call site + uint32_t Line{}; + uint32_t Column{}; + llvm::StringRef FileName{}; + bool Approximate{}; // true = fell back to enclosing function's declaration + // site, not the real call site + + friend llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, + const ResolvedLoc &Loc) { + OS.write_escaped(Loc.FileName) << ','; + return OS << Loc.Line << ',' << Loc.Column; + } }; +static constexpr llvm::StringLiteral NoDebugInfo = ""; + static ResolvedLoc resolveLocation(const llvm::Instruction *I) { - if (const llvm::DebugLoc &DL = I->getDebugLoc()) { - const llvm::DILocation *Loc = DL.get(); - return {Loc->getFilename().str(), Loc->getLine(), Loc->getColumn(), - false}; + if (auto Loc = psr::getDebugLocation(I)) { + return { + .Line = Loc->Line, + .Column = Loc->Column, + .FileName = Loc->File->getFilename(), + .Approximate = false, + }; } + // No direct location on this instruction -- fall back to the - // enclosing function's declared file, but SAY SO explicitly rather - // than silently blending it with an unrelated line number the way - // PhASAR's composed helper does. - if (const llvm::DISubprogram *SP = I->getFunction()->getSubprogram()) { - return {SP->getFilename().str(), SP->getLine(), 0, true}; + // enclosing function's declared file + if (const auto *SP = I->getFunction()->getSubprogram()) { + return { + .Line = SP->getLine(), + .Column = 0, + .FileName = SP->getFile()->getFilename(), + .Approximate = true, + }; } - return {"", 0, 0, true}; + return {.FileName = NoDebugInfo, .Approximate = true}; } static ResolvedLoc resolveLocation(const llvm::Function *F) { - if (const llvm::DISubprogram *SP = F->getSubprogram()) { - return {SP->getFilename().str(), SP->getLine(), 0, false}; + if (const auto *SP = F->getSubprogram()) { + return { + .Line = SP->getLine(), + .Column = 0, + .FileName = SP->getFile()->getFilename(), + .Approximate = false, + }; } - return {"", 0, 0, true}; -} - -static CallGraphAnalysisType parseCGType(const std::string &S) { - if (S == "cha") return CallGraphAnalysisType::CHA; - if (S == "rta") return CallGraphAnalysisType::RTA; - if (S == "vta") return CallGraphAnalysisType::VTA; - if (S == "otf") return CallGraphAnalysisType::OTF; - llvm::errs() << "Unknown call-graph analysis type '" << S - << "', defaulting to OTF\n"; - return CallGraphAnalysisType::OTF; + return {.FileName = NoDebugInfo, .Approximate = true}; } -// Minimal CSV field escaping -- source lines can contain commas/quotes. -static std::string csvField(const std::string &S) { - bool needsQuote = S.find(',') != std::string::npos || - S.find('"') != std::string::npos || - S.find('\n') != std::string::npos; - if (!needsQuote) - return S; - std::string Out = "\""; - for (char C : S) { - if (C == '"') - Out += "\"\""; - else - Out += C; - } - Out += "\""; - return Out; -} - -int main(int argc, char **argv) { - if (argc < 5) { - llvm::errs() << "usage: " << argv[0] - << " [cha|rta|vta|otf] \n"; +int main(int Argc, char **Argv) { + cl::HideUnrelatedOptions(Cat); + cl::ParseCommandLineOptions( + Argc, Argv, + "Simple CLI tool to build a PhASAR-based call-graph and print it as CSV. " + "Uses on-the-fly printing, so you get results even when aborting the " + "process."); + + std::error_code EC; + llvm::raw_fd_ostream OS(OutFile, EC); + if (EC) { + llvm::WithColor::error() + << "While opening output file '" << OutFile << "':\n"; + llvm::WithColor::error() << EC.message() << '\n'; return 1; } - std::string BitcodeFile = argv[1]; - std::vector EntryPoints = {argv[2]}; - CallGraphAnalysisType CGTy = parseCGType(argv[3]); - std::string OutPath = argv[4]; + // CSV header: + OS << "caller_function,caller_file,caller_line,caller_column," + "callee_function,callee_file,callee_line,callee_column," + "caller_loc_approximate\n"; + OS.flush(); - FILE *Out = std::fopen(OutPath.c_str(), "w"); - if (!Out) { - llvm::errs() << "could not open " << OutPath << " for writing\n"; - return 1; - } - std::fprintf(Out, "caller_function,caller_file,caller_line,caller_column," - "callee_function,callee_file,callee_line,callee_column," - "caller_loc_approximate\n"); - std::fflush(Out); - - HelperAnalyses HA(BitcodeFile, EntryPoints, - HelperAnalysisConfig{}.withCGType(CGTy)); - LLVMBasedICFG &ICF = HA.getICFG(); - - uint64_t EdgeCount = 0; - uint64_t FnCount = 0; - - // Walk every function PhASAR knows about, every call instruction - // inside it, and every resolved callee at that specific call site -- - // exactly the same traversal exportICFGAsJson does internally, just - // writing (and forgetting) each result immediately instead of + psr::HelperAnalyses HA{ + std::make_unique( + psr::LLVMProjectIRDB::loadOrExit(IRFile)), + EntryPointsOpt, + psr::HelperAnalysisConfig{.PTATy = AATy, .CGTy = CGTy}, + }; + + auto &ICF = HA.getICFG(); + + size_t EdgeCount = 0; + size_t FnCount = 0; + + // Walk every function PhASAR's call-graph knows about, every call + // instruction inside it, and every resolved callee at that specific call + // site, writing (and forgetting) each result immediately instead of // accumulating all of them. - for (const llvm::Function *Fun : ICF.getAllFunctions()) { - if (Fun->isDeclaration()) - continue; + for (const llvm::Function *Fun : ICF.getAllVertexFunctions()) { + auto FunName = Fun->getName(); ++FnCount; - for (const llvm::Instruction *CS : ICF.getCallsFromWithin(Fun)) { + ResolvedLoc CalleeLoc = resolveLocation(Fun); + for (const auto *CS : ICF.getCallersOf(Fun)) { ResolvedLoc CallerLoc = resolveLocation(CS); - for (const llvm::Function *Callee : ICF.getCalleesOfCallAt(CS)) { - if (!Callee) - continue; - ResolvedLoc CalleeLoc = resolveLocation(Callee); - - std::fprintf( - Out, "%s,%s,%u,%u,%s,%s,%u,%u,%s\n", - csvField(Fun->getName().str()).c_str(), - csvField(CallerLoc.File).c_str(), - CallerLoc.Line, CallerLoc.Column, - csvField(Callee->getName().str()).c_str(), - csvField(CalleeLoc.File).c_str(), - CalleeLoc.Line, CalleeLoc.Column, - CallerLoc.Approximate ? "true" : "false"); - - ++EdgeCount; - if (EdgeCount % 50000 == 0) { - std::fflush(Out); // periodic flush -- makes progress visible on - // disk instead of buffered invisibly - llvm::errs() << "[export_callsite_cg_streaming] " << EdgeCount - << " edges written, " << FnCount - << " functions processed so far\n"; - } + OS.write_escaped(CS->getFunction()->getName()) << ',' << CallerLoc << ','; + OS.write_escaped(FunName) << ',' << CalleeLoc << ','; + OS << (CallerLoc.Approximate ? "true" : "false") << '\n'; + + ++EdgeCount; + if (EdgeCount % 50000 == 0) { + OS.flush(); // periodic flush -- makes progress visible on + // disk instead of buffered invisibly + llvm::WithColor::note() + << "[export_callsite_cg_streaming] " << EdgeCount + << " edges written, " << FnCount << '/' + << ICF.getNumVertexFunctions() << " functions processed so far\n"; } } } - std::fflush(Out); - std::fclose(Out); + OS.close(); - llvm::errs() << "[export_callsite_cg_streaming] DONE: " << EdgeCount - << " total edges across " << FnCount << " functions -> " - << OutPath << "\n"; + llvm::WithColor::note() << "[export_callsite_cg_streaming] DONE: " + << EdgeCount << " total edges across " << FnCount + << " functions -> " + << (OutFile == "-" ? llvm::StringRef("stdout") + : OutFile) + << "\n"; return 0; } From aec1f37e62e5c6028f97dac7be3b2c8f9124a608 Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Wed, 23 Sep 2026 13:07:46 +0200 Subject: [PATCH 3/3] minor --- .../how-to/09-export-callsite-cg/README.md | 34 +++----- .../export_callsite_cg_streaming.cpp | 85 ++++++------------- 2 files changed, 36 insertions(+), 83 deletions(-) diff --git a/examples/how-to/09-export-callsite-cg/README.md b/examples/how-to/09-export-callsite-cg/README.md index d140eef158..1eafec8047 100644 --- a/examples/how-to/09-export-callsite-cg/README.md +++ b/examples/how-to/09-export-callsite-cg/README.md @@ -6,27 +6,15 @@ side, written incrementally as edges are discovered. This differs from `CallGraph::printAsDot()` / `printAsJson()` in two ways: -- **Callsite-level, not function-level.** Each row is a single call instruction - resolved to its callee(s), rather than an aggregated function-to-function edge. - This is what correlating a static call graph against runtime instrumentation - data needs — the runtime side reports individual call sites, not just "A calls - B somewhere". -- **Streaming output.** `exportICFGAsJson()` builds one in-memory `nlohmann::json` - object holding every edge before writing anything to disk. On a large enough - input (validated against a real ~1.2M-edge FFmpeg call graph) this grows - unbounded — 11GB resident plus climbing swap, with zero bytes written the whole - time. This driver writes each edge to disk the moment it's produced and - discards it, so memory stays roughly constant regardless of total edge count. - -It also documents a real source-location bug it works around: PhASAR's -`getSrcCodeInfoFromIR` resolves `File` and `Line` through separate helper calls, -each with its own fallback logic for instructions lacking direct `!dbg` -metadata — which can pair a `File` from one resolution path with a `Line` from -an unrelated one. A concrete case from the FFmpeg run: a callsite reported line -2776 inside a 63-line header. `resolveLocation()` here instead resolves a single -`DILocation` per instruction and reads `File`/`Line`/`Column` off that same -object, marking (rather than silently guessing at) instructions with no direct -location at all. +- **Callsite-level.** Each row is a single call instruction resolved to its callee(s), rather than an aggregated function-to-function edge. + This is what, e.g., correlating a static call graph against runtime instrumentation data needs. + The runtime side reports individual call sites, not just "A calls B somewhere". +- **Streaming output.** `exportICFGAsJson()` and `printAsJson()` build one in-memory `nlohmann::json` object holding every edge before writing anything to disk. + On a large enough input (validated against a ~1.2M-edge FFmpeg call graph) this easily goes out-of-memory. + This driver writes each edge to disk immediately and discards it, so memory stays roughly constant regardless of total edge count. + +It also documents a (current) source-location bug it works around: PhASAR's `getSrcCodeInfoFromIR` resolves `File` and `Line` through separate helper calls, +each with its own fallback logic for instructions lacking direct `!dbg` metadata, which can pair a `File` from one resolution path with a `Line` from an unrelated one. ## Build @@ -49,6 +37,4 @@ Output is CSV with one row per resolved call edge: caller_function,caller_file,caller_line,caller_column,callee_function,callee_file,callee_line,callee_column,caller_loc_approximate ``` -`caller_loc_approximate` is `true` when the call instruction had no direct debug -location and the row fell back to the enclosing function's declaration site -instead of the real call site. +`caller_loc_approximate` is `true` when the call instruction had no direct debug location and the row fell back to the enclosing function's declaration site instead of the real call site. diff --git a/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp b/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp index cfa848fe5c..0fd7955cee 100644 --- a/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp +++ b/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp @@ -4,60 +4,17 @@ #include "phasar/PhasarLLVM/HelperAnalyses.h" #include "phasar/PhasarLLVM/HelperAnalysisConfig.h" #include "phasar/PhasarLLVM/Utils/LLVMIRToSrc.h" -#include "phasar/Pointer/AliasAnalysisType.h" +#include "llvm/ADT/StringRef.h" #include "llvm/IR/DebugInfoMetadata.h" #include "llvm/IR/Function.h" #include "llvm/IR/Instructions.h" -#include "llvm/Support/CommandLine.h" #include "llvm/Support/WithColor.h" #include "llvm/Support/raw_ostream.h" -#include -#include -#include #include -#include - -namespace cl = llvm::cl; - -static cl::OptionCategory Cat("CallGraphCSV"); - -static cl::opt IRFile(cl::Positional, cl::Required, - cl::desc("The LLVM IR file to analyze"), - cl::cat(Cat)); - -static cl::opt - CGTy("call-graph-analysis", cl::init(psr::CallGraphAnalysisType::VTA), - cl::cat(Cat), - cl::ValuesClass{ -#define CALL_GRAPH_ANALYSIS_TYPE(NAME, CMDFLAG, DESC) \ - clEnumValN(psr::CallGraphAnalysisType::NAME, CMDFLAG, DESC), -#include "phasar/ControlFlow/CallGraphAnalysisType.def" - }); - -static cl::opt AATy( - "alias-analysis", cl::init(psr::AliasAnalysisType::AndersenOTF), - cl::cat(Cat), - cl::desc("The alias analysis to be used by VTA or OTF call-graph analysis. " - "Note that CFLAnders/CFLSteens should only be used with " - "call-graph-analysis=otf"), - cl::ValuesClass{ -#define ALIAS_ANALYSIS_TYPE(NAME, CMDFLAG, DESC) \ - clEnumValN(psr::AliasAnalysisType::NAME, CMDFLAG, DESC), -#include "phasar/Pointer/AliasAnalysisType.def" - }); - -static cl::list EntryPointsOpt( - "entry-points", cl::OneOrMore, cl::cat(Cat), - cl::desc("The functions from which the analysis should start. " - "For executables, usually 'main'; use '__ALL__' for all " - "externally visible functions")); - -static cl::opt - OutFile("o", cl::init("-"), cl::cat(Cat), - cl::desc("The CSV output file path. Stdout by default")); +namespace { struct ResolvedLoc { uint32_t Line{}; uint32_t Column{}; @@ -72,9 +29,9 @@ struct ResolvedLoc { } }; -static constexpr llvm::StringLiteral NoDebugInfo = ""; +constexpr llvm::StringLiteral NoDebugInfo = ""; -static ResolvedLoc resolveLocation(const llvm::Instruction *I) { +ResolvedLoc resolveLocation(const llvm::Instruction *I) { if (auto Loc = psr::getDebugLocation(I)) { return { .Line = Loc->Line, @@ -97,7 +54,7 @@ static ResolvedLoc resolveLocation(const llvm::Instruction *I) { return {.FileName = NoDebugInfo, .Approximate = true}; } -static ResolvedLoc resolveLocation(const llvm::Function *F) { +ResolvedLoc resolveLocation(const llvm::Function *F) { if (const auto *SP = F->getSubprogram()) { return { .Line = SP->getLine(), @@ -108,14 +65,24 @@ static ResolvedLoc resolveLocation(const llvm::Function *F) { } return {.FileName = NoDebugInfo, .Approximate = true}; } +} // namespace int main(int Argc, char **Argv) { - cl::HideUnrelatedOptions(Cat); - cl::ParseCommandLineOptions( - Argc, Argv, - "Simple CLI tool to build a PhASAR-based call-graph and print it as CSV. " - "Uses on-the-fly printing, so you get results even when aborting the " - "process."); + if (Argc < 5) { + llvm::errs() << "USAGE: " << Argv[0] + << " [cha|rta|vta|otf] \n"; + return 1; + } + + llvm::StringRef IRFile = Argv[1]; + std::vector EntryPoints = {Argv[2]}; + auto CGTy = psr::toCallGraphAnalysisType(Argv[3]); + llvm::StringRef OutFile = Argv[4]; + + if (CGTy == psr::CallGraphAnalysisType::Invalid) { + llvm::WithColor::error() << "Invalid call-graph type '" << Argv[3] << "'\n"; + return 1; + } std::error_code EC; llvm::raw_fd_ostream OS(OutFile, EC); @@ -132,11 +99,12 @@ int main(int Argc, char **Argv) { "caller_loc_approximate\n"; OS.flush(); + // Basic phasar pipeline: psr::HelperAnalyses HA{ std::make_unique( psr::LLVMProjectIRDB::loadOrExit(IRFile)), - EntryPointsOpt, - psr::HelperAnalysisConfig{.PTATy = AATy, .CGTy = CGTy}, + std::move(EntryPoints), + psr::HelperAnalysisConfig{.CGTy = CGTy}, }; auto &ICF = HA.getICFG(); @@ -144,9 +112,8 @@ int main(int Argc, char **Argv) { size_t EdgeCount = 0; size_t FnCount = 0; - // Walk every function PhASAR's call-graph knows about, every call - // instruction inside it, and every resolved callee at that specific call - // site, writing (and forgetting) each result immediately instead of + // Walk every function PhASAR's call-graph knows about, and ask it for all + // known callers, writing (and forgetting) each result immediately instead of // accumulating all of them. for (const llvm::Function *Fun : ICF.getAllVertexFunctions()) { auto FunName = Fun->getName();