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"""Hill climbing unitest. 5 6Part of the Chrome build flags optimization. 7 8Test the best branching hill climbing algorithms, genetic algorithm and 9iterative elimination algorithm. 10""" 11 12__author__ = "yuhenglong@google.com (Yuheng Long)" 13 14import multiprocessing 15import random 16import sys 17import unittest 18 19import flags 20from flags import Flag 21from flags import FlagSet 22from genetic_algorithm import GAGeneration 23from genetic_algorithm import GATask 24from hill_climb_best_neighbor import HillClimbingBestBranch 25from iterative_elimination import IterativeEliminationFirstGeneration 26import pipeline_process 27from steering import Steering 28from task import BUILD_STAGE 29from task import Task 30from task import TEST_STAGE 31 32 33# The number of flags be tested. 34NUM_FLAGS = 5 35 36# The value range of the flags. 37FLAG_RANGES = 10 38 39# The following variables are meta data for the Genetic Algorithm. 40STOP_THRESHOLD = 20 41NUM_CHROMOSOMES = 10 42NUM_TRIALS = 20 43MUTATION_RATE = 0.03 44 45 46def _GenerateRandomRasks(specs): 47 """Generate a task that has random values. 48 49 Args: 50 specs: A list of spec from which the flag set is created. 51 52 Returns: 53 A set containing a task that has random values. 54 """ 55 56 flag_set = [] 57 58 for spec in specs: 59 numeric_flag_match = flags.Search(spec) 60 if numeric_flag_match: 61 # Numeric flags. 62 start = int(numeric_flag_match.group("start")) 63 end = int(numeric_flag_match.group("end")) 64 65 value = random.randint(start - 1, end - 1) 66 if value != start - 1: 67 # If the value falls in the range, this flag is enabled. 68 flag_set.append(Flag(spec, value)) 69 else: 70 # Boolean flags. 71 if random.randint(0, 1): 72 flag_set.append(Flag(spec)) 73 74 return set([Task(FlagSet(flag_set))]) 75 76 77def _GenerateAllFlagsTasks(specs): 78 """Generate a task that all the flags are enable. 79 80 All the boolean flags in the specs will be enabled and all the numeric flag 81 with have the largest legal value. 82 83 Args: 84 specs: A list of spec from which the flag set is created. 85 86 Returns: 87 A set containing a task that has all flags enabled. 88 """ 89 90 flag_set = [] 91 92 for spec in specs: 93 numeric_flag_match = flags.Search(spec) 94 95 if numeric_flag_match: 96 value = int(numeric_flag_match.group("end")) - 1 97 else: 98 value = -1 99 flag_set.append(Flag(spec, value)) 100 101 return set([Task(FlagSet(flag_set))]) 102 103 104def _GenerateNoFlagTask(): 105 return set([Task(FlagSet([]))]) 106 107 108def GenerateRandomGATasks(specs, num_tasks, num_trials): 109 """Generate a set of tasks for the Genetic Algorithm. 110 111 Args: 112 specs: A list of spec from which the flag set is created. 113 num_tasks: number of tasks that should be generated. 114 num_trials: the maximum number of tries should be attempted to generate the 115 set of tasks. 116 117 Returns: 118 A set of randomly generated tasks. 119 """ 120 121 tasks = set([]) 122 123 total_trials = 0 124 while len(tasks) < num_tasks and total_trials < num_trials: 125 new_flag = FlagSet( 126 [Flag(spec) for spec in specs if random.randint(0, 1)] 127 ) 128 new_task = GATask(new_flag) 129 130 if new_task in tasks: 131 total_trials += 1 132 else: 133 tasks.add(new_task) 134 total_trials = 0 135 136 return tasks 137 138 139def _GenerateInitialFlags(specs, spec): 140 """Generate the flag_set of a task in the flag elimination algorithm. 141 142 Set the value of all the flags to the largest value, except for the flag that 143 contains spec. 144 145 For example, if the specs are [-finline-limit=[1-1000], -fstrict-aliasing] and 146 the spec is -finline-limit=[1-1000], then the result is 147 [-finline-limit=[1-1000]:-finline-limit=998, 148 -fstrict-aliasing:-fstrict-aliasing] 149 150 Args: 151 specs: an array of specifications from which the result flag_set is created. 152 The flag_set contains one and only one flag that contain the specification 153 spec. 154 spec: The flag containing this spec should have a value that is smaller than 155 the highest value the flag can have. 156 157 Returns: 158 An array of flags, each of which contains one spec in specs. All the values 159 of the flags are the largest values in specs, expect the one that contains 160 spec. 161 """ 162 163 flag_set = [] 164 for other_spec in specs: 165 numeric_flag_match = flags.Search(other_spec) 166 # Found the spec in the array specs. 167 if other_spec == spec: 168 # Numeric flag will have a value that is smaller than the largest value 169 # and Boolean flag will be deleted. 170 if numeric_flag_match: 171 end = int(numeric_flag_match.group("end")) 172 flag_set.append(flags.Flag(other_spec, end - 2)) 173 174 continue 175 176 # other_spec != spec 177 if numeric_flag_match: 178 # numeric flag 179 end = int(numeric_flag_match.group("end")) 180 flag_set.append(flags.Flag(other_spec, end - 1)) 181 continue 182 183 # boolean flag 184 flag_set.append(flags.Flag(other_spec)) 185 186 return flag_set 187 188 189def _GenerateAllIterativeEliminationTasks(specs): 190 """Generate the initial tasks for the negative flag elimination algorithm. 191 192 Generate the base line task that turns on all the boolean flags and sets the 193 value to be the largest value for the numeric flag. 194 195 For example, if the specs are [-finline-limit=[1-1000], -fstrict-aliasing], 196 the base line is [-finline-limit=[1-1000]:-finline-limit=999, 197 -fstrict-aliasing:-fstrict-aliasing] 198 199 Generate a set of task, each turns off one of the flag or sets a value that is 200 smaller than the largest value for the flag. 201 202 Args: 203 specs: an array of specifications from which the result flag_set is created. 204 205 Returns: 206 An array containing one generation of the initial tasks for the negative 207 flag elimination algorithm. 208 """ 209 210 # The set of tasks to be generated. 211 results = set([]) 212 flag_set = [] 213 214 for spec in specs: 215 numeric_flag_match = flags.Search(spec) 216 if numeric_flag_match: 217 # Numeric flag. 218 end_value = int(numeric_flag_match.group("end")) 219 flag_set.append(flags.Flag(spec, end_value - 1)) 220 continue 221 222 # Boolean flag. 223 flag_set.append(flags.Flag(spec)) 224 225 # The base line task that set all the flags to their largest values. 226 parent_task = Task(flags.FlagSet(flag_set)) 227 results.add(parent_task) 228 229 for spec in specs: 230 results.add(Task(flags.FlagSet(_GenerateInitialFlags(specs, spec)))) 231 232 return [IterativeEliminationFirstGeneration(results, parent_task)] 233 234 235def _ComputeCost(cost_func, specs, flag_set): 236 """Compute the mock cost of the flag_set using the input cost function. 237 238 All the boolean flags in the specs will be enabled and all the numeric flag 239 with have the largest legal value. 240 241 Args: 242 cost_func: The cost function which is used to compute the mock cost of a 243 dictionary of flags. 244 specs: All the specs that are used in the algorithm. This is used to check 245 whether certain flag is disabled in the flag_set dictionary. 246 flag_set: a dictionary of the spec and flag pairs. 247 248 Returns: 249 The mock cost of the input dictionary of the flags. 250 """ 251 252 values = [] 253 254 for spec in specs: 255 # If a flag is enabled, its value is added. Otherwise a padding 0 is added. 256 values.append(flag_set[spec].GetValue() if spec in flag_set else 0) 257 258 # The cost function string can use the values array. 259 return eval(cost_func) 260 261 262def _GenerateTestFlags(num_flags, upper_bound, file_name): 263 """Generate a set of mock flags and write it to a configuration file. 264 265 Generate a set of mock flags 266 267 Args: 268 num_flags: Number of numeric flags to be generated. 269 upper_bound: The value of the upper bound of the range. 270 file_name: The configuration file name into which the mock flags are put. 271 """ 272 273 with open(file_name, "w") as output_file: 274 num_flags = int(num_flags) 275 upper_bound = int(upper_bound) 276 for i in range(num_flags): 277 output_file.write("%s=[1-%d]\n" % (i, upper_bound)) 278 279 280def _TestAlgorithm(cost_func, specs, generations, best_result): 281 """Test the best result the algorithm should return. 282 283 Set up the framework, run the input algorithm and verify the result. 284 285 Args: 286 cost_func: The cost function which is used to compute the mock cost of a 287 dictionary of flags. 288 specs: All the specs that are used in the algorithm. This is used to check 289 whether certain flag is disabled in the flag_set dictionary. 290 generations: The initial generations to be evaluated. 291 best_result: The expected best result of the algorithm. If best_result is 292 -1, the algorithm may or may not return the best value. Therefore, no 293 assertion will be inserted. 294 """ 295 296 # Set up the utilities to test the framework. 297 manager = multiprocessing.Manager() 298 input_queue = manager.Queue() 299 output_queue = manager.Queue() 300 pp_steer = multiprocessing.Process( 301 target=Steering, args=(set(), generations, output_queue, input_queue) 302 ) 303 pp_steer.start() 304 305 # The best result of the algorithm so far. 306 result = sys.maxint 307 308 while True: 309 task = input_queue.get() 310 311 # POISONPILL signal the ends of the algorithm. 312 if task == pipeline_process.POISONPILL: 313 break 314 315 task.SetResult(BUILD_STAGE, (0, 0, 0, 0, 0)) 316 317 # Compute the mock cost for the task. 318 task_result = _ComputeCost(cost_func, specs, task.GetFlags()) 319 task.SetResult(TEST_STAGE, task_result) 320 321 # If the mock result of the current task is the best so far, set this 322 # result to be the best result. 323 if task_result < result: 324 result = task_result 325 326 output_queue.put(task) 327 328 pp_steer.join() 329 330 # Only do this test when best_result is not -1. 331 if best_result != -1: 332 assert best_result == result 333 334 335class MockAlgorithmsTest(unittest.TestCase): 336 """This class mock tests different steering algorithms. 337 338 The steering algorithms are responsible for generating the next set of tasks 339 to run in each iteration. This class does a functional testing on the 340 algorithms. It mocks out the computation of the fitness function from the 341 build and test phases by letting the user define the fitness function. 342 """ 343 344 def _GenerateFlagSpecifications(self): 345 """Generate the testing specifications.""" 346 347 mock_test_file = "scale_mock_test" 348 _GenerateTestFlags(NUM_FLAGS, FLAG_RANGES, mock_test_file) 349 return flags.ReadConf(mock_test_file) 350 351 def testBestHillClimb(self): 352 """Test the best hill climb algorithm. 353 354 Test whether it finds the best results as expected. 355 """ 356 357 # Initiate the build/test command and the log directory. 358 Task.InitLogCommand(None, None, "output") 359 360 # Generate the testing specs. 361 specs = self._GenerateFlagSpecifications() 362 363 # Generate the initial generations for a test whose cost function is the 364 # summation of the values of all the flags. 365 generation_tasks = _GenerateAllFlagsTasks(specs) 366 generations = [HillClimbingBestBranch(generation_tasks, set([]), specs)] 367 368 # Test the algorithm. The cost function is the summation of all the values 369 # of all the flags. Therefore, the best value is supposed to be 0, i.e., 370 # when all the flags are disabled. 371 _TestAlgorithm("sum(values[0:len(values)])", specs, generations, 0) 372 373 # This test uses a cost function that is the negative of the previous cost 374 # function. Therefore, the best result should be found in task with all the 375 # flags enabled. 376 cost_function = "sys.maxint - sum(values[0:len(values)])" 377 all_flags = list(generation_tasks)[0].GetFlags() 378 cost = _ComputeCost(cost_function, specs, all_flags) 379 380 # Generate the initial generations. 381 generation_tasks = _GenerateNoFlagTask() 382 generations = [HillClimbingBestBranch(generation_tasks, set([]), specs)] 383 384 # Test the algorithm. The cost function is negative of the summation of all 385 # the values of all the flags. Therefore, the best value is supposed to be 386 # 0, i.e., when all the flags are disabled. 387 _TestAlgorithm(cost_function, specs, generations, cost) 388 389 def testGeneticAlgorithm(self): 390 """Test the Genetic Algorithm. 391 392 Do a functional testing here and see how well it scales. 393 """ 394 395 # Initiate the build/test command and the log directory. 396 Task.InitLogCommand(None, None, "output") 397 398 # Generate the testing specs. 399 specs = self._GenerateFlagSpecifications() 400 # Initiate the build/test command and the log directory. 401 GAGeneration.InitMetaData( 402 STOP_THRESHOLD, NUM_CHROMOSOMES, NUM_TRIALS, specs, MUTATION_RATE 403 ) 404 405 # Generate the initial generations. 406 generation_tasks = GenerateRandomGATasks( 407 specs, NUM_CHROMOSOMES, NUM_TRIALS 408 ) 409 generations = [GAGeneration(generation_tasks, set([]), 0)] 410 411 # Test the algorithm. 412 _TestAlgorithm("sum(values[0:len(values)])", specs, generations, -1) 413 cost_func = "sys.maxint - sum(values[0:len(values)])" 414 _TestAlgorithm(cost_func, specs, generations, -1) 415 416 def testIterativeElimination(self): 417 """Test the iterative elimination algorithm. 418 419 Test whether it finds the best results as expected. 420 """ 421 422 # Initiate the build/test command and the log directory. 423 Task.InitLogCommand(None, None, "output") 424 425 # Generate the testing specs. 426 specs = self._GenerateFlagSpecifications() 427 428 # Generate the initial generations. The generation contains the base line 429 # task that turns on all the flags and tasks that each turn off one of the 430 # flags. 431 generations = _GenerateAllIterativeEliminationTasks(specs) 432 433 # Test the algorithm. The cost function is the summation of all the values 434 # of all the flags. Therefore, the best value is supposed to be 0, i.e., 435 # when all the flags are disabled. 436 _TestAlgorithm("sum(values[0:len(values)])", specs, generations, 0) 437 438 # This test uses a cost function that is the negative of the previous cost 439 # function. Therefore, the best result should be found in task with all the 440 # flags enabled. 441 all_flags_tasks = _GenerateAllFlagsTasks(specs) 442 cost_function = "sys.maxint - sum(values[0:len(values)])" 443 # Compute the cost of the task that turns on all the flags. 444 all_flags = list(all_flags_tasks)[0].GetFlags() 445 cost = _ComputeCost(cost_function, specs, all_flags) 446 447 # Test the algorithm. The cost function is negative of the summation of all 448 # the values of all the flags. Therefore, the best value is supposed to be 449 # 0, i.e., when all the flags are disabled. 450 # The concrete type of the generation decides how the next generation will 451 # be generated. 452 _TestAlgorithm(cost_function, specs, generations, cost) 453 454 455if __name__ == "__main__": 456 unittest.main() 457