[llvm] [llvm-exegesis] Fix ~27 minute stall when no LBR sample arrives (PR #218102)

Fangrui Song via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 21 22:51:25 PDT 2026


https://github.com/MaskRay created https://github.com/llvm/llvm-project/pull/218102

X86LbrPerfEvent programs PERF_TYPE_RAW/0x20c4, a P-core encoding. A hybrid
CPU registers one PMU per core type and cpu_core claims PERF_TYPE_RAW, so
the kernel never schedules the event while the thread runs on an E-core.
perf_event_open succeeds all the same and no sample is recorded.

doReadCounter polls 160 times with a 10s timeout, so reporting the failure
takes ~27 minutes, and checkLbrSupport then aborts instead of reporting it,
never having consumed the Expected error. lit.local.cfg probes llvm-exegesis
at test discovery, so with LLVM_ENABLE_LIBPFM=ON this stalls all of
check-llvm before any test runs.

The event is disabled before we poll, so a sample either is already in the
ring buffer -- wakeup_events == 1 leaves POLLIN asserted -- or it will never
arrive. Poll 3 times with a 1s timeout, use expectedToOptional, and bound
the lit probe. A probe that works at all takes under 30ms, 0.08s with every
core saturated.

Scheduling the event on a core that can count it is a separate matter.

LLM-aided


>From 7887d029e73b2457371b791f40b365a1b0113263 Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Fri, 21 Aug 2026 22:41:20 -0700
Subject: [PATCH] [llvm-exegesis] Fix ~27 minute stall when no LBR sample
 arrives

X86LbrPerfEvent programs PERF_TYPE_RAW/0x20c4, a P-core encoding. A hybrid
CPU registers one PMU per core type and cpu_core claims PERF_TYPE_RAW, so
the kernel never schedules the event while the thread runs on an E-core.
perf_event_open succeeds all the same and no sample is recorded.

doReadCounter polls 160 times with a 10s timeout, so reporting the failure
takes ~27 minutes, and checkLbrSupport then aborts instead of reporting it,
never having consumed the Expected error. lit.local.cfg probes llvm-exegesis
at test discovery, so with LLVM_ENABLE_LIBPFM=ON this stalls all of
check-llvm before any test runs.

The event is disabled before we poll, so a sample either is already in the
ring buffer -- wakeup_events == 1 leaves POLLIN asserted -- or it will never
arrive. Poll 3 times with a 1s timeout, use expectedToOptional, and bound
the lit probe. A probe that works at all takes under 30ms, 0.08s with every
core saturated.

Scheduling the event on a core that can count it is a separate matter.

LLM-aided
---
 llvm/test/tools/llvm-exegesis/lit.local.cfg   |  7 +++
 .../llvm-exegesis/lib/X86/X86Counter.cpp      | 47 +++++++++----------
 2 files changed, 30 insertions(+), 24 deletions(-)

diff --git a/llvm/test/tools/llvm-exegesis/lit.local.cfg b/llvm/test/tools/llvm-exegesis/lit.local.cfg
index 89110ed2816cd..e6b2ceb187fc1 100644
--- a/llvm/test/tools/llvm-exegesis/lit.local.cfg
+++ b/llvm/test/tools/llvm-exegesis/lit.local.cfg
@@ -19,13 +19,20 @@ def can_use_perf_counters(mode, extra_options=[]):
         print("could not find llvm-exegesis")
         return False
     try:
+        # This runs during test discovery, so a hung llvm-exegesis would stall the
+        # whole lit invocation before a single test starts. A probe takes well
+        # under a second when it works at all.
         return_code = subprocess.call(
             [llvm_exegesis_exe, "-mode", mode, "-opcode-name=ADD64rr"]
             + extra_options,
             stdout=subprocess.DEVNULL,
             stderr=subprocess.DEVNULL,
+            timeout=1,
         )
         return return_code == 0
+    except subprocess.TimeoutExpired:
+        print("llvm-exegesis timed out")
+        return False
     except OSError:
         print("could not exec llvm-exegesis")
         return False
diff --git a/llvm/tools/llvm-exegesis/lib/X86/X86Counter.cpp b/llvm/tools/llvm-exegesis/lib/X86/X86Counter.cpp
index 9dc6c764599cd..6a5120bc56636 100644
--- a/llvm/tools/llvm-exegesis/lib/X86/X86Counter.cpp
+++ b/llvm/tools/llvm-exegesis/lib/X86/X86Counter.cpp
@@ -14,6 +14,7 @@
 // FIXME: Use appropriate wrappers for poll.h and mman.h
 // to support Windows and remove this linux-only guard.
 
+#include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/Endian.h"
 #include "llvm/Support/Errc.h"
 
@@ -44,13 +45,16 @@ static const size_t kDataBufferSize = kBufferPages * getpagesize();
 // the next page, so we allocate one more page.
 static const size_t kMappedBufferSize = (kBufferPages + 1) * getpagesize();
 
+static constexpr int kPollTimeoutMs = 1000;
+static constexpr int kMaxPolls = 3;
+
 // Waits for the LBR perf events.
 static int pollLbrPerfEvent(const int FileDescriptor) {
   struct pollfd PollFd;
   PollFd.fd = FileDescriptor;
   PollFd.events = POLLIN;
   PollFd.revents = 0;
-  return poll(&PollFd, 1 /* num of fds */, 10000 /* timeout in ms */);
+  return poll(&PollFd, 1 /* num of fds */, kPollTimeoutMs);
 }
 
 // Copies the data-buffer into Buf, given the pointer to MMapped.
@@ -178,13 +182,11 @@ Error X86LbrCounter::checkLbrSupport() {
   counter.stop();
   (void)Sum;
 
-  auto ResultOrError = counter.doReadCounter(nullptr, nullptr);
-  if (ResultOrError)
-    if (!ResultOrError.get().empty())
-      // If there is at least one non-zero entry, then LBR is supported.
-      for (const int64_t &Value : ResultOrError.get())
-        if (Value != 0)
-          return Error::success();
+  // A read that fails just means LBR is unusable here. If there is at least one
+  // non-zero entry, then LBR is supported.
+  if (auto Result = expectedToOptional(counter.doReadCounter(nullptr, nullptr)))
+    if (any_of(*Result, [](int64_t Value) { return Value != 0; }))
+      return Error::success();
 
   return make_error<StringError>(
       "LBR format with cycles is not suppported on the host.",
@@ -209,28 +211,25 @@ X86LbrCounter::readOrError(StringRef FunctionBytes) const {
 
 Expected<SmallVector<int64_t, 4>>
 X86LbrCounter::doReadCounter(const void *From, const void *To) const {
-  // The max number of time-outs/retries before we give up.
-  static constexpr int kMaxTimeouts = 160;
-
   // Parses the LBR buffer and fills CycleArray with the sequence of cycle
   // counts from the buffer.
   SmallVector<int64_t, 4> CycleArray;
   auto DataBuf = std::make_unique<char[]>(kDataBufferSize);
-  int NumTimeouts = 0;
-  int PollResult = 0;
 
-  while (PollResult <= 0) {
+  // The event is disabled before we get here, so a sample either is already in
+  // the ring buffer -- wakeup_events == 1 leaves POLLIN asserted -- or it will
+  // never arrive. The budget only absorbs wake-up latency.
+  int PollResult = 0;
+  for (int I = 0; I != kMaxPolls && PollResult == 0; ++I)
     PollResult = pollLbrPerfEvent(getFileDescriptor());
-    if (PollResult > 0)
-      break;
-    if (PollResult == -1)
-      return make_error<StringError>("Cannot poll LBR perf event.",
-                                     errc::io_error);
-    if (NumTimeouts++ >= kMaxTimeouts)
-      return make_error<StringError>(
-          "LBR polling still timed out after max number of attempts.",
-          errc::device_or_resource_busy);
-  }
+
+  if (PollResult < 0)
+    return make_error<StringError>("Cannot poll LBR perf event.",
+                                   errc::io_error);
+  if (PollResult == 0)
+    return make_error<StringError>(
+        "LBR polling still timed out after max number of attempts.",
+        errc::device_or_resource_busy);
 
   struct perf_event_mmap_page Page;
   memcpy(&Page, MMappedBuffer, sizeof(struct perf_event_mmap_page));



More information about the llvm-commits mailing list