aboutsummaryrefslogtreecommitdiff
path: root/catapult/telemetry/third_party/web-page-replay/trafficshaper.py
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/telemetry/third_party/web-page-replay/trafficshaper.py')
-rw-r--r--catapult/telemetry/third_party/web-page-replay/trafficshaper.py186
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)