Skip to content

Commit

Permalink
Merging CNS server with scripts.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
shadi@chromium.org committed Dec 20, 2011
1 parent 8a9e1a1 commit 8bc584e
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 50 deletions.
4 changes: 2 additions & 2 deletions chrome/test/data/media/html/media_constrained_network.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</head>

<body>
<video/>
<video controls/>
</body>

<script type="text/javascript">
Expand All @@ -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.
Expand Down
15 changes: 10 additions & 5 deletions chrome/test/functional/media/media_constrained_network_perf.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@
# 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(
Expand Down Expand Up @@ -107,10 +109,11 @@ def _HaveMetrics(self, unique_url):
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):
Expand Down Expand Up @@ -152,7 +155,8 @@ def run(self):
# 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.
Expand All @@ -179,6 +183,7 @@ def setUp(self):
'--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.
Expand Down
13 changes: 5 additions & 8 deletions media/tools/constrained_network_server/cn.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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()
Expand Down
91 changes: 71 additions & 20 deletions media/tools/constrained_network_server/cns.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import sys
import threading
import time
import traffic_control

try:
import cherrypy
Expand Down Expand Up @@ -91,13 +92,31 @@ def Get(self, key, **kwargs):
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.
Expand All @@ -113,19 +132,28 @@ def _CleanupLocked(self, all_ports):
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:
Expand Down Expand Up @@ -179,36 +207,59 @@ def ServeConstrained(self, f=None, bandwidth=None, latency=None, loss=None):
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.
#
# 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)

# 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."""
Expand Down Expand Up @@ -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__':
Expand Down
31 changes: 27 additions & 4 deletions media/tools/constrained_network_server/cns_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
import urllib2

import cns
import traffic_control

# The local interface to test on.
_INTERFACE = 'lo'


class PortAllocatorTest(unittest.TestCase):
Expand All @@ -29,12 +33,28 @@ def setUp(self):

# 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.
Expand Down Expand Up @@ -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.
Expand All @@ -125,7 +145,7 @@ def setUp(self):

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()
Expand All @@ -140,6 +160,9 @@ def testServerServesFiles(self):
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)
Expand Down
4 changes: 1 addition & 3 deletions media/tools/constrained_network_server/traffic_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading

0 comments on commit 8bc584e

Please sign in to comment.