Skip to content

Conversation

dmpots
Copy link
Contributor

@dmpots dmpots commented Mar 3, 2025

Split test cases out of TestLldbGdbServer.py and TestGdbRemoteFork.py into separate files to avoid hitting the 600s timeout limit. The inferior used by these tests (main.cpp) takes approximately 20s to compile with a Debug build of clang, causing timeouts when a single test file contains many tests. By grouping similar tests into separate files, we can prevent timeouts and improve overall test efficiency.

@dmpots dmpots requested a review from JDevlieghere as a code owner March 3, 2025 23:49
@llvmbot llvmbot added the lldb label Mar 3, 2025
@llvmbot
Copy link
Member

llvmbot commented Mar 3, 2025

@llvm/pr-subscribers-lldb

Author: David Peixotto (dmpots)

Changes

This test is running right up to the 600s timeout limit. Split it in two to avoid hitting a timeout.


Patch is 72.16 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/129614.diff

2 Files Affected:

  • (modified) lldb/test/API/tools/lldb-server/TestLldbGdbServer.py (+3-898)
  • (added) lldb/test/API/tools/lldb-server/TestLldbGdbServer2.py (+922)
diff --git a/lldb/test/API/tools/lldb-server/TestLldbGdbServer.py b/lldb/test/API/tools/lldb-server/TestLldbGdbServer.py
index 592037db502aa..81639ed96597f 100644
--- a/lldb/test/API/tools/lldb-server/TestLldbGdbServer.py
+++ b/lldb/test/API/tools/lldb-server/TestLldbGdbServer.py
@@ -5,9 +5,9 @@
 lldb-server tests run where the lldb-server exe is
 available.
 
-This class will be broken into smaller test case classes by
-gdb remote packet functional areas.  For now it contains
-the initial set of tests implemented.
+The tests are split between the LldbGdbServerTestCase and
+LldbGdbServerTestCase2 classes to avoid timeouts in the
+test suite.
 """
 
 import binascii
@@ -602,898 +602,3 @@ def test_Hc_then_Csignal_signals_correct_thread_launch(self):
             self.Hc_then_Csignal_signals_correct_thread(
                 lldbutil.get_signal_number("SIGSEGV")
             )
-
-    @skipIfWindows  # No pty support to test any inferior output
-    def test_m_packet_reads_memory(self):
-        self.build()
-        self.set_inferior_startup_launch()
-        # This is the memory we will write into the inferior and then ensure we
-        # can read back with $m.
-        MEMORY_CONTENTS = "Test contents 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz"
-
-        # Start up the inferior.
-        procs = self.prep_debug_monitor_and_inferior(
-            inferior_args=[
-                "set-message:%s" % MEMORY_CONTENTS,
-                "get-data-address-hex:g_message",
-                "sleep:5",
-            ]
-        )
-
-        # Run the process
-        self.test_sequence.add_log_lines(
-            [
-                # Start running after initial stop.
-                "read packet: $c#63",
-                # Match output line that prints the memory address of the message buffer within the inferior.
-                # Note we require launch-only testing so we can get inferior otuput.
-                {
-                    "type": "output_match",
-                    "regex": self.maybe_strict_output_regex(
-                        r"data address: 0x([0-9a-fA-F]+)\r\n"
-                    ),
-                    "capture": {1: "message_address"},
-                },
-                # Now stop the inferior.
-                "read packet: {}".format(chr(3)),
-                # And wait for the stop notification.
-                {
-                    "direction": "send",
-                    "regex": r"^\$T([0-9a-fA-F]{2})thread:([0-9a-fA-F]+);",
-                    "capture": {1: "stop_signo", 2: "stop_thread_id"},
-                },
-            ],
-            True,
-        )
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-        # Grab the message address.
-        self.assertIsNotNone(context.get("message_address"))
-        message_address = int(context.get("message_address"), 16)
-
-        # Grab contents from the inferior.
-        self.reset_test_sequence()
-        self.test_sequence.add_log_lines(
-            [
-                "read packet: $m{0:x},{1:x}#00".format(
-                    message_address, len(MEMORY_CONTENTS)
-                ),
-                {
-                    "direction": "send",
-                    "regex": r"^\$(.+)#[0-9a-fA-F]{2}$",
-                    "capture": {1: "read_contents"},
-                },
-            ],
-            True,
-        )
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-        # Ensure what we read from inferior memory is what we wrote.
-        self.assertIsNotNone(context.get("read_contents"))
-        read_contents = seven.unhexlify(context.get("read_contents"))
-        self.assertEqual(read_contents, MEMORY_CONTENTS)
-
-    def test_qMemoryRegionInfo_is_supported(self):
-        self.build()
-        self.set_inferior_startup_launch()
-        # Start up the inferior.
-        procs = self.prep_debug_monitor_and_inferior()
-
-        # Ask if it supports $qMemoryRegionInfo.
-        self.test_sequence.add_log_lines(
-            ["read packet: $qMemoryRegionInfo#00", "send packet: $OK#00"], True
-        )
-        self.expect_gdbremote_sequence()
-
-    @skipIfWindows  # No pty support to test any inferior output
-    def test_qMemoryRegionInfo_reports_code_address_as_executable(self):
-        self.build()
-        self.set_inferior_startup_launch()
-
-        # Start up the inferior.
-        procs = self.prep_debug_monitor_and_inferior(
-            inferior_args=["get-code-address-hex:hello", "sleep:5"]
-        )
-
-        # Run the process
-        self.test_sequence.add_log_lines(
-            [
-                # Start running after initial stop.
-                "read packet: $c#63",
-                # Match output line that prints the memory address of the message buffer within the inferior.
-                # Note we require launch-only testing so we can get inferior otuput.
-                {
-                    "type": "output_match",
-                    "regex": self.maybe_strict_output_regex(
-                        r"code address: 0x([0-9a-fA-F]+)\r\n"
-                    ),
-                    "capture": {1: "code_address"},
-                },
-                # Now stop the inferior.
-                "read packet: {}".format(chr(3)),
-                # And wait for the stop notification.
-                {
-                    "direction": "send",
-                    "regex": r"^\$T([0-9a-fA-F]{2})thread:([0-9a-fA-F]+);",
-                    "capture": {1: "stop_signo", 2: "stop_thread_id"},
-                },
-            ],
-            True,
-        )
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-        # Grab the code address.
-        self.assertIsNotNone(context.get("code_address"))
-        code_address = int(context.get("code_address"), 16)
-
-        # Grab memory region info from the inferior.
-        self.reset_test_sequence()
-        self.add_query_memory_region_packets(code_address)
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-        mem_region_dict = self.parse_memory_region_packet(context)
-
-        # Ensure there are no errors reported.
-        self.assertNotIn("error", mem_region_dict)
-
-        # Ensure code address is readable and executable.
-        self.assertIn("permissions", mem_region_dict)
-        self.assertIn("r", mem_region_dict["permissions"])
-        self.assertIn("x", mem_region_dict["permissions"])
-
-        # Ensure the start address and size encompass the address we queried.
-        self.assert_address_within_memory_region(code_address, mem_region_dict)
-
-    @skipIfWindows  # No pty support to test any inferior output
-    def test_qMemoryRegionInfo_reports_stack_address_as_rw(self):
-        self.build()
-        self.set_inferior_startup_launch()
-
-        # Start up the inferior.
-        procs = self.prep_debug_monitor_and_inferior(
-            inferior_args=["get-stack-address-hex:", "sleep:5"]
-        )
-
-        # Run the process
-        self.test_sequence.add_log_lines(
-            [
-                # Start running after initial stop.
-                "read packet: $c#63",
-                # Match output line that prints the memory address of the message buffer within the inferior.
-                # Note we require launch-only testing so we can get inferior otuput.
-                {
-                    "type": "output_match",
-                    "regex": self.maybe_strict_output_regex(
-                        r"stack address: 0x([0-9a-fA-F]+)\r\n"
-                    ),
-                    "capture": {1: "stack_address"},
-                },
-                # Now stop the inferior.
-                "read packet: {}".format(chr(3)),
-                # And wait for the stop notification.
-                {
-                    "direction": "send",
-                    "regex": r"^\$T([0-9a-fA-F]{2})thread:([0-9a-fA-F]+);",
-                    "capture": {1: "stop_signo", 2: "stop_thread_id"},
-                },
-            ],
-            True,
-        )
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-        # Grab the address.
-        self.assertIsNotNone(context.get("stack_address"))
-        stack_address = int(context.get("stack_address"), 16)
-
-        # Grab memory region info from the inferior.
-        self.reset_test_sequence()
-        self.add_query_memory_region_packets(stack_address)
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-        mem_region_dict = self.parse_memory_region_packet(context)
-
-        # Ensure there are no errors reported.
-        self.assertNotIn("error", mem_region_dict)
-
-        # Ensure address is readable and executable.
-        self.assertIn("permissions", mem_region_dict)
-        self.assertIn("r", mem_region_dict["permissions"])
-        self.assertIn("w", mem_region_dict["permissions"])
-
-        # Ensure the start address and size encompass the address we queried.
-        self.assert_address_within_memory_region(stack_address, mem_region_dict)
-
-    @skipIfWindows  # No pty support to test any inferior output
-    def test_qMemoryRegionInfo_reports_heap_address_as_rw(self):
-        self.build()
-        self.set_inferior_startup_launch()
-
-        # Start up the inferior.
-        procs = self.prep_debug_monitor_and_inferior(
-            inferior_args=["get-heap-address-hex:", "sleep:5"]
-        )
-
-        # Run the process
-        self.test_sequence.add_log_lines(
-            [
-                # Start running after initial stop.
-                "read packet: $c#63",
-                # Match output line that prints the memory address of the message buffer within the inferior.
-                # Note we require launch-only testing so we can get inferior otuput.
-                {
-                    "type": "output_match",
-                    "regex": self.maybe_strict_output_regex(
-                        r"heap address: 0x([0-9a-fA-F]+)\r\n"
-                    ),
-                    "capture": {1: "heap_address"},
-                },
-                # Now stop the inferior.
-                "read packet: {}".format(chr(3)),
-                # And wait for the stop notification.
-                {
-                    "direction": "send",
-                    "regex": r"^\$T([0-9a-fA-F]{2})thread:([0-9a-fA-F]+);",
-                    "capture": {1: "stop_signo", 2: "stop_thread_id"},
-                },
-            ],
-            True,
-        )
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-        # Grab the address.
-        self.assertIsNotNone(context.get("heap_address"))
-        heap_address = int(context.get("heap_address"), 16)
-
-        # Grab memory region info from the inferior.
-        self.reset_test_sequence()
-        self.add_query_memory_region_packets(heap_address)
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-        mem_region_dict = self.parse_memory_region_packet(context)
-
-        # Ensure there are no errors reported.
-        self.assertNotIn("error", mem_region_dict)
-
-        # Ensure address is readable and executable.
-        self.assertIn("permissions", mem_region_dict)
-        self.assertIn("r", mem_region_dict["permissions"])
-        self.assertIn("w", mem_region_dict["permissions"])
-
-        # Ensure the start address and size encompass the address we queried.
-        self.assert_address_within_memory_region(heap_address, mem_region_dict)
-
-    def breakpoint_set_and_remove_work(self, want_hardware):
-        # Start up the inferior.
-        procs = self.prep_debug_monitor_and_inferior(
-            inferior_args=[
-                "get-code-address-hex:hello",
-                "sleep:1",
-                "call-function:hello",
-            ]
-        )
-
-        # Run the process
-        self.add_register_info_collection_packets()
-        self.add_process_info_collection_packets()
-        self.test_sequence.add_log_lines(
-            [  # Start running after initial stop.
-                "read packet: $c#63",
-                # Match output line that prints the memory address of the function call entry point.
-                # Note we require launch-only testing so we can get inferior otuput.
-                {
-                    "type": "output_match",
-                    "regex": self.maybe_strict_output_regex(
-                        r"code address: 0x([0-9a-fA-F]+)\r\n"
-                    ),
-                    "capture": {1: "function_address"},
-                },
-                # Now stop the inferior.
-                "read packet: {}".format(chr(3)),
-                # And wait for the stop notification.
-                {
-                    "direction": "send",
-                    "regex": r"^\$T([0-9a-fA-F]{2})thread:([0-9a-fA-F]+);",
-                    "capture": {1: "stop_signo", 2: "stop_thread_id"},
-                },
-            ],
-            True,
-        )
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-        # Gather process info - we need endian of target to handle register
-        # value conversions.
-        process_info = self.parse_process_info_response(context)
-        endian = process_info.get("endian")
-        self.assertIsNotNone(endian)
-
-        # Gather register info entries.
-        reg_infos = self.parse_register_info_packets(context)
-        (pc_lldb_reg_index, pc_reg_info) = self.find_pc_reg_info(reg_infos)
-        self.assertIsNotNone(pc_lldb_reg_index)
-        self.assertIsNotNone(pc_reg_info)
-
-        # Grab the function address.
-        self.assertIsNotNone(context.get("function_address"))
-        function_address = int(context.get("function_address"), 16)
-
-        # Get current target architecture
-        target_arch = self.getArchitecture()
-
-        # Set the breakpoint.
-        if target_arch in ["arm", "arm64", "aarch64"]:
-            # TODO: Handle case when setting breakpoint in thumb code
-            BREAKPOINT_KIND = 4
-        else:
-            BREAKPOINT_KIND = 1
-
-        # Set default packet type to Z0 (software breakpoint)
-        z_packet_type = 0
-
-        # If hardware breakpoint is requested set packet type to Z1
-        if want_hardware:
-            z_packet_type = 1
-
-        self.reset_test_sequence()
-        self.add_set_breakpoint_packets(
-            function_address,
-            z_packet_type,
-            do_continue=True,
-            breakpoint_kind=BREAKPOINT_KIND,
-        )
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-        # Verify the stop signal reported was the breakpoint signal number.
-        stop_signo = context.get("stop_signo")
-        self.assertIsNotNone(stop_signo)
-        self.assertEqual(int(stop_signo, 16), lldbutil.get_signal_number("SIGTRAP"))
-
-        # Ensure we did not receive any output.  If the breakpoint was not set, we would
-        # see output (from a launched process with captured stdio) printing a hello, world message.
-        # That would indicate the breakpoint didn't take.
-        self.assertEqual(len(context["O_content"]), 0)
-
-        # Verify that the PC for the main thread is where we expect it - right at the breakpoint address.
-        # This acts as a another validation on the register reading code.
-        self.reset_test_sequence()
-        self.test_sequence.add_log_lines(
-            [
-                # Print the PC.  This should match the breakpoint address.
-                "read packet: $p{0:x}#00".format(pc_lldb_reg_index),
-                # Capture $p results.
-                {
-                    "direction": "send",
-                    "regex": r"^\$([0-9a-fA-F]+)#",
-                    "capture": {1: "p_response"},
-                },
-            ],
-            True,
-        )
-
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-        # Verify the PC is where we expect.  Note response is in endianness of
-        # the inferior.
-        p_response = context.get("p_response")
-        self.assertIsNotNone(p_response)
-
-        # Convert from target endian to int.
-        returned_pc = lldbgdbserverutils.unpack_register_hex_unsigned(
-            endian, p_response
-        )
-        self.assertEqual(returned_pc, function_address)
-
-        # Verify that a breakpoint remove and continue gets us the expected
-        # output.
-        self.reset_test_sequence()
-
-        # Add breakpoint remove packets
-        self.add_remove_breakpoint_packets(
-            function_address, z_packet_type, breakpoint_kind=BREAKPOINT_KIND
-        )
-
-        self.test_sequence.add_log_lines(
-            [
-                # Continue running.
-                "read packet: $c#63",
-                # We should now receive the output from the call.
-                {"type": "output_match", "regex": r"^hello, world\r\n$"},
-                # And wait for program completion.
-                {"direction": "send", "regex": r"^\$W00(.*)#[0-9a-fA-F]{2}$"},
-            ],
-            True,
-        )
-
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-    @skipIfWindows  # No pty support to test any inferior output
-    def test_software_breakpoint_set_and_remove_work(self):
-        if self.getArchitecture() == "arm":
-            # TODO: Handle case when setting breakpoint in thumb code
-            self.build(dictionary={"CFLAGS_EXTRAS": "-marm"})
-        else:
-            self.build()
-        self.set_inferior_startup_launch()
-        self.breakpoint_set_and_remove_work(want_hardware=False)
-
-    @skipUnlessPlatform(oslist=["linux"])
-    @skipIf(archs=no_match(["arm", "aarch64"]))
-    def test_hardware_breakpoint_set_and_remove_work(self):
-        if self.getArchitecture() == "arm":
-            # TODO: Handle case when setting breakpoint in thumb code
-            self.build(dictionary={"CFLAGS_EXTRAS": "-marm"})
-        else:
-            self.build()
-        self.set_inferior_startup_launch()
-        self.breakpoint_set_and_remove_work(want_hardware=True)
-
-    def get_qSupported_dict(self, features=[]):
-        self.build()
-        self.set_inferior_startup_launch()
-
-        # Start up the stub and start/prep the inferior.
-        procs = self.prep_debug_monitor_and_inferior()
-        self.add_qSupported_packets(features)
-
-        # Run the packet stream.
-        context = self.expect_gdbremote_sequence()
-        self.assertIsNotNone(context)
-
-        # Retrieve the qSupported features.
-        return self.parse_qSupported_response(context)
-
-    def test_qSupported_returns_known_stub_features(self):
-        supported_dict = self.get_qSupported_dict()
-        self.assertIsNotNone(supported_dict)
-        self.assertGreater(len(supported_dict), 0)
-
-    def test_qSupported_auvx(self):
-        expected = (
-            "+"
-            if lldbplatformutil.getPlatform() in ["freebsd", "linux", "netbsd"]
-            else "-"
-        )
-        supported_dict = self.get_qSupported_dict()
-        self.assertEqual(supported_dict.get("qXfer:auxv:read", "-"), expected)
-
-    def test_qSupported_libraries_svr4(self):
-        expected = (
-            "+"
-            if lldbplatformutil.getPlatform() in ["freebsd", "linux", "netbsd"]
-            else "-"
-        )
-        supported_dict = self.get_qSupported_dict()
-        self.assertEqual(supported_dict.get("qXfer:libraries-svr4:read", "-"), expected)
-
-    def test_qSupported_siginfo_read(self):
-        expected = (
-            "+" if lldbplatformutil.getPlatform() in ["freebsd", "linux"] else "-"
-        )
-        supported_dict = self.get_qSupported_dict()
-        self.assertEqual(supp...
[truncated]

Copy link
Collaborator

@clayborg clayborg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't each method that starts with "test" get run with its own timeout? We might want to ensure we need all of the sleep methods in these tests. Many are being launched with "sleep:5" which can cause 5 second timeouts each time a binary is run.

@dmpots
Copy link
Contributor Author

dmpots commented Mar 4, 2025

Doesn't each method that starts with "test" get run with its own timeout?

No, it looks like the timeout comes from the lit runner which is one level up from the dotest runner that runs the individual test_ functions. Lit doesn't seem to know about how to run the individual test_ functions, just the full test class. At least that was my understanding of how it worked.

We might want to ensure we need all of the sleep methods in these tests. Many are being launched with "sleep:5" which can cause 5 second timeouts each time a binary is run.

We could try removing some of those, but it seems like it could lead to timing issues when running under stress. Looking at all the sleeps

sleep:5
sleep:5
sleep:5
sleep:5
sleep:1
sleep:1
sleep:2
sleep:10
sleep:1
sleep:10

it only adds up to < 1m, which is not too bad for the 10m test timeout we currently have (a couple of these are in helper functions but do not look to be called very often).

@DavidSpickett
Copy link
Collaborator

This test is running right up to the 600s timeout limit.

On what configuration exactly. I do not recall this happening on Linaro's buildbots at least.

(I have seen parts of this fail on Windows on Arm, but not timeouts)

@DavidSpickett DavidSpickett changed the title Split test to avoid timeout [lldb] Split test to avoid timeout Mar 4, 2025
@dmpots
Copy link
Contributor Author

dmpots commented Mar 4, 2025

This test is running right up to the 600s timeout limit.

On what configuration exactly. I do not recall this happening on Linaro's buildbots at least.

This is a Debug build on x86_64-linux running on a zen3 processor. It doesn't seem to be load related because it will still timeout running that single test.

@DavidSpickett
Copy link
Collaborator

Even for a debug build, timeouts aren't a good experience. The sleeps as you say are a minority of the time, and I would rather not disturb them anyway.

So I agree with splitting this.

How many others are close to the limit that you've found, just this one?

@dmpots
Copy link
Contributor Author

dmpots commented Mar 5, 2025

How many others are close to the limit that you've found, just this one?

I was only seeing the one timeout in the runs so I had not thought to measure. It's a good idea though. Based on the timings it looks like maybe TestGdbRemoteFork.py is worth splitting as well? That one seems to have about 30 tests in it.

llvm-lit -sv lldb/test --time-test

Slowest Tests:
--------------------------------------------------------------------------
574.78s: lldb-api :: tools/lldb-server/TestGdbRemoteFork.py
405.43s: lldb-api :: tools/lldb-server/TestLldbGdbServer2.py
401.09s: lldb-api :: api/multithreaded/TestMultithreaded.py
392.13s: lldb-api :: tools/lldb-server/TestLldbGdbServer.py
356.85s: lldb-api :: functionalities/completion/TestCompletion.py
302.01s: lldb-api :: tools/lldb-server/TestNonStop.py
296.05s: lldb-api :: functionalities/data-formatter/data-formatter-stl/libcxx-simulators/string/TestDataFormatterLibcxxStringSimulator.py
277.37s: lldb-api :: tools/lldb-server/TestGdbRemoteForkNonStop.py
268.45s: lldb-api :: functionalities/process_save_core_minidump/TestProcessSaveCoreMinidump.py
242.04s: lldb-api :: commands/register/register/register_command/TestRegisters.py
240.33s: lldb-api :: functionalities/single-thread-step/TestSingleThreadStepTimeout.py
237.83s: lldb-api :: commands/statistics/basic/TestStats.py
184.74s: lldb-api :: functionalities/thread/state/TestThreadStates.py
183.73s: lldb-api :: commands/settings/TestSettings.py
178.80s: lldb-api :: tools/lldb-server/vCont-threads/TestSignal.py
175.06s: lldb-api :: tools/lldb-server/TestGdbRemote_vCont.py
158.33s: lldb-api :: functionalities/data-formatter/data-formatter-stl/generic/set/TestDataFormatterGenericSet.py
157.08s: lldb-api :: commands/process/attach/TestProcessAttach.py
156.07s: lldb-api :: tools/lldb-server/TestGdbRemoteLaunch.py
155.51s: lldb-api :: tools/lldb-dap/variables/TestDAP_variables.py

Tests Times:
--------------------------------------------------------------------------
[   Range   ] :: [               Percentage               ] :: [  Count  ]
--------------------------------------------------------------------------
[550s,600s) :: [                                        ] :: [   1/2134]
[500s,550s) :: [                                        ] :: [   0/2134]
[450s,500s) :: [                                        ] :: [   0/2134]
[400s,450s) :: [                                        ] :: [   3/2134]
[350s,400s) :: [                                        ] :: [   1/2134]
[300s,350s) :: [                                        ] :: [   2/2134]
[250s,300s) :: [                                        ] :: [   4/2134]
[200s,250s) :: [                                        ] :: [   1/2134]
[150s,200s) :: [                                        ] :: [  11/2134]
[100s,150s) :: [                                        ] :: [  35/2134]
[ 50s,100s) :: [                                        ] :: [  52/2134]
[  0s, 50s) :: [*************************************   ] :: [2024/2134]
--------------------------------------------------------------------------

@DavidSpickett
Copy link
Collaborator

If this PR is approved, and TestGdbRemoteFork.py gives you trouble, splitting it sounds good.

I suspect folks will be on board with splitting these extremely long tests, but I will hold off approving for a few days in case that's not the case.

@DavidSpickett DavidSpickett changed the title [lldb] Split test to avoid timeout [lldb] Split TestGdbRemoteFork test to avoid timeout Mar 6, 2025
@dmpots
Copy link
Contributor Author

dmpots commented Mar 7, 2025

I looked into why this test (and TestGdbRemoteFork.py) is so slow. Turns out its nothing related to lldb itself, but the build step that occurs as part of the test.

These tests methods pretty much all call to build() (directly or indirectly)

to build the test lldb-server
https://github.com/llvm/llvm-project/blob/main/lldb/test/API/tools/lldb-server/main.cpp

And it takes about 20s to compile this file with a debug build! So each test is running for at least 20s. The actual test part (after the build) seems to usually be quite fast (< 1s).

The tests use the just-built clang to compile the file. Looking at the output from -ftime-passes most of this time is in the clang front end.

  Total Execution Time: 20.7652 seconds (20.7634 wall clock)

   ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
  18.0725 ( 88.1%)   0.1921 ( 73.6%)  18.2646 ( 88.0%)  18.2628 ( 88.0%)  Front end
   1.1838 (  5.8%)   0.0538 ( 20.6%)   1.2376 (  6.0%)   1.2377 (  6.0%)  Machine code generation
   1.1807 (  5.8%)   0.0141 (  5.4%)   1.1948 (  5.8%)   1.1946 (  5.8%)  LLVM IR generation
   0.0671 (  0.3%)   0.0010 (  0.4%)   0.0682 (  0.3%)   0.0682 (  0.3%)  Optimizer
  20.5041 (100.0%)   0.2611 (100.0%)  20.7652 (100.0%)  20.7634 (100.0%)  Total

Looking at the build() implementation, it looks like it builds a separate binary for each test_ method even though those seem to the same binary.

build() will call getBuildDirBasename() to get the output directory

testname = self.getBuildDirBasename()

which is defined so that the test to include name as part of the path
https://github.com/llvm/llvm-project/blob/96f369791d6c146ccadb03e7f9d6b1be37990425/lldb/packages/Python/lldbsuite/test/lldbtest.py#L726C9-L726C69

Any suggestions on how to improve this? It seems like we could maybe share the built executable amongst all tests in the file, but the test framework does put them into separate per-test locations.

@DavidSpickett
Copy link
Collaborator

You could make the test binary a cmake dependency, see lldb/test/CMakeLists.txt. Perhaps in:

if(TARGET lldb-server)
  add_lldb_test_dependency(lldb-server)
endif()

This means if you ran the test directly without ninja check-lldb, it would fail, but this would apply to any test using lldb-server as well so it's no worse.

@labath
Copy link
Collaborator

labath commented Mar 10, 2025

The test really is big and splitting it up is a good idea, but instead of an arbitrary 1/2/3 split, I think it be better to do it by functionality. Looking at the part of the code you moved, I can see several "themes" that we could use to split this up. One of them is "memory operations" (TestGdbRemoteMemory?) where we could put all of the memory and qMemoryRegionInfo tests. Another might be "qSupported" tests. And a third test for the register operations (p/P packets).

If that doesn't reduce the file sufficiently, I'm sure we can find some more.

@dmpots
Copy link
Contributor Author

dmpots commented Mar 10, 2025

You could make the test binary a cmake dependency, see lldb/test/CMakeLists.txt. Perhaps in:

if(TARGET lldb-server)
  add_lldb_test_dependency(lldb-server)
endif()

Just so it's clear, it is not lldb-server itself that is the slow dependency, but the cpp file in the test directory. I think you mean we could create a new binary from the test file.

Pulling out the cpp file into a separate tool might work. I don't know if there are any issues trying that. It's now building with the just-built clang binaries hooks into the dotest build system which seems to allow more flexibility in building it in different ways at test runtime (although I'm not sure we actually rely on that flexibility).

@dmpots
Copy link
Contributor Author

dmpots commented Mar 10, 2025

The test really is big and splitting it up is a good idea, but instead of an arbitrary 1/2/3 split, I think it be better to do it by functionality. Looking at the part of the code you moved, I can see several "themes" that we could use to split this up. One of them is "memory operations" (TestGdbRemoteMemory?) where we could put all of the memory and qMemoryRegionInfo tests. Another might be "qSupported" tests. And a third test for the register operations (p/P packets).

If that doesn't reduce the file sufficiently, I'm sure we can find some more.

Thanks for the suggestion. I wasn't familiar enough to do a proper split, but I can take a look based on your suggestion. It seems we already have some memory tests split out:

https://github.com/llvm/llvm-project/blob/main/lldb/test/API/tools/lldb-server/memory-allocation/TestGdbRemoteMemoryAllocation.py

@labath
Copy link
Collaborator

labath commented Mar 11, 2025

You could make the test binary a cmake dependency, see lldb/test/CMakeLists.txt. Perhaps in:

if(TARGET lldb-server)
  add_lldb_test_dependency(lldb-server)
endif()

Just so it's clear, it is not lldb-server itself that is the slow dependency, but the cpp file in the test directory. I think you mean we could create a new binary from the test file.

Pulling out the cpp file into a separate tool might work. I don't know if there are any issues trying that. It's now building with the just-built clang binaries hooks into the dotest build system which seems to allow more flexibility in building it in different ways at test runtime (although I'm not sure we actually rely on that flexibility).

I think that would be a problem for remote tests (where the architecture of the binary needs to be different from the lldb architecture) and would be a significant change in how the tests work. I really wouldn't want to go in that direction. What I could imagine is building the binary in the "static" setUpClass method so that it's only built once per test, but I suspect that even that could run be fairly tricky. It might be easiest to just merge some trivial tests. Targeted tests are sort of a "best practice", but there's obviously a tradeoff between that and test startup time, and I think some of these are going too far. For example, I don't think that test_Hg_fails_on_another_pid and test_Hg_fails_on_zero_pid really need to be separate tests (I'm not even sure we really need both of those).

@labath
Copy link
Collaborator

labath commented Mar 11, 2025

The test really is big and splitting it up is a good idea, but instead of an arbitrary 1/2/3 split, I think it be better to do it by functionality. Looking at the part of the code you moved, I can see several "themes" that we could use to split this up. One of them is "memory operations" (TestGdbRemoteMemory?) where we could put all of the memory and qMemoryRegionInfo tests. Another might be "qSupported" tests. And a third test for the register operations (p/P packets).
If that doesn't reduce the file sufficiently, I'm sure we can find some more.

Thanks for the suggestion. I wasn't familiar enough to do a proper split, but I can take a look based on your suggestion. It seems we already have some memory tests split out:

https://github.com/llvm/llvm-project/blob/main/lldb/test/API/tools/lldb-server/memory-allocation/TestGdbRemoteMemoryAllocation.py

Sounds good. And if you do that, then we can also create another/simpler binary for those tests. For example, all of the qSupported tests don't really care about the binary they're running. They just need a binary. It might as well just be int main(){}.

@dmpots dmpots changed the title [lldb] Split TestGdbRemoteFork test to avoid timeout [lldb] Split some tests to avoid timeout Mar 13, 2025
@dmpots
Copy link
Contributor Author

dmpots commented Mar 13, 2025

@labath @DavidSpickett I split the tests based on functional area. Please take a look, thanks!

Copy link

github-actions bot commented Mar 13, 2025

✅ With the latest revision this PR passed the Python code formatter.

Copy link
Collaborator

@labath labath left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make the folder name qSupported, as this is just too generic.

@DavidSpickett DavidSpickett changed the title [lldb] Split some tests to avoid timeout [lldb] Split some lldb-server tests to avoid timeout Mar 13, 2025
Copy link
Collaborator

@DavidSpickett DavidSpickett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with the folder name changed as suggested.

It'll be easier to find everything this way as well.

@dmpots dmpots merged commit d1deaed into llvm:main Mar 13, 2025
10 checks passed
@dmpots dmpots deleted the split-test branch March 13, 2025 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants