Yuheng Long | 16d7a52 | 2013-07-19 16:29:13 -0700 | [diff] [blame] | 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 | |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 5 | """A reproducing entity. |
| 6 | |
Yuheng Long | 49358b7 | 2013-07-10 14:45:29 -0700 | [diff] [blame] | 7 | Part of the Chrome build flags optimization. |
| 8 | |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 9 | The Task class is used by different modules. Each module fills in the |
| 10 | corresponding information into a Task instance. Class Task contains the bit set |
| 11 | representing the flags selection. The builder module is responsible for filling |
| 12 | the image and the checksum field of a Task. The executor module will put the |
| 13 | execution output to the execution field. |
| 14 | """ |
| 15 | |
| 16 | __author__ = 'yuhenglong@google.com (Yuheng Long)' |
| 17 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 18 | import os |
| 19 | import subprocess |
| 20 | import sys |
| 21 | from uuid import uuid4 |
| 22 | |
| 23 | BUILD_STAGE = 1 |
| 24 | TEST_STAGE = 2 |
| 25 | |
| 26 | # Message indicating that the build or test failed. |
| 27 | ERROR_STRING = 'error' |
| 28 | |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 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. |
| 32 | BUILD_TRIES = 3 |
| 33 | |
Yuheng Long | 2b514c2 | 2013-08-08 21:07:24 -0700 | [diff] [blame] | 34 | # The maximum number of tries a test can have. Some tests may fail due to |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 35 | # unexpected environment circumstance. This variable defines how many tries the |
| 36 | # test should attempt before giving up. |
| 37 | TEST_TRIES = 3 |
| 38 | |
| 39 | |
| 40 | # Create the file/directory if it does not already exist. |
| 41 | def _CreateDirectory(file_name): |
| 42 | directory = os.path.dirname(file_name) |
| 43 | if not os.path.exists(directory): |
| 44 | os.makedirs(directory) |
| 45 | |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 46 | |
| 47 | class 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 | |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 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. |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 64 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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 | |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 69 | Args: |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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. |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 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. |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 86 | """ |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 87 | |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 88 | self._flag_set = flag_set |
| 89 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 90 | # A unique identifier that distinguishes this task from other tasks. |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 91 | self._task_identifier = uuid4() |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 92 | |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 93 | self._log_path = (Task.LOG_DIRECTORY, self._task_identifier) |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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. |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 109 | |
| 110 | Args: |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 111 | other: The other task with which this task is tested equality. |
| 112 | Returns: |
| 113 | True if the encapsulated flag sets are equal. |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 114 | """ |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 115 | if isinstance(other, Task): |
| 116 | return self.GetFlags() == other.GetFlags() |
| 117 | return False |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 118 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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. |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 138 | get_identifier_functions = {BUILD_STAGE: self.FormattedFlags, |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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, |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 155 | TEST_STAGE: self.GetTestResult} |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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 | |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 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 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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 | """ |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 224 | |
| 225 | return self._flag_set |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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): |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 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 | """ |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 245 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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() |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 251 | command = '%s %s %s' % (Task.BUILD_COMMAND, ' '.join(flags), |
| 252 | self._task_identifier) |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 253 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 254 | # Try build_tries number of times before confirming that the build fails. |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 255 | for _ in range(BUILD_TRIES): |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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() |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 260 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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 |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 268 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 269 | # Build failed. |
| 270 | cost = ERROR_STRING |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 271 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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) |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 276 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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. |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 298 | command = '%s %s %s' % (Task.TEST_COMMAND, self._image, |
| 299 | self._task_identifier) |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 300 | |
| 301 | # Try build_tries number of times before confirming that the build fails. |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 302 | for _ in range(TEST_TRIES): |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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 | |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 332 | def GetTestResult(self): |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 333 | return self._exe_cost |
| 334 | |
| 335 | def __SetTestResult(self, exe_cost): |
| 336 | self._exe_cost = exe_cost |
| 337 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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 | |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 347 | _CreateDirectory(steering_log) |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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 | |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 369 | _CreateDirectory(build_log) |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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 | |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 386 | _CreateDirectory(test_log) |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame] | 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) |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 393 | |
Yuheng Long | c012322 | 2013-08-11 12:24:32 -0700 | [diff] [blame] | 394 | def IsImproved(self, other): |
Yuheng Long | ccfaf2f | 2013-08-02 14:27:45 -0700 | [diff] [blame] | 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() |