Coverage for src/asgi_profiler/detectors/slow_query.py: 100%

22 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-22 16:59 +0000

1"""One statement that is slow on its own.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Iterator 

6 

7from ..models import Problem, Profile 

8from .settings import Settings 

9from .utils import fingerprint 

10 

11TYPE = "slow_db_query" 

12 

13 

14def detect(profile: Profile, settings: Settings) -> Iterator[Problem]: 

15 """Statements over `slow_query_ms`, one problem per distinct statement. 

16 

17 Reported per statement rather than per execution: an endpoint that runs 

18 the same slow query three times has one problem to fix, not three. 

19 

20 Truncated statements are skipped. Their stored text ends in the original 

21 character count, so two executions of the same oversized statement hash 

22 differently and the "problem" would be a new one every time -- worse than 

23 silence, because it would fill the issues page with noise that never 

24 resolves. 

25 """ 

26 threshold = settings.slow_query_ms 

27 worst: dict[str, tuple[int, float]] = {} 

28 hits: dict[str, list[int]] = {} 

29 for index, query in enumerate(profile.queries): 

30 if query.duration_ms < threshold or query.truncated: 

31 continue 

32 key = query.sql_hash 

33 hits.setdefault(key, []).append(index) 

34 if key not in worst or query.duration_ms > worst[key][1]: 

35 worst[key] = (index, query.duration_ms) 

36 

37 for key, (index, duration) in worst.items(): 

38 query = profile.queries[index] 

39 occurrences = hits[key] 

40 times = "once" if len(occurrences) == 1 else f"{len(occurrences)} times" 

41 yield Problem( 

42 type=TYPE, 

43 fingerprint=fingerprint(TYPE, key), 

44 title=f"Slow {query.operation}: {duration:.0f} ms", 

45 detail=( 

46 f"One statement took {duration:.1f} ms, which is " 

47 f"{duration / profile.duration_ms:.0%} of the request. " 

48 f"It ran {times}." 

49 ), 

50 evidence={ 

51 "sql": query.sql, 

52 "duration_ms": round(duration, 3), 

53 "occurrences": len(occurrences), 

54 "frame": query.stack[-1] if query.stack else "", 

55 "operation": query.operation, 

56 }, 

57 offenders=occurrences[:50], 

58 time_ms=duration, 

59 )