OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Records metrics on playing media under constrained network conditions. |
| 7 |
| 8 Spins up a Constrained Network Server (CNS) and runs through a test matrix of |
| 9 bandwidth, latency, and packet loss settings. Each run records a |
| 10 time-to-playback (TTP) and extra-play-percentage (EPP) metric in a format |
| 11 consumable by the Chromium perf bots. |
| 12 |
| 13 Since even a small number of different settings yields a large test matrix, the |
| 14 design is threaded... however PyAuto is not, so a global lock is used when calls |
| 15 into PyAuto are necessary. The number of threads can be set by _TEST_THREADS. |
| 16 |
| 17 The CNS code is located under: <root>/src/media/tools/constrained_network_server |
| 18 """ |
| 19 |
| 20 import itertools |
| 21 import os |
| 22 import Queue |
| 23 import subprocess |
| 24 import sys |
| 25 import threading |
| 26 |
| 27 import pyauto_media |
| 28 import pyauto |
| 29 import pyauto_paths |
| 30 import pyauto_utils |
| 31 |
| 32 |
| 33 # Settings for each network constraint. |
| 34 _BANDWIDTH_SETTINGS_KBPS = {'None': 0, 'Low': 256, 'Medium': 2000, 'High': 5000} |
| 35 _LATENCY_SETTINGS_MS = {'None': 0, 'Low': 43, 'Medium': 105, 'High': 180} |
| 36 _PACKET_LOSS_SETTINGS_PERCENT = {'None': 0, 'Medium': 2, 'High': 5} |
| 37 |
| 38 # Test constraints are all possible combination of the above settings. Each |
| 39 # tuple must be of the form (Bandwidth, Latency, Packet Loss). |
| 40 _TEST_CONSTRAINTS = itertools.product( |
| 41 _BANDWIDTH_SETTINGS_KBPS.values(), |
| 42 _LATENCY_SETTINGS_MS.values(), |
| 43 _PACKET_LOSS_SETTINGS_PERCENT.values()) |
| 44 |
| 45 _TEST_CONSTRAINT_NAMES = itertools.product( |
| 46 _BANDWIDTH_SETTINGS_KBPS.keys(), |
| 47 _LATENCY_SETTINGS_MS.keys(), |
| 48 _PACKET_LOSS_SETTINGS_PERCENT.keys()) |
| 49 |
| 50 # HTML test path; relative to src/chrome/test/data. Loads a test video and |
| 51 # records metrics in JavaScript. |
| 52 _TEST_HTML_PATH = os.path.join( |
| 53 'media', 'html', 'media_constrained_network.html') |
| 54 |
| 55 # Number of threads to use during testing. |
| 56 _TEST_THREADS = 3 |
| 57 |
| 58 # File name of video to collect metrics for. |
| 59 # TODO(dalecurtis): Should be set on the command line. |
| 60 _TEST_VIDEO = 'roller.webm' |
| 61 |
| 62 # Path to CNS executable relative to source root. |
| 63 _CNS_PATH = os.path.join( |
| 64 'media', 'tools', 'constrained_network_server', 'cns.py') |
| 65 |
| 66 # Port to start the CNS on. |
| 67 _CNS_PORT = 9000 |
| 68 |
| 69 # Base CNS URL, only requires & separated parameter names appended. |
| 70 _CNS_BASE_URL = 'http://127.0.0.1:%d/ServeConstrained?' % _CNS_PORT |
| 71 |
| 72 |
| 73 class TestWorker(threading.Thread): |
| 74 """Worker thread. For each queue entry: opens tab, runs test, closes tab.""" |
| 75 |
| 76 # Atomic, monotonically increasing task identifier. Used to ID tabs. |
| 77 _task_id = itertools.count() |
| 78 |
| 79 def __init__(self, pyauto_test, tasks, automation_lock, url): |
| 80 """Sets up TestWorker class variables. |
| 81 |
| 82 Args: |
| 83 pyauto_test: Reference to a pyauto.PyUITest instance. |
| 84 tasks: Queue containing (settings, name) tuples. |
| 85 automation_lock: Global automation lock for pyauto calls. |
| 86 url: File URL to HTML/JavaScript test code. |
| 87 """ |
| 88 threading.Thread.__init__(self) |
| 89 self._tasks = tasks |
| 90 self._automation_lock = automation_lock |
| 91 self._pyauto = pyauto_test |
| 92 self._url = url |
| 93 self.start() |
| 94 |
| 95 def _FindTabLocked(self, url): |
| 96 """Returns the tab index for the tab belonging to this url. |
| 97 |
| 98 self._automation_lock must be owned by caller. |
| 99 """ |
| 100 for tab in self._pyauto.GetBrowserInfo()['windows'][0]['tabs']: |
| 101 if tab['url'] == url: |
| 102 return tab['index'] |
| 103 |
| 104 def _HaveMetrics(self, unique_url): |
| 105 """Returns true if metrics are ready. Set self.{_epp,_ttp} < 0 pre-run.""" |
| 106 with self._automation_lock: |
| 107 tab = self._FindTabLocked(unique_url) |
| 108 |
| 109 if self._epp < 0: |
| 110 self._epp = self._pyauto.GetDOMValue( |
| 111 'extra_play_percentage', tab_index=tab) |
| 112 if self._ttp < 0: |
| 113 self._ttp = self._pyauto.GetDOMValue('time_to_playback', tab_index=tab) |
| 114 return self._epp >= 0 and self._ttp >= 0 |
| 115 |
| 116 def run(self): |
| 117 """Opens tab, starts HTML test, and records metrics for each queue entry. |
| 118 |
| 119 No exception handling is done to make sure the main thread exits properly |
| 120 during Chrome crashes or other failures. Doing otherwise has the potential |
| 121 to leave the CNS server running in the background. |
| 122 |
| 123 For a clean shutdown, put the magic exit value (None, None) in the queue. |
| 124 """ |
| 125 while True: |
| 126 settings, name = self._tasks.get() |
| 127 |
| 128 # Check for magic exit values. |
| 129 if (settings, name) == (None, None): |
| 130 break |
| 131 |
| 132 # Build video source URL. Values <= 0 mean the setting is disabled. |
| 133 video_url = [_CNS_BASE_URL, 'f=' + _TEST_VIDEO] |
| 134 if settings[0] > 0: |
| 135 video_url.append('bandwidth=%d' % settings[0]) |
| 136 if settings[1] > 0: |
| 137 video_url.append('latency=%d' % settings[1]) |
| 138 if settings[2] > 0: |
| 139 video_url.append('loss=%d' % settings[2]) |
| 140 video_url = '&'.join(video_url) |
| 141 |
| 142 # Make the test URL unique so we can figure out our tab index later. |
| 143 unique_url = '%s?%d' % (self._url, TestWorker._task_id.next()) |
| 144 |
| 145 # Start the test! |
| 146 with self._automation_lock: |
| 147 self._pyauto.AppendTab(pyauto.GURL(unique_url)) |
| 148 self._pyauto.CallJavascriptFunc( |
| 149 'startTest', [video_url], tab_index=self._FindTabLocked(unique_url)) |
| 150 |
| 151 # Wait until the necessary metrics have been collected. Okay to not lock |
| 152 # here since pyauto.WaitUntil doesn't call into Chrome. |
| 153 self._epp = self._ttp = -1 |
| 154 self._pyauto.WaitUntil( |
| 155 self._HaveMetrics, args=[unique_url], retry_sleep=2) |
| 156 |
| 157 # Record results. |
| 158 # TODO(dalecurtis): Support reference builds. |
| 159 series_name = ''.join(name) |
| 160 pyauto_utils.PrintPerfResult('epp', series_name, self._epp, '%') |
| 161 pyauto_utils.PrintPerfResult('ttp', series_name, self._ttp, 'ms') |
| 162 |
| 163 # Close the tab. |
| 164 with self._automation_lock: |
| 165 self._pyauto.GetBrowserWindow(0).GetTab( |
| 166 self._FindTabLocked(unique_url)).Close(True) |
| 167 |
| 168 # TODO(dalecurtis): Check results for regressions. |
| 169 self._tasks.task_done() |
| 170 |
| 171 |
| 172 class MediaConstrainedNetworkPerfTest(pyauto.PyUITest): |
| 173 """PyAuto test container. See file doc string for more information.""" |
| 174 |
| 175 def setUp(self): |
| 176 """Starts the Constrained Network Server (CNS).""" |
| 177 cmd = [sys.executable, os.path.join(pyauto_paths.GetSourceDir(), _CNS_PATH), |
| 178 '--port', str(_CNS_PORT), |
| 179 '--interface', 'lo', |
| 180 '--www-root', os.path.join( |
| 181 self.DataDir(), 'pyauto_private', 'media')] |
| 182 process = subprocess.Popen(cmd, stderr=subprocess.PIPE) |
| 183 |
| 184 # Wait for server to start up. |
| 185 line = True |
| 186 while line: |
| 187 line = process.stderr.readline() |
| 188 if 'STARTED' in line: |
| 189 self._server_pid = process.pid |
| 190 pyauto.PyUITest.setUp(self) |
| 191 return |
| 192 self.fail('Failed to start CNS.') |
| 193 |
| 194 def tearDown(self): |
| 195 """Stops the Constrained Network Server (CNS).""" |
| 196 pyauto.PyUITest.tearDown(self) |
| 197 self.Kill(self._server_pid) |
| 198 |
| 199 def testConstrainedNetworkPerf(self): |
| 200 """Starts CNS, spins up worker threads to run through _TEST_CONSTRAINTS.""" |
| 201 # Convert relative test path into an absolute path. |
| 202 test_url = self.GetFileURLForDataPath(_TEST_HTML_PATH) |
| 203 |
| 204 # PyAuto doesn't support threads, so we synchronize all automation calls. |
| 205 automation_lock = threading.Lock() |
| 206 |
| 207 # Spin up worker threads. |
| 208 tasks = Queue.Queue() |
| 209 threads = [] |
| 210 for _ in xrange(_TEST_THREADS): |
| 211 threads.append(TestWorker(self, tasks, automation_lock, test_url)) |
| 212 |
| 213 for settings, name in zip(_TEST_CONSTRAINTS, _TEST_CONSTRAINT_NAMES): |
| 214 tasks.put((settings, name)) |
| 215 |
| 216 # Add shutdown magic to end of queue. |
| 217 for thread in threads: |
| 218 tasks.put((None, None)) |
| 219 |
| 220 # Wait for threads to exit, gracefully or otherwise. |
| 221 for thread in threads: |
| 222 thread.join() |
| 223 |
| 224 |
| 225 if __name__ == '__main__': |
| 226 # TODO(dalecurtis): Process command line parameters here. |
| 227 pyauto_media.Main() |
OLD | NEW |