Changeset 65343 in webkit


Ignore:
Timestamp:
Aug 13, 2010 5:34:13 PM (14 years ago)
Author:
dpranke@chromium.org
Message:

2010-08-13 Dirk Pranke <dpranke@chromium.org>

Reviewed by Eric Seidel.

Rewrite new-run-webkit-test's wait_for_threads_to_finish loop to
check for exceptions on all threads, not just the first thread.

This change also changes the logging behavior for wedged threads
to only dump the stacks of threads that are actually wedged.

Refactor the thread classes in the dump_render_tree_thread module
to make the contract between TestRunner and TestShellThread clearer.

Added a bunch of unit tests.
https://bugs.webkit.org/show_bug.cgi?id=38561

  • Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py:
  • Scripts/webkitpy/layout_tests/run_webkit_tests.py:
  • Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py:
Location:
trunk/WebKitTools
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/WebKitTools/ChangeLog

    r65308 r65343  
     12010-08-13  Dirk Pranke  <dpranke@chromium.org>
     2
     3        Reviewed by Eric Seidel.
     4
     5        Rewrite new-run-webkit-test's wait_for_threads_to_finish loop to
     6        check for exceptions on all threads, not just the first thread.
     7       
     8        This change also changes the logging behavior for wedged threads
     9        to only dump the stacks of threads that are actually wedged.
     10
     11        Refactor the thread classes in the dump_render_tree_thread module
     12        to make the contract between TestRunner and TestShellThread clearer.
     13
     14        Added a bunch of unit tests.
     15        https://bugs.webkit.org/show_bug.cgi?id=38561
     16
     17        * Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py:
     18        * Scripts/webkitpy/layout_tests/run_webkit_tests.py:
     19        * Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py:
     20
    1212010-08-13  Kenichi Ishibashi  <bashi@google.com>
    222
  • trunk/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py

    r62541 r65343  
    5555
    5656
    57 def process_output(port, test_info, test_types, test_args, configuration,
    58                    output_dir, crash, timeout, test_run_time, actual_checksum,
    59                    output, error):
     57def _process_output(port, test_info, test_types, test_args, configuration,
     58                    output_dir, crash, timeout, test_run_time, actual_checksum,
     59                    output, error):
    6060    """Receives the output from a DumpRenderTree process, subjects it to a
    6161    number of tests, and returns a list of failure types the test produced.
     
    119119
    120120
     121def _pad_timeout(timeout):
     122    """Returns a safe multiple of the per-test timeout value to use
     123    to detect hung test threads.
     124
     125    """
     126    # When we're running one test per DumpRenderTree process, we can
     127    # enforce a hard timeout.  The DumpRenderTree watchdog uses 2.5x
     128    # the timeout; we want to be larger than that.
     129    return timeout * 3
     130
     131
     132def _milliseconds_to_seconds(msecs):
     133    return float(msecs) / 1000.0
     134
     135
    121136class TestResult(object):
    122137
     
    163178                            test_info.image_hash())
    164179        end = time.time()
    165         self._test_result = process_output(self._port,
     180        self._test_result = _process_output(self._port,
    166181            test_info, self._test_types, self._test_args,
    167182            self._configuration, self._output_dir, crash, timeout, end - start,
     
    173188
    174189
    175 class TestShellThread(threading.Thread):
    176 
     190class WatchableThread(threading.Thread):
     191    """This class abstracts an interface used by
     192    run_webkit_tests.TestRunner._wait_for_threads_to_finish for thread
     193    management."""
     194    def __init__(self):
     195        threading.Thread.__init__(self)
     196        self._canceled = False
     197        self._exception_info = None
     198        self._next_timeout = None
     199        self._thread_id = None
     200
     201    def cancel(self):
     202        """Set a flag telling this thread to quit."""
     203        self._canceled = True
     204
     205    def clear_next_timeout(self):
     206        """Mark a flag telling this thread to stop setting timeouts."""
     207        self._timeout = 0
     208
     209    def exception_info(self):
     210        """If run() terminated on an uncaught exception, return it here
     211        ((type, value, traceback) tuple).
     212        Returns None if run() terminated normally. Meant to be called after
     213        joining this thread."""
     214        return self._exception_info
     215
     216    def id(self):
     217        """Return a thread identifier."""
     218        return self._thread_id
     219
     220    def next_timeout(self):
     221        """Return the time the test is supposed to finish by."""
     222        return self._next_timeout
     223
     224
     225class TestShellThread(WatchableThread):
    177226    def __init__(self, port, filename_list_queue, result_queue,
    178227                 test_types, test_args, image_path, shell_args, options):
     
    193242              run_webkit_tests; they are typically passed via the
    194243              run_webkit_tests.TestRunner class."""
    195         threading.Thread.__init__(self)
     244        WatchableThread.__init__(self)
    196245        self._port = port
    197246        self._filename_list_queue = filename_list_queue
     
    204253        self._shell_args = shell_args
    205254        self._options = options
    206         self._canceled = False
    207         self._exception_info = None
    208255        self._directory_timing_stats = {}
    209256        self._test_results = []
     
    232279        return self._test_results
    233280
    234     def cancel(self):
    235         """Set a flag telling this thread to quit."""
    236         self._canceled = True
    237 
    238     def get_exception_info(self):
    239         """If run() terminated on an uncaught exception, return it here
    240         ((type, value, traceback) tuple).
    241         Returns None if run() terminated normally. Meant to be called after
    242         joining this thread."""
    243         return self._exception_info
    244 
    245281    def get_total_time(self):
    246282        return max(self._stop_time - self._start_time, 0.0)
     
    252288        """Delegate main work to a helper method and watch for uncaught
    253289        exceptions."""
     290        self._thread_id = thread.get_ident()
    254291        self._start_time = time.time()
    255292        self._num_tests = 0
     
    385422        worker.start()
    386423
    387         # When we're running one test per DumpRenderTree process, we can
    388         # enforce a hard timeout.  The DumpRenderTree watchdog uses 2.5x
    389         # the timeout; we want to be larger than that.
    390         worker.join(int(test_info.timeout) * 3.0 / 1000.0)
     424        thread_timeout = _milliseconds_to_seconds(
     425            _pad_timeout(test_info.timeout))
     426        thread._next_timeout = time.time() + thread_timeout
     427        worker.join(thread_timeout)
    391428        if worker.isAlive():
    392429            # If join() returned with the thread still running, the
     
    434471            image_hash = ""
    435472        start = time.time()
     473
     474        thread_timeout = _milliseconds_to_seconds(
     475             _pad_timeout(test_info.timeout))
     476        self._next_timeout = start + thread_timeout
     477
    436478        crash, timeout, actual_checksum, output, error = \
    437479           self._driver.run_test(test_info.uri, test_info.timeout, image_hash)
    438480        end = time.time()
    439481
    440         result = process_output(self._port, test_info, self._test_types,
     482        result = _process_output(self._port, test_info, self._test_types,
    441483                                self._test_args, self._options.configuration,
    442484                                self._options.results_directory, crash,
  • trunk/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py

    r65011 r65343  
    5454import optparse
    5555import os
     56import pdb
    5657import platform
    5758import Queue
     
    630631        return int(self._options.child_processes) == 1
    631632
    632     def _dump_thread_states(self):
    633         for thread_id, stack in sys._current_frames().items():
    634             # FIXME: Python 2.6 has thread.ident which we could
    635             # use to map from thread_id back to thread.name
    636             print "\n# Thread: %d" % thread_id
    637             for filename, lineno, name, line in traceback.extract_stack(stack):
    638                 print 'File: "%s", line %d, in %s' % (filename, lineno, name)
    639                 if line:
    640                     print "  %s" % (line.strip())
    641 
    642     def _dump_thread_states_if_necessary(self):
    643         # HACK: Dump thread states every minute to figure out what's
    644         # hanging on the bots.
    645         if not self._options.verbose:
    646             return
    647         dump_threads_every = 60  # Dump every minute
    648         if not self._last_thread_dump:
    649             self._last_thread_dump = time.time()
    650         time_since_last_dump = time.time() - self._last_thread_dump
    651         if  time_since_last_dump > dump_threads_every:
    652             self._dump_thread_states()
    653             self._last_thread_dump = time.time()
    654 
    655633    def _run_tests(self, file_list, result_summary):
    656634        """Runs the tests in the file_list.
    657635
    658         Return: A tuple (failures, thread_timings, test_timings,
     636        Return: A tuple (keyboard_interrupted, thread_timings, test_timings,
    659637            individual_test_timings)
    660             failures is a map from test to list of failure types
     638            keyboard_interrupted is whether someone typed Ctrl^C
    661639            thread_timings is a list of dicts with the total runtime
    662640              of each thread with 'name', 'num_tests', 'total_time' properties
     
    677655        self._printer.print_update("Starting testing ...")
    678656
    679         # Wait for the threads to finish and collect test failures.
    680         failures = {}
    681         test_timings = {}
    682         individual_test_timings = []
    683         thread_timings = []
     657        keyboard_interrupted = self._wait_for_threads_to_finish(threads,
     658                                                                result_summary)
     659        (thread_timings, test_timings, individual_test_timings) = \
     660            self._collect_timing_info(threads)
     661
     662        return (keyboard_interrupted, thread_timings, test_timings,
     663                individual_test_timings)
     664
     665    def _wait_for_threads_to_finish(self, threads, result_summary):
    684666        keyboard_interrupted = False
    685667        try:
    686668            # Loop through all the threads waiting for them to finish.
    687             for thread in threads:
    688                 # FIXME: We'll end up waiting on the first thread the whole
    689                 # time.  That means we won't notice exceptions on other
    690                 # threads until the first one exits.
    691                 # We should instead while True: in the outer loop
    692                 # and then loop through threads joining and checking
    693                 # isAlive and get_exception_info.  Exiting on any exception.
    694                 while thread.isAlive():
    695                     # Wake the main thread every 0.1 seconds so we
    696                     # can call update_summary in a timely fashion.
    697                     thread.join(0.1)
    698                     # HACK: Used for debugging threads on the bots.
    699                     self._dump_thread_states_if_necessary()
    700                     self.update_summary(result_summary)
     669            some_thread_is_alive = True
     670            while some_thread_is_alive:
     671                some_thread_is_alive = False
     672                t = time.time()
     673                for thread in threads:
     674                    exception_info = thread.exception_info()
     675                    if exception_info is not None:
     676                        # Re-raise the thread's exception here to make it
     677                        # clear that testing was aborted. Otherwise,
     678                        # the tests that did not run would be assumed
     679                        # to have passed.
     680                        raise (exception_info[0], exception_info[1],
     681                               exception_info[2])
     682
     683                    if thread.isAlive():
     684                        some_thread_is_alive = True
     685                        next_timeout = thread.next_timeout()
     686                        if (next_timeout and t > next_timeout):
     687                            _log_wedged_thread(thread)
     688                            thread.clear_next_timeout()
     689
     690                self.update_summary(result_summary)
     691
     692                if some_thread_is_alive:
     693                    time.sleep(0.1)
    701694
    702695        except KeyboardInterrupt:
     
    705698                thread.cancel()
    706699
    707         if not keyboard_interrupted:
    708             for thread in threads:
    709                 # Check whether a thread died before normal completion.
    710                 exception_info = thread.get_exception_info()
    711                 if exception_info is not None:
    712                     # Re-raise the thread's exception here to make it clear
    713                     # something went wrong. Otherwise, the tests that did not
    714                     # run would be assumed to have passed.
    715                     raise (exception_info[0], exception_info[1],
    716                            exception_info[2])
     700        return keyboard_interrupted
     701
     702    def _collect_timing_info(self, threads):
     703        test_timings = {}
     704        individual_test_timings = []
     705        thread_timings = []
    717706
    718707        for thread in threads:
     
    722711            test_timings.update(thread.get_directory_timing_stats())
    723712            individual_test_timings.extend(thread.get_test_results())
    724         return (keyboard_interrupted, thread_timings, test_timings,
    725                 individual_test_timings)
     713
     714        return (thread_timings, test_timings, individual_test_timings)
    726715
    727716    def needs_http(self):
     
    16811670
    16821671
     1672def _find_thread_stack(id):
     1673    """Returns a stack object that can be used to dump a stack trace for
     1674    the given thread id (or None if the id is not found)."""
     1675    for thread_id, stack in sys._current_frames().items():
     1676        if thread_id == id:
     1677            return stack
     1678    return None
     1679
     1680
     1681def _log_stack(stack):
     1682    """Log a stack trace to log.error()."""
     1683    for filename, lineno, name, line in traceback.extract_stack(stack):
     1684        _log.error('File: "%s", line %d, in %s' % (filename, lineno, name))
     1685        if line:
     1686            _log.error('  %s' % line.strip())
     1687
     1688
     1689def _log_wedged_thread(thread):
     1690    """Log information about the given thread state."""
     1691    id = thread.id()
     1692    stack = _find_thread_stack(id)
     1693    assert(stack is not None)
     1694    _log.error("")
     1695    _log.error("thread %s (%d) is wedged" % (thread.getName(), id))
     1696    _log_stack(stack)
     1697    _log.error("")
     1698
     1699
    16831700def main():
    16841701    options, args = parse_args()
  • trunk/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py

    r59725 r65343  
    3131
    3232import codecs
     33import logging
    3334import os
     35import pdb
     36import Queue
    3437import sys
     38import thread
     39import time
     40import threading
    3541import unittest
    3642
     
    3844from webkitpy.layout_tests import port
    3945from webkitpy.layout_tests import run_webkit_tests
     46from webkitpy.layout_tests.layout_package import dump_render_tree_thread
    4047
    4148from webkitpy.thirdparty.mock import Mock
     
    9198        self.assertEqual(regular_output.get(), ['\n\n'])
    9299        self.assertEqual(buildbot_output.get(), [])
     100
    93101
    94102
     
    192200
    193201
     202class TestThread(dump_render_tree_thread.WatchableThread):
     203    def __init__(self, started_queue, stopping_queue):
     204        dump_render_tree_thread.WatchableThread.__init__(self)
     205        self._started_queue = started_queue
     206        self._stopping_queue = stopping_queue
     207        self._timeout = False
     208        self._timeout_queue = Queue.Queue()
     209
     210    def run(self):
     211        self._thread_id = thread.get_ident()
     212        try:
     213            self._started_queue.put('')
     214            msg = self._stopping_queue.get()
     215            if msg == 'KeyboardInterrupt':
     216                raise KeyboardInterrupt
     217            elif msg == 'Exception':
     218                raise ValueError()
     219            elif msg == 'Timeout':
     220                self._timeout = True
     221                self._timeout_queue.get()
     222        except:
     223            self._exception_info = sys.exc_info()
     224
     225    def next_timeout(self):
     226        if self._timeout:
     227            self._timeout_queue.put('done')
     228            return time.time() - 10
     229        return time.time()
     230
     231
     232class TestHandler(logging.Handler):
     233    def __init__(self, astream):
     234        logging.Handler.__init__(self)
     235        self._stream = astream
     236
     237    def emit(self, record):
     238        self._stream.write(self.format(record))
     239
     240
     241class WaitForThreadsToFinishTest(unittest.TestCase):
     242    class MockTestRunner(run_webkit_tests.TestRunner):
     243        def __init__(self):
     244            pass
     245
     246        def __del__(self):
     247            pass
     248
     249        def update_summary(self, result_summary):
     250            pass
     251
     252    def run_one_thread(self, msg):
     253        runner = self.MockTestRunner()
     254        starting_queue = Queue.Queue()
     255        stopping_queue = Queue.Queue()
     256        child_thread = TestThread(starting_queue, stopping_queue)
     257        child_thread.start()
     258        started_msg = starting_queue.get()
     259        stopping_queue.put(msg)
     260        threads = [child_thread]
     261        return runner._wait_for_threads_to_finish(threads, None)
     262
     263    def test_basic(self):
     264        interrupted = self.run_one_thread('')
     265        self.assertFalse(interrupted)
     266
     267    def test_interrupt(self):
     268        interrupted = self.run_one_thread('KeyboardInterrupt')
     269        self.assertTrue(interrupted)
     270
     271    def test_timeout(self):
     272        interrupted = self.run_one_thread('Timeout')
     273        self.assertFalse(interrupted)
     274
     275    def test_exception(self):
     276        self.assertRaises(ValueError, self.run_one_thread, 'Exception')
     277
     278
     279class StandaloneFunctionsTest(unittest.TestCase):
     280    def test_log_wedged_thread(self):
     281        logger = run_webkit_tests._log
     282        astream = array_stream.ArrayStream()
     283        handler = TestHandler(astream)
     284        logger.addHandler(handler)
     285
     286        starting_queue = Queue.Queue()
     287        stopping_queue = Queue.Queue()
     288        child_thread = TestThread(starting_queue, stopping_queue)
     289        child_thread.start()
     290        msg = starting_queue.get()
     291
     292        run_webkit_tests._log_wedged_thread(child_thread)
     293        stopping_queue.put('')
     294        child_thread.join(timeout=1.0)
     295
     296        self.assertFalse(astream.empty())
     297        self.assertFalse(child_thread.isAlive())
     298
     299    def test_find_thread_stack(self):
     300        id, stack = sys._current_frames().items()[0]
     301        found_stack = run_webkit_tests._find_thread_stack(id)
     302        self.assertNotEqual(found_stack, None)
     303
     304        found_stack = run_webkit_tests._find_thread_stack(0)
     305        self.assertEqual(found_stack, None)
     306
    194307if __name__ == '__main__':
    195308    unittest.main()
Note: See TracChangeset for help on using the changeset viewer.