task.py revision 25cdf79e17a73858ffa28db8a5b387210fea9e25
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      # Execute the command and get the execution status/results.
259      p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE,
260                           stderr=subprocess.PIPE)
261      (out, err) = p.communicate()
262
263      if not err and out != ERROR_STRING:
264        # Each build results contains the checksum of the result image, the
265        # performance cost of the build, the compilation image, the length of
266        # the build, and the length of the text section of the build.
267        (checksum, cost, image, file_length, text_length) = out.split()
268        # Build successfully.
269        break
270
271      # Build failed.
272      cost = ERROR_STRING
273
274    # Convert the build cost from String to integer. The build cost is used to
275    # compare a task with another task. Set the build cost of the failing task
276    # to the max integer.
277    self._build_cost = sys.maxint if cost == ERROR_STRING else int(cost)
278
279    self._checksum = checksum
280    self._file_length = file_length
281    self._text_length = text_length
282    self._image = image
283
284    self.__LogBuildCost()
285
286  def __Test(self):
287    """__Test the task against benchmark(s) using the input test command."""
288
289    # Ensure that the task is compiled before being tested.
290    assert self._image is not None
291
292    # If the task does not compile, no need to test.
293    if self._image == ERROR_STRING:
294      self._exe_cost = ERROR_STRING
295      return
296
297    # The unique identifier is passed to the test command. If concurrent
298    # processes are used to compile different tasks, these processes can use the
299    # identifier to write to different file.
300    command = '%s %s %s' % (Task.TEST_COMMAND, self._image,
301                            self._task_identifier)
302
303    # Try build_tries number of times before confirming that the build fails.
304    for _ in range(TEST_TRIES):
305      p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE,
306                           stderr=subprocess.PIPE)
307      (out, err) = p.communicate()
308
309      if not err and out != ERROR_STRING:
310        # The test results contains the performance cost of the test.
311        cost = out
312        # Test successfully.
313        break
314
315      # Test failed.
316      cost = ERROR_STRING
317
318    self._exe_cost = sys.maxint if (cost == ERROR_STRING) else int(cost)
319
320    self.__LogTestCost()
321
322  def __SetBuildResult(self, (checksum, build_cost, image, file_length,
323                              text_length)):
324    self._checksum = checksum
325    self._build_cost = build_cost
326    self._image = image
327    self._file_length = file_length
328    self._text_length = text_length
329
330  def __GetBuildResult(self):
331    return (self._checksum, self._build_cost, self._image, self._file_length,
332            self._text_length)
333
334  def GetTestResult(self):
335    return self._exe_cost
336
337  def __SetTestResult(self, exe_cost):
338    self._exe_cost = exe_cost
339
340  def LogSteeringCost(self):
341    """Log the performance results for the task.
342
343    This method is called by the steering stage and this method writes the
344    results out to a file. The results include the build and the test results.
345    """
346
347    steering_log = '%s/%s/steering.txt' % self._log_path
348
349    _CreateDirectory(steering_log)
350
351    with open(steering_log, 'w') as out_file:
352      # Include the build and the test results.
353      steering_result = (self._flag_set, self._checksum, self._build_cost,
354                         self._image, self._file_length, self._text_length,
355                         self._exe_cost)
356
357      # Write out the result in the comma-separated format (CSV).
358      out_file.write('%s,%s,%s,%s,%s,%s,%s\n' % steering_result)
359
360  def __LogBuildCost(self):
361    """Log the build results for the task.
362
363    The build results include the compilation time of the build, the result
364    image, the checksum, the file length and the text length of the image.
365    The file length of the image includes the length of the file of the image.
366    The text length only includes the length of the text section of the image.
367    """
368
369    build_log = '%s/%s/build.txt' % self._log_path
370
371    _CreateDirectory(build_log)
372
373    with open(build_log, 'w') as out_file:
374      build_result = (self._flag_set, self._build_cost, self._image,
375                      self._checksum, self._file_length, self._text_length)
376
377      # Write out the result in the comma-separated format (CSV).
378      out_file.write('%s,%s,%s,%s,%s,%s\n' % build_result)
379
380  def __LogTestCost(self):
381    """Log the test results for the task.
382
383    The test results include the runtime execution time of the test.
384    """
385
386    test_log = '%s/%s/build.txt' % self._log_path
387
388    _CreateDirectory(test_log)
389
390    with open(test_log, 'w') as out_file:
391      test_result = (self._flag_set, self._checksum, self._exe_cost)
392
393      # Write out the result in the comma-separated format (CSV).
394      out_file.write('%s,%s,%s\n' % test_result)
395
396  def IsImproved(self, other):
397    """Compare the current task with another task.
398
399    Args:
400      other: The other task against which the current task is compared.
401
402    Returns:
403      True if this task has improvement upon the other task.
404    """
405
406    # The execution costs must have been initiated.
407    assert self._exe_cost is not None
408    assert other.GetTestResult() is not None
409
410    return self._exe_cost < other.GetTestResult()
411