proxyshaper.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. #!/usr/bin/env python
  2. # Copyright 2012 Google Inc. All Rights Reserved.
  3. #
  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
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  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.
  15. """Simulate network characteristics directly in Python.
  16. Allows running replay without dummynet.
  17. """
  18. import logging
  19. import platformsettings
  20. import re
  21. import time
  22. TIMER = platformsettings.timer
  23. class ProxyShaperError(Exception):
  24. """Module catch-all error."""
  25. pass
  26. class BandwidthValueError(ProxyShaperError):
  27. """Raised for unexpected dummynet-style bandwidth value."""
  28. pass
  29. class RateLimitedFile(object):
  30. """Wrap a file like object with rate limiting.
  31. TODO(slamm): Simulate slow-start.
  32. Each RateLimitedFile corresponds to one-direction of a
  33. bidirectional socket. Slow-start can be added here (algorithm needed).
  34. Will consider changing this class to take read and write files and
  35. corresponding bit rates for each.
  36. """
  37. BYTES_PER_WRITE = 1460
  38. def __init__(self, request_counter, f, bps):
  39. """Initialize a RateLimiter.
  40. Args:
  41. request_counter: callable to see how many requests share the limit.
  42. f: file-like object to wrap.
  43. bps: an integer of bits per second.
  44. """
  45. self.request_counter = request_counter
  46. self.original_file = f
  47. self.bps = bps
  48. def transfer_seconds(self, num_bytes):
  49. """Seconds to read/write |num_bytes| with |self.bps|."""
  50. return 8.0 * num_bytes / self.bps
  51. def write(self, data):
  52. num_bytes = len(data)
  53. num_sent_bytes = 0
  54. while num_sent_bytes < num_bytes:
  55. num_write_bytes = min(self.BYTES_PER_WRITE, num_bytes - num_sent_bytes)
  56. num_requests = self.request_counter()
  57. wait = self.transfer_seconds(num_write_bytes) * num_requests
  58. logging.debug('write sleep: %0.4fs (%d requests)', wait, num_requests)
  59. time.sleep(wait)
  60. self.original_file.write(
  61. data[num_sent_bytes:num_sent_bytes + num_write_bytes])
  62. num_sent_bytes += num_write_bytes
  63. def _read(self, read_func, size):
  64. start = TIMER()
  65. data = read_func(size)
  66. read_seconds = TIMER() - start
  67. num_bytes = len(data)
  68. num_requests = self.request_counter()
  69. wait = self.transfer_seconds(num_bytes) * num_requests - read_seconds
  70. if wait > 0:
  71. logging.debug('read sleep: %0.4fs %d requests)', wait, num_requests)
  72. time.sleep(wait)
  73. return data
  74. def readline(self, size=-1):
  75. return self._read(self.original_file.readline, size)
  76. def read(self, size=-1):
  77. return self._read(self.original_file.read, size)
  78. def __getattr__(self, name):
  79. """Forward any non-overriden calls."""
  80. return getattr(self.original_file, name)
  81. def GetBitsPerSecond(bandwidth):
  82. """Return bits per second represented by dummynet bandwidth option.
  83. See ipfw/dummynet.c:read_bandwidth for how it is really done.
  84. Args:
  85. bandwidth: a dummynet-style bandwidth specification (e.g. "10Kbit/s")
  86. """
  87. if bandwidth == '0':
  88. return 0
  89. bw_re = r'^(\d+)(?:([KM])?(bit|Byte)/s)?$'
  90. match = re.match(bw_re, str(bandwidth))
  91. if not match:
  92. raise BandwidthValueError('Value, "%s", does not match regex: %s' % (
  93. bandwidth, bw_re))
  94. bw = int(match.group(1))
  95. if match.group(2) == 'K':
  96. bw *= 1000
  97. if match.group(2) == 'M':
  98. bw *= 1000000
  99. if match.group(3) == 'Byte':
  100. bw *= 8
  101. return bw