task.py revision ccfaf2f382815945c1c883e1cfceb2eba55e28b0
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 build 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
105  def __eq__(self, other):
106    """Test whether two tasks are equal.
107
108    Two tasks are equal if their flag_set are equal.
109
110    Args:
111      other: The other task with which this task is tested equality.
112    Returns:
113      True if the encapsulated flag sets are equal.
114    """
115    if isinstance(other, Task):
116      return self.GetFlags() == other.GetFlags()
117    return False
118
119  def __hash__(self):
120    if self._hash_value is None:
121      # Cache the hash value of the flags, so as not to recompute them.
122      self._hash_value = hash(self._flag_set)
123    return self._hash_value
124
125  def GetIdentifier(self, stage):
126    """Get the identifier of the task in the stage.
127
128    The flag set uniquely identifies a task in the build stage. The checksum of
129    the image of the task uniquely identifies the task in the test stage.
130
131    Args:
132      stage: The stage (build/test) in which this method is called.
133    Returns:
134      Return the flag set in build stage and return the checksum in test stage.
135    """
136
137    # Define the dictionary for different stage function lookup.
138    get_identifier_functions = {BUILD_STAGE: self.FormattedFlags,
139                                TEST_STAGE: self.__GetCheckSum}
140
141    assert stage in get_identifier_functions
142    return get_identifier_functions[stage]()
143
144  def GetResult(self, stage):
145    """Get the performance results of the task in the stage.
146
147    Args:
148      stage: The stage (build/test) in which this method is called.
149    Returns:
150      Performance results.
151    """
152
153    # Define the dictionary for different stage function lookup.
154    get_result_functions = {BUILD_STAGE: self.__GetBuildResult,
155                            TEST_STAGE: self.GetTestResult}
156
157    assert stage in get_result_functions
158
159    return get_result_functions[stage]()
160
161  def SetResult(self, stage, result):
162    """Set the performance results of the task in the stage.
163
164    This method is called by the pipeling_worker to set the results for
165    duplicated tasks.
166
167    Args:
168      stage: The stage (build/test) in which this method is called.
169      result: The performance results of the stage.
170    """
171
172    # Define the dictionary for different stage function lookup.
173    set_result_functions = {BUILD_STAGE: self.__SetBuildResult,
174                            TEST_STAGE: self.__SetTestResult}
175
176    assert stage in set_result_functions
177
178    set_result_functions[stage](result)
179
180  def Done(self, stage):
181    """Check whether the stage is done.
182
183    Args:
184      stage: The stage to be checked, build or test.
185    Returns:
186      True if the stage is done.
187    """
188
189    # Define the dictionary for different result string lookup.
190    done_string = {BUILD_STAGE: self._build_cost, TEST_STAGE: self._exe_cost}
191
192    assert stage in done_string
193
194    return done_string[stage] is not None
195
196  def Work(self, stage):
197    """Perform the task.
198
199    Args:
200      stage: The stage in which the task is performed, compile or test.
201    """
202
203    # Define the dictionary for different stage function lookup.
204    work_functions = {BUILD_STAGE: self.__Compile(), TEST_STAGE: self.__Test()}
205
206    assert stage in work_functions
207
208    work_functions[stage]()
209
210  def FormattedFlags(self):
211    """Format the optimization flag set of this task.
212
213    Returns:
214      The formatted optimization flag set that is encapsulated by this task.
215    """
216    return str(self._flag_set.FormattedForUse())
217
218  def GetFlags(self):
219    """Get the optimization flag set of this task.
220
221    Returns:
222      The optimization flag set that is encapsulated by this task.
223    """
224
225    return self._flag_set
226
227  def __GetCheckSum(self):
228    """Get the compilation image checksum of this task.
229
230    Returns:
231      The compilation image checksum of this task.
232    """
233
234    # The checksum should be computed before this method is called.
235    assert self._checksum is not None
236    return self._checksum
237
238  def __Compile(self):
239    """Run a compile.
240
241    This method compile an image using the present flags, get the image,
242    test the existent of the image and gathers monitoring information, and sets
243    the internal cost (fitness) for this set of flags.
244    """
245
246    # Format the flags as a string as input to compile command. The unique
247    # identifier is passed to the compile command. If concurrent processes are
248    # used to compile different tasks, these processes can use the identifier to
249    # write to different file.
250    flags = self._flag_set.FormattedForUse()
251    command = '%s %s %s' % (Task.BUILD_COMMAND, ' '.join(flags),
252                            self._task_identifier)
253
254    # Try build_tries number of times before confirming that the build fails.
255    for _ in range(BUILD_TRIES):
256      # Execute the command and get the execution status/results.
257      p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE,
258                           stderr=subprocess.PIPE)
259      (out, err) = p.communicate()
260
261      if not err and out != ERROR_STRING:
262        # Each build results contains the checksum of the result image, the
263        # performance cost of the build, the compilation image, the length of
264        # the build, and the length of the text section of the build.
265        (checksum, cost, image, file_length, text_length) = out.split()
266        # Build successfully.
267        break
268
269      # Build failed.
270      cost = ERROR_STRING
271
272    # Convert the build cost from String to integer. The build cost is used to
273    # compare a task with another task. Set the build cost of the failing task
274    # to the max integer.
275    self._build_cost = sys.maxint if cost == ERROR_STRING else int(cost)
276
277    self._checksum = checksum
278    self._file_length = file_length
279    self._text_length = text_length
280    self._image = image
281
282    self.__LogBuildCost()
283
284  def __Test(self):
285    """__Test the task against benchmark(s) using the input test command."""
286
287    # Ensure that the task is compiled before being tested.
288    assert self._image is not None
289
290    # If the task does not compile, no need to test.
291    if self._image == ERROR_STRING:
292      self._exe_cost = ERROR_STRING
293      return
294
295    # The unique identifier is passed to the test command. If concurrent
296    # processes are used to compile different tasks, these processes can use the
297    # identifier to write to different file.
298    command = '%s %s %s' % (Task.TEST_COMMAND, self._image,
299                            self._task_identifier)
300
301    # Try build_tries number of times before confirming that the build fails.
302    for _ in range(TEST_TRIES):
303      p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE,
304                           stderr=subprocess.PIPE)
305      (out, err) = p.communicate()
306
307      if not err and out != ERROR_STRING:
308        # The test results contains the performance cost of the test.
309        cost = out
310        # Test successfully.
311        break
312
313      # Test failed.
314      cost = ERROR_STRING
315
316    self._exe_cost = sys.maxint if (cost == ERROR_STRING) else int(cost)
317
318    self.__LogTestCost()
319
320  def __SetBuildResult(self, (checksum, build_cost, image, file_length,
321                              text_length)):
322    self._checksum = checksum
323    self._build_cost = build_cost
324    self._image = image
325    self._file_length = file_length
326    self._text_length = text_length
327
328  def __GetBuildResult(self):
329    return (self._checksum, self._build_cost, self._image, self._file_length,
330            self._text_length)
331
332  def GetTestResult(self):
333    return self._exe_cost
334
335  def __SetTestResult(self, exe_cost):
336    self._exe_cost = exe_cost
337
338  def LogSteeringCost(self):
339    """Log the performance results for the task.
340
341    This method is called by the steering stage and this method writes the
342    results out to a file. The results include the build and the test results.
343    """
344
345    steering_log = '%s/%s/steering.txt' % self._log_path
346
347    _CreateDirectory(steering_log)
348
349    with open(steering_log, 'w') as out_file:
350      # Include the build and the test results.
351      steering_result = (self._flag_set, self._checksum, self._build_cost,
352                         self._image, self._file_length, self._text_length,
353                         self._exe_cost)
354
355      # Write out the result in the comma-separated format (CSV).
356      out_file.write('%s,%s,%s,%s,%s,%s,%s\n' % steering_result)
357
358  def __LogBuildCost(self):
359    """Log the build results for the task.
360
361    The build results include the compilation time of the build, the result
362    image, the checksum, the file length and the text length of the image.
363    The file length of the image includes the length of the file of the image.
364    The text length only includes the length of the text section of the image.
365    """
366
367    build_log = '%s/%s/build.txt' % self._log_path
368
369    _CreateDirectory(build_log)
370
371    with open(build_log, 'w') as out_file:
372      build_result = (self._flag_set, self._build_cost, self._image,
373                      self._checksum, self._file_length, self._text_length)
374
375      # Write out the result in the comma-separated format (CSV).
376      out_file.write('%s,%s,%s,%s,%s,%s\n' % build_result)
377
378  def __LogTestCost(self):
379    """Log the test results for the task.
380
381    The test results include the runtime execution time of the test.
382    """
383
384    test_log = '%s/%s/build.txt' % self._log_path
385
386    _CreateDirectory(test_log)
387
388    with open(test_log, 'w') as out_file:
389      test_result = (self._flag_set, self._checksum, self._exe_cost)
390
391      # Write out the result in the comma-separated format (CSV).
392      out_file.write('%s,%s,%s\n' % test_result)
393
394  def Improve(self, other):
395    """Compare the current task with another task.
396
397    Args:
398      other: The other task against which the current task is compared.
399
400    Returns:
401      True if this task has improvement upon the other task.
402    """
403
404    # The execution costs must have been initiated.
405    assert self._exe_cost is not None
406    assert other.GetTestResult() is not None
407
408    return self._exe_cost < other.GetTestResult()
409