Skip to content

Commit 144eaf6

Browse files
authored
Merge pull request #194 from JonathanLennox/fix-cburst-size
Set htb burst and cburst size to the minimum useful size.
2 parents 1a06480 + d8aaf67 commit 144eaf6

File tree

5 files changed

+163
-16
lines changed

5 files changed

+163
-16
lines changed

tcconfig/_netem_param.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ class NetemParameter:
4040
def bandwidth_rate(self):
4141
return self.__bandwidth_rate
4242

43+
@property
44+
def mtu(self):
45+
return self.__mtu
46+
47+
@property
48+
def burst(self):
49+
return self.__burst
50+
4351
def __init__(
4452
self,
4553
device,
@@ -52,6 +60,8 @@ def __init__(
5260
corruption_rate=None,
5361
reordering_rate=None,
5462
packet_limit_count=None,
63+
mtu=None,
64+
burst=None,
5565
):
5666
self.__device = device
5767

@@ -61,6 +71,8 @@ def __init__(
6171
self.__corruption_rate = convert_rate_to_f(corruption_rate) # [%]
6272
self.__reordering_rate = convert_rate_to_f(reordering_rate) # [%]
6373
self.__packet_limit_count = convert_rate_to_f(packet_limit_count) # [COUNT]
74+
self.__mtu = mtu # [bytes]
75+
self.__burst = burst # [bytes]
6476

6577
self.__latency_time = None
6678
if latency_time:
@@ -116,6 +128,8 @@ def validate_netem_parameter(self):
116128
self.__corruption_rate,
117129
self.__reordering_rate,
118130
self.__packet_limit_count,
131+
self.__mtu,
132+
self.__burst,
119133
]
120134
if self.__bandwidth_rate:
121135
netem_param_values.append(self.__bandwidth_rate.kilo_bps)

tcconfig/shaper/htb.py

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44

55
import errno
66
import re
7+
import sys
8+
from bisect import bisect_left
79
from typing import List
810

11+
import pyroute2
912
import typepy
13+
from pyroute2.netlink.rtnl.tcmsg.common import TIME_UNITS_PER_SEC, get_hz
1014

1115
from .._common import is_execute_tc_command, logging_context, run_command_helper
1216
from .._const import ShapingAlgorithm, TcSubCommand
@@ -17,6 +21,64 @@
1721
from ._interface import AbstractShaper
1822

1923

24+
# Make this a function, so we can mock it for testing
25+
def tick_in_usec() -> float:
26+
return pyroute2.netlink.rtnl.tcmsg.common.tick_in_usec
27+
28+
29+
# Emulation of tc's buggy time2tick implementation, which
30+
# rounds to int twice
31+
def time2tick_bug(time: int) -> int:
32+
return int(int(time) * tick_in_usec())
33+
34+
35+
# An accurate implementation of tick2time (unlike tc's), not rounding.
36+
def tick2time_true(tick: float) -> float:
37+
return tick / tick_in_usec()
38+
39+
40+
def calc_xmittime_bug(rate: int, size: int) -> int:
41+
return int(time2tick_bug(TIME_UNITS_PER_SEC * (float(size) / rate)))
42+
43+
44+
def calc_xmitsize_true(rate: int, ticks: int) -> float:
45+
return (float(rate) * tick2time_true(ticks)) / TIME_UNITS_PER_SEC
46+
47+
48+
# tc tries to set the default burst and cburst size to (int)(rate / get_hz() + mtu),
49+
# with the comment
50+
# compute minimal allowed burst from rate; mtu is added here to make
51+
# sure that buffer is larger than mtu and to have some safeguard space
52+
#
53+
# Unfortunately, in tc from iproute2 version 6.14 and earlier, there is a rounding
54+
# bug such that the actual burst size set will often be too small, which will cause
55+
# bandwidth limitations to be too aggressive.
56+
#
57+
# Calculate the minimum necessary size to set burst and cburst to, to ensure that they
58+
# are at least the size that tc was trying to set them to.
59+
#
60+
# TODO: check whether tc is from iproute2 version 6.15 or later, and if so bypass the
61+
# adjustment and don't set the default.
62+
def default_burst_size(rate: int, mtu: int) -> int:
63+
return int(rate / get_hz() + mtu)
64+
65+
66+
def adjusted_burst_size(desired_burst: int, rate: int) -> int:
67+
if sys.version_info >= (3, 10):
68+
return bisect_left(
69+
range(1 << 32),
70+
True,
71+
key=lambda b: calc_xmitsize_true(rate, calc_xmittime_bug(rate, b)) >= desired_burst,
72+
)
73+
else:
74+
adjusted_burst = desired_burst
75+
while adjusted_burst < (1 << 32):
76+
if calc_xmitsize_true(rate, calc_xmittime_bug(rate, adjusted_burst)) >= desired_burst:
77+
return adjusted_burst
78+
adjusted_burst += 1
79+
return adjusted_burst
80+
81+
2082
class HtbShaper(AbstractShaper):
2183
__DEFAULT_CLASS_MINOR_ID = 1
2284

@@ -111,11 +173,25 @@ def _add_rate(self):
111173
f"ceil {bandwidth.kilo_bps}Kbit",
112174
]
113175

176+
mtu = self._tc_obj.netem_param.mtu
177+
if mtu:
178+
command_item_list.extend([f"mtu {mtu:d}"])
179+
114180
if bandwidth != upper_limit_rate:
181+
rate = bandwidth.byte_per_sec
182+
desired_burst = self._tc_obj.netem_param.burst
183+
# TODO: check whether tc is from iproute2 version 6.15 or later, and if so bypass the default
184+
# and adjustment.
185+
if not desired_burst:
186+
if not mtu:
187+
mtu = 1600
188+
desired_burst = default_burst_size(rate, mtu)
189+
burst = adjusted_burst_size(desired_burst, rate)
190+
115191
command_item_list.extend(
116192
[
117-
f"burst {bandwidth.kilo_byte_per_sec}KB",
118-
f"cburst {bandwidth.kilo_byte_per_sec}KB",
193+
f"burst {burst}b",
194+
f"cburst {burst}b",
119195
]
120196
)
121197

tcconfig/shaper/tbf.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,22 @@ def _add_rate(self):
8181
if bandwidth is None:
8282
bandwidth = upper_limit_rate
8383

84-
command = " ".join(
85-
[
86-
base_command,
87-
self._dev,
88-
f"parent {parent:s}",
89-
f"handle {handle:s}",
90-
self.algorithm_name,
91-
f"rate {bandwidth.kilo_bps}kbit",
92-
"buffer {:d}".format(
93-
max(int(bandwidth.kilo_bps), self.__MIN_BUFFER_BYTE)
94-
), # [byte]
95-
"limit 10000",
96-
]
97-
)
84+
command_item_list = [
85+
base_command,
86+
self._dev,
87+
f"parent {parent:s}",
88+
f"handle {handle:s}",
89+
self.algorithm_name,
90+
f"rate {bandwidth.kilo_bps}kbit",
91+
"buffer {:d}".format(max(int(bandwidth.kilo_bps), self.__MIN_BUFFER_BYTE)), # [byte]
92+
"limit 10000",
93+
]
94+
95+
mtu = self._tc_obj.netem_param.mtu
96+
if mtu:
97+
command_item_list.extend([f"mtu {mtu:d}"])
98+
99+
command = " ".join(command_item_list)
98100

99101
run_command_helper(
100102
command,

tcconfig/tcset.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,18 @@ def get_arg_parser():
193193
default=False,
194194
help="use iptables for traffic control.",
195195
)
196+
group.add_argument(
197+
"--mtu",
198+
dest="mtu",
199+
type=int,
200+
help="MTU size to assume for interface, if not tc's default. Only used for certain calculations, not enforced.",
201+
)
202+
group.add_argument(
203+
"--burst",
204+
dest="burst",
205+
type=int,
206+
help="burst size to use for traffic shaping (htb only).",
207+
)
196208

197209
group = parser.add_routing_group()
198210
group.add_argument(
@@ -317,6 +329,8 @@ def __create_tc(self, device):
317329
corruption_rate=options.corruption_rate,
318330
reordering_rate=options.reordering_rate,
319331
packet_limit_count=options.packet_limit_count,
332+
mtu=options.mtu,
333+
burst=options.burst,
320334
),
321335
dst_network=self._extract_dst_network(),
322336
exclude_dst_network=options.exclude_dst_network,

test/test_burstsize.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""
2+
.. codeauthor:: Jonathan Lennox <jonathan.lennox@8x8.com>
3+
"""
4+
5+
import humanreadable as hr
6+
import pytest
7+
8+
from tcconfig.shaper.htb import adjusted_burst_size, default_burst_size
9+
10+
11+
@pytest.mark.parametrize(
12+
["rate_hr", "mtu", "expected"],
13+
[
14+
("1Mbps", 1600, 1600),
15+
("1Gbps", 1600, 1600),
16+
("8Gbps", 1600, 1601),
17+
("80Gbps", 1600, 1610),
18+
("80Gbps", 9000, 9010),
19+
],
20+
)
21+
def test_default_burst(rate_hr, mtu, expected):
22+
rate = hr.BitsPerSecond(rate_hr).byte_per_sec
23+
default_burst = default_burst_size(rate, mtu)
24+
assert default_burst == expected
25+
26+
27+
@pytest.mark.parametrize(
28+
["desired_burst", "rate_hr", "expected"],
29+
[
30+
(1600, "1Mbps", 1600),
31+
(1600, "1Gbps", 1625),
32+
(1600, "8Gbps", 2000),
33+
(1610, "80Gbps", 10000),
34+
(9010, "80Gbps", 10000),
35+
],
36+
)
37+
def test_adjusted_burst(monkeypatch, desired_burst, rate_hr, expected):
38+
monkeypatch.setattr("tcconfig.shaper.htb.tick_in_usec", lambda: 15.625)
39+
rate = hr.BitsPerSecond(rate_hr).byte_per_sec
40+
adjusted_burst = adjusted_burst_size(desired_burst, rate)
41+
assert adjusted_burst == expected

0 commit comments

Comments
 (0)