OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # | 2 # |
3 # Copyright 2016 Google Inc. | 3 # Copyright 2016 Google Inc. |
4 # | 4 # |
5 # Use of this source code is governed by a BSD-style license that can be | 5 # Use of this source code is governed by a BSD-style license that can be |
6 # found in the LICENSE file. | 6 # found in the LICENSE file. |
7 | 7 |
8 | 8 |
9 import contextlib | |
10 import math | |
9 import os | 11 import os |
12 import shutil | |
13 import socket | |
10 import subprocess | 14 import subprocess |
11 import sys | 15 import sys |
16 import time | |
17 import urllib2 | |
12 | 18 |
13 from flavor import android_flavor | 19 from flavor import android_flavor |
14 from flavor import chromeos_flavor | 20 from flavor import chromeos_flavor |
15 from flavor import cmake_flavor | 21 from flavor import cmake_flavor |
16 from flavor import coverage_flavor | 22 from flavor import coverage_flavor |
17 from flavor import default_flavor | 23 from flavor import default_flavor |
18 from flavor import ios_flavor | 24 from flavor import ios_flavor |
19 from flavor import valgrind_flavor | 25 from flavor import valgrind_flavor |
20 from flavor import xsan_flavor | 26 from flavor import xsan_flavor |
21 | 27 |
22 | 28 |
23 CONFIG_COVERAGE = 'Coverage' | 29 CONFIG_COVERAGE = 'Coverage' |
24 CONFIG_DEBUG = 'Debug' | 30 CONFIG_DEBUG = 'Debug' |
25 CONFIG_RELEASE = 'Release' | 31 CONFIG_RELEASE = 'Release' |
26 VALID_CONFIGS = (CONFIG_COVERAGE, CONFIG_DEBUG, CONFIG_RELEASE) | 32 VALID_CONFIGS = (CONFIG_COVERAGE, CONFIG_DEBUG, CONFIG_RELEASE) |
27 | 33 |
28 GM_ACTUAL_FILENAME = 'actual-results.json' | 34 GM_ACTUAL_FILENAME = 'actual-results.json' |
29 GM_EXPECTATIONS_FILENAME = 'expected-results.json' | 35 GM_EXPECTATIONS_FILENAME = 'expected-results.json' |
30 GM_IGNORE_TESTS_FILENAME = 'ignored-tests.txt' | 36 GM_IGNORE_TESTS_FILENAME = 'ignored-tests.txt' |
31 | 37 |
32 GS_GM_BUCKET = 'chromium-skia-gm' | 38 GS_GM_BUCKET = 'chromium-skia-gm' |
33 GS_SUMMARIES_BUCKET = 'chromium-skia-gm-summaries' | 39 GS_SUMMARIES_BUCKET = 'chromium-skia-gm-summaries' |
34 | 40 |
41 GS_SUBDIR_TMPL_SK_IMAGE = 'skimage/v%s' | |
42 GS_SUBDIR_TMPL_SKP = 'playback_%s/skps' | |
43 | |
35 SKIA_REPO = 'https://skia.googlesource.com/skia.git' | 44 SKIA_REPO = 'https://skia.googlesource.com/skia.git' |
36 INFRA_REPO = 'https://skia.googlesource.com/buildbot.git' | 45 INFRA_REPO = 'https://skia.googlesource.com/buildbot.git' |
37 | 46 |
38 SERVICE_ACCOUNT_FILE = 'service-account-skia.json' | 47 SERVICE_ACCOUNT_FILE = 'service-account-skia.json' |
39 SERVICE_ACCOUNT_INTERNAL_FILE = 'service-account-skia-internal.json' | 48 SERVICE_ACCOUNT_INTERNAL_FILE = 'service-account-skia-internal.json' |
40 | 49 |
50 VERSION_FILE_SK_IMAGE = 'SK_IMAGE_VERSION' | |
51 VERSION_FILE_SKP = 'SKP_VERSION' | |
52 | |
41 | 53 |
42 def is_android(bot_cfg): | 54 def is_android(bot_cfg): |
43 """Determine whether the given bot is an Android bot.""" | 55 """Determine whether the given bot is an Android bot.""" |
44 return ('Android' in bot_cfg.get('extra_config', '') or | 56 return ('Android' in bot_cfg.get('extra_config', '') or |
45 bot_cfg.get('os') == 'Android') | 57 bot_cfg.get('os') == 'Android') |
46 | 58 |
47 def is_chromeos(bot_cfg): | 59 def is_chromeos(bot_cfg): |
48 return ('CrOS' in bot_cfg.get('extra_config', '') or | 60 return ('CrOS' in bot_cfg.get('extra_config', '') or |
49 bot_cfg.get('os') == 'ChromeOS') | 61 bot_cfg.get('os') == 'ChromeOS') |
50 | 62 |
51 def is_cmake(bot_cfg): | 63 def is_cmake(bot_cfg): |
52 return 'CMake' in bot_cfg.get('extra_config', '') | 64 return 'CMake' in bot_cfg.get('extra_config', '') |
53 | 65 |
54 def is_ios(bot_cfg): | 66 def is_ios(bot_cfg): |
55 return ('iOS' in bot_cfg.get('extra_config', '') or | 67 return ('iOS' in bot_cfg.get('extra_config', '') or |
56 bot_cfg.get('os') == 'iOS') | 68 bot_cfg.get('os') == 'iOS') |
57 | 69 |
58 | 70 |
59 def is_valgrind(bot_cfg): | 71 def is_valgrind(bot_cfg): |
60 return 'Valgrind' in bot_cfg.get('extra_config', '') | 72 return 'Valgrind' in bot_cfg.get('extra_config', '') |
61 | 73 |
62 | 74 |
63 def is_xsan(bot_cfg): | 75 def is_xsan(bot_cfg): |
64 return (bot_cfg.get('extra_config') == 'ASAN' or | 76 return (bot_cfg.get('extra_config') == 'ASAN' or |
65 bot_cfg.get('extra_config') == 'MSAN' or | 77 bot_cfg.get('extra_config') == 'MSAN' or |
66 bot_cfg.get('extra_config') == 'TSAN') | 78 bot_cfg.get('extra_config') == 'TSAN') |
67 | 79 |
68 | 80 |
81 def download_dir(skia_dir, tmp_dir, version_file, gs_path_tmpl, dst_dir): | |
82 # Ensure that the tmp_dir exists. | |
83 if not os.path.isdir(tmp_dir): | |
84 os.makedirs(tmp_dir) | |
85 | |
86 # Get the expected version. | |
87 with open(os.path.join(skia_dir, version_file)) as f: | |
88 expected_version = f.read().rstrip() | |
89 | |
90 print 'Expected %s = %s' % (version_file, expected_version) | |
91 | |
92 # Get the actually-downloaded version, if we have one. | |
93 actual_version_file = os.path.join(tmp_dir, version_file) | |
94 try: | |
95 with open(actual_version_file) as f: | |
96 actual_version = f.read().rstrip() | |
97 except IOError: | |
98 actual_version = -1 | |
99 | |
100 print 'Actual %s = %s' % (version_file, actual_version) | |
101 | |
102 # If we don't have the desired version, download it. | |
103 if actual_version != expected_version: | |
104 if actual_version != -1: | |
105 os.remove(actual_version_file) | |
106 if os.path.isdir(dst_dir): | |
107 shutil.rmtree(dst_dir) | |
108 os.makedirs(dst_dir) | |
109 gs_path = 'gs://%s/%s/*' % (GS_GM_BUCKET, gs_path_tmpl % expected_version) | |
110 print 'Downloading from %s' % gs_path | |
111 subprocess.check_call(['gsutil', 'cp', '-R', gs_path, dst_dir]) | |
112 with open(actual_version_file, 'w') as f: | |
113 f.write(expected_version) | |
114 | |
115 | |
116 def get_uninteresting_hashes(hashes_file): | |
117 HASHES_URL = 'https://gold.skia.org/_/hashes' | |
rmistry
2016/02/29 13:01:34
Make module level constant?
borenet
2016/02/29 13:53:54
Done.
| |
118 RETRIES = 5 | |
119 TIMEOUT = 60 | |
120 WAIT_BASE = 15 | |
121 | |
122 socket.setdefaulttimeout(TIMEOUT) | |
123 for retry in range(RETRIES): | |
rmistry
2016/02/29 13:01:34
Would be useful to add a utility for exponential r
borenet
2016/02/29 13:53:54
If it's okay with you I'd prefer to save that unti
| |
124 try: | |
125 with contextlib.closing( | |
126 urllib2.urlopen(HASHES_URL, timeout=TIMEOUT)) as w: | |
127 hashes = w.read() | |
128 with open(hashes_file, 'w') as f: | |
129 f.write(hashes) | |
130 break | |
131 except Exception as e: | |
132 print >> sys.stderr, 'Failed to get uninteresting hashes from %s:\n%s' % ( | |
133 HASHES_URL, e) | |
134 if retry == RETRIES: | |
135 raise | |
136 waittime = WAIT_BASE * math.pow(2, retry) | |
137 print 'Retry in %d seconds.' % waittime | |
138 time.sleep(waittime) | |
139 | |
140 | |
69 class BotInfo(object): | 141 class BotInfo(object): |
70 def __init__(self, bot_name, slave_name, out_dir): | 142 def __init__(self, bot_name, swarm_out_dir): |
71 """Initialize the bot, given its name. | 143 """Initialize the bot, given its name. |
72 | 144 |
73 Assumes that CWD is the directory containing this file. | 145 Assumes that CWD is the directory containing this file. |
74 """ | 146 """ |
75 self.name = bot_name | 147 self.name = bot_name |
76 self.slave_name = slave_name | |
77 self.skia_dir = os.path.abspath(os.path.join( | 148 self.skia_dir = os.path.abspath(os.path.join( |
78 os.path.dirname(os.path.realpath(__file__)), | 149 os.path.dirname(os.path.realpath(__file__)), |
79 os.pardir, os.pardir)) | 150 os.pardir, os.pardir)) |
151 self.swarm_out_dir = swarm_out_dir | |
80 os.chdir(self.skia_dir) | 152 os.chdir(self.skia_dir) |
81 self.build_dir = os.path.abspath(os.path.join(self.skia_dir, os.pardir)) | 153 self.build_dir = os.path.abspath(os.path.join(self.skia_dir, os.pardir)) |
82 self.out_dir = out_dir | |
83 self.spec = self.get_bot_spec(bot_name) | 154 self.spec = self.get_bot_spec(bot_name) |
155 self.bot_cfg = self.spec['builder_cfg'] | |
156 if self.bot_cfg['role'] == 'Build': | |
157 self.out_dir = os.path.join(swarm_out_dir, 'out') | |
158 else: | |
159 self.out_dir = 'out' | |
84 self.configuration = self.spec['configuration'] | 160 self.configuration = self.spec['configuration'] |
85 self.default_env = { | 161 self.default_env = { |
86 'SKIA_OUT': self.out_dir, | 162 'SKIA_OUT': self.out_dir, |
87 'BUILDTYPE': self.configuration, | 163 'BUILDTYPE': self.configuration, |
88 'PATH': os.environ['PATH'], | 164 'PATH': os.environ['PATH'], |
89 } | 165 } |
90 self.default_env.update(self.spec['env']) | 166 self.default_env.update(self.spec['env']) |
91 self.build_targets = [str(t) for t in self.spec['build_targets']] | 167 self.build_targets = [str(t) for t in self.spec['build_targets']] |
92 self.bot_cfg = self.spec['builder_cfg'] | |
93 self.is_trybot = self.bot_cfg['is_trybot'] | 168 self.is_trybot = self.bot_cfg['is_trybot'] |
94 self.upload_dm_results = self.spec['upload_dm_results'] | 169 self.upload_dm_results = self.spec['upload_dm_results'] |
95 self.upload_perf_results = self.spec['upload_perf_results'] | 170 self.upload_perf_results = self.spec['upload_perf_results'] |
171 self.perf_data_dir = os.path.join(self.swarm_out_dir, 'perfdata', | |
172 self.name, 'data') | |
173 self.resource_dir = os.path.join(self.build_dir, 'resources') | |
174 self.images_dir = os.path.join(self.build_dir, 'images') | |
175 self.local_skp_dir = os.path.join(self.build_dir, 'playback', 'skps') | |
96 self.dm_flags = self.spec['dm_flags'] | 176 self.dm_flags = self.spec['dm_flags'] |
97 self.nanobench_flags = self.spec['nanobench_flags'] | 177 self.nanobench_flags = self.spec['nanobench_flags'] |
98 self._ccache = None | 178 self._ccache = None |
99 self._checked_for_ccache = False | 179 self._checked_for_ccache = False |
180 self._already_ran = {} | |
181 self.tmp_dir = os.path.join(self.build_dir, 'tmp') | |
100 self.flavor = self.get_flavor(self.bot_cfg) | 182 self.flavor = self.get_flavor(self.bot_cfg) |
101 | 183 |
184 # These get filled in during subsequent steps. | |
185 self.device_dirs = None | |
186 self.build_number = None | |
187 self.got_revision = None | |
188 self.master_name = None | |
189 self.slave_name = None | |
190 | |
102 @property | 191 @property |
103 def ccache(self): | 192 def ccache(self): |
104 if not self._checked_for_ccache: | 193 if not self._checked_for_ccache: |
105 self._checked_for_ccache = True | 194 self._checked_for_ccache = True |
106 if sys.platform != 'win32': | 195 if sys.platform != 'win32': |
107 try: | 196 try: |
108 result = subprocess.check_output(['which', 'ccache']) | 197 result = subprocess.check_output(['which', 'ccache']) |
109 self._ccache = result.rstrip() | 198 self._ccache = result.rstrip() |
110 except subprocess.CalledProcessError: | 199 except subprocess.CalledProcessError: |
111 pass | 200 pass |
(...skipping 29 matching lines...) Expand all Loading... | |
141 _env = {} | 230 _env = {} |
142 _env.update(self.default_env) | 231 _env.update(self.default_env) |
143 _env.update(env or {}) | 232 _env.update(env or {}) |
144 cwd = cwd or self.skia_dir | 233 cwd = cwd or self.skia_dir |
145 print '============' | 234 print '============' |
146 print 'CMD: %s' % cmd | 235 print 'CMD: %s' % cmd |
147 print 'CWD: %s' % cwd | 236 print 'CWD: %s' % cwd |
148 print 'ENV: %s' % _env | 237 print 'ENV: %s' % _env |
149 print '============' | 238 print '============' |
150 subprocess.check_call(cmd, env=_env, cwd=cwd) | 239 subprocess.check_call(cmd, env=_env, cwd=cwd) |
240 | |
241 def compile_steps(self): | |
242 for t in self.build_targets: | |
243 self.flavor.compile(t) | |
244 | |
245 def _run_once(self, fn, *args, **kwargs): | |
246 if not fn.__name__ in self._already_ran: | |
247 self._already_ran[fn.__name__] = True | |
248 fn(*args, **kwargs) | |
249 | |
250 def install(self): | |
251 """Copy the required executables and files to the device.""" | |
252 self.device_dirs = self.flavor.get_device_dirs() | |
253 | |
254 # Run any device-specific installation. | |
255 self.flavor.install() | |
256 | |
257 # TODO(borenet): Only copy files which have changed. | |
258 # Resources | |
259 self.flavor.copy_directory_contents_to_device(self.resource_dir, | |
260 self.device_dirs.resource_dir) | |
261 | |
262 def _key_params(self): | |
263 """Build a unique key from the builder name (as a list). | |
264 | |
265 E.g. arch x86 gpu GeForce320M mode MacMini4.1 os Mac10.6 | |
266 """ | |
267 # Don't bother to include role, which is always Test. | |
268 # TryBots are uploaded elsewhere so they can use the same key. | |
269 blacklist = ['role', 'is_trybot'] | |
270 | |
271 flat = [] | |
272 for k in sorted(self.bot_cfg.keys()): | |
273 if k not in blacklist: | |
274 flat.append(k) | |
275 flat.append(self.bot_cfg[k]) | |
276 return flat | |
277 | |
278 def test_steps(self, got_revision, master_name, slave_name, build_number): | |
279 """Run the DM test.""" | |
280 self.build_number = build_number | |
281 self.got_revision = got_revision | |
282 self.master_name = master_name | |
283 self.slave_name = slave_name | |
284 self._run_once(self.install) | |
285 | |
286 use_hash_file = False | |
287 if self.upload_dm_results: | |
288 # This must run before we write anything into self.device_dirs.dm_dir | |
289 # or we may end up deleting our output on machines where they're the same. | |
290 host_dm_dir = os.path.join(self.swarm_out_dir, 'dm') | |
291 print 'host dm dir: %s' % host_dm_dir | |
292 self.flavor.create_clean_host_dir(host_dm_dir) | |
293 if str(host_dm_dir) != str(self.device_dirs.dm_dir): | |
294 self.flavor.create_clean_device_dir(self.device_dirs.dm_dir) | |
295 | |
296 # Obtain the list of already-generated hashes. | |
297 hash_filename = 'uninteresting_hashes.txt' | |
298 host_hashes_file = self.tmp_dir.join(hash_filename) | |
299 hashes_file = self.flavor.device_path_join( | |
300 self.device_dirs.tmp_dir, hash_filename) | |
301 | |
302 try: | |
303 get_uninteresting_hashes(host_hashes_file) | |
304 except Exception: | |
305 pass | |
306 | |
307 if os.path.exists(host_hashes_file): | |
308 self.flavor.copy_file_to_device(host_hashes_file, hashes_file) | |
309 use_hash_file = True | |
310 | |
311 # Run DM. | |
312 properties = [ | |
313 'gitHash', self.got_revision, | |
314 'master', self.master_name, | |
315 'builder', self.name, | |
316 'build_number', self.build_number, | |
317 ] | |
318 if self.is_trybot: | |
319 properties.extend([ | |
320 'issue', self.m.properties['issue'], | |
321 'patchset', self.m.properties['patchset'], | |
322 ]) | |
323 | |
324 args = [ | |
325 'dm', | |
326 '--undefok', # This helps branches that may not know new flags. | |
327 '--verbose', | |
328 '--resourcePath', self.device_dirs.resource_dir, | |
329 '--skps', self.device_dirs.skp_dir, | |
330 '--images', self.flavor.device_path_join( | |
331 self.device_dirs.images_dir, 'dm'), | |
332 '--nameByHash', | |
333 '--properties' | |
334 ] + properties | |
335 | |
336 args.append('--key') | |
337 args.extend(self._key_params()) | |
338 if use_hash_file: | |
339 args.extend(['--uninterestingHashesFile', hashes_file]) | |
340 if self.upload_dm_results: | |
341 args.extend(['--writePath', self.device_dirs.dm_dir]) | |
342 | |
343 skip_flag = None | |
344 if self.bot_cfg.get('cpu_or_gpu') == 'CPU': | |
345 skip_flag = '--nogpu' | |
346 elif self.bot_cfg.get('cpu_or_gpu') == 'GPU': | |
347 skip_flag = '--nocpu' | |
348 if skip_flag: | |
349 args.append(skip_flag) | |
350 args.extend(self.dm_flags) | |
351 | |
352 self.flavor.run(args, env=self.default_env) | |
353 | |
354 if self.upload_dm_results: | |
355 # Copy images and JSON to host machine if needed. | |
356 self.flavor.copy_directory_contents_to_host(self.device_dirs.dm_dir, | |
357 host_dm_dir) | |
358 | |
359 # See skia:2789. | |
360 if ('Valgrind' in self.name and | |
361 self.builder_cfg.get('cpu_or_gpu') == 'GPU'): | |
362 abandonGpuContext = list(args) | |
363 abandonGpuContext.append('--abandonGpuContext') | |
364 self.flavor.run(abandonGpuContext) | |
365 preAbandonGpuContext = list(args) | |
366 preAbandonGpuContext.append('--preAbandonGpuContext') | |
367 self.flavor.run(preAbandonGpuContext) | |
OLD | NEW |