task.py revision ce860ea3fc65c488b9ea5e93efbf521dae9dc7dd
1# Copyright (c) 2013 The Chromium OS 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"""A reproducing entity.
6
7Part of the Chrome build flags optimization.
8
9The Task class is used by different modules. Each module fills in the
10corresponding information into a Task instance. Class Task contains the bit set
11representing the flags selection. The builder module is responsible for filling
12the image and the checksum field of a Task. The executor module will put the
13execution output to the execution field.
14"""
15
16__author__ = 'yuhenglong@google.com (Yuheng Long)'
17
18import os
19import subprocess
20import sys
21from uuid import uuid4
22
23BUILD_STAGE = 1
24TEST_STAGE = 2
25
26# Message indicating that the build or test failed.
27ERROR_STRING = 'error'
28
29# The maximum number of tries a build can have. Some compilations may fail due
30# to unexpected environment circumstance. This variable defines how many tries
31# the build should attempt before giving up.
32BUILD_TRIES = 3
33
34# The maximum number of tries a test can have. Some tests may fail due to
35# unexpected environment circumstance. This variable defines how many tries the
36# test should attempt before giving up.
37TEST_TRIES = 3
38
39
40# Create the file/directory if it does not already exist.
41def _CreateDirectory(file_name):
42  directory = os.path.dirname(file_name)
43  if not os.path.exists(directory):
44    os.makedirs(directory)
45
46
47class Task(object):
48  """A single reproducing entity.
49
50  A single test of performance with a particular set of flags. It records the
51  flag set, the image, the check sum of the image and the cost.
52  """
53
54  # The command that will be used in the build stage to compile the tasks.
55  BUILD_COMMAND = None
56  # The command that will be used in the test stage to test the tasks.
57  TEST_COMMAND = None
58  # The directory to log the compilation and test results.
59  LOG_DIRECTORY = None
60
61  @staticmethod
62  def InitLogCommand(build_command, test_command, log_directory):
63    """Set up the build and test command for the task and the log directory.
64
65    This framework is generic. It lets the client specify application specific
66    compile and test methods by passing different build_command and
67    test_command.
68
69    Args:
70      build_command: The command that will be used in the build stage to compile
71        this task.
72      test_command: The command that will be used in the test stage to test this
73        task.
74      log_directory: The directory to log the compilation and test results.
75    """
76
77    Task.BUILD_COMMAND = build_command
78    Task.TEST_COMMAND = test_command
79    Task.LOG_DIRECTORY = log_directory
80
81  def __init__(self, flag_set):
82    """Set up the optimization flag selection for this task.
83
84    Args:
85      flag_set: The optimization flag set that is encapsulated by this task.
86    """
87
88    self._flag_set = flag_set
89
90    # A unique identifier that distinguishes this task from other tasks.
91    self._task_identifier = uuid4()
92
93    self._log_path = (Task.LOG_DIRECTORY, self._task_identifier)
94
95    # Initiate the hash value. The hash value is used so as not to recompute it
96    # every time the hash method is called.
97    self._hash_value = None
98
99    # Indicate that the task has not been compiled/tested.
100    self._build_cost = None
101    self._exe_cost = None
102    self._checksum = None
103    self._image = None
104    self._file_length = None
105    self._text_length = None
106
107  def __eq__(self, other):
108    """Test whether two tasks are equal.
109
110    Two tasks are equal if their flag_set are equal.
111
112    Args:
113      other: The other task with which this task is tested equality.
114    Returns:
115      True if the encapsulated flag sets are equal.
116    """
117    if isinstance(other, Task):
118      return self.GetFlags() == other.GetFlags()
119    return False
120
121  def __hash__(self):
122    if self._hash_value is None:
123      # Cache the hash value of the flags, so as not to recompute them.
124      self._hash_value = hash(self._flag_set)
125    return self._hash_value
126
127  def GetIdentifier(self, stage):
128    """Get the identifier of the task in the stage.
129
130    The flag set uniquely identifies a task in the build stage. The checksum of
131    the image of the task uniquely identifies the task in the test stage.
132
133    Args:
134      stage: The stage (build/test) in which this method is called.
135    Returns:
136      Return the flag set in build stage and return the checksum in test stage.
137    """
138
139    # Define the dictionary for different stage function lookup.
140    get_identifier_functions = {BUILD_STAGE: self.FormattedFlags,
141                                TEST_STAGE: self.__GetCheckSum}
142
143    assert stage in get_identifier_functions
144    return get_identifier_functions[stage]()
145
146  def GetResult(self, stage):
147    """Get the performance results of the task in the stage.
148
149    Args:
150      stage: The stage (build/test) in which this method is called.
151    Returns:
152      Performance results.
153    """
154
155    # Define the dictionary for different stage function lookup.
156    get_result_functions = {BUILD_STAGE: self.__GetBuildResult,
157                            TEST_STAGE: self.GetTestResult}
158
159    assert stage in get_result_functions
160
161    return get_result_functions[stage]()
162
163  def SetResult(self, stage, result):
164    """Set the performance results of the task in the stage.
165
166    This method is called by the pipeling_worker to set the results for
167    duplicated tasks.
168
169    Args:
170      stage: The stage (build/test) in which this method is called.
171      result: The performance results of the stage.
172    """
173
174    # Define the dictionary for different stage function lookup.
175    set_result_functions = {BUILD_STAGE: self.__SetBuildResult,
176                            TEST_STAGE: self.__SetTestResult}
177
178    assert stage in set_result_functions
179
180    set_result_functions[stage](result)
181
182  def Done(self, stage):
183    """Check whether the stage is done.
184
185    Args:
186      stage: The stage to be checked, build or test.
187    Returns:
188      True if the stage is done.
189    """
190
191    # Define the dictionary for different result string lookup.
192    done_string = {BUILD_STAGE: self._build_cost, TEST_STAGE: self._exe_cost}
193
194    assert stage in done_string
195
196    return done_string[stage] is not None
197
198  def Work(self, stage):
199    """Perform the task.
200
201    Args:
202      stage: The stage in which the task is performed, compile or test.
203    """
204
205    # Define the dictionary for different stage function lookup.
206    work_functions = {BUILD_STAGE: self.__Compile, TEST_STAGE: self.__Test}
207
208    assert stage in work_functions
209
210    work_functions[stage]()
211
212  def FormattedFlags(self):
213    """Format the optimization flag set of this task.
214
215    Returns:
216      The formatted optimization flag set that is encapsulated by this task.
217    """
218    return str(self._flag_set.FormattedForUse())
219
220  def GetFlags(self):
221    """Get the optimization flag set of this task.
222
223    Returns:
224      The optimization flag set that is encapsulated by this task.
225    """
226
227    return self._flag_set
228
229  def __GetCheckSum(self):
230    """Get the compilation image checksum of this task.
231
232    Returns:
233      The compilation image checksum of this task.
234    """
235
236    # The checksum should be computed before this method is called.
237    assert self._checksum is not None
238    return self._checksum
239
240  def __Compile(self):
241    """Run a compile.
242
243    This method compile an image using the present flags, get the image,
244    test the existent of the image and gathers monitoring information, and sets
245    the internal cost (fitness) for this set of flags.
246    """
247
248    # Format the flags as a string as input to compile command. The unique
249    # identifier is passed to the compile command. If concurrent processes are
250    # used to compile different tasks, these processes can use the identifier to
251    # write to different file.
252    flags = self._flag_set.FormattedForUse()
253    command = '%s %s %s' % (Task.BUILD_COMMAND, ' '.join(flags),
254                            self._task_identifier)
255
256    # Try BUILD_TRIES number of times before confirming that the build fails.
257    for _ in range(BUILD_TRIES):
258      try:
259        # Execute the command and get the execution status/results.
260        p = subprocess.Popen(command.split(), stdout=subprocess.PIPE,
261                             stderr=subprocess.PIPE)
262        (out, err) = p.communicate()
263
264        if out:
265          out = out.strip()
266          if out != ERROR_STRING:
267            # Each build results contains the checksum of the result image, the
268            # performance cost of the build, the compilation image, the length
269            # of the build, and the length of the text section of the build.
270            (checksum, cost, image, file_length, text_length) = out.split()
271            # Build successfully.
272            break
273
274        # Build failed.
275        cost = ERROR_STRING
276      except _:
277        # If there is exception getting the cost information of the build, the
278        # build failed.
279        cost = ERROR_STRING
280
281    # Convert the build cost from String to integer. The build cost is used to
282    # compare a task with another task. Set the build cost of the failing task
283    # to the max integer. The for loop will keep trying until either there is a
284    # success or BUILD_TRIES number of tries have been conducted.
285    self._build_cost = sys.maxint if cost == ERROR_STRING else float(cost)
286
287    self._checksum = checksum
288    self._file_length = file_length
289    self._text_length = text_length
290    self._image = image
291
292    self.__LogBuildCost(err)
293
294  def __Test(self):
295    """__Test the task against benchmark(s) using the input test command."""
296
297    # Ensure that the task is compiled before being tested.
298    assert self._image is not None
299
300    # If the task does not compile, no need to test.
301    if self._image == ERROR_STRING:
302      self._exe_cost = ERROR_STRING
303      return
304
305    # The unique identifier is passed to the test command. If concurrent
306    # processes are used to compile different tasks, these processes can use the
307    # identifier to write to different file.
308    command = '%s %s %s' % (Task.TEST_COMMAND, self._image,
309                            self._task_identifier)
310
311    # Try TEST_TRIES number of times before confirming that the build fails.
312    for _ in range(TEST_TRIES):
313      try:
314        p = subprocess.Popen(command.split(), stdout=subprocess.PIPE,
315                             stderr=subprocess.PIPE)
316        (out, err) = p.communicate()
317
318        if out:
319          out = out.strip()
320          if out != ERROR_STRING:
321            # The test results contains the performance cost of the test.
322            cost = out
323            # Test successfully.
324            break
325
326        # Test failed.
327        cost = ERROR_STRING
328      except _:
329        # If there is exception getting the cost information of the test, the
330        # test failed. The for loop will keep trying until either there is a
331        # success or TEST_TRIES number of tries have been conducted.
332        cost = ERROR_STRING
333
334    self._exe_cost = sys.maxint if (cost == ERROR_STRING) else float(cost)
335
336    self.__LogTestCost(err)
337
338  def __SetBuildResult(self, (checksum, build_cost, image, file_length,
339                              text_length)):
340    self._checksum = checksum
341    self._build_cost = build_cost
342    self._image = image
343    self._file_length = file_length
344    self._text_length = text_length
345
346  def __GetBuildResult(self):
347    return (self._checksum, self._build_cost, self._image, self._file_length,
348            self._text_length)
349
350  def GetTestResult(self):
351    return self._exe_cost
352
353  def __SetTestResult(self, exe_cost):
354    self._exe_cost = exe_cost
355
356  def LogSteeringCost(self):
357    """Log the performance results for the task.
358
359    This method is called by the steering stage and this method writes the
360    results out to a file. The results include the build and the test results.
361    """
362
363    steering_log = '%s/%s/steering.txt' % self._log_path
364
365    _CreateDirectory(steering_log)
366
367    with open(steering_log, 'w') as out_file:
368      # Include the build and the test results.
369      steering_result = (self._flag_set, self._checksum, self._build_cost,
370                         self._image, self._file_length, self._text_length,
371                         self._exe_cost)
372
373      # Write out the result in the comma-separated format (CSV).
374      out_file.write('%s,%s,%s,%s,%s,%s,%s\n' % steering_result)
375
376  def __LogBuildCost(self, log):
377    """Log the build results for the task.
378
379    The build results include the compilation time of the build, the result
380    image, the checksum, the file length and the text length of the image.
381    The file length of the image includes the length of the file of the image.
382    The text length only includes the length of the text section of the image.
383
384    Args:
385      log: The build log of this task.
386    """
387
388    build_result_log = '%s/%s/build.txt' % self._log_path
389
390    _CreateDirectory(build_result_log)
391
392    with open(build_result_log, 'w') as out_file:
393      build_result = (self._flag_set, self._build_cost, self._image,
394                      self._checksum, self._file_length, self._text_length)
395
396      # Write out the result in the comma-separated format (CSV).
397      out_file.write('%s,%s,%s,%s,%s,%s\n' % build_result)
398
399    # The build information about running the build.
400    build_run_log = '%s/%s/build_log.txt' % self._log_path
401    _CreateDirectory(build_run_log)
402
403    with open(build_run_log, 'w') as out_log_file:
404      # Write out the execution information.
405      out_log_file.write('%s' % log)
406
407  def __LogTestCost(self, log):
408    """Log the test results for the task.
409
410    The test results include the runtime execution time of the test.
411
412    Args:
413      log: The test log of this task.
414    """
415
416    test_log = '%s/%s/test.txt' % self._log_path
417
418    _CreateDirectory(test_log)
419
420    with open(test_log, 'w') as out_file:
421      test_result = (self._flag_set, self._checksum, self._exe_cost)
422
423      # Write out the result in the comma-separated format (CSV).
424      out_file.write('%s,%s,%s\n' % test_result)
425
426    # The execution information about running the test.
427    test_run_log = '%s/%s/test_log.txt' % self._log_path
428
429    _CreateDirectory(test_run_log)
430
431    with open(test_run_log, 'w') as out_log_file:
432      # Append the test log information.
433      out_log_file.write('%s' % log)
434
435  def IsImproved(self, other):
436    """Compare the current task with another task.
437
438    Args:
439      other: The other task against which the current task is compared.
440
441    Returns:
442      True if this task has improvement upon the other task.
443    """
444
445    # The execution costs must have been initiated.
446    assert self._exe_cost is not None
447    assert other.GetTestResult() is not None
448
449    return self._exe_cost < other.GetTestResult()
450