Author: Raphael Isemann Date: 2026-06-16T11:47:48+01:00 New Revision: 33bb0e76815aa9b1b26a5850d8859cdfb2718c2f
URL: https://github.com/llvm/llvm-project/commit/33bb0e76815aa9b1b26a5850d8859cdfb2718c2f DIFF: https://github.com/llvm/llvm-project/commit/33bb0e76815aa9b1b26a5850d8859cdfb2718c2f.diff LOG: [lldb][test] Faster shut down for pexpect tests (#201171) Our pexpect tests spend most of their time in the shutdown logic waiting for the test child to shut down. For example, our editline tests spend about 95% of their 40s runtime just waiting for the pexpect child to terminate. One of the reasons is that the ptyprocess terminate approach uses a timeout to give the child time to shut down and be cleaned up by the kernel. While this timeout makes sense, our timeout is extremely long (6s) since 56fb7456950d2564d16500e40c5719c954a6987a . Because the default ptyprocess implementation is designed for very short timeouts (0.1s), it just sleeps and then checks the process status. For our long timeout, the child most likely already terminated way before the timeout on a fast system. However, because we have some very slow builders, we cannot reduce this timeout without making tests flaky again. This patch replaces the default cleanup procedure with a custom one that polls the child status after each attempt to shut it down. If we find that the child is gone during polling, we immediately exit. This patch reduces the time the pexpect tests significantly. Each editline test for example now runs in 2 seconds on my machine compared to 40s before. Also, all editline tests I'm aware of are now no longer in the top 20 slowest test ranking on my setup. Added: Modified: lldb/packages/Python/lldbsuite/test/lldbpexpect.py Removed: ################################################################################ diff --git a/lldb/packages/Python/lldbsuite/test/lldbpexpect.py b/lldb/packages/Python/lldbsuite/test/lldbpexpect.py index 60b4f7f5d4a4b..7b48def504809 100644 --- a/lldb/packages/Python/lldbsuite/test/lldbpexpect.py +++ b/lldb/packages/Python/lldbsuite/test/lldbpexpect.py @@ -1,6 +1,8 @@ # System modules import os import sys +import time +import signal # LLDB Modules import lldb @@ -72,8 +74,14 @@ def launch( env=env, encoding=encoding, ) - self.child.ptyproc.delayafterclose = timeout / 10 - self.child.ptyproc.delayafterterminate = timeout / 10 + + # The interval in which we poll the LLDB process if we are + # closing/terminating the process. + self.polling_interval = 0.05 + # Timeout for each attempt to shut down LLDB at the end + # of the test. Note that there can be multiple attempts + # to shut down LLDB. + self.shutdown_timeout = timeout / 10 if post_spawn is not None: post_spawn() @@ -104,16 +112,77 @@ def tearDown(self): # Ensure the child is always cleaned up, even if the test didn't call # quit() explicitly or failed before reaching it. if self.child is not None: - self.quit() + self.quit(gracefully=True) super().tearDown() + def _poll_until_dead(self, proc): + """Poll proc.isalive() for up to shutdown_timeout seconds. + Returns True if the process exited within that window.""" + deadline = time.monotonic() + self.shutdown_timeout + while time.monotonic() < deadline: + if not proc.isalive(): + return True + time.sleep(self.polling_interval) + return not proc.isalive() + + def _terminate_child(self, force=False): + """Terminate the child using signals.""" + proc = self.child.ptyproc + + if not proc.isalive(): + return True + try: + # List of signals to try, starting from a friendly + # SIGHUP, SIGINT to SIGKILL if all else fails. + signals = [signal.SIGHUP, signal.SIGINT] + if force: + signals.append(signal.SIGKILL) + for sig in signals: + proc.kill(sig) + if self._poll_until_dead(proc): + return True + return False + except OSError: + # This handles a TOCTOU issue where we think the process is + # considered as 'isalive', then is actually killed by the + # kernel and then we try to kill it. + time.sleep(self.polling_interval) + return not proc.isalive() + + def _close_child(self, force=False): + """Close the connection to the child and terminate it if needed.""" + + ptyproc = self.child.ptyproc + if ptyproc.closed: + return + + # Close the PTY master fd first. + try: + self.child.sendeof() + ptyproc.fileobj.close() + except OSError: + # This could happen if LLDB shut down at the same time. + pass + + if not force: + # Poll briefly for the process to respond to the EOF before + # escalating to signals. + self._poll_until_dead(ptyproc) + + if ptyproc.isalive(): + # Kill the child with signals. This should always succeed. + assert self._terminate_child(force=force) + + # Make sure the object is now marked as closed. + ptyproc.fd = -1 + ptyproc.closed = True + def quit(self, gracefully=True): if self.child is None: return try: - self.child.sendeof() - self.child.close(force=not gracefully) + self._close_child(force=not gracefully) except OSError: # This can happen if LLDB quit itself because it is running in # batch mode. _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
