diff options
Diffstat (limited to 'catapult/telemetry/third_party/web-page-replay/trafficshaper.py')
-rw-r--r-- | catapult/telemetry/third_party/web-page-replay/trafficshaper.py | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/catapult/telemetry/third_party/web-page-replay/trafficshaper.py b/catapult/telemetry/third_party/web-page-replay/trafficshaper.py new file mode 100644 index 00000000..0078218e --- /dev/null +++ b/catapult/telemetry/third_party/web-page-replay/trafficshaper.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# Copyright 2010 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import platformsettings +import re + + +# Mac has broken bandwitdh parsing, so double check the values. +# On Mac OS X 10.6, "KBit/s" actually uses "KByte/s". +BANDWIDTH_PATTERN = r'0|\d+[KM]?(bit|Byte)/s' + + +class TrafficShaperException(Exception): + pass + + +class BandwidthValueError(TrafficShaperException): + def __init__(self, value): # pylint: disable=super-init-not-called + self.value = value + + def __str__(self): + return 'Value, "%s", does not match regex: %s' % ( + self.value, BANDWIDTH_PATTERN) + + +class TrafficShaper(object): + """Manages network traffic shaping.""" + + # Pick webpagetest-compatible values (details: http://goo.gl/oghTg). + _UPLOAD_PIPE = '10' # Enforces overall upload bandwidth. + _UPLOAD_QUEUE = '10' # Shares upload bandwidth among source ports. + _UPLOAD_RULE = '5000' # Specifies when the upload queue is used. + _DOWNLOAD_PIPE = '11' # Enforces overall download bandwidth. + _DOWNLOAD_QUEUE = '11' # Shares download bandwidth among destination ports. + _DOWNLOAD_RULE = '5100' # Specifies when the download queue is used. + _QUEUE_SLOTS = 100 # Number of packets to queue. + + _BANDWIDTH_RE = re.compile(BANDWIDTH_PATTERN) + + def __init__(self, + dont_use=None, + host='127.0.0.1', + ports=None, + up_bandwidth='0', + down_bandwidth='0', + delay_ms='0', + packet_loss_rate='0', + init_cwnd='0', + use_loopback=True): + """Start shaping traffic. + + Args: + host: a host string (name or IP) for the web proxy. + ports: a list of ports to shape traffic on. + up_bandwidth: Upload bandwidth + down_bandwidth: Download bandwidth + Bandwidths measured in [K|M]{bit/s|Byte/s}. '0' means unlimited. + delay_ms: Propagation delay in milliseconds. '0' means no delay. + packet_loss_rate: Packet loss rate in range [0..1]. '0' means no loss. + init_cwnd: the initial cwnd setting. '0' means no change. + use_loopback: True iff shaping is done on the loopback (or equiv) adapter. + """ + assert dont_use is None # Force args to be named. + self.host = host + self.ports = ports + self.up_bandwidth = up_bandwidth + self.down_bandwidth = down_bandwidth + self.delay_ms = delay_ms + self.packet_loss_rate = packet_loss_rate + self.init_cwnd = init_cwnd + self.use_loopback = use_loopback + if not self._BANDWIDTH_RE.match(self.up_bandwidth): + raise BandwidthValueError(self.up_bandwidth) + if not self._BANDWIDTH_RE.match(self.down_bandwidth): + raise BandwidthValueError(self.down_bandwidth) + self.is_shaping = False + + def __enter__(self): + if self.use_loopback: + platformsettings.setup_temporary_loopback_config() + if self.init_cwnd != '0': + platformsettings.set_temporary_tcp_init_cwnd(self.init_cwnd) + try: + ipfw_list = platformsettings.ipfw('list') + if not ipfw_list.startswith('65535 '): + logging.warn('ipfw has existing rules:\n%s', ipfw_list) + self._delete_rules(ipfw_list) + except Exception: + pass + if (self.up_bandwidth == '0' and self.down_bandwidth == '0' and + self.delay_ms == '0' and self.packet_loss_rate == '0'): + logging.info('Skipped shaping traffic.') + return + if not self.ports: + raise TrafficShaperException('No ports on which to shape traffic.') + + ports = ','.join(str(p) for p in self.ports) + half_delay_ms = int(self.delay_ms) / 2 # split over up/down links + + try: + # Configure upload shaping. + platformsettings.ipfw( + 'pipe', self._UPLOAD_PIPE, + 'config', + 'bw', self.up_bandwidth, + 'delay', half_delay_ms, + ) + platformsettings.ipfw( + 'queue', self._UPLOAD_QUEUE, + 'config', + 'pipe', self._UPLOAD_PIPE, + 'plr', self.packet_loss_rate, + 'queue', self._QUEUE_SLOTS, + 'mask', 'src-port', '0xffff', + ) + platformsettings.ipfw( + 'add', self._UPLOAD_RULE, + 'queue', self._UPLOAD_QUEUE, + 'ip', + 'from', 'any', + 'to', self.host, + self.use_loopback and 'out' or 'in', + 'dst-port', ports, + ) + self.is_shaping = True + + # Configure download shaping. + platformsettings.ipfw( + 'pipe', self._DOWNLOAD_PIPE, + 'config', + 'bw', self.down_bandwidth, + 'delay', half_delay_ms, + ) + platformsettings.ipfw( + 'queue', self._DOWNLOAD_QUEUE, + 'config', + 'pipe', self._DOWNLOAD_PIPE, + 'plr', self.packet_loss_rate, + 'queue', self._QUEUE_SLOTS, + 'mask', 'dst-port', '0xffff', + ) + platformsettings.ipfw( + 'add', self._DOWNLOAD_RULE, + 'queue', self._DOWNLOAD_QUEUE, + 'ip', + 'from', self.host, + 'to', 'any', + 'out', + 'src-port', ports, + ) + logging.info('Started shaping traffic') + except Exception: + logging.error('Unable to shape traffic.') + raise + + def __exit__(self, unused_exc_type, unused_exc_val, unused_exc_tb): + if self.is_shaping: + try: + self._delete_rules() + logging.info('Stopped shaping traffic') + except Exception: + logging.error('Unable to stop shaping traffic.') + raise + + def _delete_rules(self, ipfw_list=None): + if ipfw_list is None: + ipfw_list = platformsettings.ipfw('list') + existing_rules = set( + r.split()[0].lstrip('0') for r in ipfw_list.splitlines()) + delete_rules = [r for r in (self._DOWNLOAD_RULE, self._UPLOAD_RULE) + if r in existing_rules] + if delete_rules: + platformsettings.ipfw('delete', *delete_rules) |