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
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-22 16:59 +0000
1"""Shared arithmetic for the detectors."""
3from __future__ import annotations
5from collections.abc import Iterable, Sequence
6from hashlib import sha1
8from ..models import Query
10__all__ = ["common_frames", "fingerprint", "total_span_ms"]
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"
19def total_span_ms(queries: Iterable[Query]) -> float:
20 """Wall-clock time these statements occupied, counting overlap once.
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)
42def common_frames(stacks: Sequence[Sequence[str]]) -> list[str]:
43 """The frames every one of these stacks begins with.
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
63def fingerprint(kind: str, *parts: str) -> str:
64 """A stable identity for a problem, across requests and across runs.
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]}"