Skip to content

Commit fc8eb28

Browse files
authored
perf: improve performance of internal RateLimiter class
The main improvement comes from removing the calls to `compat.monotonic`, and instead passing in the `span.start_ns` to `RateLimiter.is_allowed`. This change removes 2 calls to `compat.monotonic()` and drastically improves the performance of the rate limiter. See #3041 for more details.
1 parent a3c94a7 commit fc8eb28

File tree

4 files changed

+126
-143
lines changed

4 files changed

+126
-143
lines changed

ddtrace/internal/rate_limiter.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ class RateLimiter(object):
1313

1414
__slots__ = (
1515
"_lock",
16-
"current_window",
17-
"last_update",
16+
"current_window_ns",
17+
"last_update_ns",
1818
"max_tokens",
1919
"prev_window_rate",
2020
"rate_limit",
@@ -38,54 +38,54 @@ def __init__(self, rate_limit):
3838
self.tokens = rate_limit # type: float
3939
self.max_tokens = rate_limit
4040

41-
self.last_update = compat.monotonic()
41+
self.last_update_ns = compat.monotonic_ns()
4242

43-
self.current_window = 0 # type: float
43+
self.current_window_ns = 0 # type: float
4444
self.tokens_allowed = 0
4545
self.tokens_total = 0
4646
self.prev_window_rate = None # type: Optional[float]
4747

4848
self._lock = threading.Lock()
4949

50-
def is_allowed(self):
51-
# type: () -> bool
50+
def is_allowed(self, timestamp_ns):
51+
# type: (int) -> bool
5252
"""
5353
Check whether the current request is allowed or not
5454
5555
This method will also reduce the number of available tokens by 1
5656
57+
:param int timestamp_ns: timestamp in nanoseconds for the current request.
5758
:returns: Whether the current request is allowed or not
5859
:rtype: :obj:`bool`
5960
"""
6061
# Determine if it is allowed
61-
allowed = self._is_allowed()
62+
allowed = self._is_allowed(timestamp_ns)
6263
# Update counts used to determine effective rate
63-
self._update_rate_counts(allowed)
64+
self._update_rate_counts(allowed, timestamp_ns)
6465
return allowed
6566

66-
def _update_rate_counts(self, allowed):
67-
# type: (bool) -> None
68-
now = compat.monotonic()
69-
67+
def _update_rate_counts(self, allowed, timestamp_ns):
68+
# type: (bool, int) -> None
7069
# No tokens have been seen yet, start a new window
71-
if not self.current_window:
72-
self.current_window = now
70+
if not self.current_window_ns:
71+
self.current_window_ns = timestamp_ns
7372

7473
# If more than 1 second has past since last window, reset
75-
elif now - self.current_window >= 1.0:
74+
# DEV: We are comparing nanoseconds, so 1e9 is 1 second
75+
elif timestamp_ns - self.current_window_ns >= 1e9:
7676
# Store previous window's rate to average with current for `.effective_rate`
7777
self.prev_window_rate = self._current_window_rate()
7878
self.tokens_allowed = 0
7979
self.tokens_total = 0
80-
self.current_window = now
80+
self.current_window_ns = timestamp_ns
8181

8282
# Keep track of total tokens seen vs allowed
8383
if allowed:
8484
self.tokens_allowed += 1
8585
self.tokens_total += 1
8686

87-
def _is_allowed(self):
88-
# type: () -> bool
87+
def _is_allowed(self, timestamp_ns):
88+
# type: (int) -> bool
8989
# Rate limit of 0 blocks everything
9090
if self.rate_limit == 0:
9191
return False
@@ -96,24 +96,24 @@ def _is_allowed(self):
9696

9797
# Lock, we need this to be thread safe, it should be shared by all threads
9898
with self._lock:
99-
self._replenish()
99+
self._replenish(timestamp_ns)
100100

101101
if self.tokens >= 1:
102102
self.tokens -= 1
103103
return True
104104

105105
return False
106106

107-
def _replenish(self):
108-
# type: () -> None
107+
def _replenish(self, timestamp_ns):
108+
# type: (int) -> None
109109
# If we are at the max, we do not need to add any more
110110
if self.tokens == self.max_tokens:
111111
return
112112

113113
# Add more available tokens based on how much time has passed
114-
now = compat.monotonic()
115-
elapsed = now - self.last_update
116-
self.last_update = now
114+
# DEV: We store as nanoseconds, convert to seconds
115+
elapsed = (timestamp_ns - self.last_update_ns) / 1e9
116+
self.last_update_ns = timestamp_ns
117117

118118
# Update the number of available tokens, but ensure we do not exceed the max
119119
self.tokens = min(
@@ -147,11 +147,11 @@ def effective_rate(self):
147147
return (self._current_window_rate() + self.prev_window_rate) / 2.0
148148

149149
def __repr__(self):
150-
return "{}(rate_limit={!r}, tokens={!r}, last_update={!r}, effective_rate={!r})".format(
150+
return "{}(rate_limit={!r}, tokens={!r}, last_update_ns={!r}, effective_rate={!r})".format(
151151
self.__class__.__name__,
152152
self.rate_limit,
153153
self.tokens,
154-
self.last_update,
154+
self.last_update_ns,
155155
self.effective_rate,
156156
)
157157

ddtrace/sampler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ def sample(self, span):
290290
self._set_priority(span, USER_KEEP)
291291

292292
# Ensure all allowed traces adhere to the global rate limit
293-
allowed = self.limiter.is_allowed()
293+
allowed = self.limiter.is_allowed(span.start_ns)
294294
# Always set the sample rate metric whether it was allowed or not
295295
# DEV: Setting this allows us to properly compute metrics and debug the
296296
# various sample rates that are getting applied to this span

ddtrace/span.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def __init__(
140140
self.metrics = {} # type: _MetricDictType
141141

142142
# timing
143-
self.start_ns = time_ns() if start is None else int(start * 1e9)
143+
self.start_ns = time_ns() if start is None else int(start * 1e9) # type: int
144144
self.duration_ns = None # type: Optional[int]
145145

146146
# tracing

0 commit comments

Comments
 (0)