Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Generates test runner factory and tests for performance tests.""" | 5 """Generates test runner factory and tests for performance tests.""" |
| 6 | 6 |
| 7 import json | 7 import json |
| 8 import fnmatch | 8 import fnmatch |
| 9 import hashlib | |
| 9 import logging | 10 import logging |
| 10 import os | 11 import os |
| 11 import shutil | 12 import shutil |
| 12 | 13 |
| 13 from pylib import constants | 14 from pylib import constants |
| 14 from pylib import forwarder | 15 from pylib import forwarder |
| 15 from pylib.device import device_list | 16 from pylib.device import device_list |
| 16 from pylib.device import device_utils | 17 from pylib.device import device_utils |
| 17 from pylib.perf import test_runner | 18 from pylib.perf import test_runner |
| 18 from pylib.utils import test_environment | 19 from pylib.utils import test_environment |
| 19 | 20 |
| 20 | 21 |
| 21 def _GetAllDevices(): | |
| 22 devices_path = os.path.join(os.environ.get('CHROMIUM_OUT_DIR', 'out'), | |
| 23 device_list.LAST_DEVICES_FILENAME) | |
| 24 try: | |
| 25 devices = [device_utils.DeviceUtils(s) | |
| 26 for s in device_list.GetPersistentDeviceList(devices_path)] | |
| 27 except IOError as e: | |
| 28 logging.error('Unable to find %s [%s]', devices_path, e) | |
| 29 devices = device_utils.DeviceUtils.HealthyDevices() | |
| 30 return sorted(devices) | |
| 31 | |
| 32 | |
| 33 def _GetStepsDictFromSingleStep(test_options): | 22 def _GetStepsDictFromSingleStep(test_options): |
| 34 # Running a single command, build the tests structure. | 23 # Running a single command, build the tests structure. |
| 35 steps_dict = { | 24 steps_dict = { |
| 36 'version': 1, | 25 'version': 1, |
| 37 'steps': { | 26 'steps': { |
| 38 'single_step': { | 27 'single_step': { |
| 39 'device_affinity': 0, | 28 'device_affinity': 0, |
| 40 'cmd': test_options.single_step | 29 'cmd': test_options.single_step |
| 41 }, | 30 }, |
| 42 } | 31 } |
| 43 } | 32 } |
| 44 return steps_dict | 33 return steps_dict |
| 45 | 34 |
| 46 | 35 |
| 47 def _GetStepsDict(test_options): | 36 def _GetStepsDict(test_options): |
| 48 if test_options.single_step: | 37 if test_options.single_step: |
| 49 return _GetStepsDictFromSingleStep(test_options) | 38 return _GetStepsDictFromSingleStep(test_options) |
| 50 if test_options.steps: | 39 if test_options.steps: |
| 51 with file(test_options.steps, 'r') as f: | 40 with file(test_options.steps, 'r') as f: |
| 52 steps = json.load(f) | 41 steps = json.load(f) |
| 53 | 42 |
| 54 # Already using the new format. | 43 # Already using the new format. |
| 55 assert steps['version'] == 1 | 44 assert steps['version'] == 1 |
| 56 return steps | 45 return steps |
| 57 | 46 |
| 58 | 47 |
| 48 def DistributeAffinities(devices): | |
| 49 """Create a mapping from device to affinity using a round-robin hash scheme. | |
| 50 | |
| 51 Args: | |
| 52 devices: a list of device serials. | |
| 53 | |
| 54 Returns: a dictionary mapping each of the given device serials to a list | |
| 55 of affinities (numbers between 0 and NUM_DEVICE_AFFINITIES-1) it should | |
| 56 run. | |
| 57 | |
| 58 This function implements an algorithm to assign affinities to devices in a | |
| 59 relatively stable way while still maximizing the use of resources. | |
| 60 | |
| 61 We begin with all affinities unassigned. We cycle through the list of device | |
| 62 serials, and for each one we assign a yet-unassigned affinity. We choose the | |
| 63 affinity to assign by hashing the device serial, so that changes in the set of | |
| 64 devices will produce minimal changes in the assignments. We resolve | |
| 65 collisions by rehashing the hash that produced the collision (which will be | |
| 66 unique, since it is an md5 hash which we squish down). | |
| 67 | |
| 68 Another way to think of this: each device corresponds to a random series of | |
| 69 affinities, then we cycle through the devices giving each device its first | |
| 70 available choice. We expect the first few devices in the list to get their | |
| 71 first choice (so they will definitely be stable). Stability is only in danger | |
| 72 if a change to the device list produces a cascade of devices getting a | |
| 73 different choice than they had before. | |
| 74 | |
| 75 The probability that a later choice is directly disrupted by an earlier change | |
| 76 is 1/A, since there are A different assignments to be chosen and hash | |
| 77 functions distribute uniformly. However, there are transitive disruptions: a | |
| 78 change in the 1st choice might cause a change in the 2nd choice, which in turn | |
| 79 causes a change in the 3rd choice. Call the probability that a the n'th choice | |
| 80 is disrupted by changing i'th choice D(i,n) (assuming i < n). We have the | |
| 81 relation: | |
| 82 | |
| 83 D(i,n) = 1 - (1 - 1/A) product from j=i+1 to n-1 of { 1 - D(i,j) D(j,n) } | |
| 84 ^^^ ^^^^^^^^^^^^^ | |
| 85 Disrupted directly Transitive disruptions | |
| 86 | |
| 87 From numerical solutions with A=8, the worst case D(1,8) = 26% (the | |
| 88 probability of a device added lexicographically first disrupting the last | |
| 89 device in the cycle). The average probability of a choice being affected by a | |
| 90 change is 16%. | |
| 91 """ | |
| 92 if not devices: | |
| 93 return {} | |
| 94 devices = sorted(devices) | |
|
navabi
2015/06/04 22:30:11
You may want to not sort the devices. I know you w
luqui
2015/06/12 02:43:36
I ran some monte carlo simulations and this turned
| |
| 95 | |
| 96 device_cycle = [] | |
| 97 while len(device_cycle) < test_runner.NUM_DEVICE_AFFINITIES: | |
| 98 device_cycle.extend(devices) | |
| 99 device_cycle = device_cycle[:test_runner.NUM_DEVICE_AFFINITIES] | |
| 100 | |
| 101 affinity_to_device = {} | |
| 102 | |
| 103 # Hash and rehash each device ID until we get a unique affinity. | |
| 104 for device in device_cycle: | |
| 105 cur_hash = device | |
| 106 while True: | |
| 107 cur_hash = hashlib.md5(cur_hash).hexdigest() | |
| 108 affinity = int(int(cur_hash, 16) % test_runner.NUM_DEVICE_AFFINITIES) | |
| 109 if affinity not in affinity_to_device: | |
| 110 affinity_to_device[affinity] = device | |
| 111 break | |
| 112 | |
| 113 # Invert the dictionary we just built. | |
| 114 device_to_affinity = {} | |
| 115 for k, v in affinity_to_device.iteritems(): | |
| 116 if v not in device_to_affinity: | |
| 117 device_to_affinity[v] = [] | |
| 118 device_to_affinity[v].append(k) | |
| 119 | |
| 120 return device_to_affinity | |
| 121 | |
| 59 def Setup(test_options): | 122 def Setup(test_options): |
| 60 """Create and return the test runner factory and tests. | 123 """Create and return the test runner factory and tests. |
| 61 | 124 |
| 62 Args: | 125 Args: |
| 63 test_options: A PerformanceOptions object. | 126 test_options: A PerformanceOptions object. |
| 64 | 127 |
| 65 Returns: | 128 Returns: |
| 66 A tuple of (TestRunnerFactory, tests, devices). | 129 A tuple of (TestRunnerFactory, tests, devices). |
| 67 """ | 130 """ |
| 68 # TODO(bulach): remove this once the bot side lands. BUG=318369 | 131 # TODO(bulach): remove this once the bot side lands. BUG=318369 |
| 69 constants.SetBuildType('Release') | 132 constants.SetBuildType('Release') |
| 70 if os.path.exists(constants.PERF_OUTPUT_DIR): | 133 if os.path.exists(constants.PERF_OUTPUT_DIR): |
| 71 shutil.rmtree(constants.PERF_OUTPUT_DIR) | 134 shutil.rmtree(constants.PERF_OUTPUT_DIR) |
| 72 os.makedirs(constants.PERF_OUTPUT_DIR) | 135 os.makedirs(constants.PERF_OUTPUT_DIR) |
| 73 | 136 |
| 74 # Before running the tests, kill any leftover server. | 137 # Before running the tests, kill any leftover server. |
| 75 test_environment.CleanupLeftoverProcesses() | 138 test_environment.CleanupLeftoverProcesses() |
| 76 | 139 |
| 77 # We want to keep device affinity, so return all devices ever seen. | 140 all_devices = [d.adb.GetDeviceSerial() |
| 78 all_devices = _GetAllDevices() | 141 for d in device_utils.DeviceUtils.HealthyDevices()] |
| 142 affinity_map = DistributeAffinities(all_devices) | |
| 79 | 143 |
| 80 steps_dict = _GetStepsDict(test_options) | 144 steps_dict = _GetStepsDict(test_options) |
| 81 sorted_step_names = sorted(steps_dict['steps'].keys()) | 145 sorted_step_names = sorted(steps_dict['steps'].keys()) |
| 82 | 146 |
| 83 if test_options.test_filter: | 147 if test_options.test_filter: |
| 84 sorted_step_names = fnmatch.filter(sorted_step_names, | 148 sorted_step_names = fnmatch.filter(sorted_step_names, |
| 85 test_options.test_filter) | 149 test_options.test_filter) |
| 86 | 150 |
| 87 flaky_steps = [] | 151 flaky_steps = [] |
| 88 if test_options.flaky_steps: | 152 if test_options.flaky_steps: |
| 89 with file(test_options.flaky_steps, 'r') as f: | 153 with file(test_options.flaky_steps, 'r') as f: |
| 90 flaky_steps = json.load(f) | 154 flaky_steps = json.load(f) |
| 91 | 155 |
| 92 def TestRunnerFactory(device, shard_index): | 156 def TestRunnerFactory(device, _): |
| 157 affinities = affinity_map[device] | |
| 93 return test_runner.TestRunner( | 158 return test_runner.TestRunner( |
| 94 test_options, device, shard_index, len(all_devices), | 159 test_options, device, affinities, steps_dict, flaky_steps) |
| 95 steps_dict, flaky_steps) | |
| 96 | 160 |
| 97 return (TestRunnerFactory, sorted_step_names, all_devices) | 161 return (TestRunnerFactory, sorted_step_names, all_devices) |
| OLD | NEW |