#!/usr/bin/env python # Copyright 2012 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. """Simulate network characteristics directly in Python. Allows running replay without dummynet. """ import logging import platformsettings import re import time TIMER = platformsettings.timer class ProxyShaperError(Exception): """Module catch-all error.""" pass class BandwidthValueError(ProxyShaperError): """Raised for unexpected dummynet-style bandwidth value.""" pass class RateLimitedFile(object): """Wrap a file like object with rate limiting. TODO(slamm): Simulate slow-start. Each RateLimitedFile corresponds to one-direction of a bidirectional socket. Slow-start can be added here (algorithm needed). Will consider changing this class to take read and write files and corresponding bit rates for each. """ BYTES_PER_WRITE = 1460 def __init__(self, request_counter, f, bps): """Initialize a RateLimiter. Args: request_counter: callable to see how many requests share the limit. f: file-like object to wrap. bps: an integer of bits per second. """ self.request_counter = request_counter self.original_file = f self.bps = bps def transfer_seconds(self, num_bytes): """Seconds to read/write |num_bytes| with |self.bps|.""" return 8.0 * num_bytes / self.bps def write(self, data): num_bytes = len(data) num_sent_bytes = 0 while num_sent_bytes < num_bytes: num_write_bytes = min(self.BYTES_PER_WRITE, num_bytes - num_sent_bytes) num_requests = self.request_counter() wait = self.transfer_seconds(num_write_bytes) * num_requests logging.debug('write sleep: %0.4fs (%d requests)', wait, num_requests) time.sleep(wait) self.original_file.write( data[num_sent_bytes:num_sent_bytes + num_write_bytes]) num_sent_bytes += num_write_bytes def _read(self, read_func, size): start = TIMER() data = read_func(size) read_seconds = TIMER() - start num_bytes = len(data) num_requests = self.request_counter() wait = self.transfer_seconds(num_bytes) * num_requests - read_seconds if wait > 0: logging.debug('read sleep: %0.4fs %d requests)', wait, num_requests) time.sleep(wait) return data def readline(self, size=-1): return self._read(self.original_file.readline, size) def read(self, size=-1): return self._read(self.original_file.read, size) def __getattr__(self, name): """Forward any non-overriden calls.""" return getattr(self.original_file, name) def GetBitsPerSecond(bandwidth): """Return bits per second represented by dummynet bandwidth option. See ipfw/dummynet.c:read_bandwidth for how it is really done. Args: bandwidth: a dummynet-style bandwidth specification (e.g. "10Kbit/s") """ if bandwidth == '0': return 0 bw_re = r'^(\d+)(?:([KM])?(bit|Byte)/s)?$' match = re.match(bw_re, str(bandwidth)) if not match: raise BandwidthValueError('Value, "%s", does not match regex: %s' % ( bandwidth, bw_re)) bw = int(match.group(1)) if match.group(2) == 'K': bw *= 1000 if match.group(2) == 'M': bw *= 1000000 if match.group(3) == 'Byte': bw *= 8 return bw