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