[compiler-rt] [llvm] [lit] [compiler-rt] Add llvm-lit global command cache to speed up test config (PR #195888)

Andrew Haberlandt via llvm-commits llvm-commits at lists.llvm.org
Tue May 5 12:04:00 PDT 2026


https://github.com/ndrewh updated https://github.com/llvm/llvm-project/pull/195888

>From 335aafcc84be71c646fd0c38abb1b10cad00544f Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Tue, 5 May 2026 10:32:23 -0700
Subject: [PATCH 1/3] [lit] [compiler-rt] Add llvm-lit global command cache to
 speed up test config

Compiler-rt lit test discovery takes quite a while on Darwin (i.e.
the time from when you launch llvm-lit to when the first test runs
can be minutes for check-compiler-rt). This appears to be mostly due
to subprocess calls during test configuration.

This adds a memoized command-runner to llvm-lit, so that lit.cfg.py
scripts can re-use the result of feature-detection commands when
running multiple test suites at once.

rdar://175893448
---
 compiler-rt/test/lit.common.cfg.py | 39 +++++++-----------------------
 llvm/utils/lit/lit/util.py         | 25 ++++++++++++++++++-
 2 files changed, 33 insertions(+), 31 deletions(-)

diff --git a/compiler-rt/test/lit.common.cfg.py b/compiler-rt/test/lit.common.cfg.py
index fef8f7ab297cc..b6eb8c08578fb 100644
--- a/compiler-rt/test/lit.common.cfg.py
+++ b/compiler-rt/test/lit.common.cfg.py
@@ -20,20 +20,8 @@ def get_path_from_clang(args, allow_failure):
         f"--target={config.target_triple}",
         *args,
     ]
-    path = None
-    try:
-        result = subprocess.run(
-            clang_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True
-        )
-        path = result.stdout.decode().strip()
-    except subprocess.CalledProcessError as e:
-        msg = f"Failed to run {clang_cmd}\nrc:{e.returncode}\nstdout:{e.stdout}\ne.stderr{e.stderr}"
-        if allow_failure:
-            lit_config.warning(msg)
-        else:
-            lit_config.fatal(msg)
-    return path, clang_cmd
-
+    path = lit.util.runCommandCached(clang_cmd, allow_failure, text=True)
+    return path.strip(), clang_cmd
 
 def find_compiler_libdir():
     """
@@ -592,10 +580,10 @@ def get_ios_commands_dir():
         # There is no simulator-specific sw_vers/sysctl, so we use the host OS version
         os_detection_prefix = []
 
-    darwin_os_version = subprocess.check_output(
-        os_detection_prefix + ["sw_vers", "-productVersion"], universal_newlines=True
+    darwin_os_version = lit.util.runCommandCached(
+        os_detection_prefix + ["sw_vers", "-productVersion"], universal_newlines=True, text=True
     )
-    darwin_os_version = tuple(int(x) for x in darwin_os_version.split("."))
+    darwin_os_version = tuple(int(x) for x in darwin_os_version.strip().split("."))
 
     if len(darwin_os_version) == 2:
         darwin_os_version = (darwin_os_version[0], darwin_os_version[1], 0)
@@ -609,8 +597,8 @@ def get_ios_commands_dir():
 
     # Detect x86_64h
     try:
-        output = subprocess.check_output(
-            os_detection_prefix + ["sysctl", "hw.cpusubtype"]
+        output = lit.util.runCommandCached(
+            os_detection_prefix + ["sysctl", "hw.cpusubtype"], text=True
         )
         output_re = re.match("^hw.cpusubtype: ([0-9]+)$", output)
         if output_re:
@@ -955,18 +943,9 @@ def is_windows_lto_supported():
     if lit.util.which("log"):
         # Querying the log can only done by a privileged user so
         # so check if we can query the log.
-        exit_code = -1
-        with open("/dev/null", "r") as f:
-            # Run a `log show` command the should finish fairly quickly and produce very little output.
-            exit_code = subprocess.call(
-                ["log", "show", "--last", "1m", "--predicate", "1 == 0"],
-                stdout=f,
-                stderr=f,
-            )
-        if exit_code == 0:
+        res = lit.util.runCommandCached(["log", "show", "--last", "1m", "--predicate", "1 == 0"], allow_failure=True)
+        if res is not None:
             config.available_features.add("darwin_log_cmd")
-        else:
-            lit_config.warning("log command found but cannot queried")
     else:
         lit_config.warning("log command not found. Some tests will be skipped.")
 elif config.android:
diff --git a/llvm/utils/lit/lit/util.py b/llvm/utils/lit/lit/util.py
index a800f1f6e1419..8fd296f460a02 100644
--- a/llvm/utils/lit/lit/util.py
+++ b/llvm/utils/lit/lit/util.py
@@ -12,7 +12,7 @@
 import subprocess
 import sys
 import threading
-
+import functools
 
 def pythonize_bool(value):
     if value is None:
@@ -441,3 +441,26 @@ def killProcessAndChildren(pid):
             psutilProc.kill()
         except psutil.NoSuchProcess:
             pass
+
+ at functools.cache
+def _runCommandCachedInner(cmd, allow_failure, **kwargs):
+    try:
+        result = subprocess.run(
+            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, **kwargs
+        )
+        return result.stdout
+    except subprocess.CalledProcessError as e:
+        msg = f"Failed to run {cmd}\nrc:{e.returncode}\nstdout:{e.stdout}\ne.stderr{e.stderr}"
+        if allow_failure:
+            lit_config.warning(msg)
+        else:
+            lit_config.fatal(msg)
+        return None
+
+def runCommandCached(cmd, allow_failure=False, **kwargs):
+    if type(cmd) is list:
+        cmd = tuple(cmd)
+    elif type(cmd) is str:
+        cmd = tuple([cmd])
+
+    return _runCommandCachedInner(cmd, allow_failure, **kwargs)

>From 4989f240098009812b585febf27a947afb7f2d7b Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Tue, 5 May 2026 10:45:34 -0700
Subject: [PATCH 2/3] format

---
 compiler-rt/test/lit.common.cfg.py | 8 ++++++--
 llvm/utils/lit/lit/util.py         | 2 ++
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/compiler-rt/test/lit.common.cfg.py b/compiler-rt/test/lit.common.cfg.py
index b6eb8c08578fb..37534fcd8dba8 100644
--- a/compiler-rt/test/lit.common.cfg.py
+++ b/compiler-rt/test/lit.common.cfg.py
@@ -581,7 +581,9 @@ def get_ios_commands_dir():
         os_detection_prefix = []
 
     darwin_os_version = lit.util.runCommandCached(
-        os_detection_prefix + ["sw_vers", "-productVersion"], universal_newlines=True, text=True
+        os_detection_prefix + ["sw_vers", "-productVersion"],
+        universal_newlines=True,
+        text=True,
     )
     darwin_os_version = tuple(int(x) for x in darwin_os_version.strip().split("."))
 
@@ -943,7 +945,9 @@ def is_windows_lto_supported():
     if lit.util.which("log"):
         # Querying the log can only done by a privileged user so
         # so check if we can query the log.
-        res = lit.util.runCommandCached(["log", "show", "--last", "1m", "--predicate", "1 == 0"], allow_failure=True)
+        res = lit.util.runCommandCached(
+            ["log", "show", "--last", "1m", "--predicate", "1 == 0"], allow_failure=True
+        )
         if res is not None:
             config.available_features.add("darwin_log_cmd")
     else:
diff --git a/llvm/utils/lit/lit/util.py b/llvm/utils/lit/lit/util.py
index 8fd296f460a02..35de0d1176e05 100644
--- a/llvm/utils/lit/lit/util.py
+++ b/llvm/utils/lit/lit/util.py
@@ -442,6 +442,7 @@ def killProcessAndChildren(pid):
         except psutil.NoSuchProcess:
             pass
 
+
 @functools.cache
 def _runCommandCachedInner(cmd, allow_failure, **kwargs):
     try:
@@ -457,6 +458,7 @@ def _runCommandCachedInner(cmd, allow_failure, **kwargs):
             lit_config.fatal(msg)
         return None
 
+
 def runCommandCached(cmd, allow_failure=False, **kwargs):
     if type(cmd) is list:
         cmd = tuple(cmd)

>From 756dfb44dbdedf02f474cc92304ad6deb7cea9b4 Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Tue, 5 May 2026 12:02:14 -0700
Subject: [PATCH 3/3] Feedback + add helper on LitCOnfig

---
 compiler-rt/test/lit.common.cfg.py | 16 +++--
 llvm/utils/lit/lit/LitConfig.py    | 16 +++++
 llvm/utils/lit/lit/util.py         | 24 ++++----
 llvm/utils/lit/tests/unit/Util.py  | 94 ++++++++++++++++++++++++++++++
 4 files changed, 128 insertions(+), 22 deletions(-)
 create mode 100644 llvm/utils/lit/tests/unit/Util.py

diff --git a/compiler-rt/test/lit.common.cfg.py b/compiler-rt/test/lit.common.cfg.py
index 37534fcd8dba8..12446b4708d20 100644
--- a/compiler-rt/test/lit.common.cfg.py
+++ b/compiler-rt/test/lit.common.cfg.py
@@ -20,7 +20,7 @@ def get_path_from_clang(args, allow_failure):
         f"--target={config.target_triple}",
         *args,
     ]
-    path = lit.util.runCommandCached(clang_cmd, allow_failure, text=True)
+    path = lit_config.run_command_cached(clang_cmd, allow_failure, text=True)
     return path.strip(), clang_cmd
 
 def find_compiler_libdir():
@@ -580,7 +580,7 @@ def get_ios_commands_dir():
         # There is no simulator-specific sw_vers/sysctl, so we use the host OS version
         os_detection_prefix = []
 
-    darwin_os_version = lit.util.runCommandCached(
+    darwin_os_version = lit_config.run_command_cached(
         os_detection_prefix + ["sw_vers", "-productVersion"],
         universal_newlines=True,
         text=True,
@@ -598,17 +598,15 @@ def get_ios_commands_dir():
     config.darwin_os_version = darwin_os_version
 
     # Detect x86_64h
-    try:
-        output = lit.util.runCommandCached(
-            os_detection_prefix + ["sysctl", "hw.cpusubtype"], text=True
-        )
+    output = lit_config.run_command_cached(
+        os_detection_prefix + ["sysctl", "hw.cpusubtype"], text=True, allow_failure=True
+    )
+    if output:
         output_re = re.match("^hw.cpusubtype: ([0-9]+)$", output)
         if output_re:
             cpu_subtype = int(output_re.group(1))
             if cpu_subtype == 8:  # x86_64h
                 config.available_features.add("x86_64h")
-    except:
-        pass
 
     # 32-bit iOS simulator is deprecated and removed in latest Xcode.
     if config.apple_platform == "iossim":
@@ -945,7 +943,7 @@ def is_windows_lto_supported():
     if lit.util.which("log"):
         # Querying the log can only done by a privileged user so
         # so check if we can query the log.
-        res = lit.util.runCommandCached(
+        res = lit_config.run_command_cached(
             ["log", "show", "--last", "1m", "--predicate", "1 == 0"], allow_failure=True
         )
         if res is not None:
diff --git a/llvm/utils/lit/lit/LitConfig.py b/llvm/utils/lit/lit/LitConfig.py
index 3b0dfaa981d4a..4be2a0f6d8121 100644
--- a/llvm/utils/lit/lit/LitConfig.py
+++ b/llvm/utils/lit/lit/LitConfig.py
@@ -263,6 +263,22 @@ def fatal(self, message):
         self._write_message("fatal", message)
         sys.exit(2)
 
+    def run_command_cached(self, cmd, allow_failure=False, **kwargs):
+        """
+        Run a command with subprocess.run, with a cache global to this llvm-lit invocation
+        If allow_failure is True, lit_config.fatal will be invoked if the command fails.
+        All additional kwargs are passed to subprocess.run
+        """
+        if type(cmd) is list:
+            cmd = tuple(cmd)
+            return lit.util.runCommandCached(self, cmd, allow_failure, **kwargs)
+        elif type(cmd) is str:
+            return lit.util.runCommandCached(self, cmd, allow_failure, **kwargs)
+        else:
+            raise ValueError(
+                f"runCommandCached expected list or str, got {type(cmd)}: {cmd}"
+            )
+
 
 @enum.unique
 class DiagnosticLevel(enum.IntEnum):
diff --git a/llvm/utils/lit/lit/util.py b/llvm/utils/lit/lit/util.py
index 35de0d1176e05..13eea2045242e 100644
--- a/llvm/utils/lit/lit/util.py
+++ b/llvm/utils/lit/lit/util.py
@@ -444,25 +444,23 @@ def killProcessAndChildren(pid):
 
 
 @functools.cache
-def _runCommandCachedInner(cmd, allow_failure, **kwargs):
+def runCommandCached(lit_config, cmd, allow_failure, **kwargs):
+    """
+    Run a command with subprocess.run, with a cache global to this llvm-lit invocation
+    If allow_failure is True, lit_config.fatal will be invoked if the command fails.
+    All additional kwargs are passed to subprocess.run
+    """
     try:
         result = subprocess.run(
             cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, **kwargs
         )
         return result.stdout
+    except FileNotFoundError as e:
+        msg = f"Failed to run {cmd}: {e}"
     except subprocess.CalledProcessError as e:
         msg = f"Failed to run {cmd}\nrc:{e.returncode}\nstdout:{e.stdout}\ne.stderr{e.stderr}"
-        if allow_failure:
-            lit_config.warning(msg)
-        else:
-            lit_config.fatal(msg)
-        return None
 
+    if not allow_failure:
+        lit_config.fatal(msg)
 
-def runCommandCached(cmd, allow_failure=False, **kwargs):
-    if type(cmd) is list:
-        cmd = tuple(cmd)
-    elif type(cmd) is str:
-        cmd = tuple([cmd])
-
-    return _runCommandCachedInner(cmd, allow_failure, **kwargs)
+    return None
diff --git a/llvm/utils/lit/tests/unit/Util.py b/llvm/utils/lit/tests/unit/Util.py
new file mode 100644
index 0000000000000..a84092c9cdc7a
--- /dev/null
+++ b/llvm/utils/lit/tests/unit/Util.py
@@ -0,0 +1,94 @@
+# RUN: %{python} %s
+# UNSUPPORTED: system-windows
+
+import unittest
+import platform
+import time
+
+from lit.util import runCommandCached
+from lit.LitConfig import LitConfig
+
+
+class TestCommandCache(unittest.TestCase):
+    @staticmethod
+    def _lit_config():
+        return LitConfig(
+            progname="lit",
+            path=[],
+            diagnostic_level="note",
+            useValgrind=False,
+            valgrindLeakCheck=False,
+            valgrindArgs=[],
+            noExecute=False,
+            debug=False,
+            isWindows=(platform.system() == "Windows"),
+            order="smart",
+            params={},
+        )
+
+    def test_basic(self):
+        lit_config = self._lit_config()
+
+        self.assertEqual(lit_config.run_command_cached(["echo", "-n", "hi"]), b"hi")
+        self.assertNotEqual(lit_config.run_command_cached("ls"), None)
+
+        # Test that arguments (e.g. text=True) get forwarded to subprocess.run
+        self.assertEqual(
+            lit_config.run_command_cached(["echo", "-n", "hi"], text=True), "hi"
+        )
+
+        # shell=True is not implied
+        self.assertEqual(
+            lit_config.run_command_cached("ls -al", allow_failure=True), None
+        )
+        self.assertNotEqual(
+            lit_config.run_command_cached("ls -al", allow_failure=True, shell=True),
+            None,
+        )
+
+        self.assertEqual(
+            lit_config.run_command_cached("exit 0", shell=True, allow_failure=True), b""
+        )
+
+    def test_fatal(self):
+        lit_config = self._lit_config()
+
+        # Test fatal errors
+        fatal_counter = 0
+
+        def wrap_fatal(msg):
+            nonlocal fatal_counter
+            fatal_counter += 1
+
+        lit_config.fatal = wrap_fatal
+        lit_config.run_command_cached(["asdfghjkl"])
+        self.assertEqual(fatal_counter, 1)
+
+        self.assertEqual(
+            lit_config.run_command_cached(["asdfghjkl"], allow_failure=True), None
+        )
+        self.assertEqual(
+            lit_config.run_command_cached("exit 1", shell=True, allow_failure=True),
+            None,
+        )
+        self.assertEqual(fatal_counter, 1)
+
+    def test_cache(self):
+        lit_config = self._lit_config()
+
+        t1 = time.time()
+        self.assertNotEqual(lit_config.run_command_cached("sleep 1", shell=True), None)
+        t2 = time.time()
+        self.assertGreater(t2 - t1, 0.5)
+
+        # Second time should be cached
+        t1 = time.time()
+        self.assertNotEqual(lit_config.run_command_cached("sleep 1", shell=True), None)
+        t2 = time.time()
+
+        # The second run should be instant
+        self.assertLess(t2 - t1, 0.5)
+
+
+if __name__ == "__main__":
+    unittest.main()



More information about the llvm-commits mailing list