diff options
author | Dimitry Andric <dim@FreeBSD.org> | 2017-01-02 19:26:05 +0000 |
---|---|---|
committer | Dimitry Andric <dim@FreeBSD.org> | 2017-01-02 19:26:05 +0000 |
commit | 14f1b3e8826ce43b978db93a62d1166055db5394 (patch) | |
tree | 0a00ad8d3498783fe0193f3b656bca17c4c8697d /packages/Python/lldbsuite/pre_kill_hook | |
parent | 4ee8c119c71a06dcad1e0fecc8c675e480e59337 (diff) | |
download | src-14f1b3e8826ce43b978db93a62d1166055db5394.tar.gz src-14f1b3e8826ce43b978db93a62d1166055db5394.zip |
Vendor import of lldb trunk r290819:vendor/lldb/lldb-trunk-r290819
Notes
Notes:
svn path=/vendor/lldb/dist/; revision=311128
svn path=/vendor/lldb/lldb-trunk-r290819/; revision=311129; tag=vendor/lldb/lldb-trunk-r290819
Diffstat (limited to 'packages/Python/lldbsuite/pre_kill_hook')
7 files changed, 418 insertions, 0 deletions
diff --git a/packages/Python/lldbsuite/pre_kill_hook/README.md b/packages/Python/lldbsuite/pre_kill_hook/README.md new file mode 100644 index 000000000000..921eedb4a869 --- /dev/null +++ b/packages/Python/lldbsuite/pre_kill_hook/README.md @@ -0,0 +1,55 @@ +# pre\_kill\_hook package + +## Overview + +The pre\_kill\_hook package provides a per-platform method for running code +after a test process times out but before the concurrent test runner kills the +timed-out process. + +## Detailed Description of Usage + +If a platform defines the hook, then the hook gets called right after a timeout +is detected in a test run, but before the process is killed. + +The pre-kill-hook mechanism works as follows: + +* When a timeout is detected in the process_control.ProcessDriver class that + runs the per-test lldb process, a new overridable on\_timeout\_pre\_kill() method + is called on the ProcessDriver instance. + +* The concurrent test driver's derived ProcessDriver overrides this method. It + looks to see if a module called + "lldbsuite.pre\_kill\_hook.{platform-system-name}" module exists, where + platform-system-name is replaced with platform.system().lower(). (e.g. + "Darwin" becomes the darwin.py module). + + * If that module doesn't exist, the rest of the new behavior is skipped. + + * If that module does exist, it is loaded, and the method + "do\_pre\_kill(process\_id, context\_dict, output\_stream)" is called. If + that method throws an exception, we log it and we ignore further processing + of the pre-killed process. + + * The process\_id argument of the do\_pre\_kill function is the process id as + returned by the ProcessDriver.pid property. + + * The output\_stream argument of the do\_pre\_kill function takes a file-like + object. Output to be collected from doing any processing on the + process-to-be-killed should be written into the file-like object. The + current impl uses a six.StringIO and then writes this output to + {TestFilename}-{pid}.sample in the session directory. + +* Platforms where platform.system() is "Darwin" will get a pre-kill action that + runs the 'sample' program on the lldb that has timed out. That data will be + collected on CI and analyzed to determine what is happening during timeouts. + (This has an advantage over a core in that it is much smaller and that it + clearly demonstrates any liveness of the process, if there is any). + +## Running the tests + +To run the tests in the pre\_kill\_hook package, open a console, change into +this directory and run the following: + +``` +python -m unittest discover +``` diff --git a/packages/Python/lldbsuite/pre_kill_hook/__init__.py b/packages/Python/lldbsuite/pre_kill_hook/__init__.py new file mode 100644 index 000000000000..c3a852ea1bfe --- /dev/null +++ b/packages/Python/lldbsuite/pre_kill_hook/__init__.py @@ -0,0 +1 @@ +"""Initialize the package.""" diff --git a/packages/Python/lldbsuite/pre_kill_hook/darwin.py b/packages/Python/lldbsuite/pre_kill_hook/darwin.py new file mode 100644 index 000000000000..2bee65a01e3f --- /dev/null +++ b/packages/Python/lldbsuite/pre_kill_hook/darwin.py @@ -0,0 +1,46 @@ +"""Provides a pre-kill method to run on macOS.""" +from __future__ import print_function + +# system imports +import subprocess +import sys + +# third-party module imports +import six + + +def do_pre_kill(process_id, runner_context, output_stream, sample_time=3): + """Samples the given process id, and puts the output to output_stream. + + @param process_id the local process to sample. + + @param runner_context a dictionary of details about the architectures + and platform on which the given process is running. Expected keys are + archs (array of architectures), platform_name, platform_url, and + platform_working_dir. + + @param output_stream file-like object that should be used to write the + results of sampling. + + @param sample_time specifies the time in seconds that should be captured. + """ + + # Validate args. + if runner_context is None: + raise Exception("runner_context argument is required") + if not isinstance(runner_context, dict): + raise Exception("runner_context argument must be a dictionary") + + # We will try to run sample on the local host only if there is no URL + # to a remote. + if "platform_url" in runner_context and ( + runner_context["platform_url"] is not None): + import pprint + sys.stderr.write( + "warning: skipping timeout pre-kill sample invocation because we " + "don't know how to run on a remote yet. runner_context={}\n" + .format(pprint.pformat(runner_context))) + + output = subprocess.check_output(['sample', six.text_type(process_id), + str(sample_time)]) + output_stream.write(output) diff --git a/packages/Python/lldbsuite/pre_kill_hook/linux.py b/packages/Python/lldbsuite/pre_kill_hook/linux.py new file mode 100644 index 000000000000..d4cd9be27c82 --- /dev/null +++ b/packages/Python/lldbsuite/pre_kill_hook/linux.py @@ -0,0 +1,76 @@ +"""Provides a pre-kill method to run on Linux. + +This timeout pre-kill method relies on the Linux perf-tools +distribution. The appropriate way to obtain this set of tools +will depend on the Linux distribution. + +For Ubuntu 16.04, the invoke the following command: +sudo apt-get install perf-tools-unstable +""" +from __future__ import print_function + +# system imports +import os +import subprocess +import sys +import tempfile + + +def do_pre_kill(process_id, runner_context, output_stream, sample_time=3): + """Samples the given process id, and puts the output to output_stream. + + @param process_id the local process to sample. + + @param runner_context a dictionary of details about the architectures + and platform on which the given process is running. Expected keys are + archs (array of architectures), platform_name, platform_url, and + platform_working_dir. + + @param output_stream file-like object that should be used to write the + results of sampling. + + @param sample_time specifies the time in seconds that should be captured. + """ + + # Validate args. + if runner_context is None: + raise Exception("runner_context argument is required") + if not isinstance(runner_context, dict): + raise Exception("runner_context argument must be a dictionary") + + # We will try to run sample on the local host only if there is no URL + # to a remote. + if "platform_url" in runner_context and ( + runner_context["platform_url"] is not None): + import pprint + sys.stderr.write( + "warning: skipping timeout pre-kill sample invocation because we " + "don't know how to run on a remote yet. runner_context={}\n" + .format(pprint.pformat(runner_context))) + + # We're going to create a temp file, and immediately overwrite it with the + # following command. This just ensures we don't have any races in + # creation of the temporary sample file. + fileno, filename = tempfile.mkstemp(suffix='perfdata') + os.close(fileno) + fileno = None + + try: + with open(os.devnull, 'w') as devnull: + returncode = subprocess.call(['timeout', str(sample_time), 'perf', + 'record', '-g', '-o', filename, '-p', str(process_id)], + stdout=devnull, stderr=devnull) + if returncode == 0 or returncode == 124: + # This is okay - this is the timeout return code, which is totally + # expected. + pass + else: + raise Exception("failed to call 'perf record .., error: {}".format( + returncode)) + + with open(os.devnull, 'w') as devnull: + output = subprocess.check_output(['perf', 'report', '--call-graph', + '--stdio', '-i', filename], stderr=devnull) + output_stream.write(output) + finally: + os.remove(filename) diff --git a/packages/Python/lldbsuite/pre_kill_hook/tests/__init__.py b/packages/Python/lldbsuite/pre_kill_hook/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/packages/Python/lldbsuite/pre_kill_hook/tests/__init__.py diff --git a/packages/Python/lldbsuite/pre_kill_hook/tests/test_darwin.py b/packages/Python/lldbsuite/pre_kill_hook/tests/test_darwin.py new file mode 100644 index 000000000000..810b364b07c3 --- /dev/null +++ b/packages/Python/lldbsuite/pre_kill_hook/tests/test_darwin.py @@ -0,0 +1,107 @@ +"""Test the pre-kill hook on Darwin.""" +from __future__ import print_function + +# system imports +from multiprocessing import Process, Queue +import platform +import re +from unittest import main, TestCase + +# third party +from six import StringIO + + +def do_child_process(child_work_queue, parent_work_queue, verbose): + import os + + pid = os.getpid() + if verbose: + print("child: pid {} started, sending to parent".format(pid)) + parent_work_queue.put(pid) + if verbose: + print("child: waiting for shut-down request from parent") + child_work_queue.get() + if verbose: + print("child: received shut-down request. Child exiting.") + + +class DarwinPreKillTestCase(TestCase): + + def __init__(self, methodName): + super(DarwinPreKillTestCase, self).__init__(methodName) + self.process = None + self.child_work_queue = None + self.verbose = False + + def tearDown(self): + if self.verbose: + print("parent: sending shut-down request to child") + if self.process: + self.child_work_queue.put("hello, child") + self.process.join() + if self.verbose: + print("parent: child is fully shut down") + + def test_sample(self): + # Ensure we're Darwin. + if platform.system() != 'Darwin': + self.skipTest("requires a Darwin-based OS") + + # Start the child process. + self.child_work_queue = Queue() + parent_work_queue = Queue() + self.process = Process(target=do_child_process, + args=(self.child_work_queue, parent_work_queue, + self.verbose)) + if self.verbose: + print("parent: starting child") + self.process.start() + + # Wait for the child to report its pid. Then we know we're running. + if self.verbose: + print("parent: waiting for child to start") + child_pid = parent_work_queue.get() + + # Sample the child process. + from darwin import do_pre_kill + context_dict = { + "archs": [platform.machine()], + "platform_name": None, + "platform_url": None, + "platform_working_dir": None + } + + if self.verbose: + print("parent: running pre-kill action on child") + output_io = StringIO() + do_pre_kill(child_pid, context_dict, output_io) + output = output_io.getvalue() + + if self.verbose: + print("parent: do_pre_kill() wrote the following output:", output) + self.assertIsNotNone(output) + + # We should have a line with: + # Process: .* [{pid}] + process_re = re.compile(r"Process:[^[]+\[([^]]+)\]") + match = process_re.search(output) + self.assertIsNotNone(match, "should have found process id for " + "sampled process") + self.assertEqual(1, len(match.groups())) + self.assertEqual(child_pid, int(match.group(1))) + + # We should see a Call graph: section. + callgraph_re = re.compile(r"Call graph:") + match = callgraph_re.search(output) + self.assertIsNotNone(match, "should have found the Call graph section" + "in sample output") + + # We should see a Binary Images: section. + binary_images_re = re.compile(r"Binary Images:") + match = binary_images_re.search(output) + self.assertIsNotNone(match, "should have found the Binary Images " + "section in sample output") + + +if __name__ == "__main__": + main() diff --git a/packages/Python/lldbsuite/pre_kill_hook/tests/test_linux.py b/packages/Python/lldbsuite/pre_kill_hook/tests/test_linux.py new file mode 100644 index 000000000000..ab989df0d203 --- /dev/null +++ b/packages/Python/lldbsuite/pre_kill_hook/tests/test_linux.py @@ -0,0 +1,133 @@ +"""Test the pre-kill hook on Linux.""" +from __future__ import print_function + +# system imports +from multiprocessing import Process, Queue +import platform +import re +import subprocess +from unittest import main, TestCase + +# third party +from six import StringIO + + +def do_child_thread(): + import os + x = 0 + while True: + x = x + 42 * os.getpid() + return x + + +def do_child_process(child_work_queue, parent_work_queue, verbose): + import os + + pid = os.getpid() + if verbose: + print("child: pid {} started, sending to parent".format(pid)) + parent_work_queue.put(pid) + + # Spin up a daemon thread to do some "work", which will show + # up in a sample of this process. + import threading + worker = threading.Thread(target=do_child_thread) + worker.daemon = True + worker.start() + + if verbose: + print("child: waiting for shut-down request from parent") + child_work_queue.get() + if verbose: + print("child: received shut-down request. Child exiting.") + + +class LinuxPreKillTestCase(TestCase): + + def __init__(self, methodName): + super(LinuxPreKillTestCase, self).__init__(methodName) + self.process = None + self.child_work_queue = None + self.verbose = False + # self.verbose = True + + def tearDown(self): + if self.verbose: + print("parent: sending shut-down request to child") + if self.process: + self.child_work_queue.put("hello, child") + self.process.join() + if self.verbose: + print("parent: child is fully shut down") + + def test_sample(self): + # Ensure we're Darwin. + if platform.system() != 'Linux': + self.skipTest("requires a Linux-based OS") + + # Ensure we have the 'perf' tool. If not, skip the test. + try: + perf_version = subprocess.check_output(["perf", "version"]) + if perf_version is None or not ( + perf_version.startswith("perf version")): + raise Exception("The perf executable doesn't appear" + " to be the Linux perf tools perf") + except Exception: + self.skipTest("requires the Linux perf tools 'perf' command") + + # Start the child process. + self.child_work_queue = Queue() + parent_work_queue = Queue() + self.process = Process(target=do_child_process, + args=(self.child_work_queue, parent_work_queue, + self.verbose)) + if self.verbose: + print("parent: starting child") + self.process.start() + + # Wait for the child to report its pid. Then we know we're running. + if self.verbose: + print("parent: waiting for child to start") + child_pid = parent_work_queue.get() + + # Sample the child process. + from linux import do_pre_kill + context_dict = { + "archs": [platform.machine()], + "platform_name": None, + "platform_url": None, + "platform_working_dir": None + } + + if self.verbose: + print("parent: running pre-kill action on child") + output_io = StringIO() + do_pre_kill(child_pid, context_dict, output_io) + output = output_io.getvalue() + + if self.verbose: + print("parent: do_pre_kill() wrote the following output:", output) + self.assertIsNotNone(output) + + # We should have a samples count entry. + # Samples: + self.assertTrue("Samples:" in output, "should have found a 'Samples:' " + "field in the sampled process output") + + # We should see an event count entry + event_count_re = re.compile(r"Event count[^:]+:\s+(\d+)") + match = event_count_re.search(output) + self.assertIsNotNone(match, "should have found the event count entry " + "in sample output") + if self.verbose: + print("cpu-clock events:", match.group(1)) + + # We should see some percentages in the file. + percentage_re = re.compile(r"\d+\.\d+%") + match = percentage_re.search(output) + self.assertIsNotNone(match, "should have found at least one percentage " + "in the sample output") + + +if __name__ == "__main__": + main() |