Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / tools / telemetry / third_party / webpagereplay / trafficshaper.py
blob0078218ef68b3cab2d86e20e72a3dc2789678c79
1 #!/usr/bin/env python
2 # Copyright 2010 Google Inc. All Rights Reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
16 import logging
17 import platformsettings
18 import re
21 # Mac has broken bandwitdh parsing, so double check the values.
22 # On Mac OS X 10.6, "KBit/s" actually uses "KByte/s".
23 BANDWIDTH_PATTERN = r'0|\d+[KM]?(bit|Byte)/s'
26 class TrafficShaperException(Exception):
27 pass
30 class BandwidthValueError(TrafficShaperException):
31 def __init__(self, value): # pylint: disable=super-init-not-called
32 self.value = value
34 def __str__(self):
35 return 'Value, "%s", does not match regex: %s' % (
36 self.value, BANDWIDTH_PATTERN)
39 class TrafficShaper(object):
40 """Manages network traffic shaping."""
42 # Pick webpagetest-compatible values (details: http://goo.gl/oghTg).
43 _UPLOAD_PIPE = '10' # Enforces overall upload bandwidth.
44 _UPLOAD_QUEUE = '10' # Shares upload bandwidth among source ports.
45 _UPLOAD_RULE = '5000' # Specifies when the upload queue is used.
46 _DOWNLOAD_PIPE = '11' # Enforces overall download bandwidth.
47 _DOWNLOAD_QUEUE = '11' # Shares download bandwidth among destination ports.
48 _DOWNLOAD_RULE = '5100' # Specifies when the download queue is used.
49 _QUEUE_SLOTS = 100 # Number of packets to queue.
51 _BANDWIDTH_RE = re.compile(BANDWIDTH_PATTERN)
53 def __init__(self,
54 dont_use=None,
55 host='127.0.0.1',
56 ports=None,
57 up_bandwidth='0',
58 down_bandwidth='0',
59 delay_ms='0',
60 packet_loss_rate='0',
61 init_cwnd='0',
62 use_loopback=True):
63 """Start shaping traffic.
65 Args:
66 host: a host string (name or IP) for the web proxy.
67 ports: a list of ports to shape traffic on.
68 up_bandwidth: Upload bandwidth
69 down_bandwidth: Download bandwidth
70 Bandwidths measured in [K|M]{bit/s|Byte/s}. '0' means unlimited.
71 delay_ms: Propagation delay in milliseconds. '0' means no delay.
72 packet_loss_rate: Packet loss rate in range [0..1]. '0' means no loss.
73 init_cwnd: the initial cwnd setting. '0' means no change.
74 use_loopback: True iff shaping is done on the loopback (or equiv) adapter.
75 """
76 assert dont_use is None # Force args to be named.
77 self.host = host
78 self.ports = ports
79 self.up_bandwidth = up_bandwidth
80 self.down_bandwidth = down_bandwidth
81 self.delay_ms = delay_ms
82 self.packet_loss_rate = packet_loss_rate
83 self.init_cwnd = init_cwnd
84 self.use_loopback = use_loopback
85 if not self._BANDWIDTH_RE.match(self.up_bandwidth):
86 raise BandwidthValueError(self.up_bandwidth)
87 if not self._BANDWIDTH_RE.match(self.down_bandwidth):
88 raise BandwidthValueError(self.down_bandwidth)
89 self.is_shaping = False
91 def __enter__(self):
92 if self.use_loopback:
93 platformsettings.setup_temporary_loopback_config()
94 if self.init_cwnd != '0':
95 platformsettings.set_temporary_tcp_init_cwnd(self.init_cwnd)
96 try:
97 ipfw_list = platformsettings.ipfw('list')
98 if not ipfw_list.startswith('65535 '):
99 logging.warn('ipfw has existing rules:\n%s', ipfw_list)
100 self._delete_rules(ipfw_list)
101 except Exception:
102 pass
103 if (self.up_bandwidth == '0' and self.down_bandwidth == '0' and
104 self.delay_ms == '0' and self.packet_loss_rate == '0'):
105 logging.info('Skipped shaping traffic.')
106 return
107 if not self.ports:
108 raise TrafficShaperException('No ports on which to shape traffic.')
110 ports = ','.join(str(p) for p in self.ports)
111 half_delay_ms = int(self.delay_ms) / 2 # split over up/down links
113 try:
114 # Configure upload shaping.
115 platformsettings.ipfw(
116 'pipe', self._UPLOAD_PIPE,
117 'config',
118 'bw', self.up_bandwidth,
119 'delay', half_delay_ms,
121 platformsettings.ipfw(
122 'queue', self._UPLOAD_QUEUE,
123 'config',
124 'pipe', self._UPLOAD_PIPE,
125 'plr', self.packet_loss_rate,
126 'queue', self._QUEUE_SLOTS,
127 'mask', 'src-port', '0xffff',
129 platformsettings.ipfw(
130 'add', self._UPLOAD_RULE,
131 'queue', self._UPLOAD_QUEUE,
132 'ip',
133 'from', 'any',
134 'to', self.host,
135 self.use_loopback and 'out' or 'in',
136 'dst-port', ports,
138 self.is_shaping = True
140 # Configure download shaping.
141 platformsettings.ipfw(
142 'pipe', self._DOWNLOAD_PIPE,
143 'config',
144 'bw', self.down_bandwidth,
145 'delay', half_delay_ms,
147 platformsettings.ipfw(
148 'queue', self._DOWNLOAD_QUEUE,
149 'config',
150 'pipe', self._DOWNLOAD_PIPE,
151 'plr', self.packet_loss_rate,
152 'queue', self._QUEUE_SLOTS,
153 'mask', 'dst-port', '0xffff',
155 platformsettings.ipfw(
156 'add', self._DOWNLOAD_RULE,
157 'queue', self._DOWNLOAD_QUEUE,
158 'ip',
159 'from', self.host,
160 'to', 'any',
161 'out',
162 'src-port', ports,
164 logging.info('Started shaping traffic')
165 except Exception:
166 logging.error('Unable to shape traffic.')
167 raise
169 def __exit__(self, unused_exc_type, unused_exc_val, unused_exc_tb):
170 if self.is_shaping:
171 try:
172 self._delete_rules()
173 logging.info('Stopped shaping traffic')
174 except Exception:
175 logging.error('Unable to stop shaping traffic.')
176 raise
178 def _delete_rules(self, ipfw_list=None):
179 if ipfw_list is None:
180 ipfw_list = platformsettings.ipfw('list')
181 existing_rules = set(
182 r.split()[0].lstrip('0') for r in ipfw_list.splitlines())
183 delete_rules = [r for r in (self._DOWNLOAD_RULE, self._UPLOAD_RULE)
184 if r in existing_rules]
185 if delete_rules:
186 platformsettings.ipfw('delete', *delete_rules)