mac.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. # Copyright (C) 2011 Google Inc. All rights reserved.
  2. # Copyright (C) 2012, 2013 Apple Inc. All rights reserved.
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are
  6. # met:
  7. #
  8. # * Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. # * Redistributions in binary form must reproduce the above
  11. # copyright notice, this list of conditions and the following disclaimer
  12. # in the documentation and/or other materials provided with the
  13. # distribution.
  14. # * Neither the Google name nor the names of its
  15. # contributors may be used to endorse or promote products derived from
  16. # this software without specific prior written permission.
  17. #
  18. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. import logging
  30. import os
  31. import time
  32. from webkitpy.common.system.crashlogs import CrashLogs
  33. from webkitpy.common.system.executive import ScriptError
  34. from webkitpy.port.apple import ApplePort
  35. from webkitpy.port.leakdetector import LeakDetector
  36. _log = logging.getLogger(__name__)
  37. class MacPort(ApplePort):
  38. port_name = "mac"
  39. VERSION_FALLBACK_ORDER = ['mac-snowleopard', 'mac-lion', 'mac-mountainlion']
  40. ARCHITECTURES = ['x86_64', 'x86']
  41. def __init__(self, host, port_name, **kwargs):
  42. ApplePort.__init__(self, host, port_name, **kwargs)
  43. self._architecture = self.get_option('architecture')
  44. if not self._architecture:
  45. self._architecture = 'x86_64'
  46. self._leak_detector = LeakDetector(self)
  47. if self.get_option("leaks"):
  48. # DumpRenderTree slows down noticably if we run more than about 1000 tests in a batch
  49. # with MallocStackLogging enabled.
  50. self.set_option_default("batch_size", 1000)
  51. def default_timeout_ms(self):
  52. if self.get_option('guard_malloc'):
  53. return 350 * 1000
  54. return super(MacPort, self).default_timeout_ms()
  55. def supports_per_test_timeout(self):
  56. return True
  57. def _build_driver_flags(self):
  58. return ['ARCHS=i386'] if self.architecture() == 'x86' else []
  59. def should_retry_crashes(self):
  60. # On Apple Mac, we retry crashes due to https://bugs.webkit.org/show_bug.cgi?id=82233
  61. return True
  62. def default_baseline_search_path(self):
  63. name = self._name.replace('-wk2', '')
  64. if name.endswith(self.FUTURE_VERSION):
  65. fallback_names = [self.port_name]
  66. else:
  67. fallback_names = self.VERSION_FALLBACK_ORDER[self.VERSION_FALLBACK_ORDER.index(name):-1] + [self.port_name]
  68. if self.get_option('webkit_test_runner'):
  69. fallback_names = [self._wk2_port_name(), 'wk2'] + fallback_names
  70. return map(self._webkit_baseline_path, fallback_names)
  71. def _port_specific_expectations_files(self):
  72. return list(reversed([self._filesystem.join(self._webkit_baseline_path(p), 'TestExpectations') for p in self.baseline_search_path()]))
  73. def setup_environ_for_server(self, server_name=None):
  74. env = super(MacPort, self).setup_environ_for_server(server_name)
  75. if server_name == self.driver_name():
  76. if self.get_option('leaks'):
  77. env['MallocStackLogging'] = '1'
  78. if self.get_option('guard_malloc'):
  79. env['DYLD_INSERT_LIBRARIES'] = '/usr/lib/libgmalloc.dylib:' + self._build_path("libWebCoreTestShim.dylib")
  80. else:
  81. env['DYLD_INSERT_LIBRARIES'] = self._build_path("libWebCoreTestShim.dylib")
  82. env['XML_CATALOG_FILES'] = '' # work around missing /etc/catalog <rdar://problem/4292995>
  83. return env
  84. def operating_system(self):
  85. return 'mac'
  86. # Belongs on a Platform object.
  87. def is_snowleopard(self):
  88. return self._version == "snowleopard"
  89. # Belongs on a Platform object.
  90. def is_lion(self):
  91. return self._version == "lion"
  92. def default_child_processes(self):
  93. if self._version == "snowleopard":
  94. _log.warning("Cannot run tests in parallel on Snow Leopard due to rdar://problem/10621525.")
  95. return 1
  96. default_count = super(MacPort, self).default_child_processes()
  97. # FIXME: https://bugs.webkit.org/show_bug.cgi?id=95906 With too many WebProcess WK2 tests get stuck in resource contention.
  98. # To alleviate the issue reduce the number of running processes
  99. # Anecdotal evidence suggests that a 4 core/8 core logical machine may run into this, but that a 2 core/4 core logical machine does not.
  100. should_throttle_for_wk2 = self.get_option('webkit_test_runner') and default_count > 4
  101. # We also want to throttle for leaks bots.
  102. if should_throttle_for_wk2 or self.get_option('leaks'):
  103. default_count = int(.75 * default_count)
  104. # Make sure we have enough ram to support that many instances:
  105. total_memory = self.host.platform.total_bytes_memory()
  106. if total_memory:
  107. bytes_per_drt = 256 * 1024 * 1024 # Assume each DRT needs 256MB to run.
  108. overhead = 2048 * 1024 * 1024 # Assume we need 2GB free for the O/S
  109. supportable_instances = max((total_memory - overhead) / bytes_per_drt, 1) # Always use one process, even if we don't have space for it.
  110. if supportable_instances < default_count:
  111. _log.warning("This machine could support %s child processes, but only has enough memory for %s." % (default_count, supportable_instances))
  112. else:
  113. _log.warning("Cannot determine available memory for child processes, using default child process count of %s." % default_count)
  114. supportable_instances = default_count
  115. return min(supportable_instances, default_count)
  116. def _build_java_test_support(self):
  117. java_tests_path = self._filesystem.join(self.layout_tests_dir(), "java")
  118. build_java = [self.make_command(), "-C", java_tests_path]
  119. if self._executive.run_command(build_java, return_exit_code=True): # Paths are absolute, so we don't need to set a cwd.
  120. _log.error("Failed to build Java support files: %s" % build_java)
  121. return False
  122. return True
  123. def check_for_leaks(self, process_name, process_pid):
  124. if not self.get_option('leaks'):
  125. return
  126. # We could use http://code.google.com/p/psutil/ to get the process_name from the pid.
  127. self._leak_detector.check_for_leaks(process_name, process_pid)
  128. def print_leaks_summary(self):
  129. if not self.get_option('leaks'):
  130. return
  131. # We're in the manager process, so the leak detector will not have a valid list of leak files.
  132. # FIXME: This is a hack, but we don't have a better way to get this information from the workers yet.
  133. # FIXME: This will include too many leaks in subsequent runs until the results directory is cleared!
  134. leaks_files = self._leak_detector.leaks_files_in_directory(self.results_directory())
  135. if not leaks_files:
  136. return
  137. total_bytes_string, unique_leaks = self._leak_detector.count_total_bytes_and_unique_leaks(leaks_files)
  138. total_leaks = self._leak_detector.count_total_leaks(leaks_files)
  139. _log.info("%s total leaks found for a total of %s!" % (total_leaks, total_bytes_string))
  140. _log.info("%s unique leaks found!" % unique_leaks)
  141. def _check_port_build(self):
  142. return self.get_option('nojava') or self._build_java_test_support()
  143. def _path_to_webcore_library(self):
  144. return self._build_path('WebCore.framework/Versions/A/WebCore')
  145. def show_results_html_file(self, results_filename):
  146. # We don't use self._run_script() because we don't want to wait for the script
  147. # to exit and we want the output to show up on stdout in case there are errors
  148. # launching the browser.
  149. self._executive.popen([self.path_to_script('run-safari')] + self._arguments_for_configuration() + ['--no-saved-state', '-NSOpen', results_filename],
  150. cwd=self.webkit_base(), stdout=file(os.devnull), stderr=file(os.devnull))
  151. # FIXME: The next two routines turn off the http locking in order
  152. # to work around failures on the bots caused when the slave restarts.
  153. # See https://bugs.webkit.org/show_bug.cgi?id=64886 for more info.
  154. # The proper fix is to make sure the slave is actually stopping NRWT
  155. # properly on restart. Note that by removing the lock file and not waiting,
  156. # the result should be that if there is a web server already running,
  157. # it'll be killed and this one will be started in its place; this
  158. # may lead to weird things happening in the other run. However, I don't
  159. # think we're (intentionally) actually running multiple runs concurrently
  160. # on any Mac bots.
  161. def acquire_http_lock(self):
  162. pass
  163. def release_http_lock(self):
  164. pass
  165. def sample_file_path(self, name, pid):
  166. return self._filesystem.join(self.results_directory(), "{0}-{1}-sample.txt".format(name, pid))
  167. def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=None, sleep_fn=None, wait_for_log=True):
  168. # Note that we do slow-spin here and wait, since it appears the time
  169. # ReportCrash takes to actually write and flush the file varies when there are
  170. # lots of simultaneous crashes going on.
  171. # FIXME: Should most of this be moved into CrashLogs()?
  172. time_fn = time_fn or time.time
  173. sleep_fn = sleep_fn or time.sleep
  174. crash_log = ''
  175. crash_logs = CrashLogs(self.host)
  176. now = time_fn()
  177. # FIXME: delete this after we're sure this code is working ...
  178. _log.debug('looking for crash log for %s:%s' % (name, str(pid)))
  179. deadline = now + 5 * int(self.get_option('child_processes', 1))
  180. while not crash_log and now <= deadline:
  181. crash_log = crash_logs.find_newest_log(name, pid, include_errors=True, newer_than=newer_than)
  182. if not wait_for_log:
  183. break
  184. if not crash_log or not [line for line in crash_log.splitlines() if not line.startswith('ERROR')]:
  185. sleep_fn(0.1)
  186. now = time_fn()
  187. if not crash_log:
  188. return (stderr, None)
  189. return (stderr, crash_log)
  190. def look_for_new_crash_logs(self, crashed_processes, start_time):
  191. """Since crash logs can take a long time to be written out if the system is
  192. under stress do a second pass at the end of the test run.
  193. crashes: test_name -> pid, process_name tuple of crashed process
  194. start_time: time the tests started at. We're looking for crash
  195. logs after that time.
  196. """
  197. crash_logs = {}
  198. for (test_name, process_name, pid) in crashed_processes:
  199. # Passing None for output. This is a second pass after the test finished so
  200. # if the output had any logging we would have already collected it.
  201. crash_log = self._get_crash_log(process_name, pid, None, None, start_time, wait_for_log=False)[1]
  202. if not crash_log:
  203. continue
  204. crash_logs[test_name] = crash_log
  205. return crash_logs
  206. def look_for_new_samples(self, unresponsive_processes, start_time):
  207. sample_files = {}
  208. for (test_name, process_name, pid) in unresponsive_processes:
  209. sample_file = self.sample_file_path(process_name, pid)
  210. if not self._filesystem.isfile(sample_file):
  211. continue
  212. sample_files[test_name] = sample_file
  213. return sample_files
  214. def sample_process(self, name, pid):
  215. try:
  216. hang_report = self.sample_file_path(name, pid)
  217. self._executive.run_command([
  218. "/usr/bin/sample",
  219. pid,
  220. 10,
  221. 10,
  222. "-file",
  223. hang_report,
  224. ])
  225. except ScriptError as e:
  226. _log.warning('Unable to sample process:' + str(e))
  227. def _path_to_helper(self):
  228. binary_name = 'LayoutTestHelper'
  229. return self._build_path(binary_name)
  230. def start_helper(self):
  231. helper_path = self._path_to_helper()
  232. if helper_path:
  233. _log.debug("Starting layout helper %s" % helper_path)
  234. self._helper = self._executive.popen([helper_path],
  235. stdin=self._executive.PIPE, stdout=self._executive.PIPE, stderr=None)
  236. is_ready = self._helper.stdout.readline()
  237. if not is_ready.startswith('ready'):
  238. _log.error("LayoutTestHelper failed to be ready")
  239. def stop_helper(self):
  240. if self._helper:
  241. _log.debug("Stopping LayoutTestHelper")
  242. try:
  243. self._helper.stdin.write("x\n")
  244. self._helper.stdin.close()
  245. self._helper.wait()
  246. except IOError, e:
  247. _log.debug("IOError raised while stopping helper: %s" % str(e))
  248. self._helper = None
  249. def make_command(self):
  250. return self.xcrun_find('make', '/usr/bin/make')
  251. def nm_command(self):
  252. return self.xcrun_find('nm', 'nm')
  253. def xcrun_find(self, command, fallback):
  254. try:
  255. return self._executive.run_command(['xcrun', '-find', command]).rstrip()
  256. except ScriptError:
  257. _log.warn("xcrun failed; falling back to '%s'." % fallback)
  258. return fallback