1# Copyright 2013 The ChromiumOS Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""A reproducing entity. 5 6Part of the Chrome build flags optimization. 7 8The Task class is used by different modules. Each module fills in the 9corresponding information into a Task instance. Class Task contains the bit set 10representing the flags selection. The builder module is responsible for filling 11the image and the checksum field of a Task. The executor module will put the 12execution output to the execution field. 13""" 14 15__author__ = "[email protected] (Yuheng Long)" 16 17import os 18import subprocess 19import sys 20from uuid import uuid4 21 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 = { 141 BUILD_STAGE: self.FormattedFlags, 142 TEST_STAGE: self.__GetCheckSum, 143 } 144 145 assert stage in get_identifier_functions 146 return get_identifier_functions[stage]() 147 148 def GetResult(self, stage): 149 """Get the performance results of the task in the stage. 150 151 Args: 152 stage: The stage (build/test) in which this method is called. 153 Returns: 154 Performance results. 155 """ 156 157 # Define the dictionary for different stage function lookup. 158 get_result_functions = { 159 BUILD_STAGE: self.__GetBuildResult, 160 TEST_STAGE: self.GetTestResult, 161 } 162 163 assert stage in get_result_functions 164 165 return get_result_functions[stage]() 166 167 def SetResult(self, stage, result): 168 """Set the performance results of the task in the stage. 169 170 This method is called by the pipeling_worker to set the results for 171 duplicated tasks. 172 173 Args: 174 stage: The stage (build/test) in which this method is called. 175 result: The performance results of the stage. 176 """ 177 178 # Define the dictionary for different stage function lookup. 179 set_result_functions = { 180 BUILD_STAGE: self.__SetBuildResult, 181 TEST_STAGE: self.__SetTestResult, 182 } 183 184 assert stage in set_result_functions 185 186 set_result_functions[stage](result) 187 188 def Done(self, stage): 189 """Check whether the stage is done. 190 191 Args: 192 stage: The stage to be checked, build or test. 193 Returns: 194 True if the stage is done. 195 """ 196 197 # Define the dictionary for different result string lookup. 198 done_string = { 199 BUILD_STAGE: self._build_cost, 200 TEST_STAGE: self._exe_cost, 201 } 202 203 assert stage in done_string 204 205 return done_string[stage] is not None 206 207 def Work(self, stage): 208 """Perform the task. 209 210 Args: 211 stage: The stage in which the task is performed, compile or test. 212 """ 213 214 # Define the dictionary for different stage function lookup. 215 work_functions = {BUILD_STAGE: self.__Compile, TEST_STAGE: self.__Test} 216 217 assert stage in work_functions 218 219 work_functions[stage]() 220 221 def FormattedFlags(self): 222 """Format the optimization flag set of this task. 223 224 Returns: 225 The formatted optimization flag set that is encapsulated by this task. 226 """ 227 return str(self._flag_set.FormattedForUse()) 228 229 def GetFlags(self): 230 """Get the optimization flag set of this task. 231 232 Returns: 233 The optimization flag set that is encapsulated by this task. 234 """ 235 236 return self._flag_set 237 238 def __GetCheckSum(self): 239 """Get the compilation image checksum of this task. 240 241 Returns: 242 The compilation image checksum of this task. 243 """ 244 245 # The checksum should be computed before this method is called. 246 assert self._checksum is not None 247 return self._checksum 248 249 def __Compile(self): 250 """Run a compile. 251 252 This method compile an image using the present flags, get the image, 253 test the existent of the image and gathers monitoring information, and sets 254 the internal cost (fitness) for this set of flags. 255 """ 256 257 # Format the flags as a string as input to compile command. The unique 258 # identifier is passed to the compile command. If concurrent processes are 259 # used to compile different tasks, these processes can use the identifier to 260 # write to different file. 261 flags = self._flag_set.FormattedForUse() 262 command = "%s %s %s" % ( 263 Task.BUILD_COMMAND, 264 " ".join(flags), 265 self._task_identifier, 266 ) 267 268 # Try BUILD_TRIES number of times before confirming that the build fails. 269 for _ in range(BUILD_TRIES): 270 try: 271 # Execute the command and get the execution status/results. 272 p = subprocess.Popen( 273 command.split(), 274 stdout=subprocess.PIPE, 275 stderr=subprocess.PIPE, 276 ) 277 (out, err) = p.communicate() 278 279 if out: 280 out = out.strip() 281 if out != ERROR_STRING: 282 # Each build results contains the checksum of the result image, the 283 # performance cost of the build, the compilation image, the length 284 # of the build, and the length of the text section of the build. 285 ( 286 checksum, 287 cost, 288 image, 289 file_length, 290 text_length, 291 ) = out.split() 292 # Build successfully. 293 break 294 295 # Build failed. 296 cost = ERROR_STRING 297 except _: 298 # If there is exception getting the cost information of the build, the 299 # build failed. 300 cost = ERROR_STRING 301 302 # Convert the build cost from String to integer. The build cost is used to 303 # compare a task with another task. Set the build cost of the failing task 304 # to the max integer. The for loop will keep trying until either there is a 305 # success or BUILD_TRIES number of tries have been conducted. 306 self._build_cost = sys.maxint if cost == ERROR_STRING else float(cost) 307 308 self._checksum = checksum 309 self._file_length = file_length 310 self._text_length = text_length 311 self._image = image 312 313 self.__LogBuildCost(err) 314 315 def __Test(self): 316 """__Test the task against benchmark(s) using the input test command.""" 317 318 # Ensure that the task is compiled before being tested. 319 assert self._image is not None 320 321 # If the task does not compile, no need to test. 322 if self._image == ERROR_STRING: 323 self._exe_cost = ERROR_STRING 324 return 325 326 # The unique identifier is passed to the test command. If concurrent 327 # processes are used to compile different tasks, these processes can use the 328 # identifier to write to different file. 329 command = "%s %s %s" % ( 330 Task.TEST_COMMAND, 331 self._image, 332 self._task_identifier, 333 ) 334 335 # Try TEST_TRIES number of times before confirming that the build fails. 336 for _ in range(TEST_TRIES): 337 try: 338 p = subprocess.Popen( 339 command.split(), 340 stdout=subprocess.PIPE, 341 stderr=subprocess.PIPE, 342 ) 343 (out, err) = p.communicate() 344 345 if out: 346 out = out.strip() 347 if out != ERROR_STRING: 348 # The test results contains the performance cost of the test. 349 cost = out 350 # Test successfully. 351 break 352 353 # Test failed. 354 cost = ERROR_STRING 355 except _: 356 # If there is exception getting the cost information of the test, the 357 # test failed. The for loop will keep trying until either there is a 358 # success or TEST_TRIES number of tries have been conducted. 359 cost = ERROR_STRING 360 361 self._exe_cost = sys.maxint if (cost == ERROR_STRING) else float(cost) 362 363 self.__LogTestCost(err) 364 365 def __SetBuildResult( 366 self, (checksum, build_cost, image, file_length, text_length) 367 ): 368 self._checksum = checksum 369 self._build_cost = build_cost 370 self._image = image 371 self._file_length = file_length 372 self._text_length = text_length 373 374 def __GetBuildResult(self): 375 return ( 376 self._checksum, 377 self._build_cost, 378 self._image, 379 self._file_length, 380 self._text_length, 381 ) 382 383 def GetTestResult(self): 384 return self._exe_cost 385 386 def __SetTestResult(self, exe_cost): 387 self._exe_cost = exe_cost 388 389 def LogSteeringCost(self): 390 """Log the performance results for the task. 391 392 This method is called by the steering stage and this method writes the 393 results out to a file. The results include the build and the test results. 394 """ 395 396 steering_log = "%s/%s/steering.txt" % self._log_path 397 398 _CreateDirectory(steering_log) 399 400 with open(steering_log, "w") as out_file: 401 # Include the build and the test results. 402 steering_result = ( 403 self._flag_set, 404 self._checksum, 405 self._build_cost, 406 self._image, 407 self._file_length, 408 self._text_length, 409 self._exe_cost, 410 ) 411 412 # Write out the result in the comma-separated format (CSV). 413 out_file.write("%s,%s,%s,%s,%s,%s,%s\n" % steering_result) 414 415 def __LogBuildCost(self, log): 416 """Log the build results for the task. 417 418 The build results include the compilation time of the build, the result 419 image, the checksum, the file length and the text length of the image. 420 The file length of the image includes the length of the file of the image. 421 The text length only includes the length of the text section of the image. 422 423 Args: 424 log: The build log of this task. 425 """ 426 427 build_result_log = "%s/%s/build.txt" % self._log_path 428 429 _CreateDirectory(build_result_log) 430 431 with open(build_result_log, "w") as out_file: 432 build_result = ( 433 self._flag_set, 434 self._build_cost, 435 self._image, 436 self._checksum, 437 self._file_length, 438 self._text_length, 439 ) 440 441 # Write out the result in the comma-separated format (CSV). 442 out_file.write("%s,%s,%s,%s,%s,%s\n" % build_result) 443 444 # The build information about running the build. 445 build_run_log = "%s/%s/build_log.txt" % self._log_path 446 _CreateDirectory(build_run_log) 447 448 with open(build_run_log, "w") as out_log_file: 449 # Write out the execution information. 450 out_log_file.write("%s" % log) 451 452 def __LogTestCost(self, log): 453 """Log the test results for the task. 454 455 The test results include the runtime execution time of the test. 456 457 Args: 458 log: The test log of this task. 459 """ 460 461 test_log = "%s/%s/test.txt" % self._log_path 462 463 _CreateDirectory(test_log) 464 465 with open(test_log, "w") as out_file: 466 test_result = (self._flag_set, self._checksum, self._exe_cost) 467 468 # Write out the result in the comma-separated format (CSV). 469 out_file.write("%s,%s,%s\n" % test_result) 470 471 # The execution information about running the test. 472 test_run_log = "%s/%s/test_log.txt" % self._log_path 473 474 _CreateDirectory(test_run_log) 475 476 with open(test_run_log, "w") as out_log_file: 477 # Append the test log information. 478 out_log_file.write("%s" % log) 479 480 def IsImproved(self, other): 481 """Compare the current task with another task. 482 483 Args: 484 other: The other task against which the current task is compared. 485 486 Returns: 487 True if this task has improvement upon the other task. 488 """ 489 490 # The execution costs must have been initiated. 491 assert self._exe_cost is not None 492 assert other.GetTestResult() is not None 493 494 return self._exe_cost < other.GetTestResult() 495