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