task.py revision b15d41c6d6324e343fbea19abaed3717417f3cde
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
30class Task(object):
31  """A single reproducing entity.
32
33  A single test of performance with a particular set of flags. It records the
34  flag set, the image, the check sum of the image and the cost.
35  """
36
37  def __init__(self, flag_set, build_command, test_command, log_directory,
38               build_tries, test_tries):
39    """Set up the optimization flag selection for this task.
40
41    This framework is generic. It lets the client specify application specific
42    compile and test methods by passing different build_command and
43    test_command.
44
45    Args:
46      flag_set: The optimization flag set that is encapsulated by this task.
47      build_command: The command that will be used in the build stage to compile
48        this task.
49      test_command: The command that will be used in the test stage to test this
50        task.
51      log_directory: The directory to log the compilation and test results.
52      build_tries: The maximum number of tries a build can have. Some
53        compilations may fail due to unexpected environment circumstance. This
54        variable defines how many tries the build should attempt before giving
55        up.
56      test_tries: The maximum number of tries a build can have. Some tests may
57        fail due to unexpected environment circumstance. This variable defines
58        how many tries the test should attempt before giving up.
59    """
60
61    self._flag_set = flag_set
62    self._build_command = build_command
63    self._test_command = test_command
64    self._log_directory = log_directory
65    self._build_tries = build_tries
66    self._test_tries = test_tries
67
68    # A unique identifier that distinguishes this task from other tasks.
69    self.task_identifier = uuid4()
70
71    self._log_path = (self._log_directory, self.task_identifier)
72
73    # Initiate the hash value. The hash value is used so as not to recompute it
74    # every time the hash method is called.
75    self._hash_value = None
76
77    # Indicate that the task has not been compiled/tested.
78    self._build_cost = None
79    self._exe_cost = None
80    self._checksum = None
81    self._image = None
82
83  def __eq__(self, other):
84    """Test whether two tasks are equal.
85
86    Two tasks are equal if their flag_set are equal.
87
88    Args:
89      other: The other task with which this task is tested equality.
90    Returns:
91      True if the encapsulated flag sets are equal.
92    """
93    if isinstance(other, Task):
94      return self.GetFlags() == other.GetFlags()
95    return False
96
97  def __hash__(self):
98    if self._hash_value is None:
99      # Cache the hash value of the flags, so as not to recompute them.
100      self._hash_value = hash(self._flag_set)
101    return self._hash_value
102
103  def GetIdentifier(self, stage):
104    """Get the identifier of the task in the stage.
105
106    The flag set uniquely identifies a task in the build stage. The checksum of
107    the image of the task uniquely identifies the task in the test stage.
108
109    Args:
110      stage: The stage (build/test) in which this method is called.
111    Returns:
112      Return the flag set in build stage and return the checksum in test stage.
113    """
114
115    # Define the dictionary for different stage function lookup.
116    get_identifier_functions = {BUILD_STAGE: self.GetFlags,
117                                TEST_STAGE: self.__GetCheckSum}
118
119    assert stage in get_identifier_functions
120    return get_identifier_functions[stage]()
121
122  def GetResult(self, stage):
123    """Get the performance results of the task in the stage.
124
125    Args:
126      stage: The stage (build/test) in which this method is called.
127    Returns:
128      Performance results.
129    """
130
131    # Define the dictionary for different stage function lookup.
132    get_result_functions = {BUILD_STAGE: self.__GetBuildResult,
133                            TEST_STAGE: self.__GetTestResult}
134
135    assert stage in get_result_functions
136
137    return get_result_functions[stage]()
138
139  def SetResult(self, stage, result):
140    """Set the performance results of the task in the stage.
141
142    This method is called by the pipeling_worker to set the results for
143    duplicated tasks.
144
145    Args:
146      stage: The stage (build/test) in which this method is called.
147      result: The performance results of the stage.
148    """
149
150    # Define the dictionary for different stage function lookup.
151    set_result_functions = {BUILD_STAGE: self.__SetBuildResult,
152                            TEST_STAGE: self.__SetTestResult}
153
154    assert stage in set_result_functions
155
156    set_result_functions[stage](result)
157
158  def Done(self, stage):
159    """Check whether the stage is done.
160
161    Args:
162      stage: The stage to be checked, build or test.
163    Returns:
164      True if the stage is done.
165    """
166
167    # Define the dictionary for different result string lookup.
168    done_string = {BUILD_STAGE: self._build_cost, TEST_STAGE: self._exe_cost}
169
170    assert stage in done_string
171
172    return done_string[stage] is not None
173
174  def Work(self, stage):
175    """Perform the task.
176
177    Args:
178      stage: The stage in which the task is performed, compile or test.
179    """
180
181    # Define the dictionary for different stage function lookup.
182    work_functions = {BUILD_STAGE: self.__Compile(), TEST_STAGE: self.__Test()}
183
184    assert stage in work_functions
185
186    work_functions[stage]()
187
188  def GetFlags(self):
189    """Get the optimization flag set of this task.
190
191    Returns:
192      The optimization flag set that is encapsulated by this task.
193    """
194    return str(self._flag_set.FormattedForUse())
195
196  def __GetCheckSum(self):
197    """Get the compilation image checksum of this task.
198
199    Returns:
200      The compilation image checksum of this task.
201    """
202
203    # The checksum should be computed before this method is called.
204    assert self._checksum is not None
205    return self._checksum
206
207  def __Compile(self):
208    """Run a compile.
209
210    This method compile an image using the present flags, get the image,
211    test the existent of the image and gathers monitoring information, and sets
212    the internal cost (fitness) for this set of flags.
213    """
214
215    # Format the flags as a string as input to compile command. The unique
216    # identifier is passed to the compile command. If concurrent processes are
217    # used to compile different tasks, these processes can use the identifier to
218    # write to different file.
219    flags = self._flag_set.FormattedForUse()
220    command = '%s %s %s' % (self._build_command, ' '.join(flags),
221                            self.task_identifier)
222
223    # Try build_tries number of times before confirming that the build fails.
224    for _ in range(self._build_tries):
225      # Execute the command and get the execution status/results.
226      p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE,
227                           stderr=subprocess.PIPE)
228      (out, err) = p.communicate()
229
230      if not err and out != ERROR_STRING:
231        # Each build results contains the checksum of the result image, the
232        # performance cost of the build, the compilation image, the length of
233        # the build, and the length of the text section of the build.
234        (checksum, cost, image, file_length, text_length) = out.split()
235        # Build successfully.
236        break
237
238      # Build failed.
239      cost = ERROR_STRING
240
241    # Convert the build cost from String to integer. The build cost is used to
242    # compare a task with another task. Set the build cost of the failing task
243    # to the max integer.
244    self._build_cost = sys.maxint if cost == ERROR_STRING else int(cost)
245
246    self._checksum = checksum
247    self._file_length = file_length
248    self._text_length = text_length
249    self._image = image
250
251    self.__LogBuildCost()
252
253  def __Test(self):
254    """__Test the task against benchmark(s) using the input test command."""
255
256    # Ensure that the task is compiled before being tested.
257    assert self._image is not None
258
259    # If the task does not compile, no need to test.
260    if self._image == ERROR_STRING:
261      self._exe_cost = ERROR_STRING
262      return
263
264    # The unique identifier is passed to the test command. If concurrent
265    # processes are used to compile different tasks, these processes can use the
266    # identifier to write to different file.
267    command = '%s %s %s' % (self._test_command, self._image,
268                            self.task_identifier)
269
270    # Try build_tries number of times before confirming that the build fails.
271    for _ in range(self._test_tries):
272      p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE,
273                           stderr=subprocess.PIPE)
274      (out, err) = p.communicate()
275
276      if not err and out != ERROR_STRING:
277        # The test results contains the performance cost of the test.
278        cost = out
279        # Test successfully.
280        break
281
282      # Test failed.
283      cost = ERROR_STRING
284
285    self._exe_cost = sys.maxint if (cost == ERROR_STRING) else int(cost)
286
287    self.__LogTestCost()
288
289  def __SetBuildResult(self, (checksum, build_cost, image, file_length,
290                              text_length)):
291    self._checksum = checksum
292    self._build_cost = build_cost
293    self._image = image
294    self._file_length = file_length
295    self._text_length = text_length
296
297  def __GetBuildResult(self):
298    return (self._checksum, self._build_cost, self._image, self._file_length,
299            self._text_length)
300
301  def __GetTestResult(self):
302    return self._exe_cost
303
304  def __SetTestResult(self, exe_cost):
305    self._exe_cost = exe_cost
306
307  def __CreateDirectory(self, file_name):
308    """Create a directory/file if it does not already exist.
309
310    Args:
311      file_name: The path of the directory/file to be created.
312    """
313
314    d = os.path.dirname(file_name)
315    if not os.path.exists(d):
316      os.makedirs(d)
317
318  def LogSteeringCost(self):
319    """Log the performance results for the task.
320
321    This method is called by the steering stage and this method writes the
322    results out to a file. The results include the build and the test results.
323    """
324
325    steering_log = '%s/%s/steering.txt' % self._log_path
326
327    self.create_dir('%s/%s/' % steering_log)
328
329    with open(steering_log, 'w') as out_file:
330      # Include the build and the test results.
331      steering_result = (self._flag_set, self._checksum, self._build_cost,
332                         self._image, self._file_length, self._text_length,
333                         self._exe_cost)
334
335      # Write out the result in the comma-separated format (CSV).
336      out_file.write('%s,%s,%s,%s,%s,%s,%s\n' % steering_result)
337
338  def __LogBuildCost(self):
339    """Log the build results for the task.
340
341    The build results include the compilation time of the build, the result
342    image, the checksum, the file length and the text length of the image.
343    The file length of the image includes the length of the file of the image.
344    The text length only includes the length of the text section of the image.
345    """
346
347    build_log = '%s/%s/build.txt' % self._log_path
348
349    self.__CreateDirectory(build_log)
350
351    with open(build_log, 'w') as out_file:
352      build_result = (self._flag_set, self._build_cost, self._image,
353                      self._checksum, self._file_length, self._text_length)
354
355      # Write out the result in the comma-separated format (CSV).
356      out_file.write('%s,%s,%s,%s,%s,%s\n' % build_result)
357
358  def __LogTestCost(self):
359    """Log the test results for the task.
360
361    The test results include the runtime execution time of the test.
362    """
363
364    test_log = '%s/%s/build.txt' % self._log_path
365
366    self.__CreateDirectory(test_log)
367
368    with open(test_log, 'w') as out_file:
369      test_result = (self._flag_set, self._checksum, self._exe_cost)
370
371      # Write out the result in the comma-separated format (CSV).
372      out_file.write('%s,%s,%s\n' % test_result)
373