[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