task.py revision 25cdf79e17a73858ffa28db8a5b387210fea9e25
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 # Execute the command and get the execution status/results. 259 p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, 260 stderr=subprocess.PIPE) 261 (out, err) = p.communicate() 262 263 if not err and out != ERROR_STRING: 264 # Each build results contains the checksum of the result image, the 265 # performance cost of the build, the compilation image, the length of 266 # the build, and the length of the text section of the build. 267 (checksum, cost, image, file_length, text_length) = out.split() 268 # Build successfully. 269 break 270 271 # Build failed. 272 cost = ERROR_STRING 273 274 # Convert the build cost from String to integer. The build cost is used to 275 # compare a task with another task. Set the build cost of the failing task 276 # to the max integer. 277 self._build_cost = sys.maxint if cost == ERROR_STRING else int(cost) 278 279 self._checksum = checksum 280 self._file_length = file_length 281 self._text_length = text_length 282 self._image = image 283 284 self.__LogBuildCost() 285 286 def __Test(self): 287 """__Test the task against benchmark(s) using the input test command.""" 288 289 # Ensure that the task is compiled before being tested. 290 assert self._image is not None 291 292 # If the task does not compile, no need to test. 293 if self._image == ERROR_STRING: 294 self._exe_cost = ERROR_STRING 295 return 296 297 # The unique identifier is passed to the test command. If concurrent 298 # processes are used to compile different tasks, these processes can use the 299 # identifier to write to different file. 300 command = '%s %s %s' % (Task.TEST_COMMAND, self._image, 301 self._task_identifier) 302 303 # Try build_tries number of times before confirming that the build fails. 304 for _ in range(TEST_TRIES): 305 p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, 306 stderr=subprocess.PIPE) 307 (out, err) = p.communicate() 308 309 if not err and out != ERROR_STRING: 310 # The test results contains the performance cost of the test. 311 cost = out 312 # Test successfully. 313 break 314 315 # Test failed. 316 cost = ERROR_STRING 317 318 self._exe_cost = sys.maxint if (cost == ERROR_STRING) else int(cost) 319 320 self.__LogTestCost() 321 322 def __SetBuildResult(self, (checksum, build_cost, image, file_length, 323 text_length)): 324 self._checksum = checksum 325 self._build_cost = build_cost 326 self._image = image 327 self._file_length = file_length 328 self._text_length = text_length 329 330 def __GetBuildResult(self): 331 return (self._checksum, self._build_cost, self._image, self._file_length, 332 self._text_length) 333 334 def GetTestResult(self): 335 return self._exe_cost 336 337 def __SetTestResult(self, exe_cost): 338 self._exe_cost = exe_cost 339 340 def LogSteeringCost(self): 341 """Log the performance results for the task. 342 343 This method is called by the steering stage and this method writes the 344 results out to a file. The results include the build and the test results. 345 """ 346 347 steering_log = '%s/%s/steering.txt' % self._log_path 348 349 _CreateDirectory(steering_log) 350 351 with open(steering_log, 'w') as out_file: 352 # Include the build and the test results. 353 steering_result = (self._flag_set, self._checksum, self._build_cost, 354 self._image, self._file_length, self._text_length, 355 self._exe_cost) 356 357 # Write out the result in the comma-separated format (CSV). 358 out_file.write('%s,%s,%s,%s,%s,%s,%s\n' % steering_result) 359 360 def __LogBuildCost(self): 361 """Log the build results for the task. 362 363 The build results include the compilation time of the build, the result 364 image, the checksum, the file length and the text length of the image. 365 The file length of the image includes the length of the file of the image. 366 The text length only includes the length of the text section of the image. 367 """ 368 369 build_log = '%s/%s/build.txt' % self._log_path 370 371 _CreateDirectory(build_log) 372 373 with open(build_log, 'w') as out_file: 374 build_result = (self._flag_set, self._build_cost, self._image, 375 self._checksum, self._file_length, self._text_length) 376 377 # Write out the result in the comma-separated format (CSV). 378 out_file.write('%s,%s,%s,%s,%s,%s\n' % build_result) 379 380 def __LogTestCost(self): 381 """Log the test results for the task. 382 383 The test results include the runtime execution time of the test. 384 """ 385 386 test_log = '%s/%s/build.txt' % self._log_path 387 388 _CreateDirectory(test_log) 389 390 with open(test_log, 'w') as out_file: 391 test_result = (self._flag_set, self._checksum, self._exe_cost) 392 393 # Write out the result in the comma-separated format (CSV). 394 out_file.write('%s,%s,%s\n' % test_result) 395 396 def IsImproved(self, other): 397 """Compare the current task with another task. 398 399 Args: 400 other: The other task against which the current task is compared. 401 402 Returns: 403 True if this task has improvement upon the other task. 404 """ 405 406 # The execution costs must have been initiated. 407 assert self._exe_cost is not None 408 assert other.GetTestResult() is not None 409 410 return self._exe_cost < other.GetTestResult() 411