2 # Copyright 2012 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 """Simulate network characteristics directly in Python.
18 Allows running replay without dummynet.
22 import platformsettings
27 TIMER
= platformsettings
.timer
30 class ProxyShaperError(Exception):
31 """Module catch-all error."""
34 class BandwidthValueError(ProxyShaperError
):
35 """Raised for unexpected dummynet-style bandwidth value."""
39 class RateLimitedFile(object):
40 """Wrap a file like object with rate limiting.
42 TODO(slamm): Simulate slow-start.
43 Each RateLimitedFile corresponds to one-direction of a
44 bidirectional socket. Slow-start can be added here (algorithm needed).
45 Will consider changing this class to take read and write files and
46 corresponding bit rates for each.
48 BYTES_PER_WRITE
= 1460
50 def __init__(self
, request_counter
, f
, bps
):
51 """Initialize a RateLimiter.
54 request_counter: callable to see how many requests share the limit.
55 f: file-like object to wrap.
56 bps: an integer of bits per second.
58 self
.request_counter
= request_counter
59 self
.original_file
= f
62 def transfer_seconds(self
, num_bytes
):
63 """Seconds to read/write |num_bytes| with |self.bps|."""
64 return 8.0 * num_bytes
/ self
.bps
66 def write(self
, data
):
69 while num_sent_bytes
< num_bytes
:
70 num_write_bytes
= min(self
.BYTES_PER_WRITE
, num_bytes
- num_sent_bytes
)
71 num_requests
= self
.request_counter()
72 wait
= self
.transfer_seconds(num_write_bytes
) * num_requests
73 logging
.debug('write sleep: %0.4fs (%d requests)', wait
, num_requests
)
76 self
.original_file
.write(
77 data
[num_sent_bytes
:num_sent_bytes
+ num_write_bytes
])
78 num_sent_bytes
+= num_write_bytes
80 def _read(self
, read_func
, size
):
82 data
= read_func(size
)
83 read_seconds
= TIMER() - start
85 num_requests
= self
.request_counter()
86 wait
= self
.transfer_seconds(num_bytes
) * num_requests
- read_seconds
88 logging
.debug('read sleep: %0.4fs %d requests)', wait
, num_requests
)
92 def readline(self
, size
=-1):
93 return self
._read
(self
.original_file
.readline
, size
)
95 def read(self
, size
=-1):
96 return self
._read
(self
.original_file
.read
, size
)
98 def __getattr__(self
, name
):
99 """Forward any non-overriden calls."""
100 return getattr(self
.original_file
, name
)
103 def GetBitsPerSecond(bandwidth
):
104 """Return bits per second represented by dummynet bandwidth option.
106 See ipfw/dummynet.c:read_bandwidth for how it is really done.
109 bandwidth: a dummynet-style bandwidth specification (e.g. "10Kbit/s")
113 bw_re
= r
'^(\d+)(?:([KM])?(bit|Byte)/s)?$'
114 match
= re
.match(bw_re
, str(bandwidth
))
116 raise BandwidthValueError('Value, "%s", does not match regex: %s' % (
118 bw
= int(match
.group(1))
119 if match
.group(2) == 'K':
121 if match
.group(2) == 'M':
123 if match
.group(3) == 'Byte':