[llvm] [lit] Avoid multiprocessing for -j1 runs (PR #175587)
via llvm-commits
llvm-commits at lists.llvm.org
Wed Jan 21 11:53:54 PST 2026
https://github.com/alx32 updated https://github.com/llvm/llvm-project/pull/175587
>From 8d2b71e4c10c4752796c9e8b0010791fc81022ed Mon Sep 17 00:00:00 2001
From: Alex Borcan <alexborcan at fb.com>
Date: Mon, 12 Jan 2026 08:35:18 -0800
Subject: [PATCH 1/4] [lit] Avoid multiprocessing for -j1 runs
---
llvm/utils/lit/lit/run.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/llvm/utils/lit/lit/run.py b/llvm/utils/lit/lit/run.py
index 9c54511bfd625..cfbfd4de03b22 100644
--- a/llvm/utils/lit/lit/run.py
+++ b/llvm/utils/lit/lit/run.py
@@ -74,6 +74,15 @@ def execute(self):
def _execute(self, deadline):
self._increase_process_limit()
+ # When only one worker is requested, avoid multiprocessing entirely.
+ #
+ # This is faster for small runs and (importantly) allows lit to run in
+ # restricted environments/sandboxes where Python's multiprocessing
+ # primitives (e.g. POSIX semaphores/locks) are not permitted.
+ if self.workers == 1:
+ self._execute_single_process(deadline)
+ return
+
semaphores = {
k: multiprocessing.BoundedSemaphore(v)
for k, v in self.lit_config.parallelism_groups.items()
@@ -140,6 +149,21 @@ def _execute(self, deadline):
for pool in pools:
pool.join()
+ def _execute_single_process(self, deadline):
+ # Run tests directly in this process (see lit.worker docstring).
+ for test in self.tests:
+ if time.time() > deadline:
+ raise TimeoutError()
+
+ result = lit.worker._execute(test, self.lit_config)
+ test.setResult(result)
+ self.progress_callback(test)
+
+ if test.isFailure():
+ self.failures += 1
+ if self.failures == self.max_failures:
+ raise MaxFailuresError()
+
def _wait_for(self, async_results, deadline):
timeout = deadline - time.time()
for idx, ar in enumerate(async_results):
>From 413b61a2751b57416e862e8617cfefa7370588a8 Mon Sep 17 00:00:00 2001
From: alexborcan <alexborcan at meta.com>
Date: Mon, 12 Jan 2026 14:32:27 -0800
Subject: [PATCH 2/4] Porting promp
---
llvm/utils/lit/lit/run.py | 52 +++++++++++++++++++++-----
llvm/utils/lit/tests/lit.cfg | 6 +++
llvm/utils/lit/tests/single-process.py | 42 +++++++++++++++++++++
3 files changed, 90 insertions(+), 10 deletions(-)
create mode 100644 llvm/utils/lit/tests/single-process.py
diff --git a/llvm/utils/lit/lit/run.py b/llvm/utils/lit/lit/run.py
index cfbfd4de03b22..d2247ccbe9b02 100644
--- a/llvm/utils/lit/lit/run.py
+++ b/llvm/utils/lit/lit/run.py
@@ -151,18 +151,50 @@ def _execute(self, deadline):
def _execute_single_process(self, deadline):
# Run tests directly in this process (see lit.worker docstring).
- for test in self.tests:
- if time.time() > deadline:
- raise TimeoutError()
+ #
+ # To enforce the overall timeout (--max-time), we temporarily set
+ # maxIndividualTestTime to the remaining time before the deadline.
+ # This ensures that individual tests are killed if they would exceed
+ # the overall time limit.
+ original_timeout = self.lit_config.maxIndividualTestTime
- result = lit.worker._execute(test, self.lit_config)
- test.setResult(result)
- self.progress_callback(test)
+ try:
+ for test in self.tests:
+ remaining = deadline - time.time()
+ if remaining <= 0:
+ raise TimeoutError()
+
+ # Use the smaller of: remaining time until deadline, or the
+ # original per-test timeout (if set). A value of 0 means no limit.
+ if original_timeout > 0:
+ effective_timeout = min(int(remaining) + 1, original_timeout)
+ elif self.timeout:
+ effective_timeout = int(remaining) + 1
+ else:
+ effective_timeout = 0
+
+ self.lit_config._maxIndividualTestTime = effective_timeout
+
+ result = lit.worker._execute(test, self.lit_config)
+
+ # If the test timed out because we hit the --max-time deadline,
+ # treat it as if it was never run (SKIPPED) to match the behavior
+ # of multiprocessing mode where the pool is terminated and
+ # in-progress tests don't get results.
+ if result.code == lit.Test.TIMEOUT and self.timeout:
+ if time.time() >= deadline:
+ # Don't set result - leave it as None so it gets SKIPPED
+ raise TimeoutError()
+
+ test.setResult(result)
+ self.progress_callback(test)
- if test.isFailure():
- self.failures += 1
- if self.failures == self.max_failures:
- raise MaxFailuresError()
+ if test.isFailure():
+ self.failures += 1
+ if self.failures == self.max_failures:
+ raise MaxFailuresError()
+ finally:
+ self.lit_config._maxIndividualTestTime = original_timeout
def _wait_for(self, async_results, deadline):
timeout = deadline - time.time()
diff --git a/llvm/utils/lit/tests/lit.cfg b/llvm/utils/lit/tests/lit.cfg
index 922fbd6c9c872..5464e0fd97330 100644
--- a/llvm/utils/lit/tests/lit.cfg
+++ b/llvm/utils/lit/tests/lit.cfg
@@ -128,6 +128,12 @@ if not llvm_config:
if platform.system() == "AIX":
config.available_features.add("system-aix")
+# Check for strace availability (Linux only)
+if platform.system() == "Linux":
+ import shutil
+ if shutil.which("strace"):
+ config.available_features.add("strace")
+
# For each of lit's internal shell commands ('env', 'cd', 'diff', etc.), put
# a fake command that always fails at the start of PATH. This helps us check
# that we always use lit's internal version rather than some external version
diff --git a/llvm/utils/lit/tests/single-process.py b/llvm/utils/lit/tests/single-process.py
new file mode 100644
index 0000000000000..4003b51b60453
--- /dev/null
+++ b/llvm/utils/lit/tests/single-process.py
@@ -0,0 +1,42 @@
+# Test that -j1 runs in single-process mode (no multiprocessing).
+#
+# This ensures lit can run in sandboxed environments where multiprocessing
+# primitives (semaphores, process creation) are blocked.
+
+# REQUIRES: strace
+
+# Use strace to trace file operations. With -j1, lit should NOT create
+# POSIX semaphores in /dev/shm which are used by multiprocessing.Pool.
+#
+# Python's multiprocessing.BoundedSemaphore creates files like:
+# openat(AT_FDCWD, "/dev/shm/sem.XXXXXX", O_RDWR|O_CREAT|O_EXCL|...)
+#
+# These are the semaphores that fail in sandboxed environments, so verifying
+# they don't appear with -j1 confirms the fix works.
+#
+# Note: We use /usr/bin/env explicitly because lit's test config puts
+# fake-externals (which use #!/usr/bin/env python) at the start of PATH.
+# We also call lit.py directly (../lit.py relative to tests/) to avoid
+# the %{lit} substitution which includes the problematic 'env' command.
+
+# RUN: strace -f -e trace=openat \
+# RUN: /usr/bin/env -u FILECHECK_OPTS %{python} ../lit.py -j1 %{inputs}/shtest-format \
+# RUN: > %t.j1.out 2>&1 || true
+# RUN: FileCheck --check-prefix=CHECK-J1 %s < %t.j1.out
+#
+# CHECK-J1: 1 workers
+# CHECK-J1-NOT: /dev/shm/sem
+# CHECK-J1: Total Discovered Tests:
+
+# With -j2 and multiple tests, lit MUST use multiprocessing.Pool which creates
+# POSIX semaphores. This validates that the CHECK-NOT pattern above is correct
+# and won't become stale if Python/glibc changes how semaphores are created.
+#
+# RUN: strace -f -e trace=openat \
+# RUN: /usr/bin/env -u FILECHECK_OPTS %{python} ../lit.py -j2 %{inputs}/shtest-format \
+# RUN: > %t.j2.out 2>&1 || true
+# RUN: FileCheck --check-prefix=CHECK-J2 %s < %t.j2.out
+#
+# CHECK-J2: /dev/shm/sem
+# CHECK-J2: 2 workers
+# CHECK-J2: Total Discovered Tests:
>From 23abaf84fc573b2f06689c98beece4d8ed724f90 Mon Sep 17 00:00:00 2001
From: alexborcan <alexborcan at meta.com>
Date: Mon, 12 Jan 2026 15:46:16 -0800
Subject: [PATCH 3/4] Simplify test
---
llvm/utils/lit/tests/lit.cfg | 1 +
llvm/utils/lit/tests/single-process.py | 11 ++---------
2 files changed, 3 insertions(+), 9 deletions(-)
diff --git a/llvm/utils/lit/tests/lit.cfg b/llvm/utils/lit/tests/lit.cfg
index 5464e0fd97330..f27edc09e8924 100644
--- a/llvm/utils/lit/tests/lit.cfg
+++ b/llvm/utils/lit/tests/lit.cfg
@@ -75,6 +75,7 @@ config.substitutions.append(
),
)
)
+config.substitutions.append(("%{lit-script}", os.path.join(lit_path, "lit.py")))
config.substitutions.append(("%{python}", '"%s"' % (sys.executable)))
# This diagnostic sometimes appears in windows when using bash as an external
diff --git a/llvm/utils/lit/tests/single-process.py b/llvm/utils/lit/tests/single-process.py
index 4003b51b60453..4cf7a21756f43 100644
--- a/llvm/utils/lit/tests/single-process.py
+++ b/llvm/utils/lit/tests/single-process.py
@@ -13,14 +13,8 @@
#
# These are the semaphores that fail in sandboxed environments, so verifying
# they don't appear with -j1 confirms the fix works.
-#
-# Note: We use /usr/bin/env explicitly because lit's test config puts
-# fake-externals (which use #!/usr/bin/env python) at the start of PATH.
-# We also call lit.py directly (../lit.py relative to tests/) to avoid
-# the %{lit} substitution which includes the problematic 'env' command.
-# RUN: strace -f -e trace=openat \
-# RUN: /usr/bin/env -u FILECHECK_OPTS %{python} ../lit.py -j1 %{inputs}/shtest-format \
+# RUN: strace -f -e trace=openat %{python} %{lit-script} -j1 %{inputs}/shtest-format \
# RUN: > %t.j1.out 2>&1 || true
# RUN: FileCheck --check-prefix=CHECK-J1 %s < %t.j1.out
#
@@ -32,8 +26,7 @@
# POSIX semaphores. This validates that the CHECK-NOT pattern above is correct
# and won't become stale if Python/glibc changes how semaphores are created.
#
-# RUN: strace -f -e trace=openat \
-# RUN: /usr/bin/env -u FILECHECK_OPTS %{python} ../lit.py -j2 %{inputs}/shtest-format \
+# RUN: strace -f -e trace=openat %{python} %{lit-script} -j2 %{inputs}/shtest-format \
# RUN: > %t.j2.out 2>&1 || true
# RUN: FileCheck --check-prefix=CHECK-J2 %s < %t.j2.out
#
>From ec1bd697db683ae06f7e240d3346f6b2cad2d6e0 Mon Sep 17 00:00:00 2001
From: alexborcan <alexborcan at meta.com>
Date: Wed, 21 Jan 2026 11:53:00 -0800
Subject: [PATCH 4/4] Simplify test 2
---
llvm/utils/lit/tests/lit.cfg | 11 ++++++++++-
llvm/utils/lit/tests/single-process.py | 4 ++--
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/llvm/utils/lit/tests/lit.cfg b/llvm/utils/lit/tests/lit.cfg
index f27edc09e8924..09ed04378ddf1 100644
--- a/llvm/utils/lit/tests/lit.cfg
+++ b/llvm/utils/lit/tests/lit.cfg
@@ -75,7 +75,6 @@ config.substitutions.append(
),
)
)
-config.substitutions.append(("%{lit-script}", os.path.join(lit_path, "lit.py")))
config.substitutions.append(("%{python}", '"%s"' % (sys.executable)))
# This diagnostic sometimes appears in windows when using bash as an external
@@ -135,6 +134,16 @@ if platform.system() == "Linux":
if shutil.which("strace"):
config.available_features.add("strace")
+# Capture system paths BEFORE we prepend fake-externals to PATH.
+# This allows tests that need external commands (e.g., strace) to bypass the
+# fake binaries by using %{system-env} and %{system-path}.
+import shutil
+_real_env = shutil.which("env")
+if _real_env:
+ config.substitutions.append(("%{system-env}", _real_env))
+ # Capture the current PATH before fake-externals are prepended
+ config.substitutions.append(("%{system-path}", config.environment.get("PATH", "")))
+
# For each of lit's internal shell commands ('env', 'cd', 'diff', etc.), put
# a fake command that always fails at the start of PATH. This helps us check
# that we always use lit's internal version rather than some external version
diff --git a/llvm/utils/lit/tests/single-process.py b/llvm/utils/lit/tests/single-process.py
index 4cf7a21756f43..3c2624a029cae 100644
--- a/llvm/utils/lit/tests/single-process.py
+++ b/llvm/utils/lit/tests/single-process.py
@@ -14,7 +14,7 @@
# These are the semaphores that fail in sandboxed environments, so verifying
# they don't appear with -j1 confirms the fix works.
-# RUN: strace -f -e trace=openat %{python} %{lit-script} -j1 %{inputs}/shtest-format \
+# RUN: %{system-env} PATH="%{system-path}" strace -f -e trace=openat %{lit} -j1 %{inputs}/shtest-format \
# RUN: > %t.j1.out 2>&1 || true
# RUN: FileCheck --check-prefix=CHECK-J1 %s < %t.j1.out
#
@@ -26,7 +26,7 @@
# POSIX semaphores. This validates that the CHECK-NOT pattern above is correct
# and won't become stale if Python/glibc changes how semaphores are created.
#
-# RUN: strace -f -e trace=openat %{python} %{lit-script} -j2 %{inputs}/shtest-format \
+# RUN: %{system-env} PATH="%{system-path}" strace -f -e trace=openat %{lit} -j2 %{inputs}/shtest-format \
# RUN: > %t.j2.out 2>&1 || true
# RUN: FileCheck --check-prefix=CHECK-J2 %s < %t.j2.out
#
More information about the llvm-commits
mailing list