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