Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(32)

Side by Side Diff: slave/skia_slave_scripts/build_step.py

Issue 648353002: Remove Skia's forked buildbot code (Closed) Base URL: https://skia.googlesource.com/buildbot.git@master
Patch Set: Fix launch_slaves, remove more stuff Created 6 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Base class for all slave-side build steps. """
6
7 import config
8 # pylint: disable=W0611
9 import flavor_utils
10 import imp
11 import multiprocessing
12 import os
13 import shlex
14 import signal
15 import subprocess
16 import sys
17 import time
18 import traceback
19
20 from playback_dirs import LocalSkpPlaybackDirs
21 from playback_dirs import StorageSkpPlaybackDirs
22
23 BUILDBOT_PATH = os.path.realpath(os.path.join(
24 os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir))
25
26 # Add important directories to the PYTHONPATH
27 sys.path.append(os.path.join(BUILDBOT_PATH))
28 sys.path.append(os.path.join(BUILDBOT_PATH, 'site_config'))
29 sys.path.append(os.path.join(BUILDBOT_PATH, 'master'))
30 sys.path.insert(0, os.path.join(BUILDBOT_PATH, 'common'))
31
32 import builder_name_schema
33 import slave_hosts_cfg
34 import slaves_cfg
35
36 from py.utils import misc
37
38
39 DEFAULT_TIMEOUT = 4800
40 DEFAULT_NO_OUTPUT_TIMEOUT = 3600
41 DEFAULT_NUM_CORES = 2
42
43
44 GM_EXPECTATIONS_FILENAME = 'expected-results.json'
45 GM_IGNORE_FAILURES_FILE = 'ignored-tests.txt'
46
47
48 # multiprocessing.Value doesn't accept boolean types, so we have to use an int.
49 INT_TRUE = 1
50 INT_FALSE = 0
51 build_step_stdout_has_written = multiprocessing.Value('i', INT_FALSE)
52
53
54 class BuildStepWarning(Exception):
55 pass
56
57
58 class BuildStepFailure(Exception):
59 pass
60
61
62 class BuildStepTimeout(Exception):
63 pass
64
65
66 class BuildStepLogger(object):
67 """ Override stdout so that we can keep track of when anything has been
68 logged. This enables timeouts based on how long the process has gone without
69 writing output.
70 """
71 def __init__(self):
72 self.stdout = sys.stdout
73 sys.stdout = self
74 build_step_stdout_has_written.value = INT_FALSE
75
76 def __del__(self):
77 sys.stdout = self.stdout
78
79 def fileno(self):
80 return self.stdout.fileno()
81
82 def write(self, data):
83 build_step_stdout_has_written.value = INT_TRUE
84 self.stdout.write(data)
85
86 def flush(self):
87 self.stdout.flush()
88
89
90 def _GetBuildSlaveID(desired_slave_name):
91 """ Returns the index of the build slave in the list of build slaves running
92 on this machine, in the form of a string. """
93 for host_dict in slave_hosts_cfg.SLAVE_HOSTS.itervalues():
94 for slave_name, slave_id, _ in host_dict.slaves:
95 if slave_name == desired_slave_name:
96 return slave_id
97 raise Exception('No build slave found with name %s' % desired_slave_name)
98
99
100 class BuildStep(multiprocessing.Process):
101
102 def __init__(self, args, attempts=1, timeout=DEFAULT_TIMEOUT,
103 no_output_timeout=DEFAULT_NO_OUTPUT_TIMEOUT):
104 """ Constructs a BuildStep instance.
105
106 args: dictionary containing arguments to this BuildStep.
107 attempts: how many times to try this BuildStep before giving up.
108 timeout: maximum time allowed for this BuildStep.
109 no_output_timeout: maximum time allowed for this BuildStep to run without
110 any output.
111 """
112 multiprocessing.Process.__init__(self)
113
114 self._args = dict(args)
115
116 self.timeout = timeout
117 self.no_output_timeout = no_output_timeout
118 self.attempts = attempts
119
120 self._builder_name = args['builder_name']
121 self._build_number = args['build_number']
122 self._slavename = os.environ['TESTING_SLAVENAME']
123
124 # Change to the correct working directory. This is needed on Windows, where
125 # our path lengths would otherwise be too long.
126 if os.name == 'nt':
127 curdir = os.getcwd()
128 # The buildslave name and builder name combo is too long. Instead, obtain
129 # a unique buildslave ID.
130 buildslave_id = _GetBuildSlaveID(self._slavename)
131 workdir = os.path.join('C:\\', buildslave_id, self._builder_name,
132 curdir[curdir.rfind('build'):])
133 print 'chdir to %s' % workdir
134 if not os.path.isdir(workdir):
135 os.makedirs(workdir)
136 os.chdir(workdir)
137
138 # Add CWD to the PYTHONPATH
139 sys.path.append(os.getcwd())
140
141 self._configuration = args['configuration']
142 if os.name == 'nt' and 'x86_64' in self._builder_name:
143 self._configuration += '_x64'
144
145 self._target_platform = args['target_platform']
146 self._deps_target_os = \
147 None if args['deps_target_os'] == 'None' else args['deps_target_os']
148 self._revision = \
149 None if args['revision'] == 'None' or args['revision'] == 'HEAD' \
150 else args['revision']
151 self._got_revision = \
152 None if args['got_revision'] == 'None' else args['got_revision']
153
154 # Import the flavor-specific build step utils module.
155 flavor = args.get('flavor', 'default')
156 try:
157 flavor_utils_module_name = '%s_build_step_utils' % flavor
158 flavor_utils_path = os.path.join(os.path.dirname(__file__),
159 'flavor_utils',
160 '%s.py' % flavor_utils_module_name)
161 flavor_utils_module = imp.load_source(
162 'flavor_utils.%s' % flavor_utils_module_name, flavor_utils_path)
163 flavor_utils_class_name = ''.join([part.title() for part in
164 flavor.split('_')])
165 flavor_utils_class = getattr(flavor_utils_module,
166 '%sBuildStepUtils' % flavor_utils_class_name)
167 self._flavor_utils = flavor_utils_class(self)
168 except (ImportError, IOError) as e:
169 raise Exception('Unrecognized build flavor: %s\n%s' % (flavor, e))
170
171 # Trybots should use expectations from the corresponding waterfall bot.
172 # This fixes https://code.google.com/p/skia/issues/detail?id=1552
173 gm_expected_subdir = builder_name_schema.GetWaterfallBot(
174 self._builder_name)
175
176 # Figure out where we are going to store images generated by GM.
177 self._gm_actual_basedir = os.path.join(os.pardir, os.pardir, 'gm', 'actual')
178 self._gm_expected_dir = os.path.join('expectations', 'gm',
179 gm_expected_subdir)
180 self._gm_actual_dir = os.path.join(self._gm_actual_basedir,
181 self._builder_name)
182 self._dm_dir = os.path.join(os.pardir, os.pardir, 'dm')
183
184 self._resource_dir = 'resources'
185 self._make_flags = shlex.split(args['make_flags'].replace('"', ''))
186 self._test_args = shlex.split(args['test_args'].replace('"', ''))
187 self._gm_args = shlex.split(args['gm_args'].replace('"', ''))
188 self._bench_args = shlex.split(args['bench_args'].replace('"', ''))
189 self._is_try = args['is_try'] == 'True'
190
191 self._default_make_flags = []
192 self._default_ninja_flags = []
193
194 # TODO(epoger): Throughout the buildbot code, we use various terms to refer
195 # to the same thing: "skps", "pictures", "replay", "playback".
196 # We should pick one of those terms, and rename things so that we are
197 # consistent.
198 # See https://codereview.chromium.org/295753002/ for additional discussion.
199
200 # Adding the playback directory transfer objects.
201 self._local_playback_dirs = LocalSkpPlaybackDirs(
202 self._builder_name,
203 None if args['perf_output_basedir'] == 'None'
204 else args['perf_output_basedir'])
205 self._storage_playback_dirs = StorageSkpPlaybackDirs(
206 self._builder_name,
207 None if args['perf_output_basedir'] == 'None'
208 else args['perf_output_basedir'])
209
210 self.skp_dir = self._local_playback_dirs.PlaybackSkpDir()
211 self.playback_actual_images_dir = (
212 self._local_playback_dirs.PlaybackActualImagesDir())
213 self.playback_actual_summaries_dir = (
214 self._local_playback_dirs.PlaybackActualSummariesDir())
215 self.playback_expected_summaries_dir = (
216 self._local_playback_dirs.PlaybackExpectedSummariesDir())
217
218 # Figure out where we are going to store performance related data.
219 if args['perf_output_basedir'] != 'None':
220 self._perf_data_dir = os.path.join(args['perf_output_basedir'],
221 self._builder_name, 'data')
222 self._perf_graphs_dir = os.path.join(args['perf_output_basedir'],
223 self._builder_name, 'graphs')
224 self._perf_range_input_dir = os.path.join(
225 args['perf_output_basedir'], self._builder_name, 'expectations')
226 else:
227 self._perf_data_dir = None
228 self._perf_graphs_dir = None
229 self._perf_range_input_dir = None
230 self._skimage_in_dir = os.path.join(os.pardir, 'skimage_in')
231
232 self._skimage_expected_dir = os.path.join('expectations', 'skimage')
233
234 self._skimage_out_dir = os.path.join('out', self._configuration,
235 'skimage_out')
236
237 self._device_dirs = self._flavor_utils.GetDeviceDirs()
238
239 @property
240 def configuration(self):
241 return self._configuration
242
243 @property
244 def builder_name(self):
245 return self._builder_name
246
247 @property
248 def args(self):
249 return self._args
250
251 # TODO(epoger): remove default_make_flags property once all builds use ninja
252 @property
253 def default_make_flags(self):
254 return self._default_make_flags
255
256 @property
257 def default_ninja_flags(self):
258 return self._default_ninja_flags
259
260 @property
261 def make_flags(self):
262 return self._make_flags
263
264 @property
265 def perf_data_dir(self):
266 return self._perf_data_dir
267
268 @property
269 def resource_dir(self):
270 return self._resource_dir
271
272 @property
273 def skimage_in_dir(self):
274 return self._skimage_in_dir
275
276 @property
277 def skimage_expected_dir(self):
278 return self._skimage_expected_dir
279
280 @property
281 def skimage_out_dir(self):
282 return self._skimage_out_dir
283
284 @property
285 def local_playback_dirs(self):
286 return self._local_playback_dirs
287
288 def _PreRun(self):
289 """ Optional preprocessing step defined in the BuildStepUtils. """
290 self._flavor_utils.PreRun()
291
292 def _Run(self):
293 """ Code to be run in a given BuildStep. No return value; throws exception
294 on failure. Override this method in subclasses.
295 """
296 raise Exception('Cannot instantiate abstract BuildStep')
297
298 def run(self):
299 """ Internal method used by multiprocess.Process. _Run is provided to be
300 overridden instead of this method to ensure that this implementation always
301 runs.
302 """
303 # If a BuildStep has exceeded its allotted time, the parent process needs to
304 # be able to kill the BuildStep process AND any which it has spawned,
305 # without harming itself. On posix platforms, the terminate() method is
306 # insufficient; it fails to kill the subprocesses launched by this process.
307 # So, we use use the setpgrp() function to set a new process group for the
308 # BuildStep process and its children and call os.killpg() to kill the group.
309 if os.name == 'posix':
310 os.setpgrp()
311 try:
312 self._Run()
313 except BuildStepWarning as e:
314 print e
315 sys.exit(config.Master.retcode_warnings)
316
317 def _WaitFunc(self, attempt):
318 """ Waits a number of seconds depending upon the attempt number of a
319 retry-able BuildStep before making the next attempt. This can be overridden
320 by subclasses and should be defined for attempt in [0, self.attempts - 1]
321
322 This default implementation is exponential; we double the wait time with
323 each attempt, starting with a 15-second pause between the first and second
324 attempts.
325 """
326 base_secs = 15
327 wait = base_secs * (2 ** attempt)
328 print 'Retrying in %d seconds...' % wait
329 time.sleep(wait)
330
331 @staticmethod
332 def KillBuildStep(step):
333 """ Kills a running BuildStep.
334
335 step: the running BuildStep instance to kill.
336 """
337 # On posix platforms, the terminate() method is insufficient; it fails to
338 # kill the subprocesses launched by this process. So, we use use the
339 # setpgrp() function to set a new process group for the BuildStep process
340 # and its children and call os.killpg() to kill the group.
341 if os.name == 'posix':
342 os.killpg(os.getpgid(step.pid), signal.SIGTERM)
343 elif os.name == 'nt':
344 subprocess.call(['taskkill', '/F', '/T', '/PID', str(step.pid)])
345 else:
346 step.terminate()
347
348 @staticmethod
349 def RunBuildStep(StepType):
350 """ Run a BuildStep, possibly making multiple attempts and handling
351 timeouts.
352
353 StepType: class type which subclasses BuildStep, indicating what step should
354 be run. StepType should override _Run().
355 """
356 # pylint: disable=W0612
357 logger = BuildStepLogger()
358 args = misc.ArgsToDict(sys.argv)
359 attempt = 0
360 while True:
361 step = StepType(args=args)
362 try:
363 start_time = time.time()
364 last_written_time = start_time
365 # pylint: disable=W0212
366 step._PreRun()
367 step.start()
368 while step.is_alive():
369 current_time = time.time()
370 if current_time - start_time > step.timeout:
371 BuildStep.KillBuildStep(step)
372 raise BuildStepTimeout('Build step exceeded timeout of %d seconds' %
373 step.timeout)
374 elif current_time - last_written_time > step.no_output_timeout:
375 BuildStep.KillBuildStep(step)
376 raise BuildStepTimeout(
377 'Build step exceeded %d seconds with no output' %
378 step.no_output_timeout)
379 time.sleep(1)
380 if build_step_stdout_has_written.value == INT_TRUE:
381 last_written_time = time.time()
382 print 'Build Step Finished.'
383 if step.exitcode == 0:
384 return 0
385 elif step.exitcode == config.Master.retcode_warnings:
386 # A warning is considered to be an acceptable finishing state.
387 return config.Master.retcode_warnings
388 else:
389 raise BuildStepFailure('Build step failed.')
390 except Exception:
391 print traceback.format_exc()
392 if attempt + 1 >= step.attempts:
393 raise
394 # pylint: disable=W0212
395 step._WaitFunc(attempt)
396 attempt += 1
397 print '**** %s, attempt %d ****' % (StepType.__name__, attempt + 1)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698