Chromium Code Reviews| 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-time (EPT) metric in a format consumable | |
| 11 by the Chromium perf bots. | |
| 12 | |
| 13 See design document "Testing Video Performance Over Constrained Networks" for | |
| 14 more information -- http://goo.gl/EdAWU | |
| 15 | |
| 16 Since even a small number of different settings yields a large test matrix, the | |
| 17 design is threaded... however PyAuto is not, so a global lock is used when calls | |
| 18 into PyAuto are necessary. The number of threads can be set by _TEST_THREADS. | |
| 19 | |
| 20 The CNS code is located under: <root>/src/media/tools/constrained_network_server | |
| 21 """ | |
| 22 | |
| 23 import itertools | |
| 24 import logging | |
| 25 import os | |
| 26 import Queue | |
| 27 import signal | |
| 28 import subprocess | |
| 29 import threading | |
| 30 | |
| 31 import pyauto_media | |
| 32 import pyauto | |
| 33 import pyauto_paths | |
| 34 import pyauto_utils | |
| 35 | |
| 36 | |
| 37 # Settings for each network constraint. | |
| 38 _BANDWIDTH_SETTINGS_KBPS = {'Off': 0, 'Low': 256, 'Medium': 2000, 'High': 5000} | |
| 39 _LATENCY_SETTINGS_MS = {'Off': 0, 'Low': 43, 'Medium': 105, 'High': 180} | |
| 40 _PACKET_LOSS_SETTINGS_PERCENT = {'Off': 0, 'Medium': 2, 'High': 5} | |
| 41 | |
| 42 # Test constraints are all possible combination of the above settings. Each | |
|
Nirnimesh
2011/12/06 22:15:05
Leave 2 blank spaces after every period, ie before
DaleCurtis
2011/12/06 23:42:54
Ugh, if you insist. In protest, I'd like to point
Nirnimesh
2011/12/06 23:58:36
I don't like it either. All my initial pyauto code
| |
| 43 # tuple must be of the form (Bandwidth, Latency, Packet Loss). | |
|
Nirnimesh
2011/12/06 22:15:05
s/tuple/Tuple
DaleCurtis
2011/12/06 23:42:54
Tuple is not a proper noun and this is a continuat
| |
| 44 _TEST_CONSTRAINTS = itertools.product( | |
| 45 _BANDWIDTH_SETTINGS_KBPS.values(), | |
| 46 _LATENCY_SETTINGS_MS.values(), | |
| 47 _PACKET_LOSS_SETTINGS_PERCENT.values()) | |
| 48 | |
| 49 _TEST_CONSTRAINT_NAMES = itertools.product( | |
| 50 _BANDWIDTH_SETTINGS_KBPS.keys(), | |
| 51 _LATENCY_SETTINGS_MS.keys(), | |
| 52 _PACKET_LOSS_SETTINGS_PERCENT.keys()) | |
| 53 | |
| 54 # HTML test path; relative to src/chrome/test/data. Loads a test video and | |
| 55 # records metrics in JavaScript. | |
| 56 _TEST_HTML_PATH = os.path.join('media', 'html', 'media_cn.html') | |
| 57 | |
| 58 # Number of threads to use during testing. TODO(dalecurtis): Should be set on | |
|
Nirnimesh
2011/12/06 22:15:05
move TODO to next line
DaleCurtis
2011/12/06 23:42:54
Done.
| |
| 59 # the command line. | |
| 60 _TEST_THREADS = 3 | |
| 61 | |
| 62 # File name of video to collect metrics for. TODO(dalecurtis): Should be set on | |
|
Nirnimesh
2011/12/06 22:15:05
move TODO to next line
DaleCurtis
2011/12/06 23:42:54
Done.
| |
| 63 # the command line. | |
| 64 _TEST_VIDEO = 'dancing.webm' | |
| 65 | |
| 66 # Path to CNS executable relative to source root. | |
| 67 _CNS_PATH = os.path.join( | |
| 68 'media', 'tools', 'constrained_network_server', 'cns.py') | |
| 69 | |
| 70 # Port to start the CNS on. | |
| 71 _CNS_PORT = 9000 | |
| 72 | |
| 73 # Base CNS URL, only requires & separated parameter names appended. | |
| 74 _CNS_BASE_URL = 'http://127.0.0.1:%d/ServeConstrained?' % _CNS_PORT | |
| 75 | |
| 76 | |
| 77 class TestWorker(threading.Thread): | |
| 78 """Worker thread. For each queue entry: opens tab, runs test, closes tab.""" | |
| 79 | |
| 80 # Atomic, monotonically increasing task identifier. Used to ID tabs. | |
| 81 _task_id = itertools.count() | |
| 82 | |
| 83 def __init__(self, pyauto_test, tasks, automation_lock, url): | |
| 84 """Sets up TestWorker class variables. | |
| 85 | |
| 86 Args: | |
| 87 pyauto_test: Reference to a pyauto.PyUITest instance. | |
| 88 tasks: Queue containing (settings, name) tuples. | |
| 89 automation_lock: Global automation lock for pyauto calls. | |
| 90 url: File URL to HTML/JavaScript test code. | |
| 91 """ | |
| 92 threading.Thread.__init__(self) | |
| 93 self._tasks = tasks | |
| 94 self._automation_lock = automation_lock | |
| 95 self._pyauto = pyauto_test | |
| 96 self._url = url | |
| 97 self.start() | |
| 98 | |
| 99 def _FindTab(self, url): | |
| 100 """Returns the tab index for the tab belonging to this thread.""" | |
|
Nirnimesh
2011/12/06 22:15:05
s/thread/url/ ?
DaleCurtis
2011/12/06 23:42:54
Done.
| |
| 101 for tab in self._pyauto.GetBrowserInfo()['windows'][0]['tabs']: | |
| 102 if tab['url'] == url: | |
| 103 return tab['index'] | |
| 104 | |
| 105 def _HaveMetrics(self, unique_url): | |
| 106 """Returns if metrics are ready or not. Set self.{_ept,_ttp} < 0 pre-run.""" | |
| 107 with self._automation_lock: | |
| 108 tab = self._FindTab(unique_url) | |
| 109 | |
| 110 if self._ept < 0: | |
| 111 self._ept = self._pyauto.GetDOMValue('extra_play_time', tab_index=tab) | |
| 112 if self._ttp < 0: | |
| 113 self._ttp = self._pyauto.GetDOMValue('time_to_playback', tab_index=tab) | |
| 114 return self._ept >= 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 # Listen for magic exit values. | |
|
Nirnimesh
2011/12/06 22:15:05
s/Listen/Check/
DaleCurtis
2011/12/06 23:42:54
Done.
| |
| 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. | |
|
Nirnimesh
2011/12/06 22:15:05
why not save the tab index?
DaleCurtis
2011/12/06 23:42:54
When we talked via chat you said the tab_index cha
Nirnimesh
2011/12/06 23:58:36
I see. Yes, that's correct.
To that end, why do y
DaleCurtis
2011/12/07 01:30:05
I could probably get away with not doing so, but w
| |
| 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._FindTab(unique_url)) | |
| 150 | |
| 151 # Wait until the necessary metrics have been collected. Okay to not lock | |
|
Nirnimesh
2011/12/06 22:15:05
2 spaces after .
DaleCurtis
2011/12/06 23:42:54
Done.
| |
| 152 # here since pyauto.WaitUntil doesn't call into Chrome. | |
|
Nirnimesh
2011/12/06 22:15:05
but _HaveMetrics calls into chrome..
DaleCurtis
2011/12/06 23:42:54
...and makes those calls with the lock.
| |
| 153 self._ept = self._ttp = -1 | |
| 154 self._pyauto.WaitUntil( | |
| 155 self._HaveMetrics, args=[unique_url], retry_sleep=2) | |
| 156 | |
| 157 # Record results. TODO(dalecurtis): Support reference builds. | |
|
Nirnimesh
2011/12/06 22:15:05
move todo to next line
DaleCurtis
2011/12/06 23:42:54
Done.
| |
| 158 series_name = ''.join(name) | |
| 159 pyauto_utils.PrintPerfResult('ept', series_name, self._ept, '%') | |
| 160 pyauto_utils.PrintPerfResult('ttp', series_name, self._ttp, 'ms') | |
| 161 | |
| 162 # Close the tab. | |
| 163 with self._automation_lock: | |
| 164 self._pyauto.GetBrowserWindow(0).GetTab( | |
| 165 self._FindTab(unique_url)).Close(True) | |
| 166 | |
| 167 # TODO(dalecurtis): Check results for regressions. | |
| 168 self._tasks.task_done() | |
| 169 | |
| 170 | |
| 171 class MediaConstrainedNetworkPerfTest(pyauto.PyUITest): | |
| 172 """PyAuto test container. See file doc string for more information.""" | |
| 173 | |
| 174 def setUp(self): | |
| 175 """Starts the Constrained Network Server (CNS).""" | |
| 176 cmd = ['python', os.path.join(pyauto_paths.GetSourceDir(), _CNS_PATH), | |
|
Nirnimesh
2011/12/06 22:15:05
s/python/sys.executable
DaleCurtis
2011/12/06 23:42:54
Done.
| |
| 177 '--port', str(_CNS_PORT), | |
| 178 '--interface', 'lo', | |
| 179 '--www-root', os.path.join(self.DataDir(), 'media')] | |
| 180 process = subprocess.Popen(cmd, stderr=subprocess.PIPE) | |
| 181 | |
| 182 # Wait for server to start up. | |
| 183 line = True | |
| 184 while line: | |
| 185 line = process.stderr.readline() | |
| 186 if 'STARTED' in line: | |
| 187 self._server_pid = process.pid | |
| 188 pyauto.PyUITest.setUp(self) | |
| 189 return | |
| 190 self.fail('Failed to start CNS.') | |
| 191 | |
| 192 def tearDown(self): | |
| 193 """Stops the Constrained Network Server (CNS).""" | |
| 194 self.Kill(self._server_pid) | |
| 195 pyauto.PyUITest.tearDown(self) | |
|
Nirnimesh
2011/12/06 22:15:05
destruct in the opposite order.
ie tearDown before
DaleCurtis
2011/12/06 23:42:54
Done.
| |
| 196 | |
| 197 def testConstrainedNetworkPerf(self): | |
| 198 """Starts CNS, spins up worker threads to run through _TEST_CONSTRAINTS.""" | |
| 199 # Convert relative test path into an absolute path. | |
| 200 test_url = self.GetFileURLForDataPath(_TEST_HTML_PATH) | |
| 201 | |
| 202 # PyAuto doesn't support threads, so we synchronize all automation calls. | |
|
Nirnimesh
2011/12/06 22:15:05
s/synchronize/guard around/ ?
DaleCurtis
2011/12/06 23:42:54
Synchronize works fine: http://en.wikipedia.org/wi
| |
| 203 automation_lock = threading.Lock() | |
| 204 | |
| 205 # Spin up worker threads. | |
| 206 tasks = Queue.Queue() | |
| 207 threads = [] | |
| 208 for _ in xrange(_TEST_THREADS): | |
| 209 threads.append(TestWorker(self, tasks, automation_lock, test_url)) | |
|
Nirnimesh
2011/12/06 22:15:05
Won't having multiple threads affect the numbers y
DaleCurtis
2011/12/06 23:42:54
Not in the limited testing I've done thus far. We'
| |
| 210 | |
| 211 for settings, name in zip(_TEST_CONSTRAINTS, _TEST_CONSTRAINT_NAMES): | |
| 212 tasks.put((settings, name)) | |
| 213 | |
| 214 # Add shutdown magic to end of queue. | |
| 215 for thread in threads: | |
| 216 tasks.put((None, None)) | |
| 217 | |
| 218 # Wait for threads to exit, gracefully or otherwise. | |
| 219 for thread in threads: | |
| 220 thread.join() | |
| 221 | |
| 222 | |
| 223 if __name__ == '__main__': | |
| 224 # TODO(dalecurtis): Process command line parameters here. | |
| 225 pyauto_media.Main() | |
| OLD | NEW |