OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
Ami GONE FROM CHROMIUM
2011/12/07 01:33:08
Ditto filename comment
DaleCurtis
2011/12/07 22:57:32
Done.
| |
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 | |
Ami GONE FROM CHROMIUM
2011/12/07 01:33:08
Move this to a public doc, or remove pointer from
DaleCurtis
2011/12/07 22:57:32
Removed it, relevant information is already includ
| |
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 os | |
25 import Queue | |
26 import subprocess | |
27 import sys | |
28 import threading | |
29 | |
30 import pyauto_media | |
31 import pyauto | |
32 import pyauto_paths | |
33 import pyauto_utils | |
34 | |
35 | |
36 # Settings for each network constraint. | |
37 _BANDWIDTH_SETTINGS_KBPS = {'None': 0, 'Low': 256, 'Medium': 2000, 'High': 5000} | |
38 _LATENCY_SETTINGS_MS = {'None': 0, 'Low': 43, 'Medium': 105, 'High': 180} | |
39 _PACKET_LOSS_SETTINGS_PERCENT = {'None': 0, 'Medium': 2, 'High': 5} | |
40 | |
41 # Test constraints are all possible combination of the above settings. Each | |
42 # tuple must be of the form (Bandwidth, Latency, Packet Loss). | |
43 _TEST_CONSTRAINTS = itertools.product( | |
44 _BANDWIDTH_SETTINGS_KBPS.values(), | |
45 _LATENCY_SETTINGS_MS.values(), | |
46 _PACKET_LOSS_SETTINGS_PERCENT.values()) | |
47 | |
48 _TEST_CONSTRAINT_NAMES = itertools.product( | |
49 _BANDWIDTH_SETTINGS_KBPS.keys(), | |
50 _LATENCY_SETTINGS_MS.keys(), | |
51 _PACKET_LOSS_SETTINGS_PERCENT.keys()) | |
52 | |
53 # HTML test path; relative to src/chrome/test/data. Loads a test video and | |
54 # records metrics in JavaScript. | |
55 _TEST_HTML_PATH = os.path.join('media', 'html', 'media_cn.html') | |
56 | |
57 # Number of threads to use during testing. | |
58 # TODO(dalecurtis): Should be set on the command line. | |
Ami GONE FROM CHROMIUM
2011/12/07 01:33:08
Why?
DaleCurtis
2011/12/07 22:57:32
Good point. I was looking at this as more of a uti
| |
59 _TEST_THREADS = 3 | |
60 | |
61 # File name of video to collect metrics for. | |
62 # TODO(dalecurtis): Should be set on the command line. | |
63 _TEST_VIDEO = 'roller.webm' | |
64 | |
65 # Path to CNS executable relative to source root. | |
66 _CNS_PATH = os.path.join( | |
67 'media', 'tools', 'constrained_network_server', 'cns.py') | |
68 | |
69 # Port to start the CNS on. | |
70 _CNS_PORT = 9000 | |
Ami GONE FROM CHROMIUM
2011/12/07 01:33:08
Is there a way to get a known-unused port on the b
DaleCurtis
2011/12/07 22:57:32
Short of trying to open a socket on each port and
| |
71 | |
72 # Base CNS URL, only requires & separated parameter names appended. | |
73 _CNS_BASE_URL = 'http://127.0.0.1:%d/ServeConstrained?' % _CNS_PORT | |
74 | |
75 | |
76 class TestWorker(threading.Thread): | |
77 """Worker thread. For each queue entry: opens tab, runs test, closes tab.""" | |
78 | |
79 # Atomic, monotonically increasing task identifier. Used to ID tabs. | |
80 _task_id = itertools.count() | |
81 | |
82 def __init__(self, pyauto_test, tasks, automation_lock, url): | |
83 """Sets up TestWorker class variables. | |
84 | |
85 Args: | |
86 pyauto_test: Reference to a pyauto.PyUITest instance. | |
87 tasks: Queue containing (settings, name) tuples. | |
88 automation_lock: Global automation lock for pyauto calls. | |
89 url: File URL to HTML/JavaScript test code. | |
90 """ | |
91 threading.Thread.__init__(self) | |
92 self._tasks = tasks | |
93 self._automation_lock = automation_lock | |
94 self._pyauto = pyauto_test | |
95 self._url = url | |
96 self.start() | |
97 | |
98 def _FindTab(self, url): | |
99 """Returns the tab index for the tab belonging to this url.""" | |
100 for tab in self._pyauto.GetBrowserInfo()['windows'][0]['tabs']: | |
Ami GONE FROM CHROMIUM
2011/12/07 01:33:08
No lock?
DaleCurtis
2011/12/07 22:57:32
Already locked in _HaveMetrics() I'll add a commen
| |
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.{_ept,_ttp} < 0 pre-run.""" | |
Ami GONE FROM CHROMIUM
2011/12/07 01:33:08
Why not set to -1 in the ctor instead of "pre-run"
DaleCurtis
2011/12/07 22:57:32
It needs to be set for each call. I suspect there
| |
106 with self._automation_lock: | |
107 tab = self._FindTab(unique_url) | |
108 | |
109 if self._ept < 0: | |
110 self._ept = self._pyauto.GetDOMValue('extra_play_time', tab_index=tab) | |
111 if self._ttp < 0: | |
112 self._ttp = self._pyauto.GetDOMValue('time_to_playback', tab_index=tab) | |
113 return self._ept >= 0 and self._ttp >= 0 | |
114 | |
115 def run(self): | |
116 """Opens tab, starts HTML test, and records metrics for each queue entry. | |
117 | |
118 No exception handling is done to make sure the main thread exits properly | |
119 during Chrome crashes or other failures. Doing otherwise has the potential | |
120 to leave the CNS server running in the background. | |
121 | |
122 For a clean shutdown, put the magic exit value (None, None) in the queue. | |
123 """ | |
124 while True: | |
125 settings, name = self._tasks.get() | |
126 | |
127 # Check for magic exit values. | |
128 if (settings, name) == (None, None): | |
Ami GONE FROM CHROMIUM
2011/12/07 01:33:08
So one worker thread gets the magic and exits, but
DaleCurtis
2011/12/07 22:57:32
A magic exit pair is added to the Queue for each t
| |
129 break | |
130 | |
131 # Build video source URL. Values <= 0 mean the setting is disabled. | |
132 video_url = [_CNS_BASE_URL, 'f=' + _TEST_VIDEO] | |
133 if settings[0] > 0: | |
134 video_url.append('bandwidth=%d' % settings[0]) | |
135 if settings[1] > 0: | |
136 video_url.append('latency=%d' % settings[1]) | |
137 if settings[2] > 0: | |
138 video_url.append('loss=%d' % settings[2]) | |
139 video_url = '&'.join(video_url) | |
140 | |
141 # Make the test URL unique so we can figure out our tab index later. | |
142 unique_url = '%s?%d' % (self._url, TestWorker._task_id.next()) | |
143 | |
144 # Start the test! | |
145 with self._automation_lock: | |
146 self._pyauto.AppendTab(pyauto.GURL(unique_url)) | |
147 self._pyauto.CallJavascriptFunc( | |
148 'startTest', [video_url], tab_index=self._FindTab(unique_url)) | |
149 | |
150 # Wait until the necessary metrics have been collected. Okay to not lock | |
151 # here since pyauto.WaitUntil doesn't call into Chrome. | |
152 self._ept = self._ttp = -1 | |
153 self._pyauto.WaitUntil( | |
154 self._HaveMetrics, args=[unique_url], retry_sleep=2) | |
155 | |
156 # Record results. | |
157 # TODO(dalecurtis): Support reference builds. | |
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 = [sys.executable, os.path.join(pyauto_paths.GetSourceDir(), _CNS_PATH), | |
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 pyauto.PyUITest.tearDown(self) | |
195 self.Kill(self._server_pid) | |
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. | |
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)) | |
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 |