[llvm] [IR tracker] Add new-PM MIR tracking (PR #194626)
Yaxun Liu via llvm-commits
llvm-commits at lists.llvm.org
Tue Apr 28 06:39:16 PDT 2026
https://github.com/yxsamliu created https://github.com/llvm/llvm-project/pull/194626
Record MIR snapshots from new-PM MachineFunction pass callbacks so the tracker can follow source locations through the CodeGen pipeline without requiring legacy PM or assembly support.
Made with [Cursor](https://cursor.com)
>From 001d4a647fa0dd422d8ea8f32e88c4480e817c27 Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <yaxun.liu at amd.com>
Date: Fri, 24 Apr 2026 13:20:58 -0400
Subject: [PATCH 1/4] [Passes] Add IR tracker for source-indexed pass snapshots
Understanding how IR changes across the pass pipeline is still too slow
and too manual with the existing dump-based tools. -print-after-all and
-print-changed are useful for ad hoc inspection, but they do not provide
a queryable history for one source location, they require users to sift
through large textual dumps, and their turnaround time is often too poor
for iterative debugging on real inputs.
On a medium IR example in local measurements, -print-changed and
-print-after-all were both still in the ~140-170 s range, while the IR
tracker stayed around ~5 s. That difference matters because turnaround
time is part of the feature: when a debugging tool is slow enough that
each iteration takes minutes, or when it emits output large enough to
require manual correlation across whole-module dumps, it stops being
practical for repeated use during development.
Add an IR tracker that records per-pass IR snapshots through the new pass
manager's PassInstrumentation callbacks and emits a compact structured
stream for offline querying. The tracker lets users follow how the IR
associated with a source location evolves across passes without manually
correlating whole-module dumps.
This is also a better direction than trying to make the existing print
modes fast enough for the same workflow. Our measurements showed that the
dominant cost in the print-based path comes from materializing and
formatting full textual IR dumps, so even concise or filtered variants
remain in the same performance class once the full dump work is paid.
The tracker instead records compact structured state directly, which makes
the workflow practical while preserving the instruction-level information
needed for pass-by-pass investigation.
Extend the lit coverage for the recorder's tracking and change-detection
behavior.
---
.../llvm/Passes/IRTrackerInstrumentation.h | 36 +
.../llvm/Passes/StandardInstrumentations.h | 2 +
llvm/lib/Passes/CMakeLists.txt | 1 +
llvm/lib/Passes/IRTrackerInstrumentation.cpp | 1180 +++++++++++++++++
llvm/lib/Passes/StandardInstrumentations.cpp | 1 +
llvm/test/Other/ir-tracker-db.ll | 116 ++
6 files changed, 1336 insertions(+)
create mode 100644 llvm/include/llvm/Passes/IRTrackerInstrumentation.h
create mode 100644 llvm/lib/Passes/IRTrackerInstrumentation.cpp
create mode 100644 llvm/test/Other/ir-tracker-db.ll
diff --git a/llvm/include/llvm/Passes/IRTrackerInstrumentation.h b/llvm/include/llvm/Passes/IRTrackerInstrumentation.h
new file mode 100644
index 0000000000000..056998923a339
--- /dev/null
+++ b/llvm/include/llvm/Passes/IRTrackerInstrumentation.h
@@ -0,0 +1,36 @@
+//===- llvm/Passes/IRTrackerInstrumentation.h - IR tracker ------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// IR tracker instrumentation. Records per-pass IR snapshots and instruction-
+// level changes into a TSV stream when ``-ir-tracker-output`` is set.
+// External tooling under ``llvm/tools/ir-tracker/`` consumes the recorded
+// stream to query how IR evolves through the optimization pipeline.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_PASSES_IRTRACKERINSTRUMENTATION_H
+#define LLVM_PASSES_IRTRACKERINSTRUMENTATION_H
+
+#include "llvm/Support/Compiler.h"
+
+namespace llvm {
+
+class PassInstrumentationCallbacks;
+
+/// Wires the IR tracker into the new pass manager via ``PassInstrumentation``
+/// callbacks. ``registerCallbacks`` is a no-op when the recorder's CLI flag
+/// is not set, so embedding this in ``StandardInstrumentations`` is free for
+/// users who do not opt in.
+class IRTrackerInstrumentation {
+public:
+ LLVM_ABI void registerCallbacks(PassInstrumentationCallbacks &PIC);
+};
+
+} // namespace llvm
+
+#endif // LLVM_PASSES_IRTRACKERINSTRUMENTATION_H
diff --git a/llvm/include/llvm/Passes/StandardInstrumentations.h b/llvm/include/llvm/Passes/StandardInstrumentations.h
index 4ee5ab2554868..e1a838d4cf548 100644
--- a/llvm/include/llvm/Passes/StandardInstrumentations.h
+++ b/llvm/include/llvm/Passes/StandardInstrumentations.h
@@ -26,6 +26,7 @@
#include "llvm/IR/OptBisect.h"
#include "llvm/IR/PassTimingInfo.h"
#include "llvm/IR/ValueHandle.h"
+#include "llvm/Passes/IRTrackerInstrumentation.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Compiler.h"
#include "llvm/Support/TimeProfiler.h"
@@ -599,6 +600,7 @@ class PrintCrashIRInstrumentation {
class StandardInstrumentations {
PrintIRInstrumentation PrintIR;
PrintPassInstrumentation PrintPass;
+ IRTrackerInstrumentation IRTracker;
TimePassesHandler TimePasses;
TimeProfilingPassesHandler TimeProfilingPasses;
OptNoneInstrumentation OptNone;
diff --git a/llvm/lib/Passes/CMakeLists.txt b/llvm/lib/Passes/CMakeLists.txt
index 5d7cd3689f3ff..0ae301d38afe0 100644
--- a/llvm/lib/Passes/CMakeLists.txt
+++ b/llvm/lib/Passes/CMakeLists.txt
@@ -1,5 +1,6 @@
add_llvm_component_library(LLVMPasses
CodeGenPassBuilder.cpp
+ IRTrackerInstrumentation.cpp
OptimizationLevel.cpp
PassBuilder.cpp
PassBuilderBindings.cpp
diff --git a/llvm/lib/Passes/IRTrackerInstrumentation.cpp b/llvm/lib/Passes/IRTrackerInstrumentation.cpp
new file mode 100644
index 0000000000000..179650c245823
--- /dev/null
+++ b/llvm/lib/Passes/IRTrackerInstrumentation.cpp
@@ -0,0 +1,1180 @@
+//===- IRTrackerInstrumentation.cpp - IR tracker recorder -----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Passes/IRTrackerInstrumentation.h"
+
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StableHashing.h"
+#include "llvm/Analysis/LazyCallGraph.h"
+#include "llvm/Analysis/LoopInfo.h"
+#include "llvm/IR/BasicBlock.h"
+#include "llvm/IR/Constants.h"
+#include "llvm/IR/DIBuilder.h"
+#include "llvm/IR/DebugInfoMetadata.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/Instruction.h"
+#include "llvm/IR/Instructions.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IR/ModuleSlotTracker.h"
+#include "llvm/IR/PassInstrumentation.h"
+#include "llvm/IR/PassManager.h"
+#include "llvm/IR/PrintPasses.h"
+#include "llvm/IR/StructuralHash.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/ErrorHandling.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/raw_ostream.h"
+
+using namespace llvm;
+
+//===----------------------------------------------------------------------===//
+// CLI option (the recorder owns its own flag so this TU is self-contained).
+//===----------------------------------------------------------------------===//
+
+static cl::opt<std::string> IRTrackerOutput(
+ "ir-tracker-output",
+ cl::desc("IR tracker: per-pass IR snapshot output path (TSV row format)"),
+ cl::value_desc("file"), cl::init(""), cl::Hidden);
+
+namespace {
+
+//===----------------------------------------------------------------------===//
+// Local copies of small helpers shared with StandardInstrumentations.cpp.
+// Duplicated here so this TU is self-contained.
+//===----------------------------------------------------------------------===//
+
+template <typename IRUnitT> static const IRUnitT *unwrapIR(Any IR) {
+ const IRUnitT **IRPtr = llvm::any_cast<const IRUnitT *>(&IR);
+ return IRPtr ? *IRPtr : nullptr;
+}
+
+static std::string getIRName(Any IR) {
+ if (unwrapIR<Module>(IR))
+ return "[module]";
+ if (const auto *F = unwrapIR<Function>(IR))
+ return F->getName().str();
+ if (const auto *C = unwrapIR<LazyCallGraph::SCC>(IR))
+ return C->getName();
+ if (const auto *L = unwrapIR<Loop>(IR))
+ return "loop %" + L->getName().str() + " in function " +
+ L->getHeader()->getParent()->getName().str();
+ // Unknown IR-unit type. Mirrors the closed-set fallthrough in
+ // shouldPrintIR: degrade gracefully (empty name -> P row carries an
+ // empty ir_unit field) rather than aborting opt. The recorder is not
+ // load-bearing for compilation correctness, so a hard crash here is
+ // the wrong tradeoff if the new pass manager later grows a fifth IR
+ // unit type.
+ return {};
+}
+
+static bool moduleContainsFilterPrintFunc(const Module &M) {
+ return any_of(M.functions(),
+ [](const Function &F) {
+ return isFunctionInPrintList(F.getName());
+ }) ||
+ isFunctionInPrintList("*");
+}
+
+static bool sccContainsFilterPrintFunc(const LazyCallGraph::SCC &C) {
+ return any_of(C,
+ [](const LazyCallGraph::Node &N) {
+ return isFunctionInPrintList(N.getName());
+ }) ||
+ isFunctionInPrintList("*");
+}
+
+static bool shouldPrintIR(Any IR) {
+ if (const auto *M = unwrapIR<Module>(IR))
+ return moduleContainsFilterPrintFunc(*M);
+ if (const auto *F = unwrapIR<Function>(IR))
+ return isFunctionInPrintList(F->getName());
+ if (const auto *C = unwrapIR<LazyCallGraph::SCC>(IR))
+ return sccContainsFilterPrintFunc(*C);
+ if (const auto *L = unwrapIR<Loop>(IR))
+ return isFunctionInPrintList(L->getHeader()->getParent()->getName());
+ return false;
+}
+
+static bool isIgnored(StringRef PassID) {
+ return isSpecialPass(PassID,
+ {"PassManager", "PassAdaptor", "AnalysisManagerProxy",
+ "DevirtSCCRepeatedPass", "ModuleInlinerWrapperPass",
+ "VerifierPass", "PrintModulePass", "PrintMIRPass",
+ "PrintMIRPreparePass"});
+}
+
+//===----------------------------------------------------------------------===//
+// Auto-synthesis of missing DILocations.
+//
+// Purpose: give every instruction a stable identifier that survives
+// pass-driven cloning, moving, and rewriting, so the recorder can
+// associate instructions that originate from the same source point across
+// passes. The recorder uses an instruction's DILocation as that
+// identifier -- when a later pass copies or rewrites an instruction, the
+// resulting instruction's DILocation tells us which original instruction
+// it descends from. Inputs without -g (bitcode, clang JIT, lld embedding,
+// hand-written .ll) have no DILocations on most instructions, so the
+// recorder loses identity across passes. This function fills the gaps
+// with synthesized ordinal IDs on first sight per module.
+//
+// What it adds: for each function that lacks a DISubprogram, one synthetic
+// DISubprogram whose file is "<ir-tracker-synthetic>"; and for each
+// instruction that lacks a DebugLoc, a DILocation(line=N, column=0,
+// scope=that DISubprogram) where N is a module-wide running counter.
+// Real DI is never overwritten -- functions that already have a
+// DISubprogram and instructions that already have a DebugLoc are skipped.
+//
+// Example. Input without debug info:
+//
+// define i32 @add(i32 %a, i32 %b) {
+// %s = add i32 %a, %b
+// ret i32 %s
+// }
+//
+// After synthesizeMissingInstructionLocs:
+//
+// define i32 @add(i32 %a, i32 %b) !dbg !2 {
+// %s = add i32 %a, %b, !dbg !3
+// ret i32 %s, !dbg !4
+// }
+// !1 = !DIFile(filename: "<ir-tracker-synthetic>", directory: ".")
+// !2 = distinct !DISubprogram(name: "add", scope: !0, file: !1, line: 1,
+// ...)
+// !3 = !DILocation(line: 1, column: 0, scope: !2)
+// !4 = !DILocation(line: 2, column: 0, scope: !2)
+//
+// The line numbers are arbitrary running ordinals; they do not refer to
+// anything in the original source. They only need to be stable across
+// passes so the recorder can match a later pass's DILocation against an
+// earlier one and conclude "same logical instruction".
+//
+// No-op when the module already has llvm.dbg.cu (a real -g build), or
+// when the module has no functions.
+//===----------------------------------------------------------------------===//
+
+static void synthesizeMissingInstructionLocs(Module &M) {
+ // Skip if the module already has real debug info; we never want to
+ // override real DI.
+ if (M.getNamedMetadata("llvm.dbg.cu"))
+ return;
+ // Skip empty modules (no functions to attach locs to).
+ if (M.empty())
+ return;
+
+ if (!M.getModuleFlag("Debug Info Version"))
+ M.addModuleFlag(Module::Warning, "Debug Info Version",
+ DEBUG_METADATA_VERSION);
+
+ DIBuilder DIB(M);
+ LLVMContext &Ctx = M.getContext();
+ DIFile *File = DIB.createFile("<ir-tracker-synthetic>", ".");
+ DICompileUnit *CU = DIB.createCompileUnit(
+ dwarf::DW_LANG_C, File, "ir-tracker",
+ /*isOptimized=*/true, "", /*RV=*/0, /*SplitName=*/"",
+ DICompileUnit::FullDebug);
+ auto SPType = DIB.createSubroutineType(DIB.getOrCreateTypeArray({}));
+ unsigned NextOrdinal = 1;
+
+ for (Function &F : M) {
+ if (F.isDeclaration())
+ continue;
+ if (F.getSubprogram())
+ continue;
+ unsigned FuncLine = NextOrdinal;
+ DISubprogram::DISPFlags SPFlags =
+ DISubprogram::SPFlagDefinition | DISubprogram::SPFlagOptimized;
+ if (F.hasPrivateLinkage() || F.hasInternalLinkage())
+ SPFlags |= DISubprogram::SPFlagLocalToUnit;
+ DISubprogram *SP =
+ DIB.createFunction(CU, F.getName(), F.getName(), File, FuncLine,
+ SPType, FuncLine, DINode::FlagZero, SPFlags);
+ F.setSubprogram(SP);
+
+ for (BasicBlock &BB : F) {
+ for (Instruction &I : BB) {
+ if (I.getDebugLoc())
+ continue;
+ I.setDebugLoc(DILocation::get(Ctx, NextOrdinal, 0, SP));
+ ++NextOrdinal;
+ }
+ }
+ }
+ DIB.finalize();
+}
+
+//===----------------------------------------------------------------------===//
+// Recorder implementation.
+//===----------------------------------------------------------------------===//
+
+static std::string getIRTrackerFilePath(const DILocation *Loc) {
+ if (!Loc)
+ return {};
+
+ StringRef Dir = Loc->getDirectory();
+ StringRef File = Loc->getFilename();
+ if (File.empty())
+ return {};
+ if (Dir.empty())
+ return File.str();
+
+ SmallString<256> Path(Dir);
+ sys::path::append(Path, File);
+ return std::string(Path);
+}
+
+static stable_hash hashTrackerIdentity(const DILocation *Loc);
+
+static stable_hash hashValueIdentity(const Value &V) {
+ if (const auto *Arg = dyn_cast<Argument>(&V))
+ return stable_hash_combine(static_cast<stable_hash>(1), Arg->getArgNo());
+ if (const auto *GV = dyn_cast<GlobalValue>(&V))
+ return stable_hash_combine(static_cast<stable_hash>(2),
+ static_cast<stable_hash>(hash_value(GV->getName())));
+ if (const auto *BB = dyn_cast<BasicBlock>(&V)) {
+ if (BB->hasName())
+ return stable_hash_combine(static_cast<stable_hash>(3),
+ static_cast<stable_hash>(hash_value(BB->getName())));
+ return 0;
+ }
+ if (const auto *I = dyn_cast<Instruction>(&V)) {
+ if (const DILocation *Loc = I->getDebugLoc() ? I->getDebugLoc().get() : nullptr)
+ return stable_hash_combine(static_cast<stable_hash>(4),
+ hashTrackerIdentity(Loc));
+ if (I->hasName())
+ return stable_hash_combine(static_cast<stable_hash>(5),
+ static_cast<stable_hash>(hash_value(I->getName())));
+ return 0;
+ }
+ if (V.hasName())
+ return stable_hash_combine(static_cast<stable_hash>(6),
+ static_cast<stable_hash>(hash_value(V.getName())));
+ return 0;
+}
+
+/// Compute a stable structural fingerprint of an instruction.
+///
+/// Used by the per-instruction change-detection step: at each pass, the
+/// recorder rehashes every changed-block instruction and compares against
+/// the previous pass's hash for the same tracker ID. Equal hash → "did
+/// not change" → skip emission.
+///
+/// Captures: opcode, result type, operand count, per-operand type, the
+/// per-operand kind bits (is-Constant, is-Argument), a stable identity for
+/// non-constant operands when one is available (argument number, global name,
+/// basic-block name, instruction source-point identity), the commutative flag,
+/// the CmpInst predicate, and the value of any ConstantInt operand.
+///
+/// The important property is that operand rewrites which change the rendered
+/// instruction text should usually perturb the hash as well:
+///
+/// %a = add i32 %x, %y hash X
+/// %b = add i32 %x, %z hash Y (different source-point identity)
+/// %c = add i32 %x, 1 hash Y (kind bit changed)
+/// %d = add i32 %x, 2 hash Z (ConstantInt value changed)
+/// %e = sub i32 %x, %y hash W (opcode changed)
+/// %f = icmp eq i32 %x, %y hash V
+/// %g = icmp ne i32 %x, %y hash U (predicate changed)
+///
+/// Still missed: zero-loc unnamed instruction operands that have no stable
+/// identity source, ConstantFP value changes, ConstantExpr changes, and
+/// attached metadata changes. Metadata tracking is opt-in via separate flags
+/// (deferred to a follow-up PR).
+static stable_hash hashInstruction(const Instruction &I) {
+ stable_hash H = stable_hash_combine(I.getOpcode(), I.getType()->getTypeID(),
+ I.getNumOperands());
+ for (const Use &U : I.operands()) {
+ Value *V = U.get();
+ H = stable_hash_combine(
+ H, stable_hash_combine(
+ static_cast<stable_hash>(V->getType()->getTypeID()),
+ static_cast<stable_hash>(isa<Constant>(V) ? 1 : 0),
+ static_cast<stable_hash>(isa<Argument>(V) ? 1 : 0),
+ hashValueIdentity(*V)));
+ if (auto *C = dyn_cast<ConstantInt>(V))
+ H = stable_hash_combine(H,
+ static_cast<stable_hash>(hash_value(C->getValue())));
+ }
+ if (I.isCommutative())
+ H = stable_hash_combine(H, 1);
+ if (auto *CI = dyn_cast<CmpInst>(&I))
+ H = stable_hash_combine(H, CI->getPredicate());
+ return H;
+}
+
+/// Compute the hash that identifies "this source point" for tracker-ID
+/// interning.
+///
+/// The recorder assigns one compact integer ID to each unique source
+/// point and references that ID in instruction rows so the source
+/// location appears once (in a metadata row) rather than per
+/// instruction row. Two DILocations should map to the same ID iff
+/// they refer to the same source point.
+///
+/// Combines: source file, line, column, and the declared line of the
+/// enclosing DISubprogram. The file participates in the key because
+/// mixed-file modules can legitimately contain the same (line, col,
+/// scope-line) tuple in multiple translation units; without file
+/// identity those instructions alias to the same tracker ID and share
+/// change-detection state incorrectly. The subprogram's declared line
+/// still disambiguates instructions at the same (line, col) coming
+/// from different functions in the same file:
+///
+/// file.c:1 in function foo (foo is declared at line 1)
+/// hash = combine("file.c", 1, 0, 1)
+/// file.c:1 in function bar (bar is declared at line 3)
+/// hash = combine("file.c", 1, 0, 3) // distinct
+/// other.c:1 in function foo (foo is declared at line 1)
+/// hash = combine("other.c", 1, 0, 1) // distinct
+///
+/// Without the scope-line component, inlined or template-instantiated
+/// instructions at the same (line, col) would alias and be treated as
+/// the same source point. Including the DISubprogram pointer directly
+/// would make the hash run-unstable (pointer addresses are not stable
+/// across LLVM invocations); using the file path plus the subprogram's
+/// declared line gives us the needed disambiguation while keeping the
+/// hash deterministic.
+///
+/// Returns 0 for a null DILocation. The recorder uses 0 as a sentinel
+/// "no real source point" and routes such instructions through a
+/// per-block temp-ID fallback path (synthesized phi nodes, etc.).
+static stable_hash hashTrackerIdentity(const DILocation *Loc) {
+ if (!Loc)
+ return 0;
+ stable_hash FileKey =
+ stable_hash_combine(static_cast<stable_hash>(hash_value(Loc->getDirectory())),
+ static_cast<stable_hash>(hash_value(Loc->getFilename())));
+ unsigned ScopeLine = 0;
+ if (DISubprogram *SP = Loc->getScope()->getSubprogram())
+ ScopeLine = SP->getLine();
+ return stable_hash_combine(FileKey, Loc->getLine(), Loc->getColumn(),
+ ScopeLine);
+}
+
+static void printAPIntValue(raw_ostream &OS, const APInt &V) {
+ SmallString<32> Tmp;
+ if (V.isNegative())
+ V.toStringSigned(Tmp, 10);
+ else
+ V.toStringUnsigned(Tmp, 10);
+ OS << Tmp;
+}
+
+class IRTrackerRecorder {
+ /// Open file handle for the TSV row stream. Opened by the constructor;
+ /// closed when the IRTrackerRecorder is destroyed.
+ std::unique_ptr<raw_fd_ostream> OS;
+
+ /// Pass-record sequence number assigned to the next P row. Starts at 1
+ /// because seq=0 is reserved for the implicit initial-capture pass that
+ /// records the IR before any user pass runs.
+ unsigned NextSeq = 1;
+
+ /// Guard so the initial IR is captured exactly once, on the first
+ /// non-skipped beforePass we see. Set to true after the initial capture
+ /// runs.
+ bool InitialCaptured = false;
+
+ /// Counter that allocates fresh tracker IDs. ID 0 is the sentinel for
+ /// "no real source location"; real IDs start at 1.
+ unsigned NextTrackerID = 1;
+
+ /// Cached pointer to the last module observed by afterPass, used by the
+ /// destructor to emit a final full-instruction snapshot so downstream
+ /// tooling can reconstruct the post-optimization IR from the stream
+ /// alone (no separate -S re-run required).
+ const Module *LastModule = nullptr;
+
+ /// Set of modules whose missing DILocations have already been
+ /// synthesized by ensureSyntheticLocs. Each module gets the synthesis
+ /// walk at most once per recorder lifetime.
+ DenseSet<const Module *> ModulesWithSynthesizedLocs;
+
+ /// Per-function combined structural hash. Equal value across passes
+ /// means the function produced the same per-block instruction hashes,
+ /// so the detailed emission walk can be skipped.
+ DenseMap<const Function *, stable_hash> FunctionHashes;
+
+ /// Per-function vector of full per-block hashes. Indexed positionally
+ /// by basic-block order. Recomputed every pass so function-level
+ /// equality has no false negatives at block granularity.
+ DenseMap<const Function *, SmallVector<stable_hash>> BlockHashes;
+
+ /// Per-function, per-block bool flag: "does this block contain any
+ /// instruction without a DILocation?" Indexed positionally. Drives the
+ /// per-instruction temp-ID fallback path for zero-loc instructions
+ /// (phi nodes, LCSSA-inserted ops).
+ DenseMap<const Function *, SmallVector<bool>> BlockHasZeroIDs;
+
+ /// Per-function, per-block, per-instruction structural hash. Used by
+ /// the per-instruction skip layer ("emit a row only for instructions
+ /// whose hash changed since last pass"). Outer index is the block
+ /// position; inner index is the instruction position within the block.
+ DenseMap<const Function *, SmallVector<SmallVector<stable_hash>>>
+ BlockInstHashes;
+
+ /// Per-function, per-block, per-instruction tracker ID. Same indexing
+ /// as BlockInstHashes. Stores the temp ID assigned to each zero-loc
+ /// instruction in a block so the recorder can refer to it consistently
+ /// across passes.
+ DenseMap<const Function *, SmallVector<SmallVector<unsigned>>> BlockTempIDs;
+
+ /// Intern table from a source-point hash (hashTrackerIdentity) to the
+ /// compact integer tracker ID. First time a source point is seen, a
+ /// fresh ID is allocated; subsequent sightings of the same source
+ /// point return the existing ID, giving stable cross-pass identity.
+ DenseMap<stable_hash, unsigned> LocKeyToTrackerID;
+
+ /// Per-tracker-ID memory of the last instruction-hash we emitted for
+ /// that ID. The change-detection check is "is the current instruction
+ /// hash equal to the value stored here?"; if equal, skip emission.
+ DenseMap<unsigned, stable_hash> TrackerIDToPrevHash;
+
+ /// Set of tracker IDs we have already emitted a T (metadata) row for.
+ /// Used by writeTrackerRecord to dedup so each unique source point
+ /// appears in exactly one T row over the recorder's lifetime.
+ DenseSet<unsigned> EmittedTrackerMetadata;
+
+ /// Emit one P (pass) row. P rows delimit the per-pass instruction
+ /// records that follow.
+ ///
+ /// Format: ``P\t<seq>\t<phase>\t<pass_name>\t<ir_unit>``.
+ ///
+ /// * ``seq``: monotonically increasing pass index. 0 is the initial
+ /// capture, 1..N are normal passes, and one final "phase=final"
+ /// record is emitted at teardown.
+ /// * ``phase``: ``initial``, ``after``, or ``final``.
+ /// * ``pass_name``: pass class name as resolved by
+ /// PassInstrumentationCallbacks::getPassNameForClassName.
+ /// * ``ir_unit``: human-readable IR unit name from getIRName ("[module]",
+ /// a function name, an SCC name, or "loop %X in function Y").
+ ///
+ /// Example output:
+ ///
+ /// P\t0\tinitial\t<initial>\t[module]
+ /// P\t1\tafter\tmemprof-remove-attributes\t[module]
+ /// P\t5\tafter\tsroa\tcli_wcwidth
+ void writePassRecord(unsigned Seq, StringRef Phase, StringRef PassName,
+ StringRef IRUnit) {
+ *OS << "P\t" << Seq << '\t' << Phase << '\t' << PassName << '\t' << IRUnit
+ << '\n';
+ }
+
+ /// Emit one T (tracker-metadata) row, at most once per tracker ID over
+ /// the recorder's lifetime.
+ ///
+ /// Each T row binds a tracker ID to its source location so I rows
+ /// (instruction rows) can reference the ID compactly without repeating
+ /// the file/line/col text. The dedup is via EmittedTrackerMetadata; a
+ /// second call with the same ID is a silent no-op.
+ ///
+ /// Format: ``T\t<id>\t<file>\t<line>\t<col>``.
+ ///
+ /// For instructions that have no DILocation (phi nodes, many LCSSA
+ /// inserts) the recorder still allocates a temp tracker ID and calls
+ /// this with Loc==nullptr. We emit the row anyway with placeholder
+ /// "<synthetic>" / 0 / 0 so the downstream consumer's ID->location
+ /// lookup is total -- if we silently dropped the T row, the consumer
+ /// would have to special-case "I row references unknown ID".
+ ///
+ /// Example output:
+ ///
+ /// T\t1\t/home/yaxunl/foo.c\t42\t7
+ /// T\t2\t/home/yaxunl/foo.c\t43\t3
+ /// T\t17\t<synthetic>\t0\t0
+ void writeTrackerRecord(unsigned ID, const DILocation *Loc) {
+ if (ID == 0 || !EmittedTrackerMetadata.insert(ID).second)
+ return;
+ std::string FilePath = Loc ? getIRTrackerFilePath(Loc) : "<synthetic>";
+ unsigned LineN = Loc ? Loc->getLine() : 0;
+ unsigned ColN = Loc ? Loc->getColumn() : 0;
+ *OS << "T\t" << ID << '\t' << FilePath << '\t' << LineN << '\t'
+ << ColN << '\n';
+ }
+
+ /// Look up (or assign) the tracker ID for a source location.
+ ///
+ /// Two DILocations referring to the same source point (per
+ /// hashTrackerIdentity) return the same ID. The first sighting of a
+ /// previously-unseen source point allocates and returns the next
+ /// available ID. A null Loc returns the sentinel value 0, signaling
+ /// "no real source point" -- the caller is responsible for routing
+ /// such instructions through the per-block temp-ID fallback.
+ ///
+ /// Example. With LocKeyToTrackerID initially empty:
+ ///
+ /// getOrCreateTrackerID(loc_at_foo_c_42_7) -> 1 (newly minted)
+ /// getOrCreateTrackerID(loc_at_foo_c_43_3) -> 2
+ /// getOrCreateTrackerID(loc_at_foo_c_42_7) -> 1 (reused)
+ /// getOrCreateTrackerID(nullptr) -> 0 (sentinel)
+ /// getOrCreateTrackerID(loc_at_bar_c_42_7) -> 3 (different scope)
+ unsigned getOrCreateTrackerID(const DILocation *Loc) {
+ stable_hash Key = hashTrackerIdentity(Loc);
+ if (Key == 0)
+ return 0;
+ auto It = LocKeyToTrackerID.find(Key);
+ if (It != LocKeyToTrackerID.end())
+ return It->second;
+ unsigned ID = NextTrackerID++;
+ LocKeyToTrackerID[Key] = ID;
+ return ID;
+ }
+
+ /// Record one function's per-pass instruction-level diff into the TSV
+ /// stream. Called from writeIR for every function reachable from the
+ /// pass's IR unit.
+ ///
+ /// SkipUnchanged controls the change-detection mode:
+ ///
+ /// * SkipUnchanged=false: emit every instruction in the function (used
+ /// for the initial capture and the destructor's final snapshot).
+ /// * SkipUnchanged=true (the normal per-pass path): only emit
+ /// instructions whose structural hash changed since the last
+ /// recorded pass.
+ ///
+ /// The implementation is staged so that cheap equality checks can skip
+ /// the more expensive detailed emission work:
+ ///
+ /// 1. Recompute one structural hash per block from the current IR.
+ /// This is the conservative step that guarantees no false
+ /// negatives at block granularity.
+ /// 2. Function-level hash skip. If the combined function hash
+ /// matches the previous pass's, return without emitting anything.
+ /// 3. Per-block changed-or-not list (ChangedBlocks). Only blocks whose
+ /// full hash changed enter the detailed emission loop.
+ /// 4. Per-instruction hash skip. For
+ /// each changed block, emit an I row only for instructions whose
+ /// structural hash differs from TrackerIDToPrevHash.
+ ///
+ /// Example. Pipeline with three passes A, B, C on a function with two
+ /// blocks bb0, bb1, where pass B rewrites only bb1:
+ ///
+ /// pass A (initial, SkipUnchanged=false):
+ /// emits I rows for every instruction in bb0 and bb1.
+ /// pass B (after, SkipUnchanged=true):
+ /// bb0 full block hash matches -> skipped.
+ /// bb1 full block hash differs -> walked, changed instructions emitted.
+ /// pass C (after, SkipUnchanged=true):
+ /// both block hashes match -> function-level FuncH matches ->
+ /// return immediately. Zero rows.
+ void writeInstructionsInFunction(const Function &F, bool SkipUnchanged) {
+ if (F.isDeclaration() || !isFunctionInPrintList(F.getName()))
+ return;
+
+ auto &PrevBlkH = BlockHashes[&F];
+ auto &PrevBlkHasZeroIDs = BlockHasZeroIDs[&F];
+ auto &PrevInstH = BlockInstHashes[&F];
+ auto &PrevTempIDs = BlockTempIDs[&F];
+ SmallVector<stable_hash> NewBlkH;
+ SmallVector<bool> NewBlkHasZeroIDs;
+ SmallVector<unsigned> ChangedBlocks;
+ stable_hash FuncH = 0;
+
+ unsigned BlkIdx = 0;
+ for (const BasicBlock &BB : F) {
+ stable_hash BlkH = 0;
+ bool HasZeroID = false;
+ for (const Instruction &I : BB) {
+ stable_hash H = hashInstruction(I);
+ BlkH = stable_hash_combine(BlkH, H);
+ if (!I.getDebugLoc())
+ HasZeroID = true;
+ }
+ NewBlkHasZeroIDs.push_back(HasZeroID);
+ NewBlkH.push_back(BlkH);
+ FuncH = stable_hash_combine(FuncH, BlkH);
+ if (!SkipUnchanged || BlkIdx >= PrevBlkH.size() ||
+ PrevBlkH[BlkIdx] != BlkH)
+ ChangedBlocks.push_back(BlkIdx);
+ ++BlkIdx;
+ }
+
+ if (SkipUnchanged) {
+ auto It = FunctionHashes.find(&F);
+ if (It != FunctionHashes.end() && It->second == FuncH) {
+ return;
+ }
+ FunctionHashes[&F] = FuncH;
+ } else {
+ FunctionHashes[&F] = FuncH;
+ }
+
+ if (ChangedBlocks.empty()) {
+ PrevBlkHasZeroIDs = std::move(NewBlkHasZeroIDs);
+ PrevBlkH = std::move(NewBlkH);
+ return;
+ }
+
+ // Local state shared by the three printer lambdas defined below.
+ StringRef FunctionName = F.getName();
+ // Reusable per-instruction text buffer; avoids reallocating per emit.
+ SmallString<256> InstBuf;
+ // Per-function intern table for the %u<N> fallback (used when a value
+ // has no name, no global address, and no DILocation we can derive a
+ // tracker ID from).
+ DenseMap<const Value *, unsigned> LocalValueNames;
+ unsigned NextLocalValueName = 0;
+
+ /// Return a short text token for any Value reference, in priority
+ /// order: named global -> ``@name``; named BB -> ``%name``; unnamed
+ /// Argument -> ``%<argNo>``; any value with a name -> ``%name``;
+ /// instruction with a DILocation -> ``%t<trackerID>`` (stable
+ /// across passes); anything else -> ``%u<N>`` from the per-function
+ /// fallback table.
+ ///
+ /// The ``%t<N>`` case is the key one: it gives instructions a
+ /// stable handle that survives SSA renames, so the recorded text
+ /// can be diffed across passes meaningfully.
+ auto getValueName = [&](const Value *V) -> std::string {
+ if (auto *GV = dyn_cast<GlobalValue>(V)) {
+ if (GV->hasName())
+ return (Twine("@") + GV->getName()).str();
+ }
+ if (auto *BB = dyn_cast<BasicBlock>(V)) {
+ if (BB->hasName())
+ return (Twine("%") + BB->getName()).str();
+ }
+ // Render unnamed function arguments using the textual-IR convention
+ // (``%0``, ``%1``, ...) instead of the per-emission ``%u<N>`` fallback,
+ // since the argument index is stable across passes.
+ if (auto *Arg = dyn_cast<Argument>(V)) {
+ if (!Arg->hasName())
+ return (Twine("%") + Twine(Arg->getArgNo())).str();
+ }
+ if (V->hasName())
+ return (Twine("%") + V->getName()).str();
+ if (auto *I = dyn_cast<Instruction>(V)) {
+ if (const DILocation *Loc =
+ I->getDebugLoc() ? I->getDebugLoc().get() : nullptr) {
+ unsigned ID = getOrCreateTrackerID(Loc);
+ if (ID != 0)
+ return (Twine("%t") + Twine(ID)).str();
+ }
+ }
+ if (auto It = LocalValueNames.find(V); It != LocalValueNames.end())
+ return (Twine("%u") + Twine(It->second)).str();
+ unsigned ID = NextLocalValueName++;
+ LocalValueNames[V] = ID;
+ return (Twine("%u") + Twine(ID)).str();
+ };
+
+ /// Write one operand reference to ``OS``. Handles literal
+ /// constants (ConstantInt, ConstantFP, null/undef/poison,
+ /// zeroinitializer, string ConstantDataArray) inline; for
+ /// everything else, falls through to ``getValueName``.
+ ///
+ /// Stored as ``std::function`` rather than ``auto`` so
+ /// ``printInstructionText`` below can name its type when it
+ /// captures it.
+ std::function<void(raw_ostream &, const Value *)> writeValueRef =
+ [&](raw_ostream &OS, const Value *V) {
+ if (auto *CI = dyn_cast<ConstantInt>(V)) {
+ printAPIntValue(OS, CI->getValue());
+ return;
+ }
+ if (auto *CF = dyn_cast<ConstantFP>(V)) {
+ SmallString<32> Tmp;
+ CF->getValueAPF().toString(Tmp);
+ OS << Tmp;
+ return;
+ }
+ if (isa<ConstantPointerNull>(V)) {
+ OS << "null";
+ return;
+ }
+ if (isa<UndefValue>(V)) {
+ OS << "undef";
+ return;
+ }
+ if (isa<PoisonValue>(V)) {
+ OS << "poison";
+ return;
+ }
+ if (isa<ConstantAggregateZero>(V)) {
+ OS << "zeroinitializer";
+ return;
+ }
+ if (auto *CA = dyn_cast<ConstantDataArray>(V)) {
+ if (CA->isString()) {
+ OS << "c\"";
+ printEscapedString(CA->getAsString(), OS);
+ OS << "\"";
+ return;
+ }
+ }
+ OS << getValueName(V);
+ };
+
+ /// Format one instruction into ``OS`` as the structural-form text
+ /// that lands in the I row. Walks the opcode-specific branches
+ /// (ret, br, switch, phi, alloca, gep, load, store, call, invoke,
+ /// cmp) to lay out operands in canonical order, then a generic
+ /// fallback for arithmetic / cast / etc.
+ ///
+ /// Deliberately omits attribute lists, alignment suffixes,
+ /// attached metadata, sync scope, atomic ordering, GEP inrange
+ /// markers, fast-math flags, and ``tail`` markers -- the recorder
+ /// records structural shape, not full LLVM IR text. The omissions
+ /// are what keep the emitted text compact and inexpensive to produce.
+ ///
+ /// CurID is the current instruction's tracker ID; passed in so
+ /// printInstructionText can put ``%t<CurID>`` as the result name
+ /// for instructions that have a real DILocation.
+ auto printInstructionText = [&](raw_ostream &OS, const Instruction &I,
+ unsigned CurID) {
+ if (!I.getType()->isVoidTy()) {
+ if (I.hasName())
+ OS << "%" << I.getName();
+ else if (CurID != 0)
+ OS << "%t" << CurID;
+ else
+ OS << getValueName(&I);
+ OS << " = ";
+ }
+
+ OS << I.getOpcodeName();
+ if (const auto *CI = dyn_cast<CmpInst>(&I))
+ OS << ' ' << CI->getPredicate();
+
+ if (const auto *RI = dyn_cast<ReturnInst>(&I)) {
+ if (RI->getNumOperands() == 0) {
+ OS << " void";
+ return;
+ }
+ OS << ' ';
+ RI->getReturnValue()->getType()->print(OS);
+ OS << ' ';
+ writeValueRef(OS, RI->getReturnValue());
+ return;
+ }
+
+ if (const auto *BI = dyn_cast<BranchInst>(&I)) {
+ if (BI->isUnconditional()) {
+ OS << ' ';
+ writeValueRef(OS, BI->getSuccessor(0));
+ } else {
+ OS << ' ';
+ writeValueRef(OS, BI->getCondition());
+ OS << ", ";
+ writeValueRef(OS, BI->getSuccessor(0));
+ OS << ", ";
+ writeValueRef(OS, BI->getSuccessor(1));
+ }
+ return;
+ }
+
+ if (const auto *PN = dyn_cast<PHINode>(&I)) {
+ OS << ' ';
+ I.getType()->print(OS);
+ bool First = true;
+ for (unsigned Idx = 0; Idx < PN->getNumIncomingValues(); ++Idx) {
+ OS << (First ? ' ' : ',');
+ if (!First)
+ OS << ' ';
+ First = false;
+ OS << "[ ";
+ writeValueRef(OS, PN->getIncomingValue(Idx));
+ OS << ", ";
+ writeValueRef(OS, PN->getIncomingBlock(Idx));
+ OS << " ]";
+ }
+ return;
+ }
+
+ if (const auto *CB = dyn_cast<CallBase>(&I)) {
+ if (!CB->getType()->isVoidTy()) {
+ OS << ' ';
+ CB->getType()->print(OS);
+ }
+ OS << ' ';
+ writeValueRef(OS, CB->getCalledOperand());
+ OS << '(';
+ for (unsigned Idx = 0; Idx < CB->arg_size(); ++Idx) {
+ if (Idx)
+ OS << ", ";
+ writeValueRef(OS, CB->getArgOperand(Idx));
+ }
+ OS << ')';
+ return;
+ }
+
+ if (I.getNumOperands()) {
+ if (!I.getType()->isVoidTy()) {
+ OS << ' ';
+ I.getType()->print(OS);
+ }
+ OS << ' ';
+ for (unsigned Idx = 0; Idx < I.getNumOperands(); ++Idx) {
+ if (Idx)
+ OS << ", ";
+ writeValueRef(OS, I.getOperand(Idx));
+ }
+ }
+ };
+ BlkIdx = 0;
+ unsigned ChangedBlockPos = 0;
+
+ for (const BasicBlock &BB : F) {
+ bool BlockChanged = ChangedBlockPos < ChangedBlocks.size() &&
+ ChangedBlocks[ChangedBlockPos] == BlkIdx;
+
+ // Detailed per-instruction emission for one changed block.
+ //
+ // For each instruction we:
+ // 1. Compute its structural hash (CurH).
+ // 2. Resolve a stable CurID -- either from the DILocation
+ // (real tracker ID) or via the per-block temp-ID matching
+ // heuristic for zero-loc instructions (phi, LCSSA inserts).
+ // 3. Decide if the instruction changed since last pass
+ // (InstChanged) by comparing CurH against the recorded
+ // previous hash for CurID (or against the same-position
+ // previous hash for unmatched zero-loc temps).
+ // 4. If changed, format the instruction text via the
+ // lightweight printer and emit one I row.
+ // 5. Update TrackerIDToPrevHash so the next pass's
+ // change-detection sees CurH as "previous".
+ //
+ // OldInstH / OldTempIDs point into the previous pass's per-block
+ // vectors when available; nullptr on first pass or when caller
+ // requested SkipUnchanged=false.
+ if (BlockChanged) {
+ ++ChangedBlockPos;
+ StringRef BBLabel =
+ BB.hasName() ? BB.getName() : StringRef("<unnamed>");
+ bool NeedFallback = NewBlkHasZeroIDs[BlkIdx];
+ SmallVector<stable_hash> CurInstH;
+ SmallVector<unsigned> CurTempIDs;
+ auto *OldInstH = (SkipUnchanged && BlkIdx < PrevInstH.size())
+ ? &PrevInstH[BlkIdx]
+ : nullptr;
+ auto *OldTempIDs = (SkipUnchanged && BlkIdx < PrevTempIDs.size())
+ ? &PrevTempIDs[BlkIdx]
+ : nullptr;
+ // Marks which previous-pass temp IDs have already been matched
+ // to a current-pass instruction; prevents one previous ID from
+ // being claimed by two current instructions.
+ SmallVector<bool> UsedOldTempIDs;
+ if (OldTempIDs)
+ UsedOldTempIDs.assign(OldTempIDs->size(), false);
+ unsigned InstSeq = 0;
+ unsigned InstIdx = 0;
+ for (Instruction &I : const_cast<BasicBlock &>(BB)) {
+ stable_hash CurH = hashInstruction(I);
+ if (NeedFallback)
+ CurInstH.push_back(CurH);
+ const DILocation *Loc =
+ I.getDebugLoc() ? I.getDebugLoc().get() : nullptr;
+ unsigned CurID = getOrCreateTrackerID(Loc);
+ // Zero-loc instruction: try to inherit a previous-pass temp
+ // ID so the same logical phi/LCSSA insert keeps a stable
+ // identity across passes. Two-stage match:
+ // (a) Fast path: same position, unused, matching hash.
+ // (b) Slow path: nearest unused position with matching
+ // hash; reject ties to avoid guessing.
+ // Fall back to allocating a fresh tracker ID if no match.
+ if (CurID == 0) {
+ int MatchedIdx = -1;
+ if (OldTempIDs && OldInstH) {
+ if (InstIdx < OldTempIDs->size() && InstIdx < OldInstH->size() &&
+ (*OldTempIDs)[InstIdx] != 0 && !UsedOldTempIDs[InstIdx] &&
+ (*OldInstH)[InstIdx] == CurH) {
+ MatchedIdx = InstIdx;
+ } else {
+ int BestIdx = -1;
+ int BestDist = std::numeric_limits<int>::max();
+ bool AmbiguousBest = false;
+ for (size_t J = 0,
+ E = std::min(OldTempIDs->size(), OldInstH->size());
+ J != E; ++J) {
+ if ((*OldTempIDs)[J] == 0 || UsedOldTempIDs[J] ||
+ (*OldInstH)[J] != CurH)
+ continue;
+ int Dist =
+ std::abs(static_cast<int>(J) - static_cast<int>(InstIdx));
+ if (Dist < BestDist) {
+ BestDist = Dist;
+ BestIdx = static_cast<int>(J);
+ AmbiguousBest = false;
+ } else if (Dist == BestDist) {
+ AmbiguousBest = true;
+ }
+ }
+ if (BestIdx >= 0 && !AmbiguousBest)
+ MatchedIdx = BestIdx;
+ }
+ }
+ if (MatchedIdx >= 0) {
+ CurID = (*OldTempIDs)[MatchedIdx];
+ UsedOldTempIDs[MatchedIdx] = true;
+ } else {
+ CurID = NextTrackerID++;
+ }
+ CurTempIDs.push_back(CurID);
+ } else if (NeedFallback) {
+ CurTempIDs.push_back(0);
+ }
+ bool InstChanged = true;
+ if (CurID != 0) {
+ auto It = TrackerIDToPrevHash.find(CurID);
+ InstChanged = It == TrackerIDToPrevHash.end() || It->second != CurH;
+ } else {
+ InstChanged = !OldInstH || InstIdx >= OldInstH->size() ||
+ (*OldInstH)[InstIdx] != CurH;
+ }
+
+ if (InstChanged) {
+ InstBuf.clear();
+ raw_svector_ostream IOS(InstBuf);
+ printInstructionText(IOS, I, CurID);
+
+ if (CurID != 0)
+ writeTrackerRecord(CurID, Loc);
+
+ *OS << "I\t" << FunctionName << '\t' << BBLabel << '\t' << InstSeq
+ << '\t' << I.getOpcodeName() << '\t' << CurID << '\t' << InstBuf
+ << '\n';
+ }
+ if (CurID != 0)
+ TrackerIDToPrevHash[CurID] = CurH;
+ ++InstSeq;
+ ++InstIdx;
+ }
+ if (NeedFallback) {
+ // Per-block writeback (only for blocks with zero-loc
+ // instructions). The next pass will read PrevInstH and
+ // PrevTempIDs to drive the temp-ID matching heuristic.
+ // Resize on demand to handle functions whose block count
+ // grew since the last pass.
+ if (BlkIdx >= PrevInstH.size())
+ PrevInstH.resize(BlkIdx + 1);
+ PrevInstH[BlkIdx] = std::move(CurInstH);
+ if (BlkIdx >= PrevTempIDs.size())
+ PrevTempIDs.resize(BlkIdx + 1);
+ PrevTempIDs[BlkIdx] = std::move(CurTempIDs);
+ }
+ }
+ ++BlkIdx;
+ }
+ // Per-function writeback. PrevBlkH / PrevBlkHasZeroIDs
+ // are references into the per-function DenseMaps grabbed at the top
+ // of writeInstructionsInFunction; moving into them
+ // mutates the maps directly, so the next pass on this function sees
+ // the fresh state.
+ PrevBlkHasZeroIDs = std::move(NewBlkHasZeroIDs);
+ PrevBlkH = std::move(NewBlkH);
+ }
+
+ /// Emit one P row for the pass and dispatch the per-function recording
+ /// work to writeInstructionsInFunction for every function reachable
+ /// from the IR unit.
+ ///
+ /// The new pass manager passes IR through a type-erased Any container
+ /// that may hold a Module*, Function*, LazyCallGraph::SCC*, or Loop*.
+ /// We unwrap to whichever one applies and walk it accordingly.
+ ///
+ /// Loops are recorded at function granularity rather than at loop
+ /// granularity because writeInstructionsInFunction's change-detection
+ /// state is keyed on the function.
+ ///
+ /// Example. Suppose the pipeline contains a Module pass MP and a
+ /// Function pass FP, and the module has functions foo, bar, baz:
+ ///
+ /// writeIR(IR=Module, ...) for MP
+ /// -> P row, then writeInstructionsInFunction(foo / bar / baz)
+ /// writeIR(IR=Function foo, ...) for FP
+ /// -> P row, then writeInstructionsInFunction(foo)
+ void writeIR(Any IR, unsigned Seq, StringRef Phase, StringRef PassName,
+ StringRef IRUnit, bool SkipUnchanged) {
+ writePassRecord(Seq, Phase, PassName, IRUnit);
+ if (const auto *M = unwrapIR<Module>(IR)) {
+ for (const Function &F : *M)
+ writeInstructionsInFunction(F, SkipUnchanged);
+ return;
+ }
+ if (const auto *F = unwrapIR<Function>(IR)) {
+ writeInstructionsInFunction(*F, SkipUnchanged);
+ return;
+ }
+ if (const auto *C = unwrapIR<LazyCallGraph::SCC>(IR)) {
+ for (const LazyCallGraph::Node &N : *C)
+ writeInstructionsInFunction(N.getFunction(), SkipUnchanged);
+ return;
+ }
+ if (const auto *L = unwrapIR<Loop>(IR))
+ writeInstructionsInFunction(*L->getHeader()->getParent(), SkipUnchanged);
+ }
+
+ /// Return true iff every function in the given IR unit has already
+ /// been seen and recorded at least once (i.e., has an entry in
+ /// FunctionHashes). Used by afterPass for the C4 short-circuit:
+ ///
+ /// if (PA.areAllPreserved() && allFunctionsKnown(IR))
+ /// write a P row and return; skip the per-function walk.
+ ///
+ /// PreservedAnalyses::areAllPreserved() means the pass declared it
+ /// preserved everything (typically: did not transform the IR). When
+ /// combined with allFunctionsKnown, we have a proof that there is
+ /// nothing for the recorder to capture this pass.
+ ///
+ /// The allFunctionsKnown gate is what prevents the short-circuit from
+ /// silently dropping the very first encounter of a function: until the
+ /// first writeInstructionsInFunction call has populated FunctionHashes
+ /// for F, we cannot skip a pass on F even if PA says nothing changed,
+ /// because we have no recorded baseline yet.
+ bool allFunctionsKnown(Any IR) {
+ if (const auto *M = unwrapIR<Module>(IR)) {
+ for (const Function &F : *M)
+ if (!F.isDeclaration() && !FunctionHashes.count(&F))
+ return false;
+ return true;
+ }
+ if (const auto *F = unwrapIR<Function>(IR))
+ return F->isDeclaration() || FunctionHashes.count(F);
+ if (const auto *C = unwrapIR<LazyCallGraph::SCC>(IR)) {
+ for (const LazyCallGraph::Node &N : *C)
+ if (!FunctionHashes.count(&N.getFunction()))
+ return false;
+ return true;
+ }
+ if (const auto *L = unwrapIR<Loop>(IR))
+ return FunctionHashes.count(L->getHeader()->getParent());
+ return false;
+ }
+
+public:
+ /// Open the TSV output file. report_fatal_error if the path is not
+ /// writable -- there is nothing useful the recorder can do without
+ /// a working output sink.
+ explicit IRTrackerRecorder(StringRef Path) {
+ std::error_code EC;
+ OS = std::make_unique<raw_fd_ostream>(Path, EC, sys::fs::OF_Text);
+ if (EC)
+ report_fatal_error(Twine("ir-tracker output open: ") + EC.message());
+ }
+
+ // Emit one synthetic "final" pass record on teardown covering every
+ // function of the last-seen module with ``SkipUnchanged=false`` so the
+ // post-optimization IR is fully materialized in the DB. The pass
+ // manager's ``PassInstrumentationCallbacks`` (which owns the shared_ptr
+ // holding this object) is destroyed before the module in the standard
+ // opt/clang flow, so ``LastModule`` is still live at this point. The
+ // cache reset is required because writeInstructionsInFunction also
+ // short-circuits per-instruction on matching ``TrackerIDToPrevHash``
+ // even when ``SkipUnchanged`` is false; without the reset the final
+ // record would only contain whatever changed since the last pass.
+ ~IRTrackerRecorder() {
+ if (!LastModule)
+ return;
+ FunctionHashes.clear();
+ BlockHashes.clear();
+ BlockHasZeroIDs.clear();
+ BlockInstHashes.clear();
+ BlockTempIDs.clear();
+ TrackerIDToPrevHash.clear();
+ writePassRecord(NextSeq++, "final", "<final>", "[module]");
+ for (const Function &F : *LastModule)
+ writeInstructionsInFunction(F, /*SkipUnchanged=*/false);
+ }
+
+ // Before any other beforePass work runs, ensure the module containing
+ // this IR unit has DILocations on every instruction. No-op if the
+ // module already has real debug info or if we have already synthesized
+ // for this module.
+ void ensureSyntheticLocs(Any IR) {
+ Module *M = nullptr;
+ if (const auto *MM = unwrapIR<Module>(IR))
+ M = const_cast<Module *>(MM);
+ else if (const auto *F = unwrapIR<Function>(IR))
+ M = const_cast<Module *>(F->getParent());
+ else if (const auto *C = unwrapIR<LazyCallGraph::SCC>(IR)) {
+ if (C->begin() != C->end())
+ M = const_cast<Module *>(C->begin()->getFunction().getParent());
+ } else if (const auto *L = unwrapIR<Loop>(IR))
+ M = const_cast<Module *>(
+ L->getHeader()->getParent()->getParent());
+ if (!M)
+ return;
+ if (!ModulesWithSynthesizedLocs.insert(M).second)
+ return;
+ synthesizeMissingInstructionLocs(*M);
+ }
+
+ /// Pre-pass callback. Skip wrapper passes and IR units filtered by
+ /// -filter-print-funcs, then ensure the enclosing module has DILocations
+ /// on every instruction (one-shot per module). On the very first
+ /// non-skipped invocation, emit the seq=0 "initial" snapshot so the
+ /// stream has a baseline against which subsequent per-pass diffs make
+ /// sense; subsequent invocations are no-ops.
+ void beforePass(StringRef PassID, Any IR) {
+ if (isIgnored(PassID) || !shouldPrintIR(IR))
+ return;
+ ensureSyntheticLocs(IR);
+ if (InitialCaptured)
+ return;
+ InitialCaptured = true;
+ writeIR(IR, 0, "initial", "<initial>", getIRName(IR),
+ /*SkipUnchanged=*/false);
+ }
+
+ /// Post-pass callback -- the per-pass workhorse. After the same two
+ /// filters as beforePass, cache the enclosing module so the destructor
+ /// can locate the final snapshot, resolve a friendly pass name, then
+ /// either (C4 short-circuit) emit a bare P row when the pass preserved
+ /// everything and we already have a baseline for every function, or
+ /// dispatch to writeIR with SkipUnchanged=true so only changed blocks
+ /// of changed functions show up as I rows.
+ void afterPass(StringRef PassID, Any IR, PassInstrumentationCallbacks &PIC,
+ const PreservedAnalyses &PA) {
+ if (isIgnored(PassID) || !shouldPrintIR(IR))
+ return;
+
+ // Track the enclosing module so the destructor can dump a final
+ // full-instruction snapshot regardless of the IR unit type this pass
+ // saw.
+ if (const auto *M = unwrapIR<Module>(IR))
+ LastModule = M;
+ else if (const auto *F = unwrapIR<Function>(IR))
+ LastModule = F->getParent();
+ else if (const auto *C = unwrapIR<LazyCallGraph::SCC>(IR)) {
+ if (C->begin() != C->end())
+ LastModule = C->begin()->getFunction().getParent();
+ } else if (const auto *L = unwrapIR<Loop>(IR))
+ LastModule = L->getHeader()->getParent()->getParent();
+
+ StringRef PassName = PIC.getPassNameForClassName(PassID);
+ if (PassName.empty())
+ PassName = PassID;
+
+ if (PA.areAllPreserved() && allFunctionsKnown(IR)) {
+ writePassRecord(NextSeq++, "after", PassName, getIRName(IR));
+ return;
+ }
+
+ writeIR(IR, NextSeq++, "after", PassName, getIRName(IR),
+ /*SkipUnchanged=*/true);
+ }
+};
+
+} // namespace
+
+void IRTrackerInstrumentation::registerCallbacks(
+ PassInstrumentationCallbacks &PIC) {
+ StringRef Path = IRTrackerOutput;
+ if (Path.empty())
+ return;
+
+ auto State = std::make_shared<IRTrackerRecorder>(Path);
+ PIC.registerBeforeNonSkippedPassCallback(
+ [State](StringRef PassID, Any IR) { State->beforePass(PassID, IR); });
+ PIC.registerAfterPassCallback(
+ [State, &PIC](StringRef PassID, Any IR, const PreservedAnalyses &PA) {
+ State->afterPass(PassID, IR, PIC, PA);
+ });
+}
diff --git a/llvm/lib/Passes/StandardInstrumentations.cpp b/llvm/lib/Passes/StandardInstrumentations.cpp
index 19e72a8612c4a..2de63d925b44e 100644
--- a/llvm/lib/Passes/StandardInstrumentations.cpp
+++ b/llvm/lib/Passes/StandardInstrumentations.cpp
@@ -2510,6 +2510,7 @@ void StandardInstrumentations::registerCallbacks(
PassInstrumentationCallbacks &PIC, ModuleAnalysisManager *MAM) {
PrintIR.registerCallbacks(PIC);
PrintPass.registerCallbacks(PIC);
+ IRTracker.registerCallbacks(PIC);
TimePasses.registerCallbacks(PIC);
OptNone.registerCallbacks(PIC);
OptPassGate.registerCallbacks(PIC);
diff --git a/llvm/test/Other/ir-tracker-db.ll b/llvm/test/Other/ir-tracker-db.ll
new file mode 100644
index 0000000000000..06c84be427745
--- /dev/null
+++ b/llvm/test/Other/ir-tracker-db.ll
@@ -0,0 +1,116 @@
+; RUN: opt -disable-output -passes=instcombine -ir-tracker-output=%t.tsv %s
+; RUN: FileCheck %s --input-file=%t.tsv --check-prefix=ALL
+; RUN: opt -disable-output -passes=instcombine -filter-print-funcs=f,h -ir-tracker-output=%t-cross.tsv %s
+; RUN: FileCheck %s --input-file=%t-cross.tsv --check-prefix=CROSS
+; RUN: opt -disable-output -passes=instcombine -filter-print-funcs=mid -ir-tracker-output=%t-mid.tsv %s
+; RUN: FileCheck %s --input-file=%t-mid.tsv --check-prefix=MID
+; RUN: opt -disable-output -passes=instcombine -filter-print-funcs=ssa -ir-tracker-output=%t-ssa.tsv %s
+; RUN: FileCheck %s --input-file=%t-ssa.tsv --check-prefix=SSA
+; RUN: opt -disable-output -passes=instcombine -filter-print-funcs=f -ir-tracker-output=%t-filter.tsv %s
+; RUN: FileCheck %s --input-file=%t-filter.tsv --check-prefix=FILTER
+
+define i32 @f(i32 %x) !dbg !6 {
+entry:
+ %add = add i32 %x, 1, !dbg !8
+ ret i32 %add, !dbg !9
+}
+
+define i32 @g(i32 %x) !dbg !7 {
+entry:
+ %mul = mul i32 %x, 2, !dbg !10
+ ret i32 %mul, !dbg !11
+}
+
+define i32 @h(i32 %x) !dbg !12 {
+entry:
+ %add = add i32 %x, 1, !dbg !13
+ ret i32 %add, !dbg !14
+}
+
+define i32 @mid(i32 %x) !dbg !16 {
+entry:
+ %a = freeze i32 %x, !dbg !17
+ %b = mul i32 %a, 2, !dbg !18
+ ret i32 %b, !dbg !19
+}
+
+define i32 @ssa(i32 %x) !dbg !20 {
+entry:
+ %a = add i32 %x, 1, !dbg !21
+ %b = add i32 %a, 0, !dbg !22
+ ret i32 %b, !dbg !25
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "ir-tracker-test", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "ir-tracker.c", directory: "/tmp")
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!4 = !DISubroutineType(types: !5)
+!5 = !{!3, !3}
+!6 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 7, type: !4, scopeLine: 7, spFlags: DISPFlagDefinition, unit: !0)
+!7 = distinct !DISubprogram(name: "g", scope: !1, file: !1, line: 13, type: !4, scopeLine: 13, spFlags: DISPFlagDefinition, unit: !0)
+!12 = distinct !DISubprogram(name: "h", scope: !15, file: !15, line: 7, type: !4, scopeLine: 7, spFlags: DISPFlagDefinition, unit: !0)
+!16 = distinct !DISubprogram(name: "mid", scope: !1, file: !1, line: 19, type: !4, scopeLine: 19, spFlags: DISPFlagDefinition, unit: !0)
+!20 = distinct !DISubprogram(name: "ssa", scope: !1, file: !1, line: 25, type: !4, scopeLine: 25, spFlags: DISPFlagDefinition, unit: !0)
+!8 = !DILocation(line: 8, column: 3, scope: !6)
+!9 = !DILocation(line: 9, column: 3, scope: !6)
+!10 = !DILocation(line: 14, column: 3, scope: !7)
+!11 = !DILocation(line: 15, column: 3, scope: !7)
+!13 = !DILocation(line: 8, column: 3, scope: !12)
+!14 = !DILocation(line: 9, column: 3, scope: !12)
+!17 = !DILocation(line: 20, column: 3, scope: !16)
+!18 = !DILocation(line: 21, column: 3, scope: !16)
+!19 = !DILocation(line: 22, column: 3, scope: !16)
+!21 = !DILocation(line: 26, column: 3, scope: !20)
+!22 = !DILocation(line: 27, column: 3, scope: !20)
+!25 = !DILocation(line: 30, column: 3, scope: !20)
+!15 = !DIFile(filename: "ir-tracker-other.c", directory: "/tmp")
+
+; Output is the cost-improvement TSV form: P/T/I rows. T rows carry source
+; locations once per tracker ID; I rows reference tracker IDs.
+
+; ALL: P{{ }}0{{ }}initial{{ }}<initial>{{ }}f
+; ALL-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}8{{ }}3
+; ALL-NEXT: I{{ }}f{{ }}entry{{ }}0{{ }}add{{ }}{{[0-9]+}}{{ }}%add = add i32 %x, 1
+; ALL-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}9{{ }}3
+; ALL-NEXT: I{{ }}f{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %add
+; ALL: P{{ }}1{{ }}after{{ }}instcombine{{ }}f
+; ALL: P{{ }}2{{ }}after{{ }}instcombine{{ }}g
+; ALL-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}14{{ }}3
+; ALL-NEXT: I{{ }}g{{ }}entry{{ }}0{{ }}shl{{ }}{{[0-9]+}}{{ }}%mul = shl i32 %x, 1
+; ALL-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}15{{ }}3
+; ALL-NEXT: I{{ }}g{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %mul
+; CROSS: P{{ }}0{{ }}initial{{ }}<initial>{{ }}f
+; CROSS-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}8{{ }}3
+; CROSS-NEXT: I{{ }}f{{ }}entry{{ }}0{{ }}add{{ }}{{[0-9]+}}{{ }}%add = add i32 %x, 1
+; CROSS-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}9{{ }}3
+; CROSS-NEXT: I{{ }}f{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %add
+; CROSS: P{{ }}2{{ }}after{{ }}instcombine{{ }}h
+; CROSS-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker-other.c{{ }}8{{ }}3
+; CROSS-NEXT: I{{ }}h{{ }}entry{{ }}0{{ }}add{{ }}{{[0-9]+}}{{ }}%add = add i32 %x, 1
+; CROSS-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker-other.c{{ }}9{{ }}3
+; CROSS-NEXT: I{{ }}h{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %add
+
+; MID: P{{ }}0{{ }}initial{{ }}<initial>{{ }}mid
+; MID-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}20{{ }}3
+; MID-NEXT: I{{ }}mid{{ }}entry{{ }}0{{ }}freeze{{ }}{{[0-9]+}}{{ }}%a = freeze i32 %x
+; MID-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}21{{ }}3
+; MID-NEXT: I{{ }}mid{{ }}entry{{ }}1{{ }}mul{{ }}{{[0-9]+}}{{ }}%b = mul i32 %a, 2
+; MID-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}22{{ }}3
+; MID-NEXT: I{{ }}mid{{ }}entry{{ }}2{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %b
+; MID: P{{ }}1{{ }}after{{ }}instcombine{{ }}mid
+; MID-NEXT: I{{ }}mid{{ }}entry{{ }}1{{ }}shl{{ }}{{[0-9]+}}{{ }}%b = shl i32 %a, 1
+
+; SSA: P{{ }}0{{ }}initial{{ }}<initial>{{ }}ssa
+; SSA: I{{ }}ssa{{ }}entry{{ }}2{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %b
+; SSA: P{{ }}1{{ }}after{{ }}instcombine{{ }}ssa
+; SSA-NEXT: I{{ }}ssa{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %a
+
+; FILTER: P{{ }}0{{ }}initial{{ }}<initial>{{ }}f
+; FILTER: I{{ }}f{{ }}entry{{ }}0{{ }}add
+; FILTER: I{{ }}f{{ }}entry{{ }}1{{ }}ret
+; FILTER-NOT: I{{ }}g{{ }}
+; FILTER-NOT: ir_unit{{[" :]+}}g
>From 0863e76f26c5cfd525d9ba2d6b90ef575cc24443 Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <yaxun.liu at amd.com>
Date: Mon, 27 Apr 2026 10:42:30 -0400
Subject: [PATCH 2/4] [Passes] Fix IR tracker branch printing after BranchInst
split
Use the non-deprecated conditional and unconditional branch instruction classes so the tracker builds cleanly with current LLVM warnings-as-errors.
---
llvm/lib/Passes/IRTrackerInstrumentation.cpp | 87 +++++++++++---------
1 file changed, 46 insertions(+), 41 deletions(-)
diff --git a/llvm/lib/Passes/IRTrackerInstrumentation.cpp b/llvm/lib/Passes/IRTrackerInstrumentation.cpp
index 179650c245823..c33470864d226 100644
--- a/llvm/lib/Passes/IRTrackerInstrumentation.cpp
+++ b/llvm/lib/Passes/IRTrackerInstrumentation.cpp
@@ -176,10 +176,10 @@ static void synthesizeMissingInstructionLocs(Module &M) {
DIBuilder DIB(M);
LLVMContext &Ctx = M.getContext();
DIFile *File = DIB.createFile("<ir-tracker-synthetic>", ".");
- DICompileUnit *CU = DIB.createCompileUnit(
- dwarf::DW_LANG_C, File, "ir-tracker",
- /*isOptimized=*/true, "", /*RV=*/0, /*SplitName=*/"",
- DICompileUnit::FullDebug);
+ DICompileUnit *CU =
+ DIB.createCompileUnit(dwarf::DW_LANG_C, File, "ir-tracker",
+ /*isOptimized=*/true, "", /*RV=*/0,
+ /*SplitName=*/"", DICompileUnit::FullDebug);
auto SPType = DIB.createSubroutineType(DIB.getOrCreateTypeArray({}));
unsigned NextOrdinal = 1;
@@ -194,8 +194,8 @@ static void synthesizeMissingInstructionLocs(Module &M) {
if (F.hasPrivateLinkage() || F.hasInternalLinkage())
SPFlags |= DISubprogram::SPFlagLocalToUnit;
DISubprogram *SP =
- DIB.createFunction(CU, F.getName(), F.getName(), File, FuncLine,
- SPType, FuncLine, DINode::FlagZero, SPFlags);
+ DIB.createFunction(CU, F.getName(), F.getName(), File, FuncLine, SPType,
+ FuncLine, DINode::FlagZero, SPFlags);
F.setSubprogram(SP);
for (BasicBlock &BB : F) {
@@ -236,26 +236,31 @@ static stable_hash hashValueIdentity(const Value &V) {
if (const auto *Arg = dyn_cast<Argument>(&V))
return stable_hash_combine(static_cast<stable_hash>(1), Arg->getArgNo());
if (const auto *GV = dyn_cast<GlobalValue>(&V))
- return stable_hash_combine(static_cast<stable_hash>(2),
- static_cast<stable_hash>(hash_value(GV->getName())));
+ return stable_hash_combine(
+ static_cast<stable_hash>(2),
+ static_cast<stable_hash>(hash_value(GV->getName())));
if (const auto *BB = dyn_cast<BasicBlock>(&V)) {
if (BB->hasName())
- return stable_hash_combine(static_cast<stable_hash>(3),
- static_cast<stable_hash>(hash_value(BB->getName())));
+ return stable_hash_combine(
+ static_cast<stable_hash>(3),
+ static_cast<stable_hash>(hash_value(BB->getName())));
return 0;
}
if (const auto *I = dyn_cast<Instruction>(&V)) {
- if (const DILocation *Loc = I->getDebugLoc() ? I->getDebugLoc().get() : nullptr)
+ if (const DILocation *Loc =
+ I->getDebugLoc() ? I->getDebugLoc().get() : nullptr)
return stable_hash_combine(static_cast<stable_hash>(4),
hashTrackerIdentity(Loc));
if (I->hasName())
- return stable_hash_combine(static_cast<stable_hash>(5),
- static_cast<stable_hash>(hash_value(I->getName())));
+ return stable_hash_combine(
+ static_cast<stable_hash>(5),
+ static_cast<stable_hash>(hash_value(I->getName())));
return 0;
}
if (V.hasName())
- return stable_hash_combine(static_cast<stable_hash>(6),
- static_cast<stable_hash>(hash_value(V.getName())));
+ return stable_hash_combine(
+ static_cast<stable_hash>(6),
+ static_cast<stable_hash>(hash_value(V.getName())));
return 0;
}
@@ -293,14 +298,14 @@ static stable_hash hashInstruction(const Instruction &I) {
for (const Use &U : I.operands()) {
Value *V = U.get();
H = stable_hash_combine(
- H, stable_hash_combine(
- static_cast<stable_hash>(V->getType()->getTypeID()),
- static_cast<stable_hash>(isa<Constant>(V) ? 1 : 0),
- static_cast<stable_hash>(isa<Argument>(V) ? 1 : 0),
- hashValueIdentity(*V)));
+ H,
+ stable_hash_combine(static_cast<stable_hash>(V->getType()->getTypeID()),
+ static_cast<stable_hash>(isa<Constant>(V) ? 1 : 0),
+ static_cast<stable_hash>(isa<Argument>(V) ? 1 : 0),
+ hashValueIdentity(*V)));
if (auto *C = dyn_cast<ConstantInt>(V))
- H = stable_hash_combine(H,
- static_cast<stable_hash>(hash_value(C->getValue())));
+ H = stable_hash_combine(
+ H, static_cast<stable_hash>(hash_value(C->getValue())));
}
if (I.isCommutative())
H = stable_hash_combine(H, 1);
@@ -348,9 +353,9 @@ static stable_hash hashInstruction(const Instruction &I) {
static stable_hash hashTrackerIdentity(const DILocation *Loc) {
if (!Loc)
return 0;
- stable_hash FileKey =
- stable_hash_combine(static_cast<stable_hash>(hash_value(Loc->getDirectory())),
- static_cast<stable_hash>(hash_value(Loc->getFilename())));
+ stable_hash FileKey = stable_hash_combine(
+ static_cast<stable_hash>(hash_value(Loc->getDirectory())),
+ static_cast<stable_hash>(hash_value(Loc->getFilename())));
unsigned ScopeLine = 0;
if (DISubprogram *SP = Loc->getScope()->getSubprogram())
ScopeLine = SP->getLine();
@@ -495,8 +500,8 @@ class IRTrackerRecorder {
std::string FilePath = Loc ? getIRTrackerFilePath(Loc) : "<synthetic>";
unsigned LineN = Loc ? Loc->getLine() : 0;
unsigned ColN = Loc ? Loc->getColumn() : 0;
- *OS << "T\t" << ID << '\t' << FilePath << '\t' << LineN << '\t'
- << ColN << '\n';
+ *OS << "T\t" << ID << '\t' << FilePath << '\t' << LineN << '\t' << ColN
+ << '\n';
}
/// Look up (or assign) the tracker ID for a source location.
@@ -755,18 +760,19 @@ class IRTrackerRecorder {
return;
}
- if (const auto *BI = dyn_cast<BranchInst>(&I)) {
- if (BI->isUnconditional()) {
- OS << ' ';
- writeValueRef(OS, BI->getSuccessor(0));
- } else {
- OS << ' ';
- writeValueRef(OS, BI->getCondition());
- OS << ", ";
- writeValueRef(OS, BI->getSuccessor(0));
- OS << ", ";
- writeValueRef(OS, BI->getSuccessor(1));
- }
+ if (const auto *BI = dyn_cast<UncondBrInst>(&I)) {
+ OS << ' ';
+ writeValueRef(OS, BI->getSuccessor(0));
+ return;
+ }
+
+ if (const auto *BI = dyn_cast<CondBrInst>(&I)) {
+ OS << ' ';
+ writeValueRef(OS, BI->getCondition());
+ OS << ", ";
+ writeValueRef(OS, BI->getSuccessor(0));
+ OS << ", ";
+ writeValueRef(OS, BI->getSuccessor(1));
return;
}
@@ -1097,8 +1103,7 @@ class IRTrackerRecorder {
if (C->begin() != C->end())
M = const_cast<Module *>(C->begin()->getFunction().getParent());
} else if (const auto *L = unwrapIR<Loop>(IR))
- M = const_cast<Module *>(
- L->getHeader()->getParent()->getParent());
+ M = const_cast<Module *>(L->getHeader()->getParent()->getParent());
if (!M)
return;
if (!ModulesWithSynthesizedLocs.insert(M).second)
>From 53f115fd1dc54d41d656cdb64d8b92855476e8db Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <yaxun.liu at amd.com>
Date: Sat, 25 Apr 2026 00:25:11 -0400
Subject: [PATCH 3/4] [IR tracker] Add TSV query tool, docs, and HTML report
Add the Python ir-tracker driver and install target, with subcommands to
build a SQLite database from the recorder's TSV stream and query it with
passes, trace, show, and sql.
Add the TSV database importer and function-centric HTML report generator.
The HTML report writes a static index, stylesheet, and per-function pages
showing initial IR, final IR when available, and pass history for selected
tracked locations.
Document recording, database construction, source-location attribution, and
HTML generation, and wire %ir-tracker into lit with focused coverage for TSV
build, help, show, and HTML output.
---
llvm/docs/IRTracker.rst | 244 ++++++
llvm/docs/UserGuides.rst | 1 +
llvm/test/lit.cfg.py | 6 +
.../test/tools/llvm-ir-tracker/build-tsv.test | 12 +
llvm/test/tools/llvm-ir-tracker/help.test | 17 +
llvm/test/tools/llvm-ir-tracker/html.ll | 42 ++
llvm/test/tools/llvm-ir-tracker/show.ll | 52 ++
llvm/tools/ir-tracker/CMakeLists.txt | 18 +
llvm/tools/ir-tracker/ir-tracker.py | 164 ++++
llvm/tools/ir-tracker/irtrackdb.py | 512 +++++++++++++
llvm/tools/ir-tracker/irtrackhtml.py | 698 ++++++++++++++++++
11 files changed, 1766 insertions(+)
create mode 100644 llvm/docs/IRTracker.rst
create mode 100644 llvm/test/tools/llvm-ir-tracker/build-tsv.test
create mode 100644 llvm/test/tools/llvm-ir-tracker/help.test
create mode 100644 llvm/test/tools/llvm-ir-tracker/html.ll
create mode 100644 llvm/test/tools/llvm-ir-tracker/show.ll
create mode 100644 llvm/tools/ir-tracker/CMakeLists.txt
create mode 100644 llvm/tools/ir-tracker/ir-tracker.py
create mode 100644 llvm/tools/ir-tracker/irtrackdb.py
create mode 100644 llvm/tools/ir-tracker/irtrackhtml.py
diff --git a/llvm/docs/IRTracker.rst b/llvm/docs/IRTracker.rst
new file mode 100644
index 0000000000000..c14613680c995
--- /dev/null
+++ b/llvm/docs/IRTracker.rst
@@ -0,0 +1,244 @@
+IR Tracker (IR TSV + SQLite query DB)
+=====================================
+
+.. contents::
+ :local:
+
+Overview
+========
+
+The **IR tracker** records how LLVM IR evolves through the new pass manager into a
+compact tab-separated file. A small Python tool can then post-process that TSV
+output into a SQLite database for indexed queries. Each instruction row is tied
+to a tracker ID, which maps to a ``DILocation`` when one exists. If an input
+module has no debug information, the recorder synthesizes locations first so the
+tool can still track IR evolution through the pipeline.
+
+Real source locations are only needed when you want to map tracked instructions
+back to the original source file and line. For that workflow, compile with
+``-g`` so the input IR carries source ``!dbg`` attachments.
+
+Recording is enabled with the hidden LLVM option
+``-ir-tracker-output=/absolute/path.tsv``. The hooks live in
+``StandardInstrumentations`` and therefore apply to any tool that runs the new
+pass manager with that instrumentation (``opt``, ``clang``, etc.).
+
+The stream uses ``P`` rows for pass snapshots, ``T`` rows for tracker-ID source
+locations, and ``I`` rows for instruction snapshots.
+
+Recording with ``opt``
+======================
+
+.. code-block:: bash
+
+ opt -disable-output -passes='default<O2>' \
+ -ir-tracker-output=/tmp/pipeline.tsv input.ll
+
+Use an absolute output path. The input IR does not need debug info for
+pass-pipeline tracking; the recorder will synthesize locations for instructions
+that need them. If you want queries to refer to the original source file and
+line, produce the input IR with ``-g``.
+
+Recording with ``clang``
+========================
+
+Forward the option through Clang with ``-mllvm`` so the middle-end sees the same
+flag as ``opt``:
+
+.. code-block:: bash
+
+ clang -O1 -emit-llvm -S -g sum.c -o sum.ll \
+ -mllvm -ir-tracker-output=/tmp/pipeline.tsv
+
+Here ``-g`` is optional for tracking the IR itself, but it preserves source
+locations so later ``trace`` / ``show`` queries can use source file and line
+numbers. ``-O1`` (or another ``-O`` level) selects the usual optimization
+pipeline that ``opt`` would run for that tier.
+
+SQLite build step
+=================
+
+The Python driver can convert the TSV output into a SQLite database:
+
+.. code-block:: bash
+
+ python3 llvm/tools/ir-tracker/ir-tracker.py build \
+ --input /tmp/pipeline.tsv --db /tmp/pipeline.db
+
+The resulting database uses ``schema_version = 1`` in ``ir_tracker_meta``. The
+main tables are:
+
+* ``ir_tracker_meta`` — key/value metadata (including ``schema_version``)
+* ``ir_tracker_files`` — deduplicated paths from ``DIFile`` (often a basename
+ such as ``sum.c``)
+* ``ir_tracker_passes`` — one row per snapshot: ``seq``, ``phase`` (``initial``
+ or ``after``), ``pass_class``, ``ir_unit``
+* ``ir_tracker_instructions`` — instruction text and opcode per pass, keyed by
+ ``file_id``, ``line``, ``col``
+
+Query tool
+==========
+
+The Python driver lives at ``llvm/tools/ir-tracker/ir-tracker.py`` (installed
+under ``<prefix>/share/ir-tracker/`` when the ``ir-tracker`` install component
+is enabled). It can build the SQLite DB from tracker TSV output and then query
+that DB. Subcommands:
+
+* ``build`` — convert tracker TSV output into a SQLite database
+* ``passes`` — list recorded passes in ``seq`` order
+* ``trace`` — summarize the first and last pass that still have instructions
+ matching a source location
+* ``show`` — print the instructions matching ``--file`` / ``--line`` (and
+ optional ``--col`` / ``--opcode``) across passes; by default only passes where
+ the printed IR **changed** are shown; use ``--all-passes`` for every pass, or
+ ``--seq N`` for one pass
+* ``html`` — generate a static HTML report with one page per function
+* ``sql`` — run a single read-only SQL statement
+
+The ``--file`` argument is matched against the path stored in
+``ir_tracker_files`` (substring match, case-insensitive). Clang usually records
+the ``DIFile`` basename, so prefer ``--file sum.c`` rather than a full host path.
+
+HTML report
+===========
+
+The ``html`` subcommand generates a static report directory from a built query
+database:
+
+.. code-block:: bash
+
+ python3 llvm/tools/ir-tracker/ir-tracker.py html \
+ --db /tmp/pipeline.db -o /tmp/pipeline-html
+
+The output contains ``index.html``, ``style.css``, and one ``fn-*.html`` page
+per function. Each function page has four panels:
+
+* a function list grouped by file
+* the initial IR snapshot for the selected function
+* the final IR snapshot, when the recorder emitted a ``phase='final'`` snapshot
+* pass history for the selected instruction location
+
+Clicking an instruction in the initial or final panel shows the pass-by-pass
+history for the same tracked location. Rows that share a tracked location are
+highlighted with the same background color across panels.
+
+Useful options:
+
+* ``--file TEXT`` — only emit pages for source paths containing ``TEXT``
+
+The current function-centric report is generated entirely from the database and
+does not read source files. ``--source-dir``, ``--all-passes``, and
+``--no-highlight`` are accepted for command-line consistency with related report
+generators, but they do not change this layout.
+
+Example: following one source line through ``clang -O1``
+========================================================
+
+Source file ``sum.c``:
+
+.. code-block:: c
+
+ /* Example: trivial fold (x + 0) -> x */
+ int bump(int x) {
+ return x + 0;
+ }
+
+Recording (same command as in *Recording with ``clang``*):
+
+.. code-block:: bash
+
+ clang -O1 -emit-llvm -S -g sum.c -o sum.ll \
+ -mllvm -ir-tracker-output=/tmp/pipeline.tsv
+
+Then build the query database:
+
+.. code-block:: bash
+
+ python3 llvm/tools/ir-tracker/ir-tracker.py build \
+ --input /tmp/pipeline.tsv --db /tmp/pipeline.db
+
+The following excerpts come from a real ``ir-tracker`` run against the database
+produced that way. **Pass names and sequence numbers depend on your Clang/LLVM
+version, target, and optimization level**; treat pass sequence numbers as
+illustrative, not a stable ABI.
+
+List passes (truncated):
+
+.. code-block:: text
+
+ 0 id=1 initial '<initial>' on '[module]'
+ 1 id=2 after 'memprof-remove-attributes' on '[module]'
+ 2 id=3 after 'annotation2metadata' on '[module]'
+ …
+ 10 id=11 after 'sroa' on 'bump'
+ 11 id=12 after 'early-cse' on 'bump'
+ …
+
+Trace line ``3`` (the ``return x + 0;`` line in ``sum.c``):
+
+.. code-block:: bash
+
+ python3 llvm/tools/ir-tracker/ir-tracker.py trace \
+ --db /tmp/pipeline.db --file sum.c --line 3
+
+.. code-block:: text
+
+ Matches at final pass (seq=94): 1 instruction(s)
+ First pass with any matching instruction: seq=0 <initial> on [module] (3 row(s))
+
+``show`` without ``--all-passes`` prints only passes where the matched IR text
+changed: here the load/add/return cluster simplifies until ``early-cse`` folds
+``x + 0`` to ``x``:
+
+.. code-block:: bash
+
+ python3 llvm/tools/ir-tracker/ir-tracker.py show \
+ --db /tmp/pipeline.db --file sum.c --line 3
+
+.. code-block:: text
+
+ seq=0 '<initial>' on '[module]'
+ function bump, block entry:
+ %0 = load i32, ptr %x.addr, align 4
+ %add = add nsw i32 %0, 0
+ ret i32 %add
+ seq=10 'sroa' on 'bump'
+ function bump, block entry:
+ %add = add nsw i32 %x, 0
+ ret i32 %add
+ seq=11 'early-cse' on 'bump'
+ function bump, block entry:
+ ret i32 %x
+
+The initial snapshot for the same line (``--seq 0``) recovers the unoptimized
+cluster before any pass runs:
+
+.. code-block:: bash
+
+ python3 llvm/tools/ir-tracker/ir-tracker.py show \
+ --db /tmp/pipeline.db --file sum.c --line 3 --seq 0
+
+.. code-block:: text
+
+ seq=0 '<initial>' on '[module]'
+ function bump, block entry:
+ %0 = load i32, ptr %x.addr, align 4
+ %add = add nsw i32 %0, 0
+ ret i32 %add
+
+Tests
+=====
+
+* Recorder: ``llvm/test/Other/ir-tracker-db.ll``
+* Query tool: ``llvm/test/tools/llvm-ir-tracker/``
+
+Limitations
+===========
+
+* **IR only** — there is no MIR, object, or assembly capture in this schema.
+* **Source attribution needs source locations** — the tracker can follow IR
+ evolution without debug info by synthesizing locations, but those synthetic
+ locations do not identify original source files and lines.
+* **Locations are keys, not proofs** — optimizations can merge, clone, or drop
+ instructions; the database lists what survived each pass with a given
+ location, not a formal def-use proof.
diff --git a/llvm/docs/UserGuides.rst b/llvm/docs/UserGuides.rst
index f33ddcabc8d89..5294e0d4ba0b1 100644
--- a/llvm/docs/UserGuides.rst
+++ b/llvm/docs/UserGuides.rst
@@ -49,6 +49,7 @@ intermediate LLVM representation.
InstCombineContributorGuide
InstrProfileFormat
InstrRefDebugInfo
+ IRTracker
KeyInstructionsDebugInfo
LFI
LinkTimeOptimization
diff --git a/llvm/test/lit.cfg.py b/llvm/test/lit.cfg.py
index af704d67b91f2..3c8aa494aa381 100644
--- a/llvm/test/lit.cfg.py
+++ b/llvm/test/lit.cfg.py
@@ -214,6 +214,11 @@ def get_asan_rtlib():
config.llvm_src_root,
)
+ir_tracker_cmd = "%s %s/tools/ir-tracker/ir-tracker.py" % (
+ sys.executable,
+ config.llvm_src_root,
+)
+
llvm_original_di_preservation_cmd = os.path.join(
config.llvm_src_root, "utils", "llvm-original-di-preservation.py"
)
@@ -239,6 +244,7 @@ def get_asan_rtlib():
ToolSubst("%ocamlc", ocamlc_command, unresolved="ignore"),
ToolSubst("%ocamlopt", ocamlopt_command, unresolved="ignore"),
ToolSubst("%opt-viewer", opt_viewer_cmd),
+ ToolSubst("%ir-tracker", ir_tracker_cmd),
ToolSubst("%llvm-objcopy", FindTool("llvm-objcopy")),
ToolSubst("%llvm-strip", FindTool("llvm-strip")),
ToolSubst("%llvm-install-name-tool", FindTool("llvm-install-name-tool")),
diff --git a/llvm/test/tools/llvm-ir-tracker/build-tsv.test b/llvm/test/tools/llvm-ir-tracker/build-tsv.test
new file mode 100644
index 0000000000000..60cd78e807813
--- /dev/null
+++ b/llvm/test/tools/llvm-ir-tracker/build-tsv.test
@@ -0,0 +1,12 @@
+RUN: rm -f %t.tsv %t.db
+RUN: printf 'P\t0\tinitial\t<initial>\tf\nT\t1\t/tmp/tsv.c\t8\t3\nI\tf\tentry\t0\tadd\t1\tadd i32 1, 2\nP\t1\tafter\tinstcombine\tf\nI\tf\tentry\t0\tadd\t1\tadd i32 1, 3\n' > %t.tsv
+RUN: %ir-tracker build --input %t.tsv --db %t.db | FileCheck %s --check-prefix=BUILD
+RUN: %ir-tracker passes --db %t.db | FileCheck %s --check-prefix=PASSES
+RUN: %ir-tracker trace --db %t.db --file tsv.c --line 8 | FileCheck %s --check-prefix=TRACE
+
+BUILD: built {{.*}} pass snapshots, 2 instruction rows
+PASSES: 0 id={{[0-9]+}} initial '<initial>' on 'f'
+PASSES: 1 id={{[0-9]+}} after 'instcombine' on 'f'
+PASSES: total passes recorded: 2
+TRACE: Matches at final pass (seq=1): 1 instruction(s)
+TRACE: First pass with any matching instruction: seq=0 <initial> on f (1 row(s))
diff --git a/llvm/test/tools/llvm-ir-tracker/help.test b/llvm/test/tools/llvm-ir-tracker/help.test
new file mode 100644
index 0000000000000..6e4f7389e1ace
--- /dev/null
+++ b/llvm/test/tools/llvm-ir-tracker/help.test
@@ -0,0 +1,17 @@
+RUN: %ir-tracker build --help | FileCheck %s --check-prefix=BUILD
+RUN: %ir-tracker --help | FileCheck %s --check-prefix=MAIN
+RUN: %ir-tracker passes --help | FileCheck %s --check-prefix=PASSES
+RUN: %ir-tracker trace --help | FileCheck %s --check-prefix=TRACE
+RUN: %ir-tracker show --help | FileCheck %s --check-prefix=SHOW
+RUN: %ir-tracker html --help | FileCheck %s --check-prefix=HTML
+RUN: %ir-tracker sql --help | FileCheck %s --check-prefix=SQL
+
+MAIN: Build and query IR-tracker SQLite databases
+MAIN: {build,passes,trace,show,html,sql}
+
+BUILD: usage: ir-tracker build
+PASSES: usage: ir-tracker passes
+TRACE: usage: ir-tracker trace
+SHOW: usage: ir-tracker show
+HTML: usage: ir-tracker html
+SQL: usage: ir-tracker sql
diff --git a/llvm/test/tools/llvm-ir-tracker/html.ll b/llvm/test/tools/llvm-ir-tracker/html.ll
new file mode 100644
index 0000000000000..4d84b51eca6a6
--- /dev/null
+++ b/llvm/test/tools/llvm-ir-tracker/html.ll
@@ -0,0 +1,42 @@
+; RUN: rm -f %t.tsv %t.db
+; RUN: printf 'P\t0\tinitial\t<initial>\tf\nT\t1\t/tmp/show.c\t8\t3\nI\tf\tentry\t0\tadd\t1\tadd i32 1, 2\nT\t2\t/tmp/show.c\t9\t3\nI\tf\tentry\t1\tret\t2\tret i32 3\nP\t1\tafter\tinstcombine\tf\nI\tf\tentry\t0\tadd\t1\tadd i32 1, 3\n' > %t.tsv
+; RUN: %ir-tracker build --input %t.tsv --db %t.db
+; RUN: rm -rf %t.html
+; RUN: %ir-tracker html --db %t.db -o %t.html --no-highlight | FileCheck %s --check-prefix=LOG
+; RUN: ls %t.html | FileCheck %s --check-prefix=FILES
+; RUN: FileCheck %s --check-prefix=INDEX --input-file=%t.html/index.html
+; RUN: FileCheck %s --check-prefix=PAGE --input-file=%t.html/fn-f.html
+
+define i32 @f(i32 %x) !dbg !6 {
+entry:
+ %a = add i32 %x, 1, !dbg !8
+ ret i32 %a, !dbg !9
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "ir-tracker-test", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "show.c", directory: "/tmp")
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!4 = !DISubroutineType(types: !5)
+!5 = !{!3, !3}
+!6 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 7, type: !4, scopeLine: 7, spFlags: DISPFlagDefinition, unit: !0)
+!8 = !DILocation(line: 8, column: 3, scope: !6)
+!9 = !DILocation(line: 9, column: 3, scope: !6)
+
+; LOG: ir-tracker: wrote 1 function page(s) + index
+
+; FILES-DAG: index.html
+; FILES-DAG: fn-f.html
+; FILES-DAG: style.css
+
+; INDEX: ir-tracker report
+; INDEX: show.c
+; INDEX: fn-f.html
+
+; PAGE: <title>f</title>
+; PAGE: seq=0
+; PAGE: data-loc="/tmp/show.c|8|3"
+; PAGE: add i32 1, 2
diff --git a/llvm/test/tools/llvm-ir-tracker/show.ll b/llvm/test/tools/llvm-ir-tracker/show.ll
new file mode 100644
index 0000000000000..c928a664e8062
--- /dev/null
+++ b/llvm/test/tools/llvm-ir-tracker/show.ll
@@ -0,0 +1,52 @@
+; RUN: rm -f %t.tsv %t.db
+; RUN: printf 'P\t0\tinitial\t<initial>\tf\nT\t1\t/tmp/show.c\t8\t3\nI\tf\tentry\t0\tadd\t1\tadd i32 1, 2\nP\t1\tafter\tinstcombine\tf\nI\tf\tentry\t0\tadd\t1\tadd i32 1, 3\n' > %t.tsv
+; RUN: %ir-tracker build --input %t.tsv --db %t.db
+; RUN: %ir-tracker passes --db %t.db | FileCheck %s --check-prefix=PASSES
+; RUN: %ir-tracker trace --db %t.db --file show.c --line 8 | FileCheck %s --check-prefix=TRACE
+; RUN: %ir-tracker show --db %t.db --file show.c --line 8 --seq 0 | FileCheck %s --check-prefix=SEQ0
+; RUN: %ir-tracker show --db %t.db --file show.c --line 8 | FileCheck %s --check-prefix=CHANGED
+; RUN: %ir-tracker show --db %t.db --file show.c --line 8 --all-passes | FileCheck %s --check-prefix=ALL
+
+define i32 @f(i32 %x) !dbg !6 {
+entry:
+ %a = add i32 %x, 1, !dbg !8
+ ret i32 %a, !dbg !9
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "ir-tracker-test", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "show.c", directory: "/tmp")
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!4 = !DISubroutineType(types: !5)
+!5 = !{!3, !3}
+!6 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 7, type: !4, scopeLine: 7, spFlags: DISPFlagDefinition, unit: !0)
+!8 = !DILocation(line: 8, column: 3, scope: !6)
+!9 = !DILocation(line: 9, column: 3, scope: !6)
+
+; PASSES: 0 id={{[0-9]+}} initial '<initial>' on 'f'
+; PASSES: 1 id={{[0-9]+}} after 'instcombine' on 'f'
+; PASSES: total passes recorded: 2
+
+; TRACE: Matches at final pass (seq=1): 1 instruction(s)
+; TRACE: First pass with any matching instruction: seq=0 <initial> on f (1 row(s))
+
+; SEQ0: seq=0 '<initial>' on 'f'
+; SEQ0-NEXT: function f, block entry:
+; SEQ0-NEXT: add i32 1, 2
+
+; CHANGED: seq=0 '<initial>' on 'f'
+; CHANGED-NEXT: function f, block entry:
+; CHANGED-NEXT: add i32 1, 2
+; CHANGED: seq=1 'instcombine' on 'f'
+; CHANGED-NEXT: function f, block entry:
+; CHANGED-NEXT: add i32 1, 3
+
+; ALL: seq=0 '<initial>' on 'f'
+; ALL-NEXT: function f, block entry:
+; ALL-NEXT: add i32 1, 2
+; ALL: seq=1 'instcombine' on 'f'
+; ALL-NEXT: function f, block entry:
+; ALL-NEXT: add i32 1, 3
diff --git a/llvm/tools/ir-tracker/CMakeLists.txt b/llvm/tools/ir-tracker/CMakeLists.txt
new file mode 100644
index 0000000000000..a8a4efb65afac
--- /dev/null
+++ b/llvm/tools/ir-tracker/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(files
+ "ir-tracker.py"
+ "irtrackdb.py"
+ "irtrackhtml.py")
+
+foreach(file ${files})
+ install(PROGRAMS ${file}
+ DESTINATION "${CMAKE_INSTALL_DATADIR}/ir-tracker"
+ COMPONENT ir-tracker)
+endforeach(file)
+
+add_custom_target(ir-tracker DEPENDS ${files})
+set_target_properties(ir-tracker PROPERTIES FOLDER "LLVM/Tools")
+if(NOT LLVM_ENABLE_IDE)
+ add_llvm_install_targets("install-ir-tracker"
+ DEPENDS ir-tracker
+ COMPONENT ir-tracker)
+endif()
diff --git a/llvm/tools/ir-tracker/ir-tracker.py b/llvm/tools/ir-tracker/ir-tracker.py
new file mode 100644
index 0000000000000..6f0fdefa260fe
--- /dev/null
+++ b/llvm/tools/ir-tracker/ir-tracker.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""Build and query SQLite databases for llvm/tools/ir-tracker."""
+
+from __future__ import annotations
+
+import argparse
+import sys
+from typing import Optional, Sequence
+
+import irtrackdb
+import irtrackhtml
+
+
+def cmd_build(args: argparse.Namespace) -> int:
+ return irtrackdb.build_db(args.input, args.db)
+
+
+def cmd_passes(args: argparse.Namespace) -> int:
+ con = irtrackdb.open_db_readonly(args.db)
+ if not con:
+ return 1
+ try:
+ return irtrackdb.run_passes(con)
+ finally:
+ con.close()
+
+
+def cmd_trace(args: argparse.Namespace) -> int:
+ con = irtrackdb.open_db_readonly(args.db)
+ if not con:
+ return 1
+ try:
+ return irtrackdb.run_trace(
+ con, args.file, args.line, args.col, args.opcode or ""
+ )
+ finally:
+ con.close()
+
+
+def cmd_show(args: argparse.Namespace) -> int:
+ con = irtrackdb.open_db_readonly(args.db)
+ if not con:
+ return 1
+ try:
+ return irtrackdb.run_show(
+ con,
+ args.file,
+ args.line,
+ args.col,
+ args.opcode or "",
+ args.seq,
+ args.all_passes,
+ )
+ finally:
+ con.close()
+
+
+def cmd_html(args: argparse.Namespace) -> int:
+ con = irtrackdb.open_db_readonly(args.db)
+ if not con:
+ return 1
+ try:
+ return irtrackhtml.generate_html(
+ con,
+ args.output_dir,
+ args.source_dir or [],
+ args.all_passes,
+ args.no_highlight,
+ args.file or "",
+ )
+ finally:
+ con.close()
+
+
+def cmd_sql(args: argparse.Namespace) -> int:
+ con = irtrackdb.open_db_readonly(args.db)
+ if not con:
+ return 1
+ try:
+ return irtrackdb.run_sql(con, args.query)
+ finally:
+ con.close()
+
+
+def main(argv: Optional[Sequence[str]] = None) -> int:
+ argv = list(sys.argv[1:] if argv is None else argv)
+ parser = argparse.ArgumentParser(
+ prog="ir-tracker",
+ description="Build and query IR-tracker SQLite databases",
+ )
+ sub = parser.add_subparsers(dest="cmd", required=True)
+
+ build = sub.add_parser("build", help="Build a SQLite DB from tracker TSV output")
+ build.add_argument("--input", required=True)
+ build.add_argument("--db", required=True)
+ build.set_defaults(func=cmd_build)
+
+ passes = sub.add_parser("passes", help="List recorded passes")
+ passes.add_argument("--db", required=True)
+ passes.set_defaults(func=cmd_passes)
+
+ trace = sub.add_parser("trace", help="Find first/final pass for a source line")
+ trace.add_argument("--db", required=True)
+ trace.add_argument("--file", required=True)
+ trace.add_argument("--line", required=True)
+ trace.add_argument("--col", type=int, default=None)
+ trace.add_argument("--opcode", default="")
+ trace.set_defaults(func=cmd_trace)
+
+ show = sub.add_parser("show", help="Show tracked instructions for a source line")
+ show.add_argument("--db", required=True)
+ show.add_argument("--file", required=True)
+ show.add_argument("--line", required=True)
+ show.add_argument("--col", type=int, default=None)
+ show.add_argument("--opcode", default="")
+ show.add_argument("--seq", type=int, default=-1)
+ show.add_argument("--all-passes", action="store_true")
+ show.set_defaults(func=cmd_show)
+
+ html_p = sub.add_parser(
+ "html", help="Generate a static HTML report from a tracker DB"
+ )
+ html_p.add_argument("--db", required=True)
+ html_p.add_argument(
+ "--output-dir", "-o", required=True, help="Directory to write HTML files into"
+ )
+ html_p.add_argument(
+ "--source-dir",
+ "-s",
+ action="append",
+ default=[],
+ help="Directory to search for source files (may be passed multiple times)",
+ )
+ html_p.add_argument(
+ "--file",
+ default="",
+ help="Only emit pages for source paths containing this substring",
+ )
+ html_p.add_argument(
+ "--all-passes",
+ action="store_true",
+ help="Emit every pass snapshot, not just changed ones",
+ )
+ html_p.add_argument(
+ "--no-highlight",
+ action="store_true",
+ help="Do not use Pygments for source syntax highlighting",
+ )
+ html_p.set_defaults(func=cmd_html)
+
+ sql = sub.add_parser("sql", help="Run a read-only SQL query")
+ sql.add_argument("--db", required=True)
+ sql.add_argument("query")
+ sql.set_defaults(func=cmd_sql)
+
+ args = parser.parse_args(argv)
+ return int(args.func(args))
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/llvm/tools/ir-tracker/irtrackdb.py b/llvm/tools/ir-tracker/irtrackdb.py
new file mode 100644
index 0000000000000..1eb893fe09284
--- /dev/null
+++ b/llvm/tools/ir-tracker/irtrackdb.py
@@ -0,0 +1,512 @@
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""SQLite helpers for llvm/tools/ir-tracker."""
+
+from __future__ import annotations
+
+import os
+import sqlite3
+import sys
+from typing import Dict, List, NamedTuple, Optional, Sequence
+
+T_FILES = "ir_tracker_files"
+T_INSTR = "ir_tracker_instructions"
+T_META = "ir_tracker_meta"
+T_PASSES = "ir_tracker_passes"
+SCHEMA_VERSION = 1
+
+
+def open_db_readonly(path: str) -> Optional[sqlite3.Connection]:
+ if not path:
+ print("ir-tracker: empty database path", file=sys.stderr)
+ return None
+ if not os.path.isfile(path):
+ print(f"ir-tracker: database not found: {path}", file=sys.stderr)
+ return None
+ con = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
+ con.row_factory = sqlite3.Row
+ return con
+
+
+def open_db_write(path: str) -> Optional[sqlite3.Connection]:
+ if not path:
+ print("ir-tracker: empty database path", file=sys.stderr)
+ return None
+ parent = os.path.dirname(path)
+ if parent:
+ os.makedirs(parent, exist_ok=True)
+ if os.path.exists(path):
+ os.remove(path)
+ con = sqlite3.connect(path)
+ con.row_factory = sqlite3.Row
+ return con
+
+
+def init_schema(con: sqlite3.Connection) -> None:
+ con.executescript(
+ f"""
+ PRAGMA foreign_keys = ON;
+ CREATE TABLE {T_META} (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ );
+ CREATE TABLE {T_FILES} (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ path TEXT NOT NULL UNIQUE
+ );
+ CREATE TABLE {T_PASSES} (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ seq INTEGER NOT NULL,
+ phase TEXT NOT NULL,
+ pass_class TEXT NOT NULL,
+ ir_unit TEXT NOT NULL
+ );
+ CREATE UNIQUE INDEX ir_tracker_idx_passes_seq
+ ON {T_PASSES}(seq);
+ CREATE TABLE {T_INSTR} (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ pass_id INTEGER NOT NULL REFERENCES {T_PASSES}(id),
+ function TEXT NOT NULL,
+ basicblock TEXT NOT NULL,
+ inst_seq INTEGER NOT NULL,
+ opcode TEXT NOT NULL,
+ inst_text TEXT NOT NULL,
+ file_id INTEGER NOT NULL REFERENCES {T_FILES}(id),
+ line INTEGER NOT NULL,
+ col INTEGER NOT NULL
+ );
+ CREATE INDEX ir_tracker_idx_instr_file_loc
+ ON {T_INSTR}(file_id, line, col);
+ CREATE INDEX ir_tracker_idx_instr_pass
+ ON {T_INSTR}(pass_id);
+ """
+ )
+ con.execute(
+ f"INSERT INTO {T_META}(key, value) VALUES('schema_version', ?)",
+ (str(SCHEMA_VERSION),),
+ )
+
+
+def _get_or_create_file_id(
+ con: sqlite3.Connection, cache: Dict[str, int], path: str
+) -> int:
+ cached = cache.get(path)
+ if cached is not None:
+ return cached
+ con.execute(f"INSERT OR IGNORE INTO {T_FILES}(path) VALUES(?)", (path,))
+ row = con.execute(f"SELECT id FROM {T_FILES} WHERE path = ?", (path,)).fetchone()
+ assert row is not None
+ file_id = int(row["id"])
+ cache[path] = file_id
+ return file_id
+
+
+def _insert_pass(
+ con: sqlite3.Connection, seq: int, phase: str, pass_name: str, ir_unit: str
+) -> int:
+ return int(
+ con.execute(
+ f"INSERT INTO {T_PASSES}(seq, phase, pass_class, ir_unit) "
+ f"VALUES(?, ?, ?, ?)",
+ (seq, phase, pass_name, ir_unit),
+ ).lastrowid
+ )
+
+
+def _insert_inst(
+ con: sqlite3.Connection,
+ file_cache: Dict[str, int],
+ current_pass_id: int,
+ file_path: str,
+ line_s: int,
+ col_s: int,
+ func: str,
+ bb: str,
+ inst_seq_s: int,
+ opcode: str,
+ inst_text: str,
+) -> None:
+ file_id = _get_or_create_file_id(con, file_cache, file_path)
+ con.execute(
+ f"INSERT INTO {T_INSTR}("
+ "pass_id, function, basicblock, inst_seq, opcode, inst_text, "
+ "file_id, line, col) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ (
+ current_pass_id,
+ func,
+ bb,
+ inst_seq_s,
+ opcode,
+ inst_text,
+ file_id,
+ line_s,
+ col_s,
+ ),
+ )
+
+
+def _parse_int(field: str, value: str, line_no: int) -> int:
+ try:
+ return int(value)
+ except ValueError as err:
+ print(
+ f"ir-tracker: invalid {field} at line {line_no}: {value!r}",
+ file=sys.stderr,
+ )
+ raise ValueError(f"invalid {field}") from err
+
+
+def _build_db_from_tsv(con: sqlite3.Connection, input_path: str) -> tuple[int, int]:
+ file_cache: Dict[str, int] = {}
+ tracker_locs: Dict[str, tuple[str, int, int]] = {}
+ current_pass_id: Optional[int] = None
+ n_passes = 0
+ n_rows = 0
+
+ with open(input_path, "r", encoding="utf-8") as f:
+ for line_no, raw in enumerate(f, 1):
+ line = raw.rstrip("\n")
+ if not line:
+ continue
+
+ tag = line[0]
+ if tag == "P":
+ parts = line.split("\t")
+ if len(parts) != 5:
+ print(
+ f"ir-tracker: malformed pass row at line {line_no}",
+ file=sys.stderr,
+ )
+ raise ValueError("malformed pass row")
+ current_pass_id = _insert_pass(
+ con,
+ _parse_int("pass sequence", parts[1], line_no),
+ parts[2],
+ parts[3],
+ parts[4],
+ )
+ n_passes += 1
+ continue
+
+ if tag == "T":
+ parts = line.split("\t")
+ if len(parts) != 5:
+ print(
+ f"ir-tracker: malformed tracker-id row at line {line_no}",
+ file=sys.stderr,
+ )
+ raise ValueError("malformed tracker-id row")
+ tracker_locs[parts[1]] = (
+ parts[2],
+ _parse_int("source line", parts[3], line_no),
+ _parse_int("source column", parts[4], line_no),
+ )
+ continue
+
+ if tag == "I":
+ if current_pass_id is None:
+ print(
+ f"ir-tracker: instruction row before first pass record at line {line_no}",
+ file=sys.stderr,
+ )
+ raise ValueError("instruction row before first pass record")
+
+ parts = line.split("\t", 6)
+ if len(parts) != 7:
+ print(
+ f"ir-tracker: malformed instruction row at line {line_no}",
+ file=sys.stderr,
+ )
+ raise ValueError("malformed instruction row")
+
+ tracker_id = parts[5]
+ loc = tracker_locs.get(tracker_id)
+ if loc is None:
+ print(
+ f"ir-tracker: unknown tracker id at line {line_no}: {tracker_id}",
+ file=sys.stderr,
+ )
+ raise ValueError("unknown tracker id")
+
+ file_path, src_line, src_col = loc
+ _insert_inst(
+ con,
+ file_cache,
+ current_pass_id,
+ file_path,
+ src_line,
+ src_col,
+ parts[1],
+ parts[2],
+ _parse_int("instruction sequence", parts[3], line_no),
+ parts[4],
+ parts[6],
+ )
+ n_rows += 1
+ continue
+
+ print(
+ f"ir-tracker: unknown TSV row kind at line {line_no}: {tag!r}",
+ file=sys.stderr,
+ )
+ raise ValueError("unknown TSV row kind")
+ return n_passes, n_rows
+
+
+def build_db(input_path: str, db_path: str) -> int:
+ if not input_path:
+ print("ir-tracker: empty input path", file=sys.stderr)
+ return 1
+ if not os.path.isfile(input_path):
+ print(f"ir-tracker: input not found: {input_path}", file=sys.stderr)
+ return 1
+
+ con = open_db_write(db_path)
+ if not con:
+ return 1
+
+ try:
+ init_schema(con)
+ con.commit()
+ con.execute("BEGIN IMMEDIATE")
+ n_passes, n_rows = _build_db_from_tsv(con, input_path)
+ con.commit()
+ except (sqlite3.Error, ValueError) as err:
+ print(f"ir-tracker: sqlite error while building db: {err}", file=sys.stderr)
+ return 1
+ finally:
+ con.close()
+
+ print(
+ f"built {db_path} from {input_path}: {n_passes} pass snapshots, {n_rows} instruction rows"
+ )
+ return 0
+
+
+def get_schema_version(con: sqlite3.Connection) -> int:
+ row = con.execute(
+ f"SELECT value FROM {T_META} WHERE key = 'schema_version'"
+ ).fetchone()
+ if not row or row["value"] is None:
+ return -1
+ try:
+ return int(row["value"])
+ except ValueError:
+ return -1
+
+
+def resolve_file_ids(con: sqlite3.Connection, file_pat: str) -> List[int]:
+ needle = file_pat.lower()
+ ids: List[int] = []
+ for row in con.execute(f"SELECT id, path FROM {T_FILES}"):
+ path = (row["path"] or "").lower()
+ if needle in path or path.endswith(needle):
+ ids.append(int(row["id"]))
+ return ids
+
+
+def run_passes(con: sqlite3.Connection) -> int:
+ rows = con.execute(
+ f"SELECT id, seq, phase, pass_class, ir_unit FROM {T_PASSES} ORDER BY seq"
+ ).fetchall()
+ for row in rows:
+ print(
+ f"{int(row['seq']):5d} id={int(row['id']):<6} "
+ f"{row['phase']} '{row['pass_class']}' on '{row['ir_unit']}'"
+ )
+ print(f"total passes recorded: {len(rows)}")
+ return 0
+
+
+def _parse_line(line_s: str) -> Optional[int]:
+ try:
+ line = int(line_s, 0)
+ except ValueError:
+ return None
+ return line if line > 0 else None
+
+
+def _filter_clause(
+ file_ids: Sequence[int], line: int, trace_col: Optional[int], trace_opcode: str
+) -> tuple[str, List[object]]:
+ in_clause = ",".join("?" * len(file_ids))
+ sql = f"i.file_id IN ({in_clause}) AND i.line = ?"
+ params: List[object] = [*file_ids, line]
+ if trace_col is not None:
+ sql += " AND i.col = ?"
+ params.append(trace_col)
+ if trace_opcode:
+ sql += " AND i.opcode = ?"
+ params.append(trace_opcode)
+ return sql, params
+
+
+def run_trace(
+ con: sqlite3.Connection,
+ file_pat: str,
+ line_s: str,
+ trace_col: Optional[int],
+ trace_opcode: str,
+) -> int:
+ if get_schema_version(con) < 1:
+ print("ir-tracker: unsupported schema version", file=sys.stderr)
+ return 1
+
+ file_ids = resolve_file_ids(con, file_pat)
+ if not file_ids:
+ print("ir-tracker: no matching file rows", file=sys.stderr)
+ return 1
+
+ line = _parse_line(line_s)
+ if line is None:
+ print("ir-tracker: invalid --line", file=sys.stderr)
+ return 1
+
+ where_sql, params = _filter_clause(file_ids, line, trace_col, trace_opcode)
+
+ row = con.execute(
+ f"SELECT MAX(p.seq) AS max_seq "
+ f"FROM {T_INSTR} i JOIN {T_PASSES} p ON i.pass_id = p.id "
+ f"WHERE {where_sql}",
+ params,
+ ).fetchone()
+ if not row or row["max_seq"] is None:
+ print("ir-tracker: no matching instructions found", file=sys.stderr)
+ return 1
+
+ max_seq = int(row["max_seq"])
+ count_row = con.execute(
+ f"SELECT COUNT(*) AS c "
+ f"FROM {T_INSTR} i JOIN {T_PASSES} p ON i.pass_id = p.id "
+ f"WHERE p.seq = ? AND {where_sql}",
+ [max_seq, *params],
+ ).fetchone()
+ print(
+ f"Matches at final pass (seq={max_seq}): {int(count_row['c'])} "
+ f"instruction(s)"
+ )
+
+ first_row = con.execute(
+ f"SELECT p.seq, p.pass_class, p.ir_unit, COUNT(*) AS c "
+ f"FROM {T_INSTR} i JOIN {T_PASSES} p ON i.pass_id = p.id "
+ f"WHERE {where_sql} GROUP BY p.id ORDER BY p.seq ASC LIMIT 1",
+ params,
+ ).fetchone()
+ if first_row:
+ print(
+ f"First pass with any matching instruction: seq={int(first_row['seq'])} "
+ f"{first_row['pass_class']} on {first_row['ir_unit']} "
+ f"({int(first_row['c'])} row(s))"
+ )
+ return 0
+
+
+class ShowInstRow(NamedTuple):
+ seq: int
+ pass_class: str
+ ir_unit: str
+ function: str
+ basicblock: str
+ inst_text: str
+
+
+def _print_group(rows: Sequence[ShowInstRow]) -> None:
+ if not rows:
+ return
+ head = rows[0]
+ print(f"seq={head.seq} '{head.pass_class}' on '{head.ir_unit}'")
+ current_func = ""
+ current_bb = ""
+ for row in rows:
+ if row.function != current_func or row.basicblock != current_bb:
+ print(f" function {row.function}, block {row.basicblock}:")
+ current_func = row.function
+ current_bb = row.basicblock
+ print(f" {row.inst_text}")
+
+
+def run_show(
+ con: sqlite3.Connection,
+ file_pat: str,
+ line_s: str,
+ trace_col: Optional[int],
+ trace_opcode: str,
+ seq: int,
+ show_all_passes: bool,
+) -> int:
+ if get_schema_version(con) < 1:
+ print("ir-tracker: unsupported schema version", file=sys.stderr)
+ return 1
+ if show_all_passes and seq >= 0:
+ print(
+ "ir-tracker: --all-passes and --seq are mutually exclusive", file=sys.stderr
+ )
+ return 1
+
+ file_ids = resolve_file_ids(con, file_pat)
+ if not file_ids:
+ print("ir-tracker: no matching file rows", file=sys.stderr)
+ return 1
+
+ line = _parse_line(line_s)
+ if line is None:
+ print("ir-tracker: invalid --line", file=sys.stderr)
+ return 1
+
+ where_sql, params = _filter_clause(file_ids, line, trace_col, trace_opcode)
+ seq_sql = ""
+ if seq >= 0:
+ seq_sql = " AND p.seq = ?"
+ params = [*params, seq]
+
+ query = (
+ f"SELECT p.seq, p.pass_class, p.ir_unit, i.function, i.basicblock, "
+ f"i.inst_seq, i.inst_text "
+ f"FROM {T_INSTR} i JOIN {T_PASSES} p ON i.pass_id = p.id "
+ f"WHERE {where_sql}{seq_sql} "
+ f"ORDER BY p.seq ASC, i.function ASC, i.basicblock ASC, i.inst_seq ASC"
+ )
+ rows = [
+ ShowInstRow(
+ int(row["seq"]),
+ row["pass_class"] or "",
+ row["ir_unit"] or "",
+ row["function"] or "",
+ row["basicblock"] or "",
+ row["inst_text"] or "",
+ )
+ for row in con.execute(query, params)
+ ]
+ if not rows:
+ print("ir-tracker: no matching instructions found", file=sys.stderr)
+ return 1
+
+ by_seq: Dict[int, List[ShowInstRow]] = {}
+ for row in rows:
+ by_seq.setdefault(row.seq, []).append(row)
+
+ last_fp = None
+ for current_seq in sorted(by_seq):
+ group = by_seq[current_seq]
+ fp = "\n".join(row.inst_text for row in group)
+ if seq < 0 and not show_all_passes and fp == last_fp:
+ continue
+ _print_group(group)
+ last_fp = fp
+ return 0
+
+
+def run_sql(con: sqlite3.Connection, sql: str) -> int:
+ try:
+ cur = con.execute(sql)
+ except sqlite3.Error as err:
+ print(f"ir-tracker: prepare(sql): {err}", file=sys.stderr)
+ return 1
+
+ while True:
+ row = cur.fetchone()
+ if row is None:
+ break
+ print("(" + ", ".join("None" if v is None else str(v) for v in row) + ")")
+ return 0
diff --git a/llvm/tools/ir-tracker/irtrackhtml.py b/llvm/tools/ir-tracker/irtrackhtml.py
new file mode 100644
index 0000000000000..7d2658555ded2
--- /dev/null
+++ b/llvm/tools/ir-tracker/irtrackhtml.py
@@ -0,0 +1,698 @@
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""HTML report generator for ir-tracker SQLite databases.
+
+Generates a static, three-panel site organized by function:
+
+ * ``index.html`` — function list grouped by file, with row counts.
+ * One ``<safe>.html`` per function containing:
+
+ - Left panel: full function list (links to other function pages).
+ - Middle panel: the *initial* IR snapshot of the function (the smallest
+ recorded ``seq``). Each instruction is clickable.
+ - Right panel: when an instruction is clicked, the panel shows the
+ full pass-by-pass history of all instructions sharing that source
+ location, deduplicated when text is unchanged.
+
+The page is fully static; per-function history is embedded inline as JSON.
+"""
+
+from __future__ import annotations
+
+import html
+import json
+import os
+import re
+import sqlite3
+import sys
+import zlib
+from typing import Dict, FrozenSet, List, Optional, Sequence, Tuple
+
+import irtrackdb
+
+
+def _loc_color(loc: str) -> str:
+ """Map a ``data-loc`` string to a stable pastel ``hsl(...)`` color.
+
+ Hue is derived from a CRC32 of the loc so the same source location
+ always paints to the same color across both panels and across runs.
+ Saturation/lightness are kept low so the color sits visually behind
+ the text and does not fight the ``selected`` / ``linked`` highlight.
+ """
+ h = zlib.crc32(loc.encode("utf-8")) % 360
+ return f"hsl({h}, 70%, 90%)"
+
+
+_STYLE_CSS = """\
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; height: 100%; font-family: -apple-system, Segoe UI, sans-serif; color: #222; }
+header { padding: 6px 12px; background: #223; color: #fff; font-size: 13px; }
+header a { color: #cdf; text-decoration: none; }
+.layout { display: flex; height: calc(100vh - 30px); }
+.panel { overflow: auto; padding: 8px 12px; }
+.panel h3 { font-size: 11px; text-transform: uppercase; color: #666; margin: 0 0 6px 0; letter-spacing: .5px; }
+#funcs { width: 18%; border-right: 1px solid #ccc; background: #f7f7fb; font-size: 12px; }
+#funcs ul { list-style: none; padding-left: 6px; margin: 4px 0 12px 0; }
+#funcs li { margin: 1px 0; }
+#funcs a { text-decoration: none; color: #224; }
+#funcs a.current { font-weight: bold; color: #b00; }
+#funcs .file { font-weight: bold; margin-top: 6px; color: #555; }
+#filter { width: 95%; padding: 3px; margin-top: 4px; }
+#initial, #final { width: 27%; border-right: 1px solid #ccc; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
+#history { width: 28%; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
+.bb { color: #553; font-weight: bold; margin: 8px 0 2px 0; }
+.inst { white-space: pre; padding: 1px 4px; cursor: pointer; border-radius: 3px; }
+.inst:hover { background: #eef !important; }
+.inst.linked { background: #fff1c4 !important; }
+.inst.selected { background: #ffe080 !important; outline: 1px solid #caa040; }
+.inst.no-loc { cursor: default; color: #888; }
+.inst.no-loc:hover { background: transparent; }
+.inst.dead { color: #999; text-decoration: line-through; }
+.final-text { white-space: pre; margin: 0; font-size: 12px; color: #111; }
+.signature { white-space: pre-wrap; word-break: break-all; padding: 4px 6px;
+ margin-bottom: 6px; background: #f3f3f9; border-left: 3px solid #557;
+ color: #225; font-weight: 500; }
+.loc { color: #888; font-size: 10px; margin-left: 8px; }
+.opc { color: #058; }
+.empty { color: #888; padding: 12px; }
+.pass-hdr { font-weight: bold; color: #335; margin: 8px 0 2px 0; }
+.func-hdr { color: #553; margin-left: 1em; font-size: 11px; }
+.snap-inst { white-space: pre; margin-left: 2em; color: #111; }
+.changed { background: #f0fff0; }
+.tag { display: inline-block; padding: 0 6px; border-radius: 8px; font-size: 10px; margin-left: 6px; vertical-align: 1px; }
+.tag-final { background: #d6f5d6; color: #064; }
+.tag-last { background: #ffe0b3; color: #840; }
+.tag-gone { background: #f7c8c8; color: #800; }
+table.idx { border-collapse: collapse; }
+table.idx td, table.idx th { padding: 3px 8px; border-bottom: 1px solid #eee; text-align: left; font-size: 12px; }
+"""
+
+_SCRIPT_JS = """\
+function renderHistory(loc) {
+ var box = document.getElementById('history');
+ var groups = (window.HIST || {})[loc];
+ if (!groups || groups.length === 0) {
+ box.innerHTML = '<div class="empty">No history for this location.</div>';
+ return;
+ }
+ var parts = ['<div class="pass-hdr">Location: ' + escapeHtml(loc) + '</div>'];
+ parts.push('<div class="empty">' + groups.length + ' distinct snapshot(s)</div>');
+ for (var i = 0; i < groups.length; i++) {
+ var g = groups[i];
+ var tag = '';
+ if (g.vanished) {
+ tag = ' <span class="tag tag-gone">removed by/before this pass</span>';
+ } else if (g.final) {
+ tag = g.alive_at_end
+ ? ' <span class="tag tag-final">final (alive at end)</span>'
+ : ' <span class="tag tag-last">last seen here</span>';
+ }
+ parts.push('<div class="pass-hdr">seq=' + g.seq + ' ' + escapeHtml(g.pass)
+ + tag + ' <span class="loc">on ' + escapeHtml(g.ir_unit) + '</span></div>');
+ if (g.vanished) {
+ parts.push('<div class="empty">(no instructions at this location)</div>');
+ continue;
+ }
+ var lastBlock = '';
+ for (var j = 0; j < g.insts.length; j++) {
+ var it = g.insts[j];
+ if (it.block !== lastBlock) {
+ parts.push('<div class="func-hdr">block ' + escapeHtml(it.block) + ':</div>');
+ lastBlock = it.block;
+ }
+ parts.push('<div class="snap-inst">' + escapeHtml(it.text) + '</div>');
+ }
+ }
+ box.innerHTML = parts.join('');
+}
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, function(c) {
+ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
+ });
+}
+function instsWithLoc(loc) {
+ var all = document.querySelectorAll('.inst[data-loc]');
+ var out = [];
+ for (var i = 0; i < all.length; i++) {
+ if (all[i].getAttribute('data-loc') === loc) out.push(all[i]);
+ }
+ return out;
+}
+function clearClass(cls) {
+ var prev = document.getElementsByClassName(cls);
+ while (prev.length) prev[0].classList.remove(cls);
+}
+function panelOf(el) {
+ while (el && !(el.classList && el.classList.contains('panel')))
+ el = el.parentElement;
+ return el;
+}
+function alignPeerToClicked(clicked, peer) {
+ var cp = panelOf(clicked), pp = panelOf(peer);
+ if (!cp || !pp || cp === pp) return;
+ // Vertical offset of the clicked line within its own panel's viewport.
+ var anchor = clicked.getBoundingClientRect().top
+ - cp.getBoundingClientRect().top;
+ // Where the peer currently sits within its panel's viewport.
+ var peerTop = peer.getBoundingClientRect().top
+ - pp.getBoundingClientRect().top;
+ // Scroll the peer's panel so the peer ends up at the same vertical
+ // position as the clicked line. Clamped automatically by the browser.
+ pp.scrollTop += peerTop - anchor;
+}
+function selectInst(el) {
+ clearClass('selected');
+ clearClass('linked');
+ var loc = el.getAttribute('data-loc');
+ if (!loc) return;
+ var peers = instsWithLoc(loc);
+ for (var i = 0; i < peers.length; i++) {
+ peers[i].classList.add('selected');
+ if (peers[i] !== el) alignPeerToClicked(el, peers[i]);
+ }
+ renderHistory(loc);
+}
+function hoverInst(el, enter) {
+ var loc = el.getAttribute('data-loc');
+ if (!loc) return;
+ var peers = instsWithLoc(loc);
+ for (var i = 0; i < peers.length; i++) {
+ if (peers[i].classList.contains('selected')) continue;
+ if (enter) peers[i].classList.add('linked');
+ else peers[i].classList.remove('linked');
+ }
+}
+function filterFuncs() {
+ var q = document.getElementById('filter').value.toLowerCase();
+ var items = document.querySelectorAll('#funcs li');
+ for (var i = 0; i < items.length; i++) {
+ var t = items[i].textContent.toLowerCase();
+ items[i].style.display = (q === '' || t.indexOf(q) >= 0) ? '' : 'none';
+ }
+}
+window.addEventListener('DOMContentLoaded', function() {
+ var first = document.querySelector('#initial .inst[data-loc]');
+ if (first) selectInst(first);
+});
+"""
+
+
+def _safe_filename(name: str) -> str:
+ return re.sub(r"[^A-Za-z0-9._-]", "_", name).strip("_") or "x"
+
+
+def _esc(s: str) -> str:
+ return html.escape(s, quote=False)
+
+
+def _functions(con: sqlite3.Connection) -> List[Tuple[str, str, int, int, str]]:
+ """Return list of ``(function, file_path, n_insts, n_passes, page)``."""
+ rows = con.execute(
+ f"SELECT i.function AS function, f.path AS file_path, "
+ f"COUNT(*) AS n_insts, COUNT(DISTINCT i.pass_id) AS n_passes "
+ f"FROM {irtrackdb.T_INSTR} i "
+ f"JOIN {irtrackdb.T_FILES} f ON i.file_id = f.id "
+ f"WHERE i.function != '' "
+ f"GROUP BY i.function, f.id "
+ f"ORDER BY f.path, i.function"
+ ).fetchall()
+ out = []
+ used: Dict[str, int] = {}
+ for r in rows:
+ base = "fn-" + _safe_filename(r["function"])
+ n = used.get(base, 0)
+ used[base] = n + 1
+ page = base + (f"-{n}" if n else "") + ".html"
+ out.append(
+ (r["function"], r["file_path"], int(r["n_insts"]), int(r["n_passes"]), page)
+ )
+ return out
+
+
+def _initial_seq(con: sqlite3.Connection, function: str) -> int:
+ """Smallest recorded pass ``seq`` for a function whose phase is the
+ initial capture. We intentionally exclude ``phase='final'`` rows so that
+ a function first seen in the synthetic final snapshot (e.g. one that had
+ no activity during the pipeline) does not get treated as an "initial"."""
+ row = con.execute(
+ f"SELECT MIN(p.seq) AS s "
+ f"FROM {irtrackdb.T_INSTR} i "
+ f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
+ f"WHERE i.function = ? AND p.phase <> 'final'",
+ (function,),
+ ).fetchone()
+ return -1 if row is None or row["s"] is None else int(row["s"])
+
+
+def _final_seq(con: sqlite3.Connection, function: str) -> int:
+ """Return the ``seq`` of the synthetic ``phase='final'`` pass that has
+ rows for this function, or ``-1`` when the DB predates that recorder
+ change or the function did not survive to the final snapshot."""
+ row = con.execute(
+ f"SELECT MAX(p.seq) AS s "
+ f"FROM {irtrackdb.T_INSTR} i "
+ f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
+ f"WHERE i.function = ? AND p.phase = 'final'",
+ (function,),
+ ).fetchone()
+ return -1 if row is None or row["s"] is None else int(row["s"])
+
+
+def _final_ir_rows(
+ con: sqlite3.Connection, function: str, seq: int
+) -> List[sqlite3.Row]:
+ return con.execute(
+ f"SELECT i.basicblock, i.inst_seq, i.opcode, i.inst_text, "
+ f"f.path AS file_path, i.line, i.col "
+ f"FROM {irtrackdb.T_INSTR} i "
+ f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
+ f"JOIN {irtrackdb.T_FILES} f ON i.file_id = f.id "
+ f"WHERE i.function = ? AND p.seq = ? "
+ f"ORDER BY i.id",
+ (function, seq),
+ ).fetchall()
+
+
+def _initial_ir(con: sqlite3.Connection, function: str, seq: int) -> List[sqlite3.Row]:
+ # Order by recording order (``i.id``) rather than ``(basicblock, inst_seq)``
+ # because the cost-improvement recorder often labels every block as
+ # ``<unnamed>`` and ``inst_seq`` is the *per-block* instruction index that
+ # restarts at 0 for each new block. Sorting on those columns therefore
+ # interleaves instructions from different blocks. Insertion order on a
+ # single ``writeInstructionsInFunction`` walk preserves the natural
+ # block-by-block order.
+ return con.execute(
+ f"SELECT i.basicblock, i.inst_seq, i.opcode, i.inst_text, "
+ f"f.path AS file_path, i.line, i.col "
+ f"FROM {irtrackdb.T_INSTR} i "
+ f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
+ f"JOIN {irtrackdb.T_FILES} f ON i.file_id = f.id "
+ f"WHERE i.function = ? AND p.seq = ? "
+ f"ORDER BY i.id",
+ (function, seq),
+ ).fetchall()
+
+
+def _history(
+ con: sqlite3.Connection, function: str
+) -> Dict[str, List[Dict[str, object]]]:
+ rows = con.execute(
+ f"SELECT f.path, i.line, i.col, p.seq, p.pass_class, p.ir_unit, "
+ f"i.basicblock, i.inst_seq, i.inst_text "
+ f"FROM {irtrackdb.T_INSTR} i "
+ f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
+ f"JOIN {irtrackdb.T_FILES} f ON i.file_id = f.id "
+ f"WHERE i.function = ? "
+ f"ORDER BY i.line, i.col, p.seq, i.basicblock, i.inst_seq",
+ (function,),
+ ).fetchall()
+
+ # Maximum seq for which this function has any recorded rows, used to
+ # decide whether a location reached the end of the pipeline or vanished.
+ func_max_row = con.execute(
+ f"SELECT MAX(p.seq) AS s "
+ f"FROM {irtrackdb.T_INSTR} i "
+ f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
+ f"WHERE i.function = ?",
+ (function,),
+ ).fetchone()
+ func_max_seq = (
+ -1
+ if func_max_row is None or func_max_row["s"] is None
+ else int(func_max_row["s"])
+ )
+
+ # Group rows by location key, then by seq within each location.
+ by_loc: Dict[str, Dict[int, List[Dict[str, str]]]] = {}
+ pass_meta: Dict[int, Tuple[str, str]] = {}
+ for r in rows:
+ key = f"{r['path']}|{int(r['line'])}|{int(r['col'])}"
+ by_loc.setdefault(key, {}).setdefault(int(r["seq"]), []).append(
+ {"block": r["basicblock"] or "", "text": r["inst_text"] or ""}
+ )
+ pass_meta[int(r["seq"])] = (r["pass_class"] or "", r["ir_unit"] or "")
+
+ out: Dict[str, List[Dict[str, object]]] = {}
+ for key, by_seq in by_loc.items():
+ seqs = sorted(by_seq)
+ loc_max_seq = seqs[-1]
+
+ groups: List[Dict[str, object]] = []
+ last_fp = None
+ for seq in seqs:
+ insts = by_seq[seq]
+ fp = "\n".join(it["text"] for it in insts)
+ is_last = seq == loc_max_seq
+ if fp == last_fp and not is_last:
+ continue
+ pass_name, ir_unit = pass_meta[seq]
+ entry: Dict[str, object] = {
+ "seq": seq,
+ "pass": pass_name,
+ "ir_unit": ir_unit,
+ "insts": insts,
+ }
+ if is_last:
+ # Tag the final recorded snapshot so the UI can highlight
+ # whether the location survived to the end of the pipeline
+ # or was dropped by a later pass.
+ entry["final"] = True
+ entry["alive_at_end"] = loc_max_seq == func_max_seq
+ groups.append(entry)
+ last_fp = fp
+
+ # If the location vanished before the function's last pass, append a
+ # synthetic record naming the first pass after it that was recorded
+ # (best estimate of the pass that removed it).
+ if loc_max_seq < func_max_seq:
+ after = None
+ for s in sorted(pass_meta):
+ if s > loc_max_seq:
+ after = s
+ break
+ if after is not None:
+ pass_name, ir_unit = pass_meta[after]
+ groups.append(
+ {
+ "seq": after,
+ "pass": pass_name,
+ "ir_unit": ir_unit,
+ "insts": [],
+ "vanished": True,
+ }
+ )
+
+ out[key] = groups
+ return out
+
+
+def _render_func_list(
+ funcs: Sequence[Tuple[str, str, int, int, str]], current: str
+) -> str:
+ parts: List[str] = []
+ parts.append(
+ '<input id="filter" type="text" placeholder="Filter functions..." '
+ 'oninput="filterFuncs()">'
+ )
+ cur_file = None
+ parts.append("<ul>")
+ for fn, fpath, n_i, n_p, page in funcs:
+ if fpath != cur_file:
+ if cur_file is not None:
+ parts.append("</ul>")
+ parts.append(
+ '<div class="file">' + _esc(os.path.basename(fpath) or fpath) + "</div>"
+ )
+ parts.append("<ul>")
+ cur_file = fpath
+ klass = ' class="current"' if fn == current else ""
+ parts.append(
+ f'<li><a href="{page}"{klass}>{_esc(fn)}</a> '
+ f'<span class="loc">{n_i}r {n_p}p</span></li>'
+ )
+ parts.append("</ul>")
+ return "".join(parts)
+
+
+def _render_initial_ir(
+ rows: Sequence[sqlite3.Row],
+ seq: int,
+ color_locs: Optional[FrozenSet[str]] = None,
+) -> str:
+ return _render_ir_panel(rows, f"initial snapshot: seq={seq}", color_locs)
+
+
+_DEFINE_RE = re.compile(r"^\s*define\b[^@]*@([A-Za-z0-9_.$]+)\s*\(")
+_DILOC_RE = re.compile(
+ r"^!(\d+)\s*=\s*!DILocation\(\s*line:\s*(\d+)(?:,\s*column:\s*(\d+))?"
+)
+_DBG_REF_RE = re.compile(r"!dbg !(\d+)")
+
+
+def _row_loc(r: sqlite3.Row) -> Optional[str]:
+ line = int(r["line"])
+ if line <= 0:
+ return None
+ return f'{r["file_path"]}|{line}|{int(r["col"])}'
+
+
+def _render_ir_panel(
+ rows: Sequence[sqlite3.Row],
+ header: str,
+ color_locs: Optional[FrozenSet[str]] = None,
+) -> str:
+ """Shared renderer for a single-pass IR snapshot (used by both the
+ Initial and Final panels). Groups into pseudo-blocks by ``inst_seq``
+ resetting to zero, since the compact printer often emits ``<unnamed>``
+ instead of a real block name."""
+ if not rows:
+ return '<div class="empty">No IR recorded.</div>'
+ parts: List[str] = [f'<div class="loc">{_esc(header)}</div>']
+
+ # Pull the synthetic signature row (basicblock=='<sig>') to the top so
+ # the panel opens with the textual-IR-style ``define ... @name(...)``.
+ sig_row = next((r for r in rows if (r["basicblock"] or "") == "<sig>"), None)
+ body_rows = [r for r in rows if (r["basicblock"] or "") != "<sig>"]
+ if sig_row is not None:
+ parts.append(
+ '<div class="signature">' f'{_esc(sig_row["inst_text"] or "")}' "</div>"
+ )
+
+ bb_index = 0
+ last_seq = -1
+ last_bb_name: Optional[str] = None
+ for idx, r in enumerate(body_rows):
+ bb_name = r["basicblock"] or ""
+ is_real_name = bool(bb_name) and bb_name != "<unnamed>"
+ seq_resets = int(r["inst_seq"]) == 0 and last_seq >= 0
+ starts_new_block = (
+ idx == 0 or seq_resets or (is_real_name and bb_name != last_bb_name)
+ )
+ if starts_new_block:
+ label = bb_name if is_real_name else f"bb{bb_index}"
+ parts.append('<div class="bb">' + _esc(label) + ":</div>")
+ bb_index += 1
+ last_bb_name = bb_name
+ last_seq = int(r["inst_seq"])
+
+ loc = f'{r["file_path"]}|{int(r["line"])}|{int(r["col"])}'
+ has_loc = int(r["line"]) > 0
+ klass = "inst" + ("" if has_loc else " no-loc")
+ attrs = (
+ f' data-loc="{_esc(loc)}" onclick="selectInst(this)"'
+ f' onmouseenter="hoverInst(this,true)"'
+ f' onmouseleave="hoverInst(this,false)"'
+ if has_loc
+ else ""
+ )
+ # Inline a stable per-loc background only for locations that show
+ # up in both the Initial and Final panels — that is the cross-panel
+ # mapping the user wants to see at a glance. Lone-side locations
+ # stay the default color so the noise stays manageable.
+ style = ""
+ if has_loc and color_locs is not None and loc in color_locs:
+ style = f' style="background:{_loc_color(loc)}"'
+ loc_str = (
+ f'<span class="loc">{int(r["line"])}:{int(r["col"])}</span>'
+ if has_loc
+ else '<span class="loc">no loc</span>'
+ )
+ parts.append(
+ f'<div class="{klass}"{attrs}{style}>'
+ f'<span class="opc">{_esc(r["opcode"] or "")}</span> '
+ f'{_esc(r["inst_text"] or "")}{loc_str}</div>'
+ )
+ return "".join(parts)
+
+
+def parse_ll_functions(path: str) -> Tuple[Dict[str, str], Dict[int, Tuple[int, int]]]:
+ """Return ``({function_name: body_text}, {metadata_id: (line, col)})`` from
+ an LLVM textual IR file. Debug-location mapping only includes direct
+ ``!DILocation`` nodes (not ``DILexicalBlock``-scoped chains); that is
+ enough for ``--add-ir-tracker-locs``-produced IR which emits one
+ ``!DILocation`` per synthetic line."""
+ funcs: Dict[str, str] = {}
+ dilocs: Dict[int, Tuple[int, int]] = {}
+ cur: Optional[str] = None
+ buf: List[str] = []
+ depth = 0
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
+ for line in f:
+ if cur is None:
+ m = _DILOC_RE.match(line)
+ if m:
+ line_n = int(m.group(2))
+ col_n = int(m.group(3)) if m.group(3) is not None else 0
+ dilocs[int(m.group(1))] = (line_n, col_n)
+ continue
+ dm = _DEFINE_RE.match(line)
+ if not dm:
+ continue
+ cur = dm.group(1)
+ buf = [line.rstrip("\n")]
+ depth = line.count("{") - line.count("}")
+ if depth <= 0:
+ funcs[cur] = "\n".join(buf)
+ cur = None
+ buf = []
+ continue
+ buf.append(line.rstrip("\n"))
+ depth += line.count("{") - line.count("}")
+ if depth <= 0:
+ funcs[cur] = "\n".join(buf)
+ cur = None
+ buf = []
+ return funcs, dilocs
+
+
+def _render_final_ir(
+ rows: Sequence[sqlite3.Row],
+ seq: int,
+ color_locs: Optional[FrozenSet[str]] = None,
+) -> str:
+ if not rows or seq < 0:
+ return (
+ '<div class="empty">No final snapshot for this function. '
+ "Ensure the tracker DB was produced by a recorder that emits a "
+ "<code>phase='final'</code> pass at teardown.</div>"
+ )
+ return _render_ir_panel(rows, f"final snapshot: seq={seq}", color_locs)
+
+
+def _render_function_page(
+ function: str,
+ file_path: str,
+ funcs: Sequence[Tuple[str, str, int, int, str]],
+ initial_rows: Sequence[sqlite3.Row],
+ initial_seq: int,
+ final_rows: Sequence[sqlite3.Row],
+ final_seq: int,
+ history: Dict[str, List[Dict[str, object]]],
+) -> str:
+ # Locations shared by Initial and Final get a stable per-loc background
+ # color so the cross-panel mapping is visible at a glance.
+ initial_locs = {l for l in (_row_loc(r) for r in initial_rows) if l}
+ final_locs = {l for l in (_row_loc(r) for r in final_rows) if l}
+ shared_locs: FrozenSet[str] = frozenset(initial_locs & final_locs)
+ func_list = _render_func_list(funcs, function)
+ initial = _render_initial_ir(initial_rows, initial_seq, shared_locs)
+ final = _render_final_ir(final_rows, final_seq, shared_locs)
+ hist_json = json.dumps(history, separators=(",", ":"))
+ return (
+ "<!doctype html><html><head><meta charset='utf-8'>"
+ f"<title>{_esc(function)}</title>"
+ "<link rel='stylesheet' href='style.css'>"
+ f"<script>{_SCRIPT_JS}</script>"
+ "</head><body>"
+ f"<header><a href='index.html'>← index</a> "
+ f"<b>{_esc(function)}</b> "
+ f"<span class='loc'>{_esc(file_path)}</span></header>"
+ "<div class='layout'>"
+ f"<div id='funcs' class='panel'>{func_list}</div>"
+ f"<div id='initial' class='panel'><h3>Initial IR</h3>{initial}</div>"
+ f"<div id='final' class='panel'><h3>Final IR</h3>{final}</div>"
+ "<div id='history' class='panel'>"
+ "<h3>Pass history</h3>"
+ "<div class='empty'>Click an instruction in either IR panel to "
+ "see its pass history.</div>"
+ "</div>"
+ "</div>"
+ f"<script>window.HIST={hist_json};</script>"
+ "</body></html>"
+ )
+
+
+def _render_index(
+ funcs: Sequence[Tuple[str, str, int, int, str]],
+ pass_count: int,
+ inst_count: int,
+) -> str:
+ rows = []
+ cur_file = None
+ for fn, fpath, n_i, n_p, page in funcs:
+ if fpath != cur_file:
+ cur_file = fpath
+ rows.append(
+ f"<tr><th colspan='4'>{_esc(fpath)}</th></tr>"
+ "<tr><th>Function</th><th>Inst rows</th>"
+ "<th>Passes touched</th><th></th></tr>"
+ )
+ rows.append(
+ f"<tr><td><a href='{page}'>{_esc(fn)}</a></td>"
+ f"<td>{n_i}</td><td>{n_p}</td><td></td></tr>"
+ )
+ return (
+ "<!doctype html><html><head><meta charset='utf-8'>"
+ "<title>ir-tracker report</title>"
+ "<link rel='stylesheet' href='style.css'></head><body>"
+ "<header>ir-tracker report — "
+ f"{pass_count} pass snapshots, {inst_count} instruction rows, "
+ f"{len(funcs)} functions</header>"
+ "<div style='padding:10px'>"
+ f"<table class='idx'>{''.join(rows)}</table>"
+ "</div></body></html>"
+ )
+
+
+def generate_html(
+ con: sqlite3.Connection,
+ output_dir: str,
+ source_dirs: Sequence[str],
+ all_passes: bool,
+ no_highlight: bool,
+ file_filter: str = "",
+) -> int:
+ # ``source_dirs``, ``all_passes`` and ``no_highlight`` are accepted for CLI
+ # compatibility but unused in the function-centric layout (no source view,
+ # always emits per-location dedup, no syntax highlighting).
+ del source_dirs, all_passes, no_highlight
+
+ if irtrackdb.get_schema_version(con) < 1:
+ print("ir-tracker: unsupported schema version", file=sys.stderr)
+ return 1
+
+ os.makedirs(output_dir, exist_ok=True)
+
+ funcs = _functions(con)
+ if file_filter:
+ needle = file_filter.lower()
+ funcs = [t for t in funcs if needle in t[1].lower()]
+ if not funcs:
+ print("ir-tracker: no functions with instructions in DB", file=sys.stderr)
+ return 1
+
+ pass_count = int(
+ con.execute(f"SELECT COUNT(*) AS c FROM {irtrackdb.T_PASSES}").fetchone()["c"]
+ )
+ inst_count = int(
+ con.execute(f"SELECT COUNT(*) AS c FROM {irtrackdb.T_INSTR}").fetchone()["c"]
+ )
+
+ with open(os.path.join(output_dir, "style.css"), "w", encoding="utf-8") as f:
+ f.write(_STYLE_CSS)
+
+ for function, file_path, _ni, _np, page in funcs:
+ iseq = _initial_seq(con, function)
+ initial = _initial_ir(con, function, iseq) if iseq >= 0 else []
+ fseq = _final_seq(con, function)
+ final = _final_ir_rows(con, function, fseq) if fseq >= 0 else []
+ hist = _history(con, function)
+ html_text = _render_function_page(
+ function,
+ file_path,
+ funcs,
+ initial,
+ iseq,
+ final,
+ fseq,
+ hist,
+ )
+ with open(os.path.join(output_dir, page), "w", encoding="utf-8") as f:
+ f.write(html_text)
+
+ with open(os.path.join(output_dir, "index.html"), "w", encoding="utf-8") as f:
+ f.write(_render_index(funcs, pass_count, inst_count))
+
+ print(f"ir-tracker: wrote {len(funcs)} function page(s) + index to {output_dir}")
+ return 0
>From b606b0ec38a5af0e4e2bb01d516d39625eaa6145 Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <yaxun.liu at amd.com>
Date: Mon, 27 Apr 2026 22:11:53 -0400
Subject: [PATCH 4/4] [IR tracker] Add new-PM MIR tracking
Record MIR snapshots from new-PM MachineFunction pass callbacks so the tracker can follow source locations through the CodeGen pipeline without requiring legacy PM or assembly support.
---
llvm/docs/IRTracker.rst | 45 ++-
llvm/lib/Passes/IRTrackerInstrumentation.cpp | 303 +++++++++++++++++-
llvm/test/Other/ir-tracker-db.ll | 20 +-
llvm/test/tools/llvm-ir-tracker/kind-tsv.test | 17 +
llvm/test/tools/llvm-ir-tracker/mir-newpm.ll | 31 ++
llvm/tools/ir-tracker/ir-tracker.py | 8 +-
llvm/tools/ir-tracker/irtrackdb.py | 117 +++++--
llvm/tools/ir-tracker/irtrackhtml.py | 25 +-
8 files changed, 491 insertions(+), 75 deletions(-)
create mode 100644 llvm/test/tools/llvm-ir-tracker/kind-tsv.test
create mode 100644 llvm/test/tools/llvm-ir-tracker/mir-newpm.ll
diff --git a/llvm/docs/IRTracker.rst b/llvm/docs/IRTracker.rst
index c14613680c995..e5d5895fe4f32 100644
--- a/llvm/docs/IRTracker.rst
+++ b/llvm/docs/IRTracker.rst
@@ -8,11 +8,13 @@ Overview
========
The **IR tracker** records how LLVM IR evolves through the new pass manager into a
-compact tab-separated file. A small Python tool can then post-process that TSV
-output into a SQLite database for indexed queries. Each instruction row is tied
-to a tracker ID, which maps to a ``DILocation`` when one exists. If an input
-module has no debug information, the recorder synthesizes locations first so the
-tool can still track IR evolution through the pipeline.
+compact tab-separated file. It can also record MIR snapshots from new-PM CodeGen
+pipelines that use ``MachineFunction`` pass instrumentation. A small Python tool
+can then post-process that TSV output into a SQLite database for indexed
+queries. Each instruction row is tied to a tracker ID, which maps to a
+``DILocation`` when one exists. If an input module has no debug information, the
+recorder synthesizes locations first so the tool can still track IR evolution
+through the pipeline.
Real source locations are only needed when you want to map tracked instructions
back to the original source file and line. For that workflow, compile with
@@ -21,7 +23,9 @@ back to the original source file and line. For that workflow, compile with
Recording is enabled with the hidden LLVM option
``-ir-tracker-output=/absolute/path.tsv``. The hooks live in
``StandardInstrumentations`` and therefore apply to any tool that runs the new
-pass manager with that instrumentation (``opt``, ``clang``, etc.).
+pass manager with that instrumentation (``opt``, ``clang``, etc.). MIR tracking
+is currently limited to new-PM CodeGen; default legacy-PM ``llc`` pipelines do
+not emit MIR tracker rows.
The stream uses ``P`` rows for pass snapshots, ``T`` rows for tracker-ID source
locations, and ``I`` rows for instruction snapshots.
@@ -55,6 +59,20 @@ locations so later ``trace`` / ``show`` queries can use source file and line
numbers. ``-O1`` (or another ``-O`` level) selects the usual optimization
pipeline that ``opt`` would run for that tier.
+Recording MIR with new-PM ``llc``
+=================================
+
+MIR tracking uses the same hidden option, but requires the new CodeGen pass
+manager:
+
+.. code-block:: bash
+
+ llc -enable-new-pm -filetype=null -ir-tracker-output=/tmp/pipeline.tsv input.ll
+
+The final MIR snapshots are often close enough to assembly to debug instruction
+selection, register allocation, spills, and late machine optimizations. The
+tracker does not record final assembly text or MC streamer directives.
+
SQLite build step
=================
@@ -65,14 +83,14 @@ The Python driver can convert the TSV output into a SQLite database:
python3 llvm/tools/ir-tracker/ir-tracker.py build \
--input /tmp/pipeline.tsv --db /tmp/pipeline.db
-The resulting database uses ``schema_version = 1`` in ``ir_tracker_meta``. The
+The resulting database uses ``schema_version = 2`` in ``ir_tracker_meta``. The
main tables are:
* ``ir_tracker_meta`` — key/value metadata (including ``schema_version``)
* ``ir_tracker_files`` — deduplicated paths from ``DIFile`` (often a basename
such as ``sum.c``)
-* ``ir_tracker_passes`` — one row per snapshot: ``seq``, ``phase`` (``initial``
- or ``after``), ``pass_class``, ``ir_unit``
+* ``ir_tracker_passes`` — one row per snapshot: ``seq``, ``kind`` (``ir`` or
+ ``mir``), ``phase`` (``initial`` or ``after``), ``pass_class``, ``ir_unit``
* ``ir_tracker_instructions`` — instruction text and opcode per pass, keyed by
``file_id``, ``line``, ``col``
@@ -85,13 +103,14 @@ is enabled). It can build the SQLite DB from tracker TSV output and then query
that DB. Subcommands:
* ``build`` — convert tracker TSV output into a SQLite database
-* ``passes`` — list recorded passes in ``seq`` order
+* ``passes`` — list recorded passes in ``seq`` order; use ``--kind ir``,
+ ``--kind mir``, or ``--kind all`` to filter representations
* ``trace`` — summarize the first and last pass that still have instructions
- matching a source location
+ matching a source location; defaults to ``--kind ir``
* ``show`` — print the instructions matching ``--file`` / ``--line`` (and
optional ``--col`` / ``--opcode``) across passes; by default only passes where
- the printed IR **changed** are shown; use ``--all-passes`` for every pass, or
- ``--seq N`` for one pass
+ the printed IR **changed** are shown; use ``--kind mir`` for MIR rows,
+ ``--all-passes`` for every pass, or ``--seq N`` for one pass
* ``html`` — generate a static HTML report with one page per function
* ``sql`` — run a single read-only SQL statement
diff --git a/llvm/lib/Passes/IRTrackerInstrumentation.cpp b/llvm/lib/Passes/IRTrackerInstrumentation.cpp
index c33470864d226..60d665aab671f 100644
--- a/llvm/lib/Passes/IRTrackerInstrumentation.cpp
+++ b/llvm/lib/Passes/IRTrackerInstrumentation.cpp
@@ -14,6 +14,11 @@
#include "llvm/ADT/StableHashing.h"
#include "llvm/Analysis/LazyCallGraph.h"
#include "llvm/Analysis/LoopInfo.h"
+#include "llvm/CodeGen/MachineBasicBlock.h"
+#include "llvm/CodeGen/MachineFunction.h"
+#include "llvm/CodeGen/MachineInstr.h"
+#include "llvm/CodeGen/MachineOperand.h"
+#include "llvm/CodeGen/TargetInstrInfo.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/DIBuilder.h"
@@ -103,6 +108,12 @@ static bool shouldPrintIR(Any IR) {
return false;
}
+static bool shouldPrintMIR(Any IR) {
+ if (const auto *MF = unwrapIR<MachineFunction>(IR))
+ return isFunctionInPrintList(MF->getName());
+ return false;
+}
+
static bool isIgnored(StringRef PassID) {
return isSpecialPass(PassID,
{"PassManager", "PassAdaptor", "AnalysisManagerProxy",
@@ -314,6 +325,78 @@ static stable_hash hashInstruction(const Instruction &I) {
return H;
}
+static stable_hash hashMachineOperand(const MachineOperand &MO) {
+ stable_hash H = stable_hash_combine(static_cast<stable_hash>(MO.getType()));
+ if (MO.isReg()) {
+ H = stable_hash_combine(H, static_cast<stable_hash>(MO.getReg().id()));
+ H = stable_hash_combine(H, static_cast<stable_hash>(MO.isDef()),
+ static_cast<stable_hash>(MO.isImplicit()),
+ static_cast<stable_hash>(MO.isDead()));
+ H = stable_hash_combine(H, static_cast<stable_hash>(MO.isKill()));
+ if (MO.getSubReg())
+ H = stable_hash_combine(H, static_cast<stable_hash>(MO.getSubReg()));
+ return H;
+ }
+ if (MO.isImm())
+ return stable_hash_combine(H, static_cast<stable_hash>(MO.getImm()));
+ if (MO.isCImm())
+ return stable_hash_combine(
+ H, static_cast<stable_hash>(hash_value(MO.getCImm()->getValue())));
+ if (MO.isFPImm())
+ return stable_hash_combine(
+ H, static_cast<stable_hash>(
+ hash_value(MO.getFPImm()->getValueAPF().bitcastToAPInt())));
+ if (MO.isMBB())
+ return stable_hash_combine(
+ H, static_cast<stable_hash>(MO.getMBB()->getNumber()));
+ if (MO.isGlobal())
+ return stable_hash_combine(
+ H, static_cast<stable_hash>(hash_value(MO.getGlobal()->getName())),
+ static_cast<stable_hash>(MO.getOffset()),
+ static_cast<stable_hash>(MO.getTargetFlags()));
+ if (MO.isSymbol())
+ return stable_hash_combine(
+ H, static_cast<stable_hash>(hash_value(MO.getSymbolName())),
+ static_cast<stable_hash>(MO.getOffset()),
+ static_cast<stable_hash>(MO.getTargetFlags()));
+ if (MO.isBlockAddress())
+ return stable_hash_combine(
+ H,
+ static_cast<stable_hash>(
+ hash_value(MO.getBlockAddress()->getFunction()->getName())),
+ static_cast<stable_hash>(MO.getOffset()),
+ static_cast<stable_hash>(MO.getTargetFlags()));
+ if (MO.isFI())
+ return stable_hash_combine(H, static_cast<stable_hash>(MO.getIndex()));
+ if (MO.isCPI())
+ return stable_hash_combine(H, static_cast<stable_hash>(MO.getIndex()),
+ static_cast<stable_hash>(MO.getOffset()),
+ static_cast<stable_hash>(MO.getTargetFlags()));
+ if (MO.isJTI())
+ return stable_hash_combine(H, static_cast<stable_hash>(MO.getIndex()),
+ static_cast<stable_hash>(MO.getTargetFlags()));
+ if (MO.isTargetIndex())
+ return stable_hash_combine(H, static_cast<stable_hash>(MO.getIndex()),
+ static_cast<stable_hash>(MO.getOffset()),
+ static_cast<stable_hash>(MO.getTargetFlags()));
+ if (MO.isMetadata())
+ return stable_hash_combine(
+ H, static_cast<stable_hash>(hash_value(MO.getMetadata())));
+ if (MO.isMCSymbol())
+ return stable_hash_combine(
+ H, static_cast<stable_hash>(hash_value(MO.getMCSymbol()->getName())));
+ return H;
+}
+
+static stable_hash hashMachineInstr(const MachineInstr &MI) {
+ stable_hash H =
+ stable_hash_combine(static_cast<stable_hash>(MI.getOpcode()),
+ static_cast<stable_hash>(MI.getNumOperands()));
+ for (const MachineOperand &MO : MI.operands())
+ H = stable_hash_combine(H, hashMachineOperand(MO));
+ return H;
+}
+
/// Compute the hash that identifies "this source point" for tracker-ID
/// interning.
///
@@ -431,6 +514,17 @@ class IRTrackerRecorder {
/// across passes.
DenseMap<const Function *, SmallVector<SmallVector<unsigned>>> BlockTempIDs;
+ /// Per-MachineFunction MIR state. Kept separate from the IR Function maps
+ /// because MIR passes are per-machine-function and may run after all IR
+ /// snapshots have already been recorded.
+ DenseSet<const MachineFunction *> MIRInitialCaptured;
+ DenseMap<const MachineFunction *, stable_hash> MIRFunctionHashes;
+ DenseMap<const MachineFunction *, SmallVector<stable_hash>> MIRBlockHashes;
+ DenseMap<const MachineFunction *, SmallVector<SmallVector<stable_hash>>>
+ MIRBlockInstHashes;
+ DenseMap<const MachineFunction *, SmallVector<SmallVector<unsigned>>>
+ MIRBlockTempIDs;
+
/// Intern table from a source-point hash (hashTrackerIdentity) to the
/// compact integer tracker ID. First time a source point is seen, a
/// fresh ID is allocated; subsequent sightings of the same source
@@ -450,11 +544,12 @@ class IRTrackerRecorder {
/// Emit one P (pass) row. P rows delimit the per-pass instruction
/// records that follow.
///
- /// Format: ``P\t<seq>\t<phase>\t<pass_name>\t<ir_unit>``.
+ /// Format: ``P\t<seq>\t<kind>\t<phase>\t<pass_name>\t<ir_unit>``.
///
/// * ``seq``: monotonically increasing pass index. 0 is the initial
/// capture, 1..N are normal passes, and one final "phase=final"
/// record is emitted at teardown.
+ /// * ``kind``: ``ir`` or ``mir``.
/// * ``phase``: ``initial``, ``after``, or ``final``.
/// * ``pass_name``: pass class name as resolved by
/// PassInstrumentationCallbacks::getPassNameForClassName.
@@ -463,13 +558,13 @@ class IRTrackerRecorder {
///
/// Example output:
///
- /// P\t0\tinitial\t<initial>\t[module]
- /// P\t1\tafter\tmemprof-remove-attributes\t[module]
- /// P\t5\tafter\tsroa\tcli_wcwidth
- void writePassRecord(unsigned Seq, StringRef Phase, StringRef PassName,
- StringRef IRUnit) {
- *OS << "P\t" << Seq << '\t' << Phase << '\t' << PassName << '\t' << IRUnit
- << '\n';
+ /// P\t0\tir\tinitial\t<initial>\t[module]
+ /// P\t1\tir\tafter\tmemprof-remove-attributes\t[module]
+ /// P\t5\tmir\tafter\tgreedy\tcli_wcwidth
+ void writePassRecord(unsigned Seq, StringRef Kind, StringRef Phase,
+ StringRef PassName, StringRef IRUnit) {
+ *OS << "P\t" << Seq << '\t' << Kind << '\t' << Phase << '\t' << PassName
+ << '\t' << IRUnit << '\n';
}
/// Emit one T (tracker-metadata) row, at most once per tracker ID over
@@ -977,6 +1072,163 @@ class IRTrackerRecorder {
PrevBlkH = std::move(NewBlkH);
}
+ void writeMachineInstructions(const MachineFunction &MF, bool SkipUnchanged) {
+ if (!isFunctionInPrintList(MF.getName()))
+ return;
+
+ auto &PrevBlkH = MIRBlockHashes[&MF];
+ auto &PrevInstH = MIRBlockInstHashes[&MF];
+ auto &PrevTempIDs = MIRBlockTempIDs[&MF];
+ SmallVector<stable_hash> NewBlkH;
+ SmallVector<unsigned> ChangedBlocks;
+ stable_hash FuncH = 0;
+
+ unsigned BlkIdx = 0;
+ for (const MachineBasicBlock &MBB : MF) {
+ stable_hash BlkH = 0;
+ for (const MachineInstr &MI : MBB)
+ BlkH = stable_hash_combine(BlkH, hashMachineInstr(MI));
+ NewBlkH.push_back(BlkH);
+ FuncH = stable_hash_combine(FuncH, BlkH);
+ if (!SkipUnchanged || BlkIdx >= PrevBlkH.size() ||
+ PrevBlkH[BlkIdx] != BlkH)
+ ChangedBlocks.push_back(BlkIdx);
+ ++BlkIdx;
+ }
+
+ if (SkipUnchanged) {
+ auto It = MIRFunctionHashes.find(&MF);
+ if (It != MIRFunctionHashes.end() && It->second == FuncH)
+ return;
+ MIRFunctionHashes[&MF] = FuncH;
+ } else {
+ MIRFunctionHashes[&MF] = FuncH;
+ }
+
+ if (ChangedBlocks.empty()) {
+ PrevBlkH = std::move(NewBlkH);
+ return;
+ }
+
+ const TargetInstrInfo *TII = MF.getSubtarget().getInstrInfo();
+ ModuleSlotTracker MST(MF.getFunction().getParent());
+ MST.incorporateFunction(MF.getFunction());
+ SmallString<256> InstBuf;
+
+ BlkIdx = 0;
+ unsigned ChangedBlockPos = 0;
+ for (const MachineBasicBlock &MBB : MF) {
+ bool BlockChanged = ChangedBlockPos < ChangedBlocks.size() &&
+ ChangedBlocks[ChangedBlockPos] == BlkIdx;
+ if (BlockChanged) {
+ ++ChangedBlockPos;
+ SmallVector<stable_hash> CurInstH;
+ SmallVector<unsigned> CurTempIDs;
+ auto *OldInstH = (SkipUnchanged && BlkIdx < PrevInstH.size())
+ ? &PrevInstH[BlkIdx]
+ : nullptr;
+ auto *OldTempIDs = (SkipUnchanged && BlkIdx < PrevTempIDs.size())
+ ? &PrevTempIDs[BlkIdx]
+ : nullptr;
+ SmallVector<bool> UsedOldTempIDs;
+ if (OldTempIDs)
+ UsedOldTempIDs.assign(OldTempIDs->size(), false);
+
+ std::string MBBName;
+ raw_string_ostream MBBOS(MBBName);
+ MBB.printName(MBBOS, /*PrintNameFlags=*/0, &MST);
+
+ unsigned InstSeq = 0;
+ unsigned InstIdx = 0;
+ for (const MachineInstr &MI : MBB) {
+ stable_hash CurH = hashMachineInstr(MI);
+ CurInstH.push_back(CurH);
+
+ const DILocation *Loc =
+ MI.getDebugLoc() ? MI.getDebugLoc().get() : nullptr;
+ unsigned CurID = getOrCreateTrackerID(Loc);
+ if (CurID == 0) {
+ int MatchedIdx = -1;
+ if (OldTempIDs && OldInstH) {
+ if (InstIdx < OldTempIDs->size() && InstIdx < OldInstH->size() &&
+ (*OldTempIDs)[InstIdx] != 0 && !UsedOldTempIDs[InstIdx] &&
+ (*OldInstH)[InstIdx] == CurH) {
+ MatchedIdx = InstIdx;
+ } else {
+ int BestIdx = -1;
+ int BestDist = std::numeric_limits<int>::max();
+ bool AmbiguousBest = false;
+ for (size_t J = 0,
+ E = std::min(OldTempIDs->size(), OldInstH->size());
+ J != E; ++J) {
+ if ((*OldTempIDs)[J] == 0 || UsedOldTempIDs[J] ||
+ (*OldInstH)[J] != CurH)
+ continue;
+ int Dist =
+ std::abs(static_cast<int>(J) - static_cast<int>(InstIdx));
+ if (Dist < BestDist) {
+ BestDist = Dist;
+ BestIdx = static_cast<int>(J);
+ AmbiguousBest = false;
+ } else if (Dist == BestDist) {
+ AmbiguousBest = true;
+ }
+ }
+ if (BestIdx >= 0 && !AmbiguousBest)
+ MatchedIdx = BestIdx;
+ }
+ }
+ if (MatchedIdx >= 0) {
+ CurID = (*OldTempIDs)[MatchedIdx];
+ UsedOldTempIDs[MatchedIdx] = true;
+ } else {
+ CurID = NextTrackerID++;
+ }
+ }
+ CurTempIDs.push_back(CurID);
+
+ bool InstChanged = true;
+ auto It = TrackerIDToPrevHash.find(CurID);
+ InstChanged = It == TrackerIDToPrevHash.end() || It->second != CurH;
+ if (InstChanged) {
+ InstBuf.clear();
+ raw_svector_ostream IOS(InstBuf);
+ MI.print(IOS, MST, /*IsStandalone=*/true, /*SkipOpers=*/false,
+ /*SkipDebugLoc=*/true, /*AddNewLine=*/false, TII);
+
+ writeTrackerRecord(CurID, Loc);
+
+ StringRef OpcodeName = TII ? TII->getName(MI.getOpcode()) : "";
+ if (OpcodeName.empty())
+ OpcodeName = "<unknown>";
+ *OS << "I\t" << MF.getName() << '\t' << MBBName << '\t' << InstSeq
+ << '\t' << OpcodeName << '\t' << CurID << '\t' << InstBuf
+ << '\n';
+ }
+ TrackerIDToPrevHash[CurID] = CurH;
+ ++InstSeq;
+ ++InstIdx;
+ }
+
+ if (BlkIdx >= PrevInstH.size())
+ PrevInstH.resize(BlkIdx + 1);
+ PrevInstH[BlkIdx] = std::move(CurInstH);
+ if (BlkIdx >= PrevTempIDs.size())
+ PrevTempIDs.resize(BlkIdx + 1);
+ PrevTempIDs[BlkIdx] = std::move(CurTempIDs);
+ }
+ ++BlkIdx;
+ }
+
+ PrevBlkH = std::move(NewBlkH);
+ }
+
+ void writeMIR(const MachineFunction &MF, unsigned Seq, StringRef Phase,
+ StringRef PassName, bool SkipUnchanged) {
+ writePassRecord(Seq, "mir", Phase, PassName, MF.getName());
+ writeMachineInstructions(MF, SkipUnchanged);
+ }
+
/// Emit one P row for the pass and dispatch the per-function recording
/// work to writeInstructionsInFunction for every function reachable
/// from the IR unit.
@@ -998,7 +1250,7 @@ class IRTrackerRecorder {
/// -> P row, then writeInstructionsInFunction(foo)
void writeIR(Any IR, unsigned Seq, StringRef Phase, StringRef PassName,
StringRef IRUnit, bool SkipUnchanged) {
- writePassRecord(Seq, Phase, PassName, IRUnit);
+ writePassRecord(Seq, "ir", Phase, PassName, IRUnit);
if (const auto *M = unwrapIR<Module>(IR)) {
for (const Function &F : *M)
writeInstructionsInFunction(F, SkipUnchanged);
@@ -1051,6 +1303,8 @@ class IRTrackerRecorder {
}
if (const auto *L = unwrapIR<Loop>(IR))
return FunctionHashes.count(L->getHeader()->getParent());
+ if (const auto *MF = unwrapIR<MachineFunction>(IR))
+ return MIRFunctionHashes.count(MF);
return false;
}
@@ -1084,7 +1338,7 @@ class IRTrackerRecorder {
BlockInstHashes.clear();
BlockTempIDs.clear();
TrackerIDToPrevHash.clear();
- writePassRecord(NextSeq++, "final", "<final>", "[module]");
+ writePassRecord(NextSeq++, "ir", "final", "<final>", "[module]");
for (const Function &F : *LastModule)
writeInstructionsInFunction(F, /*SkipUnchanged=*/false);
}
@@ -1118,6 +1372,16 @@ class IRTrackerRecorder {
/// stream has a baseline against which subsequent per-pass diffs make
/// sense; subsequent invocations are no-ops.
void beforePass(StringRef PassID, Any IR) {
+ if (const auto *MF = unwrapIR<MachineFunction>(IR)) {
+ if (isIgnored(PassID) || !shouldPrintMIR(IR))
+ return;
+ if (!MIRInitialCaptured.insert(MF).second)
+ return;
+ writeMIR(*MF, NextSeq++, "initial", "<initial>",
+ /*SkipUnchanged=*/false);
+ return;
+ }
+
if (isIgnored(PassID) || !shouldPrintIR(IR))
return;
ensureSyntheticLocs(IR);
@@ -1137,6 +1401,23 @@ class IRTrackerRecorder {
/// of changed functions show up as I rows.
void afterPass(StringRef PassID, Any IR, PassInstrumentationCallbacks &PIC,
const PreservedAnalyses &PA) {
+ if (const auto *MF = unwrapIR<MachineFunction>(IR)) {
+ if (isIgnored(PassID) || !shouldPrintMIR(IR))
+ return;
+
+ StringRef PassName = PIC.getPassNameForClassName(PassID);
+ if (PassName.empty())
+ PassName = PassID;
+
+ if (PA.areAllPreserved() && allFunctionsKnown(IR)) {
+ writePassRecord(NextSeq++, "mir", "after", PassName, MF->getName());
+ return;
+ }
+
+ writeMIR(*MF, NextSeq++, "after", PassName, /*SkipUnchanged=*/true);
+ return;
+ }
+
if (isIgnored(PassID) || !shouldPrintIR(IR))
return;
@@ -1158,7 +1439,7 @@ class IRTrackerRecorder {
PassName = PassID;
if (PA.areAllPreserved() && allFunctionsKnown(IR)) {
- writePassRecord(NextSeq++, "after", PassName, getIRName(IR));
+ writePassRecord(NextSeq++, "ir", "after", PassName, getIRName(IR));
return;
}
diff --git a/llvm/test/Other/ir-tracker-db.ll b/llvm/test/Other/ir-tracker-db.ll
index 06c84be427745..525ef72d1e5de 100644
--- a/llvm/test/Other/ir-tracker-db.ll
+++ b/llvm/test/Other/ir-tracker-db.ll
@@ -72,44 +72,44 @@ entry:
; Output is the cost-improvement TSV form: P/T/I rows. T rows carry source
; locations once per tracker ID; I rows reference tracker IDs.
-; ALL: P{{ }}0{{ }}initial{{ }}<initial>{{ }}f
+; ALL: P{{ }}0{{ }}ir{{ }}initial{{ }}<initial>{{ }}f
; ALL-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}8{{ }}3
; ALL-NEXT: I{{ }}f{{ }}entry{{ }}0{{ }}add{{ }}{{[0-9]+}}{{ }}%add = add i32 %x, 1
; ALL-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}9{{ }}3
; ALL-NEXT: I{{ }}f{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %add
-; ALL: P{{ }}1{{ }}after{{ }}instcombine{{ }}f
-; ALL: P{{ }}2{{ }}after{{ }}instcombine{{ }}g
+; ALL: P{{ }}1{{ }}ir{{ }}after{{ }}instcombine{{ }}f
+; ALL: P{{ }}2{{ }}ir{{ }}after{{ }}instcombine{{ }}g
; ALL-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}14{{ }}3
; ALL-NEXT: I{{ }}g{{ }}entry{{ }}0{{ }}shl{{ }}{{[0-9]+}}{{ }}%mul = shl i32 %x, 1
; ALL-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}15{{ }}3
; ALL-NEXT: I{{ }}g{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %mul
-; CROSS: P{{ }}0{{ }}initial{{ }}<initial>{{ }}f
+; CROSS: P{{ }}0{{ }}ir{{ }}initial{{ }}<initial>{{ }}f
; CROSS-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}8{{ }}3
; CROSS-NEXT: I{{ }}f{{ }}entry{{ }}0{{ }}add{{ }}{{[0-9]+}}{{ }}%add = add i32 %x, 1
; CROSS-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}9{{ }}3
; CROSS-NEXT: I{{ }}f{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %add
-; CROSS: P{{ }}2{{ }}after{{ }}instcombine{{ }}h
+; CROSS: P{{ }}2{{ }}ir{{ }}after{{ }}instcombine{{ }}h
; CROSS-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker-other.c{{ }}8{{ }}3
; CROSS-NEXT: I{{ }}h{{ }}entry{{ }}0{{ }}add{{ }}{{[0-9]+}}{{ }}%add = add i32 %x, 1
; CROSS-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker-other.c{{ }}9{{ }}3
; CROSS-NEXT: I{{ }}h{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %add
-; MID: P{{ }}0{{ }}initial{{ }}<initial>{{ }}mid
+; MID: P{{ }}0{{ }}ir{{ }}initial{{ }}<initial>{{ }}mid
; MID-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}20{{ }}3
; MID-NEXT: I{{ }}mid{{ }}entry{{ }}0{{ }}freeze{{ }}{{[0-9]+}}{{ }}%a = freeze i32 %x
; MID-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}21{{ }}3
; MID-NEXT: I{{ }}mid{{ }}entry{{ }}1{{ }}mul{{ }}{{[0-9]+}}{{ }}%b = mul i32 %a, 2
; MID-NEXT: T{{ }}{{[0-9]+}}{{ }}/tmp{{[/\\]}}ir-tracker.c{{ }}22{{ }}3
; MID-NEXT: I{{ }}mid{{ }}entry{{ }}2{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %b
-; MID: P{{ }}1{{ }}after{{ }}instcombine{{ }}mid
+; MID: P{{ }}1{{ }}ir{{ }}after{{ }}instcombine{{ }}mid
; MID-NEXT: I{{ }}mid{{ }}entry{{ }}1{{ }}shl{{ }}{{[0-9]+}}{{ }}%b = shl i32 %a, 1
-; SSA: P{{ }}0{{ }}initial{{ }}<initial>{{ }}ssa
+; SSA: P{{ }}0{{ }}ir{{ }}initial{{ }}<initial>{{ }}ssa
; SSA: I{{ }}ssa{{ }}entry{{ }}2{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %b
-; SSA: P{{ }}1{{ }}after{{ }}instcombine{{ }}ssa
+; SSA: P{{ }}1{{ }}ir{{ }}after{{ }}instcombine{{ }}ssa
; SSA-NEXT: I{{ }}ssa{{ }}entry{{ }}1{{ }}ret{{ }}{{[0-9]+}}{{ }}ret i32 %a
-; FILTER: P{{ }}0{{ }}initial{{ }}<initial>{{ }}f
+; FILTER: P{{ }}0{{ }}ir{{ }}initial{{ }}<initial>{{ }}f
; FILTER: I{{ }}f{{ }}entry{{ }}0{{ }}add
; FILTER: I{{ }}f{{ }}entry{{ }}1{{ }}ret
; FILTER-NOT: I{{ }}g{{ }}
diff --git a/llvm/test/tools/llvm-ir-tracker/kind-tsv.test b/llvm/test/tools/llvm-ir-tracker/kind-tsv.test
new file mode 100644
index 0000000000000..da4aa4f8f11ab
--- /dev/null
+++ b/llvm/test/tools/llvm-ir-tracker/kind-tsv.test
@@ -0,0 +1,17 @@
+RUN: rm -f %t.tsv %t.db
+RUN: printf 'P\t0\tir\tinitial\t<initial>\tf\nT\t1\t/tmp/kind.c\t8\t3\nI\tf\tentry\t0\tadd\t1\tadd i32 1, 2\nP\t1\tmir\tinitial\t<initial>\tf\nI\tf\tbb.0\t0\tADD32ri\t1\tv0 = ADD32ri v1, 1\n' > %t.tsv
+RUN: %ir-tracker build --input %t.tsv --db %t.db | FileCheck %s --check-prefix=BUILD
+RUN: %ir-tracker passes --db %t.db | FileCheck %s --check-prefix=PASSES
+RUN: %ir-tracker show --db %t.db --file kind.c --line 8 --kind ir | FileCheck %s --check-prefix=SHOW-IR
+RUN: %ir-tracker show --db %t.db --file kind.c --line 8 --kind mir | FileCheck %s --check-prefix=SHOW-MIR
+
+BUILD: built {{.*}} 2 pass snapshots, 2 instruction rows
+PASSES: 0 id={{[0-9]+}} initial '<initial>' on 'f'
+PASSES: 1 id={{[0-9]+}} [mir] initial '<initial>' on 'f'
+PASSES: total passes recorded: 2
+SHOW-IR: seq=0 '<initial>' on 'f'
+SHOW-IR-NEXT: function f, block entry:
+SHOW-IR-NEXT: add i32 1, 2
+SHOW-MIR: seq=1 [mir] '<initial>' on 'f'
+SHOW-MIR-NEXT: function f, block bb.0:
+SHOW-MIR-NEXT: v0 = ADD32ri v1, 1
diff --git a/llvm/test/tools/llvm-ir-tracker/mir-newpm.ll b/llvm/test/tools/llvm-ir-tracker/mir-newpm.ll
new file mode 100644
index 0000000000000..770c67412a100
--- /dev/null
+++ b/llvm/test/tools/llvm-ir-tracker/mir-newpm.ll
@@ -0,0 +1,31 @@
+; RUN: rm -f %t.tsv %t.db
+; RUN: llc -enable-new-pm -mtriple=x86_64-unknown-linux-gnu -filetype=null -ir-tracker-output=%t.tsv %s
+; RUN: FileCheck %s --input-file=%t.tsv --check-prefix=TSV
+; RUN: %ir-tracker build --input %t.tsv --db %t.db
+; RUN: %ir-tracker show --db %t.db --file mir-newpm.c --line 3 --kind mir --all-passes | FileCheck %s --check-prefix=SHOW
+
+define i32 @f(i32 %x) !dbg !6 {
+entry:
+ %add = add i32 %x, 1, !dbg !8
+ ret i32 %add, !dbg !9
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "ir-tracker-test", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "mir-newpm.c", directory: "/tmp")
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!4 = !DISubroutineType(types: !5)
+!5 = !{!3, !3}
+!6 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!8 = !DILocation(line: 3, column: 3, scope: !6)
+!9 = !DILocation(line: 4, column: 3, scope: !6)
+
+; TSV: P{{ }}{{[0-9]+}}{{ }}mir{{ }}initial{{ }}<initial>{{ }}f
+; TSV: I{{ }}f{{ }}{{.*}}{{ }}{{[0-9]+}}{{ }}{{[^ ]+}}{{ }}{{[0-9]+}}{{ }}{{.*}}
+; TSV: P{{ }}{{[0-9]+}}{{ }}mir{{ }}after{{ }}{{[^ ]+}}{{ }}f
+
+; SHOW: seq={{[0-9]+}} [mir] '{{[^']+}}' on 'f'
+; SHOW: function f, block
diff --git a/llvm/tools/ir-tracker/ir-tracker.py b/llvm/tools/ir-tracker/ir-tracker.py
index 6f0fdefa260fe..3c0f8cc0cabca 100644
--- a/llvm/tools/ir-tracker/ir-tracker.py
+++ b/llvm/tools/ir-tracker/ir-tracker.py
@@ -23,7 +23,7 @@ def cmd_passes(args: argparse.Namespace) -> int:
if not con:
return 1
try:
- return irtrackdb.run_passes(con)
+ return irtrackdb.run_passes(con, args.kind)
finally:
con.close()
@@ -34,7 +34,7 @@ def cmd_trace(args: argparse.Namespace) -> int:
return 1
try:
return irtrackdb.run_trace(
- con, args.file, args.line, args.col, args.opcode or ""
+ con, args.file, args.line, args.col, args.opcode or "", args.kind
)
finally:
con.close()
@@ -53,6 +53,7 @@ def cmd_show(args: argparse.Namespace) -> int:
args.opcode or "",
args.seq,
args.all_passes,
+ args.kind,
)
finally:
con.close()
@@ -100,6 +101,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
passes = sub.add_parser("passes", help="List recorded passes")
passes.add_argument("--db", required=True)
+ passes.add_argument("--kind", choices=["ir", "mir", "all"], default="all")
passes.set_defaults(func=cmd_passes)
trace = sub.add_parser("trace", help="Find first/final pass for a source line")
@@ -108,6 +110,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
trace.add_argument("--line", required=True)
trace.add_argument("--col", type=int, default=None)
trace.add_argument("--opcode", default="")
+ trace.add_argument("--kind", choices=["ir", "mir", "all"], default="ir")
trace.set_defaults(func=cmd_trace)
show = sub.add_parser("show", help="Show tracked instructions for a source line")
@@ -118,6 +121,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
show.add_argument("--opcode", default="")
show.add_argument("--seq", type=int, default=-1)
show.add_argument("--all-passes", action="store_true")
+ show.add_argument("--kind", choices=["ir", "mir", "all"], default="ir")
show.set_defaults(func=cmd_show)
html_p = sub.add_parser(
diff --git a/llvm/tools/ir-tracker/irtrackdb.py b/llvm/tools/ir-tracker/irtrackdb.py
index 1eb893fe09284..7d5a13ffbdcab 100644
--- a/llvm/tools/ir-tracker/irtrackdb.py
+++ b/llvm/tools/ir-tracker/irtrackdb.py
@@ -14,7 +14,8 @@
T_INSTR = "ir_tracker_instructions"
T_META = "ir_tracker_meta"
T_PASSES = "ir_tracker_passes"
-SCHEMA_VERSION = 1
+SCHEMA_VERSION = 2
+VALID_KINDS = {"ir", "mir", "all"}
def open_db_readonly(path: str) -> Optional[sqlite3.Connection]:
@@ -58,6 +59,7 @@ def init_schema(con: sqlite3.Connection) -> None:
CREATE TABLE {T_PASSES} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
seq INTEGER NOT NULL,
+ kind TEXT NOT NULL,
phase TEXT NOT NULL,
pass_class TEXT NOT NULL,
ir_unit TEXT NOT NULL
@@ -103,13 +105,18 @@ def _get_or_create_file_id(
def _insert_pass(
- con: sqlite3.Connection, seq: int, phase: str, pass_name: str, ir_unit: str
+ con: sqlite3.Connection,
+ seq: int,
+ kind: str,
+ phase: str,
+ pass_name: str,
+ ir_unit: str,
) -> int:
return int(
con.execute(
- f"INSERT INTO {T_PASSES}(seq, phase, pass_class, ir_unit) "
- f"VALUES(?, ?, ?, ?)",
- (seq, phase, pass_name, ir_unit),
+ f"INSERT INTO {T_PASSES}(seq, kind, phase, pass_class, ir_unit) "
+ f"VALUES(?, ?, ?, ?, ?)",
+ (seq, kind, phase, pass_name, ir_unit),
).lastrowid
)
@@ -173,18 +180,35 @@ def _build_db_from_tsv(con: sqlite3.Connection, input_path: str) -> tuple[int, i
tag = line[0]
if tag == "P":
parts = line.split("\t")
- if len(parts) != 5:
+ if len(parts) == 5:
+ seq_s, kind, phase, pass_name, ir_unit = (
+ parts[1],
+ "ir",
+ parts[2],
+ parts[3],
+ parts[4],
+ )
+ elif len(parts) == 6:
+ seq_s, kind, phase, pass_name, ir_unit = parts[1:]
+ else:
print(
f"ir-tracker: malformed pass row at line {line_no}",
file=sys.stderr,
)
raise ValueError("malformed pass row")
+ if kind not in {"ir", "mir"}:
+ print(
+ f"ir-tracker: invalid pass kind at line {line_no}: {kind!r}",
+ file=sys.stderr,
+ )
+ raise ValueError("invalid pass kind")
current_pass_id = _insert_pass(
con,
- _parse_int("pass sequence", parts[1], line_no),
- parts[2],
- parts[3],
- parts[4],
+ _parse_int("pass sequence", seq_s, line_no),
+ kind,
+ phase,
+ pass_name,
+ ir_unit,
)
n_passes += 1
continue
@@ -306,15 +330,35 @@ def resolve_file_ids(con: sqlite3.Connection, file_pat: str) -> List[int]:
return ids
-def run_passes(con: sqlite3.Connection) -> int:
+def _check_kind(kind: str) -> bool:
+ if kind not in VALID_KINDS:
+ print("ir-tracker: --kind must be one of ir, mir, all", file=sys.stderr)
+ return False
+ return True
+
+
+def _kind_clause(kind: str, table_alias: str = "p") -> tuple[str, List[object]]:
+ if kind == "all":
+ return "", []
+ return f" AND {table_alias}.kind = ?", [kind]
+
+
+def run_passes(con: sqlite3.Connection, kind: str) -> int:
+ if not _check_kind(kind):
+ return 1
+ where_sql, params = _kind_clause(kind)
+ if where_sql:
+ where_sql = "WHERE" + where_sql[4:]
rows = con.execute(
- f"SELECT id, seq, phase, pass_class, ir_unit FROM {T_PASSES} ORDER BY seq"
+ f"SELECT id, seq, kind, phase, pass_class, ir_unit FROM {T_PASSES} "
+ f"{where_sql} ORDER BY seq",
+ params,
).fetchall()
for row in rows:
- print(
- f"{int(row['seq']):5d} id={int(row['id']):<6} "
- f"{row['phase']} '{row['pass_class']}' on '{row['ir_unit']}'"
- )
+ prefix = f"{int(row['seq']):5d} id={int(row['id']):<6} "
+ if row["kind"] != "ir":
+ prefix += f"[{row['kind']}] "
+ print(f"{prefix}{row['phase']} '{row['pass_class']}' on '{row['ir_unit']}'")
print(f"total passes recorded: {len(rows)}")
return 0
@@ -348,10 +392,13 @@ def run_trace(
line_s: str,
trace_col: Optional[int],
trace_opcode: str,
+ kind: str,
) -> int:
if get_schema_version(con) < 1:
print("ir-tracker: unsupported schema version", file=sys.stderr)
return 1
+ if not _check_kind(kind):
+ return 1
file_ids = resolve_file_ids(con, file_pat)
if not file_ids:
@@ -364,12 +411,13 @@ def run_trace(
return 1
where_sql, params = _filter_clause(file_ids, line, trace_col, trace_opcode)
+ kind_sql, kind_params = _kind_clause(kind)
row = con.execute(
f"SELECT MAX(p.seq) AS max_seq "
f"FROM {T_INSTR} i JOIN {T_PASSES} p ON i.pass_id = p.id "
- f"WHERE {where_sql}",
- params,
+ f"WHERE {where_sql}{kind_sql}",
+ [*params, *kind_params],
).fetchone()
if not row or row["max_seq"] is None:
print("ir-tracker: no matching instructions found", file=sys.stderr)
@@ -379,8 +427,8 @@ def run_trace(
count_row = con.execute(
f"SELECT COUNT(*) AS c "
f"FROM {T_INSTR} i JOIN {T_PASSES} p ON i.pass_id = p.id "
- f"WHERE p.seq = ? AND {where_sql}",
- [max_seq, *params],
+ f"WHERE p.seq = ? AND {where_sql}{kind_sql}",
+ [max_seq, *params, *kind_params],
).fetchone()
print(
f"Matches at final pass (seq={max_seq}): {int(count_row['c'])} "
@@ -388,22 +436,25 @@ def run_trace(
)
first_row = con.execute(
- f"SELECT p.seq, p.pass_class, p.ir_unit, COUNT(*) AS c "
+ f"SELECT p.seq, p.kind, p.pass_class, p.ir_unit, COUNT(*) AS c "
f"FROM {T_INSTR} i JOIN {T_PASSES} p ON i.pass_id = p.id "
- f"WHERE {where_sql} GROUP BY p.id ORDER BY p.seq ASC LIMIT 1",
- params,
+ f"WHERE {where_sql}{kind_sql} GROUP BY p.id ORDER BY p.seq ASC LIMIT 1",
+ [*params, *kind_params],
).fetchone()
if first_row:
+ pass_text = f"{first_row['pass_class']} on {first_row['ir_unit']}"
+ if first_row["kind"] != "ir":
+ pass_text = f"[{first_row['kind']}] {pass_text}"
print(
f"First pass with any matching instruction: seq={int(first_row['seq'])} "
- f"{first_row['pass_class']} on {first_row['ir_unit']} "
- f"({int(first_row['c'])} row(s))"
+ f"{pass_text} ({int(first_row['c'])} row(s))"
)
return 0
class ShowInstRow(NamedTuple):
seq: int
+ kind: str
pass_class: str
ir_unit: str
function: str
@@ -415,7 +466,8 @@ def _print_group(rows: Sequence[ShowInstRow]) -> None:
if not rows:
return
head = rows[0]
- print(f"seq={head.seq} '{head.pass_class}' on '{head.ir_unit}'")
+ kind_text = "" if head.kind == "ir" else f" [{head.kind}]"
+ print(f"seq={head.seq}{kind_text} '{head.pass_class}' on '{head.ir_unit}'")
current_func = ""
current_bb = ""
for row in rows:
@@ -434,10 +486,13 @@ def run_show(
trace_opcode: str,
seq: int,
show_all_passes: bool,
+ kind: str,
) -> int:
if get_schema_version(con) < 1:
print("ir-tracker: unsupported schema version", file=sys.stderr)
return 1
+ if not _check_kind(kind):
+ return 1
if show_all_passes and seq >= 0:
print(
"ir-tracker: --all-passes and --seq are mutually exclusive", file=sys.stderr
@@ -455,28 +510,30 @@ def run_show(
return 1
where_sql, params = _filter_clause(file_ids, line, trace_col, trace_opcode)
+ kind_sql, kind_params = _kind_clause(kind)
seq_sql = ""
if seq >= 0:
seq_sql = " AND p.seq = ?"
- params = [*params, seq]
+ kind_params = [*kind_params, seq]
query = (
- f"SELECT p.seq, p.pass_class, p.ir_unit, i.function, i.basicblock, "
+ f"SELECT p.seq, p.kind, p.pass_class, p.ir_unit, i.function, i.basicblock, "
f"i.inst_seq, i.inst_text "
f"FROM {T_INSTR} i JOIN {T_PASSES} p ON i.pass_id = p.id "
- f"WHERE {where_sql}{seq_sql} "
+ f"WHERE {where_sql}{kind_sql}{seq_sql} "
f"ORDER BY p.seq ASC, i.function ASC, i.basicblock ASC, i.inst_seq ASC"
)
rows = [
ShowInstRow(
int(row["seq"]),
+ row["kind"] or "",
row["pass_class"] or "",
row["ir_unit"] or "",
row["function"] or "",
row["basicblock"] or "",
row["inst_text"] or "",
)
- for row in con.execute(query, params)
+ for row in con.execute(query, [*params, *kind_params])
]
if not rows:
print("ir-tracker: no matching instructions found", file=sys.stderr)
diff --git a/llvm/tools/ir-tracker/irtrackhtml.py b/llvm/tools/ir-tracker/irtrackhtml.py
index 7d2658555ded2..4e6da3ca2ff83 100644
--- a/llvm/tools/ir-tracker/irtrackhtml.py
+++ b/llvm/tools/ir-tracker/irtrackhtml.py
@@ -212,8 +212,9 @@ def _functions(con: sqlite3.Connection) -> List[Tuple[str, str, int, int, str]]:
f"SELECT i.function AS function, f.path AS file_path, "
f"COUNT(*) AS n_insts, COUNT(DISTINCT i.pass_id) AS n_passes "
f"FROM {irtrackdb.T_INSTR} i "
+ f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
f"JOIN {irtrackdb.T_FILES} f ON i.file_id = f.id "
- f"WHERE i.function != '' "
+ f"WHERE i.function != '' AND p.kind = 'ir' "
f"GROUP BY i.function, f.id "
f"ORDER BY f.path, i.function"
).fetchall()
@@ -239,7 +240,7 @@ def _initial_seq(con: sqlite3.Connection, function: str) -> int:
f"SELECT MIN(p.seq) AS s "
f"FROM {irtrackdb.T_INSTR} i "
f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
- f"WHERE i.function = ? AND p.phase <> 'final'",
+ f"WHERE i.function = ? AND p.kind = 'ir' AND p.phase <> 'final'",
(function,),
).fetchone()
return -1 if row is None or row["s"] is None else int(row["s"])
@@ -253,7 +254,7 @@ def _final_seq(con: sqlite3.Connection, function: str) -> int:
f"SELECT MAX(p.seq) AS s "
f"FROM {irtrackdb.T_INSTR} i "
f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
- f"WHERE i.function = ? AND p.phase = 'final'",
+ f"WHERE i.function = ? AND p.kind = 'ir' AND p.phase = 'final'",
(function,),
).fetchone()
return -1 if row is None or row["s"] is None else int(row["s"])
@@ -268,7 +269,7 @@ def _final_ir_rows(
f"FROM {irtrackdb.T_INSTR} i "
f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
f"JOIN {irtrackdb.T_FILES} f ON i.file_id = f.id "
- f"WHERE i.function = ? AND p.seq = ? "
+ f"WHERE i.function = ? AND p.kind = 'ir' AND p.seq = ? "
f"ORDER BY i.id",
(function, seq),
).fetchall()
@@ -288,7 +289,7 @@ def _initial_ir(con: sqlite3.Connection, function: str, seq: int) -> List[sqlite
f"FROM {irtrackdb.T_INSTR} i "
f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
f"JOIN {irtrackdb.T_FILES} f ON i.file_id = f.id "
- f"WHERE i.function = ? AND p.seq = ? "
+ f"WHERE i.function = ? AND p.kind = 'ir' AND p.seq = ? "
f"ORDER BY i.id",
(function, seq),
).fetchall()
@@ -303,7 +304,7 @@ def _history(
f"FROM {irtrackdb.T_INSTR} i "
f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
f"JOIN {irtrackdb.T_FILES} f ON i.file_id = f.id "
- f"WHERE i.function = ? "
+ f"WHERE i.function = ? AND p.kind = 'ir' "
f"ORDER BY i.line, i.col, p.seq, i.basicblock, i.inst_seq",
(function,),
).fetchall()
@@ -314,7 +315,7 @@ def _history(
f"SELECT MAX(p.seq) AS s "
f"FROM {irtrackdb.T_INSTR} i "
f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
- f"WHERE i.function = ?",
+ f"WHERE i.function = ? AND p.kind = 'ir'",
(function,),
).fetchone()
func_max_seq = (
@@ -663,10 +664,16 @@ def generate_html(
return 1
pass_count = int(
- con.execute(f"SELECT COUNT(*) AS c FROM {irtrackdb.T_PASSES}").fetchone()["c"]
+ con.execute(
+ f"SELECT COUNT(*) AS c FROM {irtrackdb.T_PASSES} WHERE kind = 'ir'"
+ ).fetchone()["c"]
)
inst_count = int(
- con.execute(f"SELECT COUNT(*) AS c FROM {irtrackdb.T_INSTR}").fetchone()["c"]
+ con.execute(
+ f"SELECT COUNT(*) AS c FROM {irtrackdb.T_INSTR} i "
+ f"JOIN {irtrackdb.T_PASSES} p ON i.pass_id = p.id "
+ f"WHERE p.kind = 'ir'"
+ ).fetchone()["c"]
)
with open(os.path.join(output_dir, "style.css"), "w", encoding="utf-8") as f:
More information about the llvm-commits
mailing list