Skip to content

Commit 0a495ff

Browse files
committed
limit concurrent diagnostics
1 parent 23cb0da commit 0a495ff

File tree

3 files changed

+53
-5
lines changed

3 files changed

+53
-5
lines changed

jedi_language_server/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
"""Constants."""
2+
3+
MAX_CONCURRENT_DEBOUNCE_CALLS = 10
4+
"""The maximum number of concurrent calls allowed by the debounce decorator."""

jedi_language_server/jedi_utils.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
)
4242
from pygls.workspace import TextDocument
4343

44+
from .constants import MAX_CONCURRENT_DEBOUNCE_CALLS
4445
from .initialization_options import HoverDisableOptions, InitializationOptions
4546
from .type_map import get_lsp_completion_type, get_lsp_symbol_type
4647

@@ -53,13 +54,15 @@
5354
P = ParamSpec("P")
5455

5556

57+
_debounce_semaphore = threading.Semaphore(MAX_CONCURRENT_DEBOUNCE_CALLS)
58+
59+
5660
def debounce(
57-
interval_s: int, keyed_by: Optional[str] = None
61+
interval_s: float, keyed_by: Optional[str] = None
5862
) -> Callable[[Callable[P, None]], Callable[P, None]]:
5963
"""Debounce calls to this function until interval_s seconds have passed.
6064
61-
Decorator copied from https://github.com/python-lsp/python-lsp-
62-
server
65+
Decorator adapted from https://github.com/python-lsp/python-lsp-server
6366
"""
6467

6568
def wrapper(func: Callable[P, None]) -> Callable[P, None]:
@@ -68,20 +71,23 @@ def wrapper(func: Callable[P, None]) -> Callable[P, None]:
6871

6972
@functools.wraps(func)
7073
def debounced(*args: P.args, **kwargs: P.kwargs) -> None:
74+
_debounce_semaphore.acquire()
75+
7176
sig = inspect.signature(func)
7277
call_args = sig.bind(*args, **kwargs)
7378
key = call_args.arguments[keyed_by] if keyed_by else None
7479

7580
def run() -> None:
7681
with lock:
7782
del timers[key]
78-
return func(*args, **kwargs)
83+
func(*args, **kwargs)
84+
_debounce_semaphore.release()
7985

8086
with lock:
8187
old_timer = timers.get(key)
8288
if old_timer:
8389
old_timer.cancel()
84-
90+
_debounce_semaphore.release()
8591
timer = threading.Timer(interval_s, run)
8692
timers[key] = timer
8793
timer.start()

tests/test_debounce.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import threading
2+
import time
3+
from collections import Counter
4+
5+
from jedi_language_server.constants import MAX_CONCURRENT_DEBOUNCE_CALLS
6+
from jedi_language_server.jedi_utils import debounce
7+
8+
9+
def test_debounce() -> None:
10+
"""Test that the debounce decorator delays and limits the number of concurrent calls."""
11+
# Create a function that records call counts per URI.
12+
counter = Counter[str]()
13+
cond = threading.Condition()
14+
15+
def f(uri: str) -> None:
16+
with cond:
17+
counter.update(uri)
18+
cond.notify_all()
19+
20+
# Call the debounced function more than the max allowed concurrent calls.
21+
debounced = debounce(interval_s=1, keyed_by="uri")(f)
22+
for _ in range(3):
23+
debounced("0")
24+
for i in range(1, MAX_CONCURRENT_DEBOUNCE_CALLS + 10):
25+
debounced(str(i))
26+
27+
# Wait for at least the max allowed concurrent timers to complete.
28+
with cond:
29+
assert cond.wait_for(
30+
lambda: sum(counter.values()) >= MAX_CONCURRENT_DEBOUNCE_CALLS
31+
)
32+
33+
# Check the counter after 0.5 seconds to ensure that no additional timers
34+
# were started and have completed.
35+
time.sleep(0.5)
36+
assert sum(counter.values()) == MAX_CONCURRENT_DEBOUNCE_CALLS
37+
38+
# For uri "0", only one timer should have been started despite 3 calls.
39+
assert counter["0"] == 1

0 commit comments

Comments
 (0)