• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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