diff options
author | shadi@chromium.org <shadi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-20 01:46:37 +0000 |
---|---|---|
committer | shadi@chromium.org <shadi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-20 01:46:37 +0000 |
commit | 8bc584e620bac5f0119897fa87160da99f5ff256 (patch) | |
tree | 961810fcd6cbf06e65c92684b9b4a9b7f4ddac40 | |
parent | 8a9e1a19230ab3711d141d4b969db81aa4d10ca5 (diff) | |
download | chromium_src-8bc584e620bac5f0119897fa87160da99f5ff256.zip chromium_src-8bc584e620bac5f0119897fa87160da99f5ff256.tar.gz chromium_src-8bc584e620bac5f0119897fa87160da99f5ff256.tar.bz2 |
Merging CNS server with scripts.
Merging the constrained network scripts (http://crrev.com/114355) with the constrained network server (http://crrev.com/110458).
Some changes made to the CNS scripts to support local server setup.
BUG=104242
TEST=Unit tests and ran locally
Review URL: http://codereview.chromium.org/8856001
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@115062 0039d316-1c4b-4281-b951-d872f2087c98
7 files changed, 156 insertions, 50 deletions
diff --git a/chrome/test/data/media/html/media_constrained_network.html b/chrome/test/data/media/html/media_constrained_network.html index c20f772..37e56a9 100644 --- a/chrome/test/data/media/html/media_constrained_network.html +++ b/chrome/test/data/media/html/media_constrained_network.html @@ -6,7 +6,7 @@ </head> <body> - <video/> + <video controls/> </body> <script type="text/javascript"> @@ -33,7 +33,7 @@ playTime = new Date().getTime() - startTime; durMs = video.duration * 1000; - extra_play_percentage = Math.max(0, (playTime - durMs) / durMs) + extra_play_percentage = Math.max(0, (playTime - durMs) * 100 / durMs) }, false); // Called by the PyAuto controller to initiate testing. diff --git a/chrome/test/functional/media/media_constrained_network_perf.py b/chrome/test/functional/media/media_constrained_network_perf.py index d6dc09e..83a4583 100755 --- a/chrome/test/functional/media/media_constrained_network_perf.py +++ b/chrome/test/functional/media/media_constrained_network_perf.py @@ -55,9 +55,11 @@ _TEST_HTML_PATH = os.path.join( # Number of threads to use during testing. _TEST_THREADS = 3 -# File name of video to collect metrics for. +# File name of video to collect metrics for and its duration (used for timeout). # TODO(dalecurtis): Should be set on the command line. _TEST_VIDEO = 'roller.webm' +_TEST_VIDEO_DURATION_SEC = 28.53 + # Path to CNS executable relative to source root. _CNS_PATH = os.path.join( @@ -107,10 +109,11 @@ class TestWorker(threading.Thread): tab = self._FindTabLocked(unique_url) if self._epp < 0: - self._epp = self._pyauto.GetDOMValue( - 'extra_play_percentage', tab_index=tab) + self._epp = int(self._pyauto.GetDOMValue('extra_play_percentage', + tab_index=tab)) if self._ttp < 0: - self._ttp = self._pyauto.GetDOMValue('time_to_playback', tab_index=tab) + self._ttp = int(self._pyauto.GetDOMValue('time_to_playback', + tab_index=tab)) return self._epp >= 0 and self._ttp >= 0 def run(self): @@ -152,7 +155,8 @@ class TestWorker(threading.Thread): # here since pyauto.WaitUntil doesn't call into Chrome. self._epp = self._ttp = -1 self._pyauto.WaitUntil( - self._HaveMetrics, args=[unique_url], retry_sleep=2) + self._HaveMetrics, args=[unique_url], retry_sleep=2, + timeout=_TEST_VIDEO_DURATION_SEC * 10) # Record results. # TODO(dalecurtis): Support reference builds. @@ -179,6 +183,7 @@ class MediaConstrainedNetworkPerfTest(pyauto.PyUITest): '--interface', 'lo', '--www-root', os.path.join( self.DataDir(), 'pyauto_private', 'media')] + process = subprocess.Popen(cmd, stderr=subprocess.PIPE) # Wait for server to start up. diff --git a/media/tools/constrained_network_server/cn.py b/media/tools/constrained_network_server/cn.py index c2fd877..b79d8fb 100755 --- a/media/tools/constrained_network_server/cn.py +++ b/media/tools/constrained_network_server/cn.py @@ -1,5 +1,4 @@ #!/usr/bin/env python - # Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. @@ -67,17 +66,15 @@ def _ParseArgs(): parser.add_option('--server-port', type='int', help='Port to forward traffic on --port to.') parser.add_option('--bandwidth', type='int', - help=('Bandwidth of the network in kbps. Default: ' - '%defaultkbps.')) + help='Bandwidth of the network in kbps.') parser.add_option('--latency', type='int', help=('Latency (delay) added to each outgoing packet in ' - 'ms. Default: %defaultms.')) + 'ms.')) parser.add_option('--loss', type='int', - help=('Packet-loss percentage on outgoing packets. ' - 'Default: %default%.')) + help='Packet-loss percentage on outgoing packets. ') parser.add_option('--interface', type='string', - help=('Interface to setup constraints on. Use lo for a ' - 'local client. Default: %default.')) + help=('Interface to setup constraints on. Use "lo" for a ' + 'local client.')) parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False, help='Turn on verbose output.') options, args = parser.parse_args() diff --git a/media/tools/constrained_network_server/cns.py b/media/tools/constrained_network_server/cns.py index 6f3df21..70e68163 100755 --- a/media/tools/constrained_network_server/cns.py +++ b/media/tools/constrained_network_server/cns.py @@ -18,6 +18,7 @@ import signal import sys import threading import time +import traffic_control try: import cherrypy @@ -91,13 +92,31 @@ class PortAllocator(object): for port in xrange(self._port_range[0], self._port_range[1]): if port in self._ports: continue + if self._SetupPort(port, **kwargs): + kwargs['port'] = port + self._ports[port] = {'last_update': time.time(), 'key': full_key, + 'config': kwargs} + return port + + def _SetupPort(self, port, **kwargs): + """Setup network constraints on port using the requested parameters. - # TODO(dalecurtis): Integrate with shadi's scripts. - # We've found an open port so call the script and set it up. - #Port.Setup(port=port, **kwargs) + Args: + port: The port number to setup network constraints on. + **kwargs: Network constraints to set up on the port. - self._ports[port] = {'last_update': time.time(), 'key': full_key} - return port + Returns: + True if setting the network constraints on the port was successful, false + otherwise. + """ + kwargs['port'] = port + try: + cherrypy.log('Setting up port %d' % port) + traffic_control.CreateConstrainedPort(kwargs) + return True + except traffic_control.TrafficControlError as e: + cherrypy.log('Error: %s\nOutput: %s', e.msg, e.error) + return False def _CleanupLocked(self, all_ports): """Internal cleanup method, expects lock to have already been acquired. @@ -113,19 +132,28 @@ class PortAllocator(object): expired = now - status['last_update'] > self._expiry_time_secs if all_ports or expired: cherrypy.log('Cleaning up port %d' % port) + self._DeletePort(port) + del self._ports[port] - # TODO(dalecurtis): Integrate with shadi's scripts. - #Port.Delete(port=port) + def _DeletePort(self, port): + """Deletes network constraints on port. - del self._ports[port] + Args: + port: The port number associated with the network constraints. + """ + try: + traffic_control.DeleteConstrainedPort(self._ports[port]['config']) + except traffic_control.TrafficControlError as e: + cherrypy.log('Error: %s\nOutput: %s', e.msg, e.error) - def Cleanup(self, all_ports=False): + def Cleanup(self, interface, all_ports=False): """Cleans up expired ports, or if all_ports=True, all allocated ports. By default, ports which haven't been used for self._expiry_time_secs are torn down. If all_ports=True then they are torn down regardless. Args: + interface: Interface the constrained network is setup on. all_ports: Should all ports be torn down regardless of expiration? """ with self._port_lock: @@ -179,14 +207,12 @@ class ConstrainedNetworkServer(object): return cherrypy.lib.static.serve_file(sanitized_path) # Validate inputs. isdigit() guarantees a natural number. - if bandwidth and not bandwidth.isdigit(): - raise cherrypy.HTTPError(400, 'Invalid bandwidth constraint.') - - if latency and not latency.isdigit(): - raise cherrypy.HTTPError(400, 'Invalid latency constraint.') - - if loss and not loss.isdigit() and not int(loss) <= 100: - raise cherrypy.HTTPError(400, 'Invalid loss constraint.') + bandwidth = self._ParseIntParameter( + bandwidth, 'Invalid bandwidth constraint.', lambda x: x > 0) + latency = self._ParseIntParameter( + latency, 'Invalid latency constraint.', lambda x: x >= 0) + loss = self._ParseIntParameter( + loss, 'Invalid loss constraint.', lambda x: x <= 100 and x >= 0) # Allocate a port using the given constraints. If a port with the requested # key is already allocated, it will be reused. @@ -194,14 +220,15 @@ class ConstrainedNetworkServer(object): # TODO(dalecurtis): The key cherrypy.request.remote.ip might not be unique # if build slaves are sharing the same VM. constrained_port = self._port_allocator.Get( - cherrypy.request.remote.ip, bandwidth=bandwidth, latency=latency, + cherrypy.request.remote.ip, server_port=self._options.port, + interface=self._options.interface, bandwidth=bandwidth, latency=latency, loss=loss) if not constrained_port: raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.') # Build constrained URL. Only pass on the file parameter. - constrained_url = '%s?file=%s' % ( + constrained_url = '%s?f=%s' % ( cherrypy.url().replace( ':%d' % self._options.port, ':%d' % constrained_port), f) @@ -209,6 +236,30 @@ class ConstrainedNetworkServer(object): # Redirect request to the constrained port. cherrypy.lib.cptools.redirect(constrained_url, internal=False) + def _ParseIntParameter(self, param, msg, check): + """Returns integer value of param and verifies it satisfies the check. + + Args: + param: Parameter name to check. + msg: Message in error if raised. + check: Check to verify the parameter value. + + Returns: + None if param is None, integer value of param otherwise. + + Raises: + cherrypy.HTTPError if param can not be converted to integer or if it does + not satisfy the check. + """ + if param: + try: + int_value = int(param) + if check(int_value): + return int_value + except: + pass + raise cherrypy.HTTPError(400, msg) + def ParseArgs(): """Define and parse the command-line arguments.""" @@ -267,7 +318,7 @@ def Main(): finally: # Disable Ctrl-C handler to prevent interruption of cleanup. signal.signal(signal.SIGINT, lambda signal, frame: None) - pa.Cleanup(all_ports=True) + pa.Cleanup(options.interface, all_ports=True) if __name__ == '__main__': diff --git a/media/tools/constrained_network_server/cns_test.py b/media/tools/constrained_network_server/cns_test.py index 2be56df..319f33a 100755 --- a/media/tools/constrained_network_server/cns_test.py +++ b/media/tools/constrained_network_server/cns_test.py @@ -13,6 +13,10 @@ import unittest import urllib2 import cns +import traffic_control + +# The local interface to test on. +_INTERFACE = 'lo' class PortAllocatorTest(unittest.TestCase): @@ -29,12 +33,28 @@ class PortAllocatorTest(unittest.TestCase): # TODO(dalecurtis): Mock out actual calls to shadi's port setup. self._pa = cns.PortAllocator(cns._DEFAULT_CNS_PORT_RANGE, self._EXPIRY_TIME) + self._MockTrafficControl() def tearDown(self): - self._pa.Cleanup(all_ports=True) + self._pa.Cleanup(_INTERFACE, all_ports=True) # Ensure ports are cleaned properly. self.assertEquals(self._pa._ports, {}) time.time = self._old_time + self._RestoreTrafficControl() + + def _MockTrafficControl(self): + self.old_CreateConstrainedPort = traffic_control.CreateConstrainedPort + self.old_DeleteConstrainedPort = traffic_control.DeleteConstrainedPort + self.old_TearDown = traffic_control.TearDown + + traffic_control.CreateConstrainedPort = lambda config: True + traffic_control.DeleteConstrainedPort = lambda config: True + traffic_control.TearDown = lambda config: True + + def _RestoreTrafficControl(self): + traffic_control.CreateConstrainedPort = self.old_CreateConstrainedPort + traffic_control.DeleteConstrainedPort = self.old_DeleteConstrainedPort + traffic_control.TearDown = self.old_TearDown def testPortAllocator(self): # Ensure Get() succeeds and returns the correct port. @@ -94,11 +114,11 @@ class ConstrainedNetworkServerTest(unittest.TestCase): cns._DEFAULT_SERVING_PORT) # Setting for latency testing. - _LATENCY_TEST_SECS = 5 + _LATENCY_TEST_SECS = 1 def _StartServer(self): """Starts the CNS, returns pid.""" - cmd = ['python', 'cns.py'] + cmd = ['python', 'cns.py', '--interface=%s' % _INTERFACE] process = subprocess.Popen(cmd, stderr=subprocess.PIPE) # Wait for server to startup. @@ -125,7 +145,7 @@ class ConstrainedNetworkServerTest(unittest.TestCase): def tearDown(self): os.unlink(self._file) - os.kill(self._server_pid, signal.SIGKILL) + os.kill(self._server_pid, signal.SIGTERM) def testServerServesFiles(self): now = time.time() @@ -140,6 +160,9 @@ class ConstrainedNetworkServerTest(unittest.TestCase): self.assertTrue(time.time() - now < self._LATENCY_TEST_SECS) def testServerLatencyConstraint(self): + """Tests serving a file with a latency network constraint.""" + # Abort if does not have root access. + self.assertEqual(os.geteuid(), 0, 'You need root access to run this test.') now = time.time() base_url = '%sf=%s' % (self._SERVER_URL, self._relative_fn) diff --git a/media/tools/constrained_network_server/traffic_control.py b/media/tools/constrained_network_server/traffic_control.py index d6527ff..aa8a157 100755 --- a/media/tools/constrained_network_server/traffic_control.py +++ b/media/tools/constrained_network_server/traffic_control.py @@ -259,13 +259,11 @@ def _GetFilterHandleId(interface, port): """ command = ['tc', 'filter', 'list', 'dev', interface, 'parent', '1:'] output = _Exec(command, msg='Error listing filters.') - # Search for the filter handle ID associated with class ID '1:port'. handle_id_re = re.search( - '([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x$)' % port, output) + '([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x\s)' % port, output) if handle_id_re: return handle_id_re.group(1) - raise TrafficControlError(('Could not find filter handle ID for class ID ' '1:%x.') % port) diff --git a/media/tools/constrained_network_server/traffic_control_test.py b/media/tools/constrained_network_server/traffic_control_test.py index 583becb..a84462f 100644 --- a/media/tools/constrained_network_server/traffic_control_test.py +++ b/media/tools/constrained_network_server/traffic_control_test.py @@ -24,7 +24,7 @@ class TrafficControlTests(unittest.TestCase): def setUp(self): """Setup a dummy interface.""" # If we update to python version 2.7 or newer we can use setUpClass() or - # unittest.skipIf() + # unittest.skipIf(). if os.getuid() != 0: sys.exit('You need root access to run these tests.') @@ -64,7 +64,7 @@ class TrafficControlTests(unittest.TestCase): traffic_control._AddRootQdisc(config['interface']) output = traffic_control._Exec(command) - # Assert htb root is added + # Assert htb root is added. self.assertTrue(root_detail in output) def testConfigureClassAdd(self): @@ -75,7 +75,7 @@ class TrafficControlTests(unittest.TestCase): 'server_port': 33333, 'bandwidth': 2000 } - # Convert Kbps to Kbit + # Convert Kbps to Kbit. rate = config['bandwidth'] * 8 class_detail = ('class htb 1:%x root prio 0 rate %dKbit ceil %dKbit' % (config['port'], rate, rate)) @@ -91,15 +91,15 @@ class TrafficControlTests(unittest.TestCase): # Add class to root. traffic_control._ConfigureClass('add', config) - # Assert class is added + # Assert class is added. command = ['tc', 'class', 'ls', 'dev', config['interface']] output = traffic_control._Exec(command) self.assertTrue(class_detail in output) - # Delete class + # Delete class. traffic_control._ConfigureClass('del', config) - # Assert class is deleted + # Assert class is deleted. command = ['tc', 'class', 'ls', 'dev', config['interface']] output = traffic_control._Exec(command) self.assertFalse(class_detail in output) @@ -129,15 +129,47 @@ class TrafficControlTests(unittest.TestCase): handle_id_re = re.search(qdisc_re_detail, output) self.assertEqual(handle_id_re, None) - # Add qdisc to class + # Add qdisc to class. traffic_control._AddSubQdisc(config) - # Assert qdisc is added + # Assert qdisc is added. command = ['tc', 'qdisc', 'ls', 'dev', config['interface']] output = traffic_control._Exec(command) handle_id_re = re.search(qdisc_re_detail, output) self.assertNotEqual(handle_id_re, None) + def testAddDeleteFilter(self): + config = { + 'interface': self._INTERFACE, + 'port': 12345, + 'bandwidth': 2000 + } + # Assert no filter exists. + command = ['tc', 'filter', 'list', 'dev', config['interface'], 'parent', + '1:0'] + output = traffic_control._Exec(command) + self.assertEqual(output, '') + + # Create the root and class to which the filter will be attached. + # Add root qdisc. + traffic_control._AddRootQdisc(config['interface']) + + # Add class to root. + traffic_control._ConfigureClass('add', config) + + # Add the filter. + traffic_control._AddFilter(config['interface'], config['port']) + handle_id = traffic_control._GetFilterHandleId(config['interface'], + config['port']) + self.assertNotEqual(handle_id, None) + + # Delete the filter. + # The output of tc filter list is not None because tc adds default filters. + traffic_control._DeleteFilter(config['interface'], config['port']) + self.assertRaises(traffic_control.TrafficControlError, + traffic_control._GetFilterHandleId, config['interface'], + config['port']) + if __name__ == '__main__': unittest.main() |