testing_batch.py revision f2a3ef46f75d2196a93d3ed27f4d1fcf22b54fbe
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
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# The number of flags be tested.
33NUM_FLAGS = 5
34
35# The value range of the flags.
36FLAG_RANGES = 10
37
38# The following variables are meta data for the Genetic Algorithm.
39STOP_THRESHOLD = 20
40NUM_CHROMOSOMES = 10
41NUM_TRIALS = 20
42MUTATION_RATE = 0.03
43
44
45def _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
76def _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
103def _GenerateNoFlagTask():
104  return set([Task(FlagSet([]))])
105
106
107def 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
136def _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
186def _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
232def _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
259def _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
277def _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
332class 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
449if __name__ == '__main__':
450  unittest.main()
451