[llvm] [Support] Improve jobserver concurrency handling (PR #178434)

via llvm-commits llvm-commits at lists.llvm.org
Wed Jan 28 06:27:49 PST 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-support

Author: Yaxun (Sam) Liu (yxsamliu)

<details>
<summary>Changes</summary>

Problem:
The original JobserverClient::getNumJobs() tried to determine the total
job count by draining all available tokens at startup. This is unreliable
because sibling processes may already hold tokens, causing getNumJobs() to
return a value lower than the actual -jN limit. If we then spawn that many
threads, we underutilize the available parallelism.

A naive fix (parsing -jN from MAKEFLAGS) causes a different problem: if
make -j16 spawns 16 LLVM processes, and each creates 16 threads, we get
256 threads competing for 16 tokens - massive oversubscription with
significant memory overhead from idle threads.

Solution:
Instead of pre-spawning threads based on an unreliable job count, we use
a spawn-per-token approach in jobserver mode:
1. Start a single coordinator thread that waits for work
2. When work arrives, the coordinator acquires a jobserver token
3. Only after acquiring a token do we spawn an ephemeral worker thread
4. The worker drains available tasks, then releases the token and exits

This is implemented as a separate JobserverThreadPoolExecutor class,
keeping the standard ThreadPoolExecutor unchanged for non-jobserver mode.

Benefits:
- Thread count automatically matches tokens actually held (not -jN guess)
- No idle threads blocking on token acquisition
- Multiple LLVM processes naturally share tokens without oversubscription
- Memory overhead (VmSize) scales with actual parallelism, not -jN

A thread index pool ensures getThreadIndex() returns values in
[0, getThreadCount()) for compatibility with PerThreadBumpPtrAllocator.

Fixes https://github.com/llvm/llvm-project/issues/170184

---

Patch is 33.20 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/178434.diff


6 Files Affected:

- (modified) llvm/include/llvm/Support/Parallel.h (+4) 
- (modified) llvm/lib/Support/Jobserver.cpp (+9) 
- (modified) llvm/lib/Support/Parallel.cpp (+273-60) 
- (modified) llvm/lib/Support/Unix/Jobserver.inc (+35-11) 
- (modified) llvm/lib/Support/Windows/Jobserver.inc (+13-1) 
- (modified) llvm/unittests/Support/JobserverTest.cpp (+332) 


``````````diff
diff --git a/llvm/include/llvm/Support/Parallel.h b/llvm/include/llvm/Support/Parallel.h
index b0c9e8f29f970..efc2e60370f88 100644
--- a/llvm/include/llvm/Support/Parallel.h
+++ b/llvm/include/llvm/Support/Parallel.h
@@ -50,6 +50,10 @@ LLVM_ABI extern thread_local unsigned threadIndex;
 inline unsigned getThreadIndex() { GET_THREAD_INDEX_IMPL; }
 #endif
 
+// getThreadIndex() is assigned by ThreadPoolExecutor and must be in the range
+// [0, getThreadCount()). Some users (e.g. PerThreadBumpPtrAllocator) rely on
+// this bound to size per-thread storage, so getThreadCount() represents the
+// maximum possible worker threads, not the current active count.
 LLVM_ABI size_t getThreadCount();
 #else
 inline unsigned getThreadIndex() { return 0; }
diff --git a/llvm/lib/Support/Jobserver.cpp b/llvm/lib/Support/Jobserver.cpp
index 522ab914f7eea..794fb7c6bbf9e 100644
--- a/llvm/lib/Support/Jobserver.cpp
+++ b/llvm/lib/Support/Jobserver.cpp
@@ -9,6 +9,7 @@
 #include "llvm/Support/Jobserver.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/Support/Error.h"
+#include "llvm/Support/Threading.h"
 
 #include <atomic>
 #include <memory>
@@ -36,6 +37,8 @@ struct JobserverConfig {
   Mode TheMode = None;
   std::string Path;
   FdPair PipeFDs;
+  /// The number of jobs specified by -jN in MAKEFLAGS. 0 if not specified.
+  unsigned ParsedNumJobs = 0;
 };
 
 /// A helper function that checks if `Input` starts with `Prefix`.
@@ -114,6 +117,12 @@ Expected<JobserverConfig> parseNativeMakeFlags(StringRef MakeFlags) {
         return createStringError(inconvertibleErrorCode(),
                                  "Invalid file descriptor pair in MAKEFLAGS");
       }
+    } else if (getPrefixedValue(Arg, "-j", Value) ||
+               getPrefixedValue(Arg, "--jobs=", Value)) {
+      // Parse the job count from -jN or --jobs=N (GNU Make 4.4+).
+      unsigned Jobs;
+      if (!Value.getAsInteger(10, Jobs) && Jobs > 0)
+        Config.ParsedNumJobs = Jobs;
     }
   }
 
diff --git a/llvm/lib/Support/Parallel.cpp b/llvm/lib/Support/Parallel.cpp
index ea223f6eda8a7..fb05db224d549 100644
--- a/llvm/lib/Support/Parallel.cpp
+++ b/llvm/lib/Support/Parallel.cpp
@@ -18,6 +18,7 @@
 #include <future>
 #include <memory>
 #include <mutex>
+#include <optional>
 #include <thread>
 #include <vector>
 
@@ -44,22 +45,20 @@ class Executor {
 public:
   virtual ~Executor() = default;
   virtual void add(std::function<void()> func) = 0;
+  virtual void stop() = 0;
   virtual size_t getThreadCount() const = 0;
 
   static Executor *getDefaultExecutor();
 };
 
 /// An implementation of an Executor that runs closures on a thread pool
-///   in filo order.
+/// in filo order. This is the standard executor for non-jobserver mode.
 class ThreadPoolExecutor : public Executor {
 public:
   explicit ThreadPoolExecutor(ThreadPoolStrategy S) {
-    if (S.UseJobserver)
-      TheJobserver = JobserverClient::getInstance();
-
     ThreadCount = S.compute_thread_count();
-    // Spawn all but one of the threads in another thread as spawning threads
-    // can take a while.
+
+    // Spawn all threads up front.
     Threads.reserve(ThreadCount);
     Threads.resize(1);
     std::lock_guard<std::mutex> Lock(Mutex);
@@ -77,8 +76,6 @@ class ThreadPoolExecutor : public Executor {
     });
   }
 
-  // To make sure the thread pool executor can only be created with a parallel
-  // strategy.
   ThreadPoolExecutor() = delete;
 
   void stop() {
@@ -102,10 +99,10 @@ class ThreadPoolExecutor : public Executor {
   ~ThreadPoolExecutor() override { stop(); }
 
   struct Creator {
-    static void *call() { return new ThreadPoolExecutor(strategy); }
+    static void *call();
   };
   struct Deleter {
-    static void call(void *Ptr) { ((ThreadPoolExecutor *)Ptr)->stop(); }
+    static void call(void *Ptr) { ((Executor *)Ptr)->stop(); }
   };
 
   void add(std::function<void()> F) override {
@@ -122,25 +119,146 @@ class ThreadPoolExecutor : public Executor {
   void work(ThreadPoolStrategy S, unsigned ThreadID) {
     threadIndex = ThreadID;
     S.apply_thread_strategy(ThreadID);
-    // Note on jobserver deadlock avoidance:
-    // GNU Make grants each invoked process one implicit job slot. Our
-    // JobserverClient models this by returning an implicit JobSlot on the
-    // first successful tryAcquire() in a process. This guarantees forward
-    // progress without requiring a dedicated "always-on" thread here.
 
     while (true) {
-      if (TheJobserver) {
-        // Jobserver-mode scheduling:
-        // - Acquire one job slot (with exponential backoff to avoid busy-wait).
-        // - While holding the slot, drain and run tasks from the local queue.
-        // - Release the slot when the queue is empty or when shutting down.
-        // Rationale: Holding a slot amortizes acquire/release overhead over
-        // multiple tasks and avoids requeue/yield churn, while still enforcing
-        // the jobserver’s global concurrency limit. With K available slots,
-        // up to K workers run tasks in parallel; within each worker tasks run
-        // sequentially until the local queue is empty.
-        ExponentialBackoff Backoff(std::chrono::hours(24));
-        JobSlot Slot;
+      std::unique_lock<std::mutex> Lock(Mutex);
+      Cond.wait(Lock, [&] { return Stop || !WorkStack.empty(); });
+      if (Stop)
+        break;
+      auto Task = std::move(WorkStack.back());
+      WorkStack.pop_back();
+      Lock.unlock();
+      Task();
+    }
+  }
+
+  std::atomic<bool> Stop{false};
+  std::vector<std::function<void()>> WorkStack;
+  std::mutex Mutex;
+  std::condition_variable Cond;
+  std::promise<void> ThreadsCreated;
+  std::vector<std::thread> Threads;
+  unsigned ThreadCount;
+};
+
+/// Jobserver-aware executor that uses spawn-per-token thread management.
+///
+/// Problem (GitHub Issue #170184):
+/// The original JobserverClient::getNumJobs() tried to determine the total
+/// job count by draining all available tokens at startup. This is unreliable
+/// because sibling processes may already hold tokens, causing getNumJobs() to
+/// return a value lower than the actual -jN limit. If we then spawn that many
+/// threads (like the standard ThreadPoolExecutor does), we underutilize the
+/// available parallelism.
+///
+/// A naive fix (parsing -jN from MAKEFLAGS) causes a different problem: if
+/// make -j16 spawns 16 LLVM processes, and each creates 16 threads, we get
+/// 256 threads competing for 16 tokens - massive oversubscription.
+///
+/// Solution (spawn-per-token):
+/// Instead of pre-spawning threads based on an unreliable job count, we:
+/// 1. Start a single coordinator thread that waits for work
+/// 2. When work arrives, the coordinator acquires a jobserver token
+/// 3. Only after acquiring a token do we spawn an ephemeral worker thread
+/// 4. The worker drains available tasks, then releases the token and exits
+///
+/// Benefits:
+/// - Thread count automatically matches tokens actually held (not -jN guess)
+/// - No idle threads blocking on token acquisition
+/// - Multiple LLVM processes naturally share tokens without oversubscription
+/// - Memory overhead (VmSize) scales with actual parallelism, not -jN
+///
+/// The thread index pool ensures getThreadIndex() returns values in
+/// [0, getThreadCount()) for compatibility with PerThreadBumpPtrAllocator.
+class JobserverThreadPoolExecutor : public Executor {
+public:
+  explicit JobserverThreadPoolExecutor(ThreadPoolStrategy S) : Strategy(S) {
+    TheJobserver = JobserverClient::getInstance();
+    assert(TheJobserver && "JobserverThreadPoolExecutor requires jobserver");
+
+    ThreadCount = S.compute_thread_count();
+    initThreadIndexPool();
+
+    // Start a single coordinator thread that manages token-based spawning.
+    Threads.reserve(1);
+    Threads.resize(1);
+    std::lock_guard<std::mutex> Lock(Mutex);
+    auto &Thread0 = Threads[0];
+    Thread0 = std::thread([this] { jobserverCoordinator(); });
+    ThreadsCreated.set_value();
+  }
+
+  JobserverThreadPoolExecutor() = delete;
+
+  void stop() {
+    {
+      std::lock_guard<std::mutex> Lock(Mutex);
+      if (Stop)
+        return;
+      Stop = true;
+    }
+    Cond.notify_all();
+    IndexCond.notify_all();
+    ThreadsCreated.get_future().wait();
+
+    std::thread::id CurrentThreadId = std::this_thread::get_id();
+    for (std::thread &T : Threads)
+      if (T.get_id() == CurrentThreadId)
+        T.detach();
+      else
+        T.join();
+
+    // Join any remaining ephemeral workers.
+    joinFinishedWorkers(/*JoinAll=*/true);
+  }
+
+  ~JobserverThreadPoolExecutor() override { stop(); }
+
+  void add(std::function<void()> F) override {
+    {
+      std::lock_guard<std::mutex> Lock(Mutex);
+      WorkStack.push_back(std::move(F));
+    }
+    Cond.notify_one();
+  }
+
+  size_t getThreadCount() const override { return ThreadCount; }
+
+private:
+  struct EphemeralWorkerRecord {
+    std::thread Thread;
+    std::shared_ptr<std::atomic<bool>> Done;
+  };
+
+  /// Coordinator thread: waits for work, acquires tokens, spawns workers.
+  void jobserverCoordinator() {
+    threadIndex = 0;
+    Strategy.apply_thread_strategy(0);
+
+    while (true) {
+      // Wait until there's work or we're stopping.
+      {
+        std::unique_lock<std::mutex> Lock(Mutex);
+        Cond.wait(Lock, [&] { return Stop || !WorkStack.empty(); });
+        if (Stop)
+          break;
+      }
+
+      // Periodically join finished workers to avoid unbounded growth.
+      joinFinishedWorkers(/*JoinAll=*/false);
+
+      // Acquire a thread index before attempting to acquire a token.
+      std::optional<unsigned> ThreadIndex = acquireThreadIndex();
+      if (!ThreadIndex)
+        return;
+      unsigned Index = *ThreadIndex;
+      auto ReleaseIndex = llvm::scope_exit([&] { releaseThreadIndex(Index); });
+
+      // Try to acquire a token. If successful, spawn an ephemeral worker.
+      JobSlot Slot = TheJobserver->tryAcquire();
+      if (!Slot.isValid()) {
+        // No token available right now. Use backoff and retry.
+        ExponentialBackoff Backoff(std::chrono::seconds(1));
         do {
           if (Stop)
             return;
@@ -148,37 +266,116 @@ class ThreadPoolExecutor : public Executor {
           if (Slot.isValid())
             break;
         } while (Backoff.waitForNextAttempt());
+      }
 
-        llvm::scope_exit SlotReleaser(
-            [&] { TheJobserver->release(std::move(Slot)); });
-
-        while (true) {
-          std::function<void()> Task;
-          {
-            std::unique_lock<std::mutex> Lock(Mutex);
-            Cond.wait(Lock, [&] { return Stop || !WorkStack.empty(); });
-            if (Stop && WorkStack.empty())
-              return;
-            if (WorkStack.empty())
-              break;
-            Task = std::move(WorkStack.back());
-            WorkStack.pop_back();
-          }
-          Task();
-        }
-      } else {
+      if (!Slot.isValid())
+        continue; // ReleaseIndex will run, loop back and check Stop/WorkStack.
+
+      ReleaseIndex.release();
+
+      // Spawn an ephemeral worker that owns this token.
+      auto Done = std::make_shared<std::atomic<bool>>(false);
+      std::thread Worker([this, Slot = std::move(Slot), Index, Done]() mutable {
+        ephemeralWorker(std::move(Slot), Index, Done);
+      });
+
+      {
+        std::lock_guard<std::mutex> Lock(EphemeralMutex);
+        EphemeralWorkers.push_back({std::move(Worker), Done});
+      }
+    }
+  }
+
+  /// Ephemeral worker: drains tasks while holding a token, then exits.
+  void ephemeralWorker(JobSlot Slot, unsigned ThreadIdx,
+                       std::shared_ptr<std::atomic<bool>> Done) {
+    threadIndex = ThreadIdx;
+    Strategy.apply_thread_strategy(ThreadIdx);
+
+    auto ReleaseIndex = llvm::scope_exit([&] {
+      releaseThreadIndex(ThreadIdx);
+      Done->store(true, std::memory_order_release);
+    });
+
+    llvm::scope_exit SlotReleaser(
+        [&] { TheJobserver->release(std::move(Slot)); });
+
+    while (true) {
+      std::function<void()> Task;
+      {
         std::unique_lock<std::mutex> Lock(Mutex);
-        Cond.wait(Lock, [&] { return Stop || !WorkStack.empty(); });
+        if (WorkStack.empty())
+          return;
         if (Stop)
-          break;
-        auto Task = std::move(WorkStack.back());
+          return;
+        Task = std::move(WorkStack.back());
         WorkStack.pop_back();
-        Lock.unlock();
-        Task();
+      }
+      Task();
+    }
+  }
+
+  /// Join finished ephemeral workers to reclaim resources.
+  void joinFinishedWorkers(bool JoinAll) {
+    std::vector<std::thread> ToJoin;
+    std::thread::id Current = std::this_thread::get_id();
+
+    {
+      std::lock_guard<std::mutex> Lock(EphemeralMutex);
+      for (auto It = EphemeralWorkers.begin(); It != EphemeralWorkers.end();) {
+        bool Finished = JoinAll || It->Done->load(std::memory_order_acquire);
+        if (!Finished) {
+          ++It;
+          continue;
+        }
+
+        if (It->Thread.get_id() == Current) {
+          // Avoid self-join in shutdown paths.
+          It->Thread.detach();
+          It = EphemeralWorkers.erase(It);
+          continue;
+        }
+
+        ToJoin.push_back(std::move(It->Thread));
+        It = EphemeralWorkers.erase(It);
       }
     }
+
+    for (auto &T : ToJoin)
+      if (T.joinable())
+        T.join();
   }
 
+  void initThreadIndexPool() {
+    if (ThreadCount == 0)
+      ThreadCount = 1;
+    std::lock_guard<std::mutex> Lock(IndexMutex);
+    FreeThreadIndices.clear();
+    FreeThreadIndices.reserve(ThreadCount);
+    for (unsigned I = 0; I < ThreadCount; ++I)
+      FreeThreadIndices.push_back(I);
+  }
+
+  std::optional<unsigned> acquireThreadIndex() {
+    std::unique_lock<std::mutex> Lock(IndexMutex);
+    IndexCond.wait(Lock,
+                   [&] { return Stop.load() || !FreeThreadIndices.empty(); });
+    if (Stop)
+      return std::nullopt;
+    unsigned Index = FreeThreadIndices.back();
+    FreeThreadIndices.pop_back();
+    return Index;
+  }
+
+  void releaseThreadIndex(unsigned Index) {
+    {
+      std::lock_guard<std::mutex> Lock(IndexMutex);
+      FreeThreadIndices.push_back(Index);
+    }
+    IndexCond.notify_one();
+  }
+
+  ThreadPoolStrategy Strategy;
   std::atomic<bool> Stop{false};
   std::vector<std::function<void()>> WorkStack;
   std::mutex Mutex;
@@ -188,26 +385,42 @@ class ThreadPoolExecutor : public Executor {
   unsigned ThreadCount;
 
   JobserverClient *TheJobserver = nullptr;
+
+  // Ephemeral workers and their mutex.
+  std::mutex EphemeralMutex;
+  std::vector<EphemeralWorkerRecord> EphemeralWorkers;
+
+  // Thread index pool for getThreadIndex() semantics.
+  std::mutex IndexMutex;
+  std::condition_variable IndexCond;
+  std::vector<unsigned> FreeThreadIndices;
 };
 
+// Factory function to create the appropriate executor.
+void *ThreadPoolExecutor::Creator::call() {
+  if (strategy.UseJobserver && JobserverClient::getInstance())
+    return new JobserverThreadPoolExecutor(strategy);
+  return new ThreadPoolExecutor(strategy);
+}
+
 Executor *Executor::getDefaultExecutor() {
 #ifdef _WIN32
-  // The ManagedStatic enables the ThreadPoolExecutor to be stopped via
-  // llvm_shutdown() on Windows. This is important to avoid various race
-  // conditions at process exit that can cause crashes or deadlocks.
-
-  static ManagedStatic<ThreadPoolExecutor, ThreadPoolExecutor::Creator,
+  // The ManagedStatic enables the executor to be stopped via llvm_shutdown()
+  // on Windows. This is important to avoid various race conditions at process
+  // exit that can cause crashes or deadlocks.
+  static ManagedStatic<Executor, ThreadPoolExecutor::Creator,
                        ThreadPoolExecutor::Deleter>
       ManagedExec;
-  static std::unique_ptr<ThreadPoolExecutor> Exec(&(*ManagedExec));
+  static std::unique_ptr<Executor> Exec(&(*ManagedExec));
   return Exec.get();
 #else
-  // ManagedStatic is not desired on other platforms. When `Exec` is destroyed
-  // by llvm_shutdown(), worker threads will clean up and invoke TLS
+  // ManagedStatic is not desired on other platforms. When the executor is
+  // destroyed by llvm_shutdown(), worker threads will clean up and invoke TLS
   // destructors. This can lead to race conditions if other threads attempt to
   // access TLS objects that have already been destroyed.
-  static ThreadPoolExecutor Exec(strategy);
-  return &Exec;
+  static std::unique_ptr<Executor> Exec(
+      static_cast<Executor *>(ThreadPoolExecutor::Creator::call()));
+  return Exec.get();
 #endif
 }
 } // namespace
diff --git a/llvm/lib/Support/Unix/Jobserver.inc b/llvm/lib/Support/Unix/Jobserver.inc
index 300fb5e55045e..a5a473fc284a2 100644
--- a/llvm/lib/Support/Unix/Jobserver.inc
+++ b/llvm/lib/Support/Unix/Jobserver.inc
@@ -82,19 +82,43 @@ JobserverClientImpl::JobserverClientImpl(const JobserverConfig &Config) {
   }
 
   IsInitialized = true;
-  // Determine the total number of jobs by acquiring all available slots and
-  // then immediately releasing them.
-  SmallVector<JobSlot, 8> Slots;
-  while (true) {
-    auto S = tryAcquire();
-    if (!S.isValid())
-      break;
-    Slots.push_back(std::move(S));
+
+  // Determine NumJobs using the best available information:
+  // 1. If -jN was parsed from MAKEFLAGS (GNU Make 4.4+), use it as the base
+  // 2. Otherwise, drain available tokens to estimate (may be inaccurate if
+  //    sibling processes are holding tokens)
+  // 3. Cap at hardware_concurrency to prevent thread oversubscription when
+  //    multiple LLVM processes run in parallel
+  unsigned DrainedCount = 0;
+  {
+    SmallVector<JobSlot, 8> Slots;
+    while (true) {
+      auto S = tryAcquire();
+      if (!S.isValid())
+        break;
+      Slots.push_back(std::move(S));
+    }
+    DrainedCount = Slots.size();
+    for (auto &S : Slots)
+      release(std::move(S));
   }
-  NumJobs = Slots.size();
+
+  // Use parsed -jN if available, otherwise use drained count
+  unsigned BaseCount =
+      Config.ParsedNumJobs > 0 ? Config.ParsedNumJobs : DrainedCount;
+
+  // Ensure at least 1 (for the implicit slot)
+  if (BaseCount == 0)
+    BaseCount = 1;
+
+  // Cap at hardware_concurrency to prevent oversubscription
+  unsigned HWThreads = llvm::hardware_concurrency().compute_thread_count();
+  if (HWThreads > 0)
+    NumJobs = std::min(BaseCount, HWThreads);
+  else
+    NumJobs = BaseCount;
+
   assert(NumJobs >= 1 && "Invalid number of jobs");
-  for (auto &S : Slots)
-    release(std::move(S));
 }
 
 /// The destructor closes any open file descriptors.
diff --git a/llvm/lib/Support/Windows/Jobserver.inc b/llvm/lib/Support/Windows/Jobserver.inc
index 79028eee4b302..7bf19bacee972 100644
--- a/llvm/lib/Support/Windows/Jobserver.inc
+++ b/llvm/lib/Support/Windows/Jobserver.inc
@@ -23,8 +23,20 @@ namespace llvm {
 JobserverClientImpl::JobserverClientImpl(const JobserverConfig &Config) {
   Semaphore = (void *)::OpenSemaphoreA(SEMAPHORE_MODIFY_STATE | SYNCHRONIZE,
                                        FALSE, Config.Path.c_str());
-  if (Semaphore != nullptr)
+  if (Semaphore != nullptr) {
     IsInitialized = true;
+
+    // Use parsed -jN if available, otherwise default to 1 (can't drain
+    // semaphore count reliably on Windows)
+    unsigned BaseCount = Config.ParsedNumJobs > 0 ? Config.ParsedNumJobs : 1;
+
+    // Cap at hardware_concurrency to prevent oversubscription
+    unsigned HWThreads = llvm::hardware_concurrency().compute_thread_count();
+    if (HWThreads > 0)
+      NumJobs = std::min(BaseCount, HWThreads);
+    else
+      NumJobs = BaseCount;
+  }
 }
 
 /// The destructor closes the handle to the semaphore, releasing the resource.
diff --git a/llvm/unittests/Support/JobserverTest.cpp b/llvm/unittests/Support/JobserverTest.cpp
index c36769087aba7..ec7c545f290a0 100644
--- a/llvm/unittests/Support/JobserverTest.cpp
+++ b/llvm/unittests/Support/JobserverTest.cpp
@@ -16,8 +16,10 @@
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/FileUtilities.h"
 #include "llvm/Support/Parallel.h"
+#include "llvm/Support/PerThreadBumpPtrAllocator.h"
 #include "llvm/Support/Program.h"
 #include "llvm/Support/ThreadPool.h"
+#include "llvm/Support/Threading.h"
 #include "llvm/Support/raw_ostream.h"
 #include "gtest/gtest.h"
 #include <future>
@@ -27,10 +29,12 @@
 #if defined(LLVM_ON_UNIX)
 #include "llvm/ADT/SmallString.h"
 #inclu...
[truncated]

``````````

</details>


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


More information about the llvm-commits mailing list