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

32 statements  

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

1"""Shared arithmetic for the detectors.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Iterable, Sequence 

6from hashlib import sha1 

7 

8from ..models import Query 

9 

10__all__ = ["common_frames", "fingerprint", "total_span_ms"] 

11 

12#: Bump when the *meaning* of a fingerprint changes, so that problems recorded 

13#: by an older version never silently merge with problems recorded by a newer 

14#: one. Two groups where there should be one is visible and fixable; one group 

15#: where there should be two is neither. 

16SCHEME = "1" 

17 

18 

19def total_span_ms(queries: Iterable[Query]) -> float: 

20 """Wall-clock time these statements occupied, counting overlap once. 

21 

22 Summing durations is the obvious thing and it is wrong here. Under 

23 `asyncio.gather` twenty queries can run concurrently in 40 ms of real 

24 time and sum to 700 ms; a threshold on the sum would call that an N+1 

25 worth fixing, when the concurrency *is* the fix. The union of the 

26 intervals is what the user actually waited. 

27 """ 

28 spans = sorted((q.started_ms, q.ended_ms) for q in queries) 

29 if not spans: 

30 return 0.0 

31 total = 0.0 

32 start, end = spans[0] 

33 for next_start, next_end in spans[1:]: 

34 if next_start > end: 

35 total += end - start 

36 start, end = next_start, next_end 

37 else: 

38 end = max(end, next_end) 

39 return total + (end - start) 

40 

41 

42def common_frames(stacks: Sequence[Sequence[str]]) -> list[str]: 

43 """The frames every one of these stacks begins with. 

44 

45 Stacks are stored innermost-last, so the shared *prefix* is the outer 

46 frames -- the request handler, the service, the function containing the 

47 loop -- and the last frame of that prefix is the deepest line all the 

48 executions came through. For `for row in rows: row.owner.name` that is the 

49 loop itself, which is the line someone has to edit. 

50 """ 

51 if not stacks: 

52 return [] 

53 shortest = min(len(s) for s in stacks) 

54 shared: list[str] = [] 

55 for depth in range(shortest): 

56 frame = stacks[0][depth] 

57 if any(stack[depth] != frame for stack in stacks): 

58 break 

59 shared.append(frame) 

60 return shared 

61 

62 

63def fingerprint(kind: str, *parts: str) -> str: 

64 """A stable identity for a problem, across requests and across runs. 

65 

66 The `SCHEME-kind-` prefix is not decoration. It makes the hash readable 

67 enough to grep for, keeps two detectors from ever colliding, and gives a 

68 place to record that the grouping rule changed. 

69 """ 

70 digest = sha1("|".join(parts).encode("utf-8", "replace"), usedforsecurity=False) 

71 return f"{SCHEME}-{kind}-{digest.hexdigest()[:16]}"