bd4d40572983498adc30d701e276c58d04286143
[WebKit-https.git] / Tools / Scripts / webkitpy / port / mac.py
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
30 import logging
31 import os
32 import time
33
34 from webkitpy.common.system.crashlogs import CrashLogs
35 from webkitpy.common.system.executive import ScriptError
36 from webkitpy.port.apple import ApplePort
37 from webkitpy.port.leakdetector import LeakDetector
38
39
40 _log = logging.getLogger(__name__)
41
42
43 class MacPort(ApplePort):
44     port_name = "mac"
45
46     VERSION_FALLBACK_ORDER = ['mac-snowleopard', 'mac-lion', 'mac-mountainlion']
47
48     ARCHITECTURES = ['x86_64', 'x86']
49
50     def __init__(self, host, port_name, **kwargs):
51         ApplePort.__init__(self, host, port_name, **kwargs)
52         self._architecture = self.get_option('architecture')
53
54         if not self._architecture:
55             self._architecture = 'x86_64'
56
57         self._leak_detector = LeakDetector(self)
58         if self.get_option("leaks"):
59             # DumpRenderTree slows down noticably if we run more than about 1000 tests in a batch
60             # with MallocStackLogging enabled.
61             self.set_option_default("batch_size", 1000)
62
63     def default_timeout_ms(self):
64         if self.get_option('guard_malloc'):
65             return 350 * 1000
66         return super(MacPort, self).default_timeout_ms()
67
68     def supports_per_test_timeout(self):
69         return True
70
71     def _build_driver_flags(self):
72         return ['ARCHS=i386'] if self.architecture() == 'x86' else []
73
74     def should_retry_crashes(self):
75         # On Apple Mac, we retry crashes due to https://bugs.webkit.org/show_bug.cgi?id=82233
76         return True
77
78     def default_baseline_search_path(self):
79         name = self._name.replace('-wk2', '')
80         if name.endswith(self.FUTURE_VERSION):
81             fallback_names = [self.port_name]
82         else:
83             fallback_names = self.VERSION_FALLBACK_ORDER[self.VERSION_FALLBACK_ORDER.index(name):-1] + [self.port_name]
84         if self.get_option('webkit_test_runner'):
85             fallback_names = [self._wk2_port_name(), 'wk2'] + fallback_names
86         return map(self._webkit_baseline_path, fallback_names)
87
88     def _port_specific_expectations_files(self):
89         return list(reversed([self._filesystem.join(self._webkit_baseline_path(p), 'TestExpectations') for p in self.baseline_search_path()]))
90
91     def setup_environ_for_server(self, server_name=None):
92         env = super(MacPort, self).setup_environ_for_server(server_name)
93         if server_name == self.driver_name():
94             if self.get_option('leaks'):
95                 env['MallocStackLogging'] = '1'
96             if self.get_option('guard_malloc'):
97                 env['DYLD_INSERT_LIBRARIES'] = '/usr/lib/libgmalloc.dylib:' + self._build_path("libWebCoreTestShim.dylib")
98             else:
99                 env['DYLD_INSERT_LIBRARIES'] = self._build_path("libWebCoreTestShim.dylib")
100         env['XML_CATALOG_FILES'] = ''  # work around missing /etc/catalog <rdar://problem/4292995>
101         return env
102
103     def operating_system(self):
104         return 'mac'
105
106     # Belongs on a Platform object.
107     def is_snowleopard(self):
108         return self._version == "snowleopard"
109
110     # Belongs on a Platform object.
111     def is_lion(self):
112         return self._version == "lion"
113
114     def default_child_processes(self):
115         if self._version == "snowleopard":
116             _log.warning("Cannot run tests in parallel on Snow Leopard due to rdar://problem/10621525.")
117             return 1
118
119         default_count = super(MacPort, self).default_child_processes()
120
121         # FIXME: https://bugs.webkit.org/show_bug.cgi?id=95906  With too many WebProcess WK2 tests get stuck in resource contention.
122         # To alleviate the issue reduce the number of running processes
123         # 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.
124         if self.get_option('webkit_test_runner') and default_count > 4:
125             default_count = int(.75 * default_count)
126
127         # Make sure we have enough ram to support that many instances:
128         total_memory = self.host.platform.total_bytes_memory()
129         if total_memory:
130             bytes_per_drt = 256 * 1024 * 1024  # Assume each DRT needs 256MB to run.
131             overhead = 2048 * 1024 * 1024  # Assume we need 2GB free for the O/S
132             supportable_instances = max((total_memory - overhead) / bytes_per_drt, 1)  # Always use one process, even if we don't have space for it.
133             if supportable_instances < default_count:
134                 _log.warning("This machine could support %s child processes, but only has enough memory for %s." % (default_count, supportable_instances))
135         else:
136             _log.warning("Cannot determine available memory for child processes, using default child process count of %s." % default_count)
137             supportable_instances = default_count
138         return min(supportable_instances, default_count)
139
140     def _build_java_test_support(self):
141         java_tests_path = self._filesystem.join(self.layout_tests_dir(), "java")
142         build_java = [self.make_command(), "-C", java_tests_path]
143         if self._executive.run_command(build_java, return_exit_code=True):  # Paths are absolute, so we don't need to set a cwd.
144             _log.error("Failed to build Java support files: %s" % build_java)
145             return False
146         return True
147
148     def check_for_leaks(self, process_name, process_pid):
149         if not self.get_option('leaks'):
150             return
151         # We could use http://code.google.com/p/psutil/ to get the process_name from the pid.
152         self._leak_detector.check_for_leaks(process_name, process_pid)
153
154     def print_leaks_summary(self):
155         if not self.get_option('leaks'):
156             return
157         # We're in the manager process, so the leak detector will not have a valid list of leak files.
158         # FIXME: This is a hack, but we don't have a better way to get this information from the workers yet.
159         # FIXME: This will include too many leaks in subsequent runs until the results directory is cleared!
160         leaks_files = self._leak_detector.leaks_files_in_directory(self.results_directory())
161         if not leaks_files:
162             return
163         total_bytes_string, unique_leaks = self._leak_detector.count_total_bytes_and_unique_leaks(leaks_files)
164         total_leaks = self._leak_detector.count_total_leaks(leaks_files)
165         _log.info("%s total leaks found for a total of %s!" % (total_leaks, total_bytes_string))
166         _log.info("%s unique leaks found!" % unique_leaks)
167
168     def _check_port_build(self):
169         return self._build_java_test_support()
170
171     def _path_to_webcore_library(self):
172         return self._build_path('WebCore.framework/Versions/A/WebCore')
173
174     def show_results_html_file(self, results_filename):
175         # We don't use self._run_script() because we don't want to wait for the script
176         # to exit and we want the output to show up on stdout in case there are errors
177         # launching the browser.
178         self._executive.popen([self.path_to_script('run-safari')] + self._arguments_for_configuration() + ['--no-saved-state', '-NSOpen', results_filename],
179             cwd=self.webkit_base(), stdout=file(os.devnull), stderr=file(os.devnull))
180
181     # FIXME: The next two routines turn off the http locking in order
182     # to work around failures on the bots caused when the slave restarts.
183     # See https://bugs.webkit.org/show_bug.cgi?id=64886 for more info.
184     # The proper fix is to make sure the slave is actually stopping NRWT
185     # properly on restart. Note that by removing the lock file and not waiting,
186     # the result should be that if there is a web server already running,
187     # it'll be killed and this one will be started in its place; this
188     # may lead to weird things happening in the other run. However, I don't
189     # think we're (intentionally) actually running multiple runs concurrently
190     # on any Mac bots.
191
192     def acquire_http_lock(self):
193         pass
194
195     def release_http_lock(self):
196         pass
197
198     def sample_file_path(self, name, pid):
199         return self._filesystem.join(self.results_directory(), "{0}-{1}-sample.txt".format(name, pid))
200
201     def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=None, sleep_fn=None, wait_for_log=True):
202         # Note that we do slow-spin here and wait, since it appears the time
203         # ReportCrash takes to actually write and flush the file varies when there are
204         # lots of simultaneous crashes going on.
205         # FIXME: Should most of this be moved into CrashLogs()?
206         time_fn = time_fn or time.time
207         sleep_fn = sleep_fn or time.sleep
208         crash_log = ''
209         crash_logs = CrashLogs(self.host)
210         now = time_fn()
211         # FIXME: delete this after we're sure this code is working ...
212         _log.debug('looking for crash log for %s:%s' % (name, str(pid)))
213         deadline = now + 5 * int(self.get_option('child_processes', 1))
214         while not crash_log and now <= deadline:
215             crash_log = crash_logs.find_newest_log(name, pid, include_errors=True, newer_than=newer_than)
216             if not wait_for_log:
217                 break
218             if not crash_log or not [line for line in crash_log.splitlines() if not line.startswith('ERROR')]:
219                 sleep_fn(0.1)
220                 now = time_fn()
221
222         if not crash_log:
223             return (stderr, None)
224         return (stderr, crash_log)
225
226     def look_for_new_crash_logs(self, crashed_processes, start_time):
227         """Since crash logs can take a long time to be written out if the system is
228            under stress do a second pass at the end of the test run.
229
230            crashes: test_name -> pid, process_name tuple of crashed process
231            start_time: time the tests started at.  We're looking for crash
232                logs after that time.
233         """
234         crash_logs = {}
235         for (test_name, process_name, pid) in crashed_processes:
236             # Passing None for output.  This is a second pass after the test finished so
237             # if the output had any logging we would have already collected it.
238             crash_log = self._get_crash_log(process_name, pid, None, None, start_time, wait_for_log=False)[1]
239             if not crash_log:
240                 continue
241             crash_logs[test_name] = crash_log
242         return crash_logs
243
244     def look_for_new_samples(self, unresponsive_processes, start_time):
245         sample_files = {}
246         for (test_name, process_name, pid) in unresponsive_processes:
247             sample_file = self.sample_file_path(process_name, pid)
248             if not self._filesystem.isfile(sample_file):
249                 continue
250             sample_files[test_name] = sample_file
251         return sample_files
252
253     def sample_process(self, name, pid):
254         try:
255             hang_report = self.sample_file_path(name, pid)
256             self._executive.run_command([
257                 "/usr/bin/sample",
258                 pid,
259                 10,
260                 10,
261                 "-file",
262                 hang_report,
263             ])
264         except ScriptError as e:
265             _log.warning('Unable to sample process:' + str(e))
266
267     def _path_to_helper(self):
268         binary_name = 'LayoutTestHelper'
269         return self._build_path(binary_name)
270
271     def start_helper(self):
272         helper_path = self._path_to_helper()
273         if helper_path:
274             _log.debug("Starting layout helper %s" % helper_path)
275             self._helper = self._executive.popen([helper_path],
276                 stdin=self._executive.PIPE, stdout=self._executive.PIPE, stderr=None)
277             is_ready = self._helper.stdout.readline()
278             if not is_ready.startswith('ready'):
279                 _log.error("LayoutTestHelper failed to be ready")
280
281     def stop_helper(self):
282         if self._helper:
283             _log.debug("Stopping LayoutTestHelper")
284             try:
285                 self._helper.stdin.write("x\n")
286                 self._helper.stdin.close()
287                 self._helper.wait()
288             except IOError, e:
289                 _log.debug("IOError raised while stopping helper: %s" % str(e))
290             self._helper = None
291
292     def make_command(self):
293         return self.xcrun_find('make', '/usr/bin/make')
294
295     def nm_command(self):
296         return self.xcrun_find('nm', 'nm')
297
298     def xcrun_find(self, command, fallback):
299         try:
300             return self._executive.run_command(['xcrun', '-find', command]).rstrip()
301         except ScriptError:
302             _log.warn("xcrun failed; falling back to '%s'." % fallback)
303             return fallback