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 | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 29 | |
| 30 | class Task(object): |
| 31 | """A single reproducing entity. |
| 32 | |
| 33 | A single test of performance with a particular set of flags. It records the |
| 34 | flag set, the image, the check sum of the image and the cost. |
| 35 | """ |
| 36 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 37 | def __init__(self, flag_set, build_command, test_command, log_directory, |
| 38 | build_tries, test_tries): |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 39 | """Set up the optimization flag selection for this task. |
| 40 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 41 | This framework is generic. It lets the client specify application specific |
| 42 | compile and test methods by passing different build_command and |
| 43 | test_command. |
| 44 | |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 45 | Args: |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 46 | flag_set: The optimization flag set that is encapsulated by this task. |
| 47 | build_command: The command that will be used in the build stage to compile |
| 48 | this task. |
| 49 | test_command: The command that will be used in the test stage to test this |
| 50 | task. |
| 51 | log_directory: The directory to log the compilation and test results. |
| 52 | build_tries: The maximum number of tries a build can have. Some |
| 53 | compilations may fail due to unexpected environment circumstance. This |
| 54 | variable defines how many tries the build should attempt before giving |
| 55 | up. |
| 56 | test_tries: The maximum number of tries a build can have. Some tests may |
| 57 | fail due to unexpected environment circumstance. This variable defines |
| 58 | how many tries the test should attempt before giving up. |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 59 | """ |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 60 | |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 61 | self._flag_set = flag_set |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 62 | self._build_command = build_command |
| 63 | self._test_command = test_command |
| 64 | self._log_directory = log_directory |
| 65 | self._build_tries = build_tries |
| 66 | self._test_tries = test_tries |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 67 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 68 | # A unique identifier that distinguishes this task from other tasks. |
| 69 | self.task_identifier = uuid4() |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 70 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 71 | self._log_path = (self._log_directory, self.task_identifier) |
| 72 | |
| 73 | # Initiate the hash value. The hash value is used so as not to recompute it |
| 74 | # every time the hash method is called. |
| 75 | self._hash_value = None |
| 76 | |
| 77 | # Indicate that the task has not been compiled/tested. |
| 78 | self._build_cost = None |
| 79 | self._exe_cost = None |
| 80 | self._checksum = None |
| 81 | self._image = None |
| 82 | |
| 83 | def __eq__(self, other): |
| 84 | """Test whether two tasks are equal. |
| 85 | |
| 86 | Two tasks are equal if their flag_set are equal. |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 87 | |
| 88 | Args: |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 89 | other: The other task with which this task is tested equality. |
| 90 | Returns: |
| 91 | True if the encapsulated flag sets are equal. |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 92 | """ |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 93 | if isinstance(other, Task): |
| 94 | return self.GetFlags() == other.GetFlags() |
| 95 | return False |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 96 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 97 | def __hash__(self): |
| 98 | if self._hash_value is None: |
| 99 | # Cache the hash value of the flags, so as not to recompute them. |
| 100 | self._hash_value = hash(self._flag_set) |
| 101 | return self._hash_value |
| 102 | |
| 103 | def GetIdentifier(self, stage): |
| 104 | """Get the identifier of the task in the stage. |
| 105 | |
| 106 | The flag set uniquely identifies a task in the build stage. The checksum of |
| 107 | the image of the task uniquely identifies the task in the test stage. |
| 108 | |
| 109 | Args: |
| 110 | stage: The stage (build/test) in which this method is called. |
| 111 | Returns: |
| 112 | Return the flag set in build stage and return the checksum in test stage. |
| 113 | """ |
| 114 | |
| 115 | # Define the dictionary for different stage function lookup. |
| 116 | get_identifier_functions = {BUILD_STAGE: self.GetFlags, |
| 117 | TEST_STAGE: self.__GetCheckSum} |
| 118 | |
| 119 | assert stage in get_identifier_functions |
| 120 | return get_identifier_functions[stage]() |
| 121 | |
| 122 | def GetResult(self, stage): |
| 123 | """Get the performance results of the task in the stage. |
| 124 | |
| 125 | Args: |
| 126 | stage: The stage (build/test) in which this method is called. |
| 127 | Returns: |
| 128 | Performance results. |
| 129 | """ |
| 130 | |
| 131 | # Define the dictionary for different stage function lookup. |
| 132 | get_result_functions = {BUILD_STAGE: self.__GetBuildResult, |
| 133 | TEST_STAGE: self.__GetTestResult} |
| 134 | |
| 135 | assert stage in get_result_functions |
| 136 | |
| 137 | return get_result_functions[stage]() |
| 138 | |
| 139 | def SetResult(self, stage, result): |
| 140 | """Set the performance results of the task in the stage. |
| 141 | |
| 142 | This method is called by the pipeling_worker to set the results for |
| 143 | duplicated tasks. |
| 144 | |
| 145 | Args: |
| 146 | stage: The stage (build/test) in which this method is called. |
| 147 | result: The performance results of the stage. |
| 148 | """ |
| 149 | |
| 150 | # Define the dictionary for different stage function lookup. |
| 151 | set_result_functions = {BUILD_STAGE: self.__SetBuildResult, |
| 152 | TEST_STAGE: self.__SetTestResult} |
| 153 | |
| 154 | assert stage in set_result_functions |
| 155 | |
| 156 | set_result_functions[stage](result) |
| 157 | |
| 158 | def Done(self, stage): |
| 159 | """Check whether the stage is done. |
| 160 | |
| 161 | Args: |
| 162 | stage: The stage to be checked, build or test. |
| 163 | Returns: |
| 164 | True if the stage is done. |
| 165 | """ |
| 166 | |
| 167 | # Define the dictionary for different result string lookup. |
| 168 | done_string = {BUILD_STAGE: self._build_cost, TEST_STAGE: self._exe_cost} |
| 169 | |
| 170 | assert stage in done_string |
| 171 | |
| 172 | return done_string[stage] is not None |
| 173 | |
| 174 | def Work(self, stage): |
| 175 | """Perform the task. |
| 176 | |
| 177 | Args: |
| 178 | stage: The stage in which the task is performed, compile or test. |
| 179 | """ |
| 180 | |
| 181 | # Define the dictionary for different stage function lookup. |
| 182 | work_functions = {BUILD_STAGE: self.__Compile(), TEST_STAGE: self.__Test()} |
| 183 | |
| 184 | assert stage in work_functions |
| 185 | |
| 186 | work_functions[stage]() |
| 187 | |
| 188 | def GetFlags(self): |
| 189 | """Get the optimization flag set of this task. |
| 190 | |
| 191 | Returns: |
| 192 | The optimization flag set that is encapsulated by this task. |
| 193 | """ |
| 194 | return str(self._flag_set.FormattedForUse()) |
| 195 | |
| 196 | def __GetCheckSum(self): |
| 197 | """Get the compilation image checksum of this task. |
| 198 | |
| 199 | Returns: |
| 200 | The compilation image checksum of this task. |
| 201 | """ |
| 202 | |
| 203 | # The checksum should be computed before this method is called. |
| 204 | assert self._checksum is not None |
| 205 | return self._checksum |
| 206 | |
| 207 | def __Compile(self): |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 208 | """Run a compile. |
| 209 | |
| 210 | This method compile an image using the present flags, get the image, |
| 211 | test the existent of the image and gathers monitoring information, and sets |
| 212 | the internal cost (fitness) for this set of flags. |
| 213 | """ |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 214 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 215 | # Format the flags as a string as input to compile command. The unique |
| 216 | # identifier is passed to the compile command. If concurrent processes are |
| 217 | # used to compile different tasks, these processes can use the identifier to |
| 218 | # write to different file. |
| 219 | flags = self._flag_set.FormattedForUse() |
| 220 | command = '%s %s %s' % (self._build_command, ' '.join(flags), |
| 221 | self.task_identifier) |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 222 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 223 | # Try build_tries number of times before confirming that the build fails. |
| 224 | for _ in range(self._build_tries): |
| 225 | # Execute the command and get the execution status/results. |
| 226 | p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, |
| 227 | stderr=subprocess.PIPE) |
| 228 | (out, err) = p.communicate() |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 229 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 230 | if not err and out != ERROR_STRING: |
| 231 | # Each build results contains the checksum of the result image, the |
| 232 | # performance cost of the build, the compilation image, the length of |
| 233 | # the build, and the length of the text section of the build. |
| 234 | (checksum, cost, image, file_length, text_length) = out.split() |
| 235 | # Build successfully. |
| 236 | break |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 237 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 238 | # Build failed. |
| 239 | cost = ERROR_STRING |
Yuheng Long | f20cffa | 2013-06-03 18:46:00 -0700 | [diff] [blame] | 240 | |
Yuheng Long | b15d41c | 2013-07-25 10:02:36 -0700 | [diff] [blame^] | 241 | # Convert the build cost from String to integer. The build cost is used to |
| 242 | # compare a task with another task. Set the build cost of the failing task |
| 243 | # to the max integer. |
| 244 | self._build_cost = sys.maxint if cost == ERROR_STRING else int(cost) |
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 | self._checksum = checksum |
| 247 | self._file_length = file_length |
| 248 | self._text_length = text_length |
| 249 | self._image = image |
| 250 | |
| 251 | self.__LogBuildCost() |
| 252 | |
| 253 | def __Test(self): |
| 254 | """__Test the task against benchmark(s) using the input test command.""" |
| 255 | |
| 256 | # Ensure that the task is compiled before being tested. |
| 257 | assert self._image is not None |
| 258 | |
| 259 | # If the task does not compile, no need to test. |
| 260 | if self._image == ERROR_STRING: |
| 261 | self._exe_cost = ERROR_STRING |
| 262 | return |
| 263 | |
| 264 | # The unique identifier is passed to the test command. If concurrent |
| 265 | # processes are used to compile different tasks, these processes can use the |
| 266 | # identifier to write to different file. |
| 267 | command = '%s %s %s' % (self._test_command, self._image, |
| 268 | self.task_identifier) |
| 269 | |
| 270 | # Try build_tries number of times before confirming that the build fails. |
| 271 | for _ in range(self._test_tries): |
| 272 | p = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, |
| 273 | stderr=subprocess.PIPE) |
| 274 | (out, err) = p.communicate() |
| 275 | |
| 276 | if not err and out != ERROR_STRING: |
| 277 | # The test results contains the performance cost of the test. |
| 278 | cost = out |
| 279 | # Test successfully. |
| 280 | break |
| 281 | |
| 282 | # Test failed. |
| 283 | cost = ERROR_STRING |
| 284 | |
| 285 | self._exe_cost = sys.maxint if (cost == ERROR_STRING) else int(cost) |
| 286 | |
| 287 | self.__LogTestCost() |
| 288 | |
| 289 | def __SetBuildResult(self, (checksum, build_cost, image, file_length, |
| 290 | text_length)): |
| 291 | self._checksum = checksum |
| 292 | self._build_cost = build_cost |
| 293 | self._image = image |
| 294 | self._file_length = file_length |
| 295 | self._text_length = text_length |
| 296 | |
| 297 | def __GetBuildResult(self): |
| 298 | return (self._checksum, self._build_cost, self._image, self._file_length, |
| 299 | self._text_length) |
| 300 | |
| 301 | def __GetTestResult(self): |
| 302 | return self._exe_cost |
| 303 | |
| 304 | def __SetTestResult(self, exe_cost): |
| 305 | self._exe_cost = exe_cost |
| 306 | |
| 307 | def __CreateDirectory(self, file_name): |
| 308 | """Create a directory/file if it does not already exist. |
| 309 | |
| 310 | Args: |
| 311 | file_name: The path of the directory/file to be created. |
| 312 | """ |
| 313 | |
| 314 | d = os.path.dirname(file_name) |
| 315 | if not os.path.exists(d): |
| 316 | os.makedirs(d) |
| 317 | |
| 318 | def LogSteeringCost(self): |
| 319 | """Log the performance results for the task. |
| 320 | |
| 321 | This method is called by the steering stage and this method writes the |
| 322 | results out to a file. The results include the build and the test results. |
| 323 | """ |
| 324 | |
| 325 | steering_log = '%s/%s/steering.txt' % self._log_path |
| 326 | |
| 327 | self.create_dir('%s/%s/' % steering_log) |
| 328 | |
| 329 | with open(steering_log, 'w') as out_file: |
| 330 | # Include the build and the test results. |
| 331 | steering_result = (self._flag_set, self._checksum, self._build_cost, |
| 332 | self._image, self._file_length, self._text_length, |
| 333 | self._exe_cost) |
| 334 | |
| 335 | # Write out the result in the comma-separated format (CSV). |
| 336 | out_file.write('%s,%s,%s,%s,%s,%s,%s\n' % steering_result) |
| 337 | |
| 338 | def __LogBuildCost(self): |
| 339 | """Log the build results for the task. |
| 340 | |
| 341 | The build results include the compilation time of the build, the result |
| 342 | image, the checksum, the file length and the text length of the image. |
| 343 | The file length of the image includes the length of the file of the image. |
| 344 | The text length only includes the length of the text section of the image. |
| 345 | """ |
| 346 | |
| 347 | build_log = '%s/%s/build.txt' % self._log_path |
| 348 | |
| 349 | self.__CreateDirectory(build_log) |
| 350 | |
| 351 | with open(build_log, 'w') as out_file: |
| 352 | build_result = (self._flag_set, self._build_cost, self._image, |
| 353 | self._checksum, self._file_length, self._text_length) |
| 354 | |
| 355 | # Write out the result in the comma-separated format (CSV). |
| 356 | out_file.write('%s,%s,%s,%s,%s,%s\n' % build_result) |
| 357 | |
| 358 | def __LogTestCost(self): |
| 359 | """Log the test results for the task. |
| 360 | |
| 361 | The test results include the runtime execution time of the test. |
| 362 | """ |
| 363 | |
| 364 | test_log = '%s/%s/build.txt' % self._log_path |
| 365 | |
| 366 | self.__CreateDirectory(test_log) |
| 367 | |
| 368 | with open(test_log, 'w') as out_file: |
| 369 | test_result = (self._flag_set, self._checksum, self._exe_cost) |
| 370 | |
| 371 | # Write out the result in the comma-separated format (CSV). |
| 372 | out_file.write('%s,%s,%s\n' % test_result) |