task.py revision ccfaf2f382815945c1c883e1cfceb2eba55e28b0
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 build 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 105 def __eq__(self, other): 106 """Test whether two tasks are equal. 107 108 Two tasks are equal if their flag_set are equal. 109 110 Args: 111 other: The other task with which this task is tested equality. 112 Returns: 113 True if the encapsulated flag sets are equal. 114 """ 115 if isinstance(other, Task): 116 return self.GetFlags() == other.GetFlags() 117 return False 118 119 def __hash__(self): 120 if self._hash_value is None: 121 # Cache the hash value of the flags, so as not to recompute them. 122 self._hash_value = hash(self._flag_set) 123 return self._hash_value 124 125 def GetIdentifier(self, stage): 126 """Get the identifier of the task in the stage. 127 128 The flag set uniquely identifies a task in the build stage. The checksum of 129 the image of the task uniquely identifies the task in the test stage. 130 131 Args: 132 stage: The stage (build/test) in which this method is called. 133 Returns: 134 Return the flag set in build stage and return the checksum in test stage. 135 """ 136 137 # Define the dictionary for different stage function lookup. 138 get_identifier_functions = {BUILD_STAGE: self.FormattedFlags, 139 TEST_STAGE: self.__GetCheckSum} 140 141 assert stage in get_identifier_functions 142 return get_identifier_functions[stage]() 143 144 def GetResult(self, stage): 145 """Get the performance results of the task in the stage. 146 147 Args: 148 stage: The stage (build/test) in which this method is called. 149 Returns: 150 Performance results. 151 """ 152 153 # Define the dictionary for different stage function lookup. 154 get_result_functions = {BUILD_STAGE: self.__GetBuildResult, 155 TEST_STAGE: self.GetTestResult} 156 157 assert stage in get_result_functions 158 159 return get_result_functions[stage]() 160 161 def SetResult(self, stage, result): 162 """Set the performance results of the task in the stage. 163 164 This method is called by the pipeling_worker to set the results for 165 duplicated tasks. 166 167 Args: 168 stage: The stage (build/test) in which this method is called. 169 result: The performance results of the stage. 170 """ 171 172 # Define the dictionary for different stage function lookup. 173 set_result_functions = {BUILD_STAGE: self.__SetBuildResult, 174 TEST_STAGE: self.__SetTestResult} 175 176 assert stage in set_result_functions 177 178 set_result_functions[stage](result) 179 180 def Done(self, stage): 181 """Check whether the stage is done. 182 183 Args: 184 stage: The stage to be checked, build or test. 185 Returns: 186 True if the stage is done. 187 """ 188 189 # Define the dictionary for different result string lookup. 190 done_string = {BUILD_STAGE: self._build_cost, TEST_STAGE: self._exe_cost} 191 192 assert stage in done_string 193 194 return done_string[stage] is not None 195 196 def Work(self, stage): 197 """Perform the task. 198 199 Args: 200 stage: The stage in which the task is performed, compile or test. 201 """ 202 203 # Define the dictionary for different stage function lookup. 204 work_functions = {BUILD_STAGE: self.__Compile(), TEST_STAGE: self.__Test()} 205 206 assert stage in work_functions 207 208 work_functions[stage]() 209 210 def FormattedFlags(self): 211 """Format the optimization flag set of this task. 212 213 Returns: 214 The formatted optimization flag set that is encapsulated by this task. 215 """ 216 return str(self._flag_set.FormattedForUse()) 217 218 def GetFlags(self): 219 """Get the optimization flag set of this task. 220 221 Returns: 222 The optimization flag set that is encapsulated by this task. 223 """ 224 225 return self._flag_set 226 227 def __GetCheckSum(self): 228 """Get the compilation image checksum of this task. 229 230 Returns: 231 The compilation image checksum of this task. 232 """ 233 234 # The checksum should be computed before this method is called. 235 assert self._checksum is not None 236 return self._checksum 237 238 def __Compile(self): 239 """Run a compile. 240 241 This method compile an image using the present flags, get the image, 242 test the existent of the image and gathers monitoring information, and sets 243 the internal cost (fitness) for this set of flags. 244 """ 245 246 # Format the flags as a string as input to compile command. The unique 247 # identifier is passed to the compile command. If concurrent processes are 248 # used to compile different tasks, these processes can use the identifier to 249 # write to different file. 250 flags = self._flag_set.FormattedForUse() 251 command = '%s %s %s' % (Task.BUILD_COMMAND, ' '.join(flags), 252 self._task_identifier) 253 254 # Try build_tries number of times before confirming that the build fails. 255 for _ in range(BUILD_TRIES): 256 # Execute the command and get the execution status/results. 257 p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, 258 stderr=subprocess.PIPE) 259 (out, err) = p.communicate() 260 261 if not err and out != ERROR_STRING: 262 # Each build results contains the checksum of the result image, the 263 # performance cost of the build, the compilation image, the length of 264 # the build, and the length of the text section of the build. 265 (checksum, cost, image, file_length, text_length) = out.split() 266 # Build successfully. 267 break 268 269 # Build failed. 270 cost = ERROR_STRING 271 272 # Convert the build cost from String to integer. The build cost is used to 273 # compare a task with another task. Set the build cost of the failing task 274 # to the max integer. 275 self._build_cost = sys.maxint if cost == ERROR_STRING else int(cost) 276 277 self._checksum = checksum 278 self._file_length = file_length 279 self._text_length = text_length 280 self._image = image 281 282 self.__LogBuildCost() 283 284 def __Test(self): 285 """__Test the task against benchmark(s) using the input test command.""" 286 287 # Ensure that the task is compiled before being tested. 288 assert self._image is not None 289 290 # If the task does not compile, no need to test. 291 if self._image == ERROR_STRING: 292 self._exe_cost = ERROR_STRING 293 return 294 295 # The unique identifier is passed to the test command. If concurrent 296 # processes are used to compile different tasks, these processes can use the 297 # identifier to write to different file. 298 command = '%s %s %s' % (Task.TEST_COMMAND, self._image, 299 self._task_identifier) 300 301 # Try build_tries number of times before confirming that the build fails. 302 for _ in range(TEST_TRIES): 303 p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, 304 stderr=subprocess.PIPE) 305 (out, err) = p.communicate() 306 307 if not err and out != ERROR_STRING: 308 # The test results contains the performance cost of the test. 309 cost = out 310 # Test successfully. 311 break 312 313 # Test failed. 314 cost = ERROR_STRING 315 316 self._exe_cost = sys.maxint if (cost == ERROR_STRING) else int(cost) 317 318 self.__LogTestCost() 319 320 def __SetBuildResult(self, (checksum, build_cost, image, file_length, 321 text_length)): 322 self._checksum = checksum 323 self._build_cost = build_cost 324 self._image = image 325 self._file_length = file_length 326 self._text_length = text_length 327 328 def __GetBuildResult(self): 329 return (self._checksum, self._build_cost, self._image, self._file_length, 330 self._text_length) 331 332 def GetTestResult(self): 333 return self._exe_cost 334 335 def __SetTestResult(self, exe_cost): 336 self._exe_cost = exe_cost 337 338 def LogSteeringCost(self): 339 """Log the performance results for the task. 340 341 This method is called by the steering stage and this method writes the 342 results out to a file. The results include the build and the test results. 343 """ 344 345 steering_log = '%s/%s/steering.txt' % self._log_path 346 347 _CreateDirectory(steering_log) 348 349 with open(steering_log, 'w') as out_file: 350 # Include the build and the test results. 351 steering_result = (self._flag_set, self._checksum, self._build_cost, 352 self._image, self._file_length, self._text_length, 353 self._exe_cost) 354 355 # Write out the result in the comma-separated format (CSV). 356 out_file.write('%s,%s,%s,%s,%s,%s,%s\n' % steering_result) 357 358 def __LogBuildCost(self): 359 """Log the build results for the task. 360 361 The build results include the compilation time of the build, the result 362 image, the checksum, the file length and the text length of the image. 363 The file length of the image includes the length of the file of the image. 364 The text length only includes the length of the text section of the image. 365 """ 366 367 build_log = '%s/%s/build.txt' % self._log_path 368 369 _CreateDirectory(build_log) 370 371 with open(build_log, 'w') as out_file: 372 build_result = (self._flag_set, self._build_cost, self._image, 373 self._checksum, self._file_length, self._text_length) 374 375 # Write out the result in the comma-separated format (CSV). 376 out_file.write('%s,%s,%s,%s,%s,%s\n' % build_result) 377 378 def __LogTestCost(self): 379 """Log the test results for the task. 380 381 The test results include the runtime execution time of the test. 382 """ 383 384 test_log = '%s/%s/build.txt' % self._log_path 385 386 _CreateDirectory(test_log) 387 388 with open(test_log, 'w') as out_file: 389 test_result = (self._flag_set, self._checksum, self._exe_cost) 390 391 # Write out the result in the comma-separated format (CSV). 392 out_file.write('%s,%s,%s\n' % test_result) 393 394 def Improve(self, other): 395 """Compare the current task with another task. 396 397 Args: 398 other: The other task against which the current task is compared. 399 400 Returns: 401 True if this task has improvement upon the other task. 402 """ 403 404 # The execution costs must have been initiated. 405 assert self._exe_cost is not None 406 assert other.GetTestResult() is not None 407 408 return self._exe_cost < other.GetTestResult() 409