[lld] [llvm] [DTLTO] Add DTLTO time traces (and llvm-lto2 time tracing to test) (PR #171600)

via llvm-commits llvm-commits at lists.llvm.org
Wed Dec 10 03:03:00 PST 2025


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lto

Author: Ben Dunbobbin (bd1976bris)

<details>
<summary>Changes</summary>

The primary goal of this patch is to add a comprehensive set of DTLTO time traces.

I also have implemented support for time traces in llvm-lto2, to allow for adding a test via Lit.

---
Full diff: https://github.com/llvm/llvm-project/pull/171600.diff


3 Files Affected:

- (modified) llvm/lib/LTO/LTO.cpp (+88-69) 
- (added) llvm/test/ThinLTO/X86/dtlto/timetrace.ll (+75) 
- (modified) llvm/tools/llvm-lto2/llvm-lto2.cpp (+29-1) 


``````````diff
diff --git a/llvm/lib/LTO/LTO.cpp b/llvm/lib/LTO/LTO.cpp
index a02af59600c44..32bb415ccd518 100644
--- a/llvm/lib/LTO/LTO.cpp
+++ b/llvm/lib/LTO/LTO.cpp
@@ -2289,13 +2289,13 @@ class OutOfProcessThinBackend : public CGThinBackend {
       const FunctionImporter::ExportSetTy &ExportList,
       const std::map<GlobalValue::GUID, GlobalValue::LinkageTypes>
           &ResolvedODR) {
-
-    llvm::TimeTraceScope timeScope(
-        "Run ThinLTO backend thread (out-of-process)", J.ModuleID);
-
-    if (auto E = emitFiles(ImportList, J.ModuleID, J.ModuleID.str(),
-                           J.SummaryIndexPath, J.ImportsFiles))
-      return E;
+    {
+      TimeTraceScope TimeScope("Emit individual index for DTLTO",
+                               J.SummaryIndexPath);
+      if (auto E = emitFiles(ImportList, J.ModuleID, J.ModuleID.str(),
+                             J.SummaryIndexPath, J.ImportsFiles))
+        return E;
+    }
 
     if (!Cache.isValid() || !CombinedIndex.modulePaths().count(J.ModuleID) ||
         all_of(CombinedIndex.getModuleHash(J.ModuleID),
@@ -2304,6 +2304,7 @@ class OutOfProcessThinBackend : public CGThinBackend {
       // no module hash.
       return Error::success();
 
+    TimeTraceScope TimeScope("Check cache for DTLTO", J.SummaryIndexPath);
     const GVSummaryMapTy &DefinedGlobals =
         ModuleToDefinedGVSummaries.find(J.ModuleID)->second;
 
@@ -2363,6 +2364,10 @@ class OutOfProcessThinBackend : public CGThinBackend {
             const FunctionImporter::ExportSetTy &ExportList,
             const std::map<GlobalValue::GUID, GlobalValue::LinkageTypes>
                 &ResolvedODR) {
+          if (LLVM_ENABLE_THREADS && Conf.TimeTraceEnabled)
+            timeTraceProfilerInitialize(
+                Conf.TimeTraceGranularity,
+                "Emit individual index and check cache for DTLTO");
           Error E =
               runThinLTOBackendThread(J, ImportList, ExportList, ResolvedODR);
           if (E) {
@@ -2372,6 +2377,8 @@ class OutOfProcessThinBackend : public CGThinBackend {
             else
               Err = std::move(E);
           }
+          if (LLVM_ENABLE_THREADS && Conf.TimeTraceEnabled)
+            timeTraceProfilerFinishThread();
         },
         std::ref(J), std::ref(ImportList), std::ref(ExportList),
         std::ref(ResolvedODR));
@@ -2520,6 +2527,7 @@ class OutOfProcessThinBackend : public CGThinBackend {
       return std::move(*Err);
 
     auto CleanPerJobFiles = llvm::make_scope_exit([&] {
+      llvm::TimeTraceScope TimeScope("Remove DTLTO temporary files");
       if (!SaveTemps)
         for (auto &Job : Jobs) {
           removeFile(Job.NativeObjectPath);
@@ -2533,76 +2541,87 @@ class OutOfProcessThinBackend : public CGThinBackend {
     buildCommonRemoteCompilerOptions();
 
     SString JsonFile = sys::path::parent_path(LinkerOutputFile);
-    sys::path::append(JsonFile, sys::path::stem(LinkerOutputFile) + "." + UID +
-                                    ".dist-file.json");
-    if (!emitDistributorJson(JsonFile))
-      return make_error<StringError>(
-          BCError + "failed to generate distributor JSON script: " + JsonFile,
-          inconvertibleErrorCode());
+    {
+      llvm::TimeTraceScope TimeScope("Emit DTLTO JSON");
+      sys::path::append(JsonFile, sys::path::stem(LinkerOutputFile) + "." +
+                                      UID + ".dist-file.json");
+      if (!emitDistributorJson(JsonFile))
+        return make_error<StringError>(
+            BCError + "failed to generate distributor JSON script: " + JsonFile,
+            inconvertibleErrorCode());
+    }
     auto CleanJson = llvm::make_scope_exit([&] {
       if (!SaveTemps)
         removeFile(JsonFile);
     });
 
-    // Checks if we have any jobs that don't have corresponding cache entries.
-    if (CachedJobs.load() < Jobs.size()) {
-      SmallVector<StringRef, 3> Args = {DistributorPath};
-      llvm::append_range(Args, DistributorArgs);
-      Args.push_back(JsonFile);
-      std::string ErrMsg;
-      if (sys::ExecuteAndWait(Args[0], Args,
-                              /*Env=*/std::nullopt, /*Redirects=*/{},
-                              /*SecondsToWait=*/0, /*MemoryLimit=*/0,
-                              &ErrMsg)) {
-        return make_error<StringError>(
-            BCError + "distributor execution failed" +
-                (!ErrMsg.empty() ? ": " + ErrMsg + Twine(".") : Twine(".")),
-            inconvertibleErrorCode());
+    {
+      llvm::TimeTraceScope TimeScope("Execute DTLTO distributor",
+                                     DistributorPath);
+      // Checks if we have any jobs that don't have corresponding cache entries.
+      if (CachedJobs.load() < Jobs.size()) {
+        SmallVector<StringRef, 3> Args = {DistributorPath};
+        llvm::append_range(Args, DistributorArgs);
+        Args.push_back(JsonFile);
+        std::string ErrMsg;
+        if (sys::ExecuteAndWait(Args[0], Args,
+                                /*Env=*/std::nullopt, /*Redirects=*/{},
+                                /*SecondsToWait=*/0, /*MemoryLimit=*/0,
+                                &ErrMsg)) {
+          return make_error<StringError>(
+              BCError + "distributor execution failed" +
+                  (!ErrMsg.empty() ? ": " + ErrMsg + Twine(".") : Twine(".")),
+              inconvertibleErrorCode());
+        }
       }
     }
 
-    for (auto &Job : Jobs) {
-      if (!Job.CacheKey.empty() && Job.Cached) {
-        assert(Cache.isValid());
-        continue;
-      }
-      // Load the native object from a file into a memory buffer
-      // and store its contents in the output buffer.
-      auto ObjFileMbOrErr =
-          MemoryBuffer::getFile(Job.NativeObjectPath, /*IsText=*/false,
-                                /*RequiresNullTerminator=*/false);
-      if (std::error_code EC = ObjFileMbOrErr.getError())
-        return make_error<StringError>(
-            BCError + "cannot open native object file: " +
-                Job.NativeObjectPath + ": " + EC.message(),
-            inconvertibleErrorCode());
-
-      MemoryBufferRef ObjFileMbRef = ObjFileMbOrErr->get()->getMemBufferRef();
-      if (Cache.isValid()) {
-        // Cache hits are taken care of earlier. At this point, we could only
-        // have cache misses.
-        assert(Job.CacheAddStream);
-        // Obtain a file stream for a storing a cache entry.
-        auto CachedFileStreamOrErr = Job.CacheAddStream(Job.Task, Job.ModuleID);
-        if (!CachedFileStreamOrErr)
-          return joinErrors(
-              CachedFileStreamOrErr.takeError(),
-              createStringError(inconvertibleErrorCode(),
-                                "Cannot get a cache file stream: %s",
-                                Job.NativeObjectPath.data()));
-        // Store a file buffer into the cache stream.
-        auto &CacheStream = *(CachedFileStreamOrErr->get());
-        *(CacheStream.OS) << ObjFileMbRef.getBuffer();
-        if (Error Err = CacheStream.commit())
-          return Err;
-      } else {
-        auto StreamOrErr = AddStream(Job.Task, Job.ModuleID);
-        if (Error Err = StreamOrErr.takeError())
-          report_fatal_error(std::move(Err));
-        auto &Stream = *StreamOrErr->get();
-        *Stream.OS << ObjFileMbRef.getBuffer();
-        if (Error Err = Stream.commit())
-          report_fatal_error(std::move(Err));
+    {
+      llvm::TimeTraceScope FilesScope("Add DTLTO files to the link");
+      for (auto &Job : Jobs) {
+        if (!Job.CacheKey.empty() && Job.Cached) {
+          assert(Cache.isValid());
+          continue;
+        }
+        // Load the native object from a file into a memory buffer
+        // and store its contents in the output buffer.
+        auto ObjFileMbOrErr =
+            MemoryBuffer::getFile(Job.NativeObjectPath, /*IsText=*/false,
+                                  /*RequiresNullTerminator=*/false);
+        if (std::error_code EC = ObjFileMbOrErr.getError())
+          return make_error<StringError>(
+              BCError + "cannot open native object file: " +
+                  Job.NativeObjectPath + ": " + EC.message(),
+              inconvertibleErrorCode());
+
+        MemoryBufferRef ObjFileMbRef = ObjFileMbOrErr->get()->getMemBufferRef();
+        if (Cache.isValid()) {
+          // Cache hits are taken care of earlier. At this point, we could only
+          // have cache misses.
+          assert(Job.CacheAddStream);
+          // Obtain a file stream for a storing a cache entry.
+          auto CachedFileStreamOrErr =
+              Job.CacheAddStream(Job.Task, Job.ModuleID);
+          if (!CachedFileStreamOrErr)
+            return joinErrors(
+                CachedFileStreamOrErr.takeError(),
+                createStringError(inconvertibleErrorCode(),
+                                  "Cannot get a cache file stream: %s",
+                                  Job.NativeObjectPath.data()));
+          // Store a file buffer into the cache stream.
+          auto &CacheStream = *(CachedFileStreamOrErr->get());
+          *(CacheStream.OS) << ObjFileMbRef.getBuffer();
+          if (Error Err = CacheStream.commit())
+            return Err;
+        } else {
+          auto StreamOrErr = AddStream(Job.Task, Job.ModuleID);
+          if (Error Err = StreamOrErr.takeError())
+            report_fatal_error(std::move(Err));
+          auto &Stream = *StreamOrErr->get();
+          *Stream.OS << ObjFileMbRef.getBuffer();
+          if (Error Err = Stream.commit())
+            report_fatal_error(std::move(Err));
+        }
       }
     }
     return Error::success();
diff --git a/llvm/test/ThinLTO/X86/dtlto/timetrace.ll b/llvm/test/ThinLTO/X86/dtlto/timetrace.ll
new file mode 100644
index 0000000000000..cab9fa04a3bb9
--- /dev/null
+++ b/llvm/test/ThinLTO/X86/dtlto/timetrace.ll
@@ -0,0 +1,75 @@
+; Test that the LLD produces expected time trace output for DTLTO.
+
+RUN: rm -rf %t && split-file %s %t && cd %t
+
+; Generate bitcode files with summary.
+RUN: opt -thinlto-bc t1.ll -o t1.bc
+RUN: opt -thinlto-bc t2.ll -o t2.bc
+
+; Generate fake object files for mock.py to return.
+RUN: touch t1.o t2.o
+
+; Perform DTLTO and generate a time trace. mock.py does not do any compilation,
+; instead it simply writes the contents of the object files supplied on the
+; command line into the output object files in job order.
+RUN: llvm-lto2 run t1.bc t2.bc -o t.o \
+RUN:   -dtlto-distributor=%python \
+RUN:   -dtlto-distributor-arg=%llvm_src_root/utils/dtlto/mock.py,t1.o,t2.o \
+RUN:   --time-trace --time-trace-granularity=0 --time-trace-file=%t.json \
+RUN:   -r=t1.bc,t1,px \
+RUN:   -r=t2.bc,t2,px
+RUN: %python filter_order_and_pprint.py %t.json | FileCheck %s
+
+## Check that DTLTO events are recorded.
+CHECK-NOT:  "name"
+CHECK:      "name": "Add DTLTO files to the link"
+CHECK-SAME:   "pid": [[#PID:]],
+CHECK-NEXT: "name": "Emit DTLTO JSON"
+CHECK-NEXT: "name": "Emit individual index for DTLTO"
+CHECK-SAME:   t1.1.[[#PID]].native.o.thinlto.bc"
+CHECK-NEXT: "name": "Emit individual index for DTLTO"
+CHECK-SAME:   t2.2.[[#PID]].native.o.thinlto.bc"
+CHECK-NEXT: "name": "Execute DTLTO distributor", "{{.*}}"
+CHECK-NEXT: "name": "Remove DTLTO temporary files"
+CHECK-NEXT: "name": "Total Add DTLTO files to the link"
+CHECK-SAME:   "count": 1,
+CHECK-NEXT: "name": "Total Emit DTLTO JSON"
+CHECK-SAME:   "count": 1,
+CHECK-NEXT: "name": "Total Emit individual index for DTLTO"
+CHECK-SAME:   "count": 2,
+CHECK-NEXT: "name": "Total Execute DTLTO distributor"
+CHECK-SAME:   "count": 1,
+CHECK-NEXT: "name": "Total Remove DTLTO temporary files"
+CHECK-SAME:   "count": 1,
+CHECK-NOT:  "name"
+
+;--- t1.ll
+target triple = "x86_64-unknown-linux-gnu"
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+
+define void @t1() {
+  ret void
+}
+
+;--- t2.ll
+target triple = "x86_64-unknown-linux-gnu"
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+
+define void @t2() {
+  ret void
+}
+
+#--- filter_order_and_pprint.py
+import json, sys
+
+data = json.load(open(sys.argv[1], "r", encoding="utf-8"))
+
+# Get DTLTO events.
+events = [e for e in data["traceEvents"] if "DTLTO" in e["name"]]
+events.sort(key=lambda e: (e["name"], str(e.get("args", {}).get("detail", ""))))
+
+# Print an event per line. Ensure 'name' is the first key.
+for ev in events:
+    name = ev.pop("name")
+    print(json.dumps({"name": name, **ev}))
+
diff --git a/llvm/tools/llvm-lto2/llvm-lto2.cpp b/llvm/tools/llvm-lto2/llvm-lto2.cpp
index 399306f39daeb..ca6864cbf9b91 100644
--- a/llvm/tools/llvm-lto2/llvm-lto2.cpp
+++ b/llvm/tools/llvm-lto2/llvm-lto2.cpp
@@ -15,6 +15,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "llvm/ADT/ScopeExit.h"
 #include "llvm/Bitcode/BitcodeReader.h"
 #include "llvm/CodeGen/CommandFlags.h"
 #include "llvm/IR/DiagnosticPrinter.h"
@@ -28,6 +29,7 @@
 #include "llvm/Support/PluginLoader.h"
 #include "llvm/Support/TargetSelect.h"
 #include "llvm/Support/Threading.h"
+#include "llvm/Support/TimeProfiler.h"
 #include <atomic>
 
 using namespace llvm;
@@ -232,6 +234,19 @@ static cl::opt<bool>
     AllVtablesHaveTypeInfos("all-vtables-have-type-infos", cl::Hidden,
                             cl::desc("All vtables have type infos"));
 
+static cl::opt<bool> TimeTrace("time-trace", cl::desc("Record time trace"));
+
+static cl::opt<unsigned> TimeTraceGranularity(
+    "time-trace-granularity",
+    cl::desc(
+        "Minimum time granularity (in microseconds) traced by time profiler"),
+    cl::init(500), cl::Hidden);
+
+static cl::opt<std::string>
+    TimeTraceFile("time-trace-file",
+                  cl::desc("Specify time trace file destination"),
+                  cl::value_desc("filename"));
+
 static void check(Error E, std::string Msg) {
   if (!E)
     return;
@@ -267,6 +282,16 @@ static int usage() {
 static int run(int argc, char **argv) {
   cl::ParseCommandLineOptions(argc, argv, "Resolution-based LTO test harness");
 
+  if (TimeTrace)
+    timeTraceProfilerInitialize(TimeTraceGranularity, argv[0]);
+  auto TimeTraceScopeExit = make_scope_exit([]() {
+    if (TimeTrace) {
+      check(timeTraceProfilerWrite(TimeTraceFile, OutputFilename),
+            "timeTraceProfilerWrite failed");
+      timeTraceProfilerCleanup();
+    }
+  });
+
   // FIXME: Workaround PR30396 which means that a symbol can appear
   // more than once if it is defined in module-level assembly and
   // has a GV declaration. We allow (file, symbol) pairs to have multiple
@@ -304,7 +329,10 @@ static int run(int argc, char **argv) {
   std::vector<std::unique_ptr<MemoryBuffer>> MBs;
 
   Config Conf;
-
+  if (TimeTrace) {
+    Conf.TimeTraceEnabled = TimeTrace;
+    Conf.TimeTraceGranularity = TimeTraceGranularity;
+  }
   Conf.CPU = codegen::getMCPU();
   Conf.Options = codegen::InitTargetOptionsFromCodeGenFlags(Triple());
   Conf.MAttrs = codegen::getMAttrs();

``````````

</details>


https://github.com/llvm/llvm-project/pull/171600


More information about the llvm-commits mailing list