getsentry / getsentry/sentry-python
`_generate_sample_rand` seeds a Mersenne Twister per Transaction, eagerly, even when unsampled (6.4 µs)
- Lenguaje dominante
- Python
- Estrellas
- 2.2k
- Forks
- 669
- Merge medio
- 1 d 1 h
- PR fusionados (30 d)
- 213
Descripción
### Summary
`Transaction.__init__` unconditionally computes `_generate_sample_rand(self.trace_id)`, and `_generate_sample_rand` seeds a Mersenne Twister to produce a single float. That is **6.4 µs per call** on CPython 3.14 / Apple M2, paid on every request through the ASGI integrations even when tracing is disabled and the value can never be used.
Two independent problems:
**1. It is eager.** `sentry_sdk/tracing.py`, `Transaction.__init__`:
```python
baggage_sample_rand = None if self._baggage is None else self._baggage._sample_rand()
if baggage_sample_rand is not None:
self._sample_rand = baggage_sample_rand
else:
self._sample_rand = _generate_sample_rand(self.trace_id)
```
`_sample_rand` is only read when a sampling decision is actually made. With `traces_sample_rate` unset the transaction is never sampled, so this is pure waste. Making it a lazy property costs nothing.
**2. It is expensive.** `sentry_sdk/tracing_utils.py`:
```python
def _generate_sample_rand(trace_id, *, interval=(0.0, 1.0)):
...
rng = Random(trace_id)
sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled)
return sample_rand_scaled / 1_000_000
```
`Random(seed)` runs the full MT19937 `init_by_array` over a 625-word state. Measured with `timeit`, 50k iterations, best of 5:
| | µs |
|---|---:|
| `Random(trace_id)` (32-char hex string) | 6.39 |
| `Random(int(trace_id, 16))` | 5.90 |
| `Random(12345)` | 5.89 |
| `int(trace_id, 16) / 2**128` | **0.23** |
The cost is the Mersenne Twister initialisation, not the string hashing - seeding with a small int is just as slow. Deriving a uniformly distributed value in `[0, 1)` arithmetically from the same trace id is **27x cheaper** and just as deterministic.
### Impact
On a do-nothing FastAPI endpoint with tracing disabled, making `_generate_sample_rand` cheap moves the SDK's per-request overhead from +61.3 µs to +53.3 µs (in-process measurement, baseline 15.6 µs/req) - about 13% of the SDK's cost, for a value that is discarded.
### Questions
1. Is the fix to `Transaction.__init__` simply making `_sample_rand` lazy? Happy to open a PR.
2. Is the exact output of `_generate_sample_rand` required to be bit-compatible across SDKs, or only to be deterministic-from-`trace_id` and uniformly distributed? If the latter, `int(trace_id, 16) / 2**128` (scaled into the requested interval) would be a drop-in replacement. If the former, the laziness fix alone still helps.
### Repro
```python
import timeit, uuid
from random import Random
tid = uuid.uuid4().hex
n = 50000
for label, fn in [
("Random(hex str)", lambda: Random(tid)),
("Random(int)", lambda: Random(int(tid, 16))),
("Random(12345)", lambda: Random(12345)),
("int(tid,16)/2**128", lambda: int(tid, 16) / 2**128),
]:
print(f"{label:<20} {min(timeit.repeat(fn, number=n, repeat=5)) / n * 1e6:.2f} us")
```
Environment: CPython 3.14.7, sentry-sdk 2.67.1, Apple M2.
Context: this was found while measuring 7400, where the discarded `Transaction` is the larger half of the same problem.
Guía de contribución
Línea de trabajo
Start in sentry_sdk/tracing.py at Transaction.__init__ and sentry_sdk/tracing_utils.py at _generate_sample_rand; inspect how _sample_rand is read and whether output compatibility is required. Run the issue's benchmark to compare the alternatives, then verify that unsampled transactions avoid unnecessary work while deterministic sampling behavior is preserved.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- python
- Área
- performance
- Tipo de issue
- Refactorización
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Estado de actividad
- Activo
- Claridad
- Bastante claro
- Aptitud para principiantes
- 64/100