perf: Source context re-reads the whole file via loader.get_source() for every frame
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 72/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- python
- Ambito
- backend, performance
Direzione di ricerca
Inizia leggendo get_lines_from_file() in sentry_sdk.utils e confronta il modo in cui usa loader.get_source() con il ripiego linecache. Il riproduttore dell’issue misura le chiamate al loader e verifica che il payload dell’eccezione rimanga identico; usa questi controlli per confermare che la modifica è completata.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Problem Statement
When include_source_context=True (the default), get_lines_from_file() calls loader.get_source(module) and then .splitlines() for every frame of every event, whenever the frame's module has a __loader__ with get_source (i.e. any normally imported module). SourceFileLoader.get_source() re-reads and decodes the whole file each time; linecache, which is the fallback a few lines below, keeps it in memory.
All of this runs synchronously in the thread that raises/logs the error, before the event reaches the transport worker. In asyncio apps that log many handled exceptions in a burst, this cost is paid on the event loop (related: #2824). sample_rate / before_send don't avoid it, since the frames are built before they run (#4365).
Reproducer (only sentry_sdk + stdlib, events go to a no-op transport):
repro_source_context.py
"""Source context re-reads the whole source file through loader.get_source() for every frame of every event."""
import importlib.machinery, linecache, logging, os, sys, tempfile, time
import sentry_sdk
import sentry_sdk.utils
from sentry_sdk.transport import Transport
SENT = []
class NullTransport(Transport):
def capture_envelope(self, envelope):
SENT.append(envelope.get_event())
sentry_sdk.init(dsn="http://[email protected]/1", transport=NullTransport())
# A ~1,500-line module; the exception passes through 10 frames in it.
code = "def f0():\n raise TimeoutError('boom')\n"
code += "".join(f"def f{i}():\n return f{i - 1}()\n" for i in range(1, 10))
code += "".join(f"# filler line {j}\n" for j in range(1500))
tmp = tempfile.mkdtemp()
with open(os.path.join(tmp, "bigmod.py"), "w") as f:
f.write(code)
sys.path.insert(0, tmp)
import bigmod # noqa: E402
calls = {"get_source": 0}
_get_source = importlib.machinery.SourceFileLoader.get_source
def counting_get_source(self, name):
calls["get_source"] += 1
return _get_source(self, name)
importlib.machinery.SourceFileLoader.get_source = counting_get_source
log = logging.getLogger("repro")
log.addHandler(logging.NullHandler())
log.propagate = False
def emit():
try:
bigmod.f9()
except TimeoutError:
log.exception("something failed")
def bench(n=1000):
SENT.clear()
calls["get_source"] = 0
t = time.perf_counter()
for _ in range(n):
emit()
return 1000 * (time.perf_counter() - t) / n, calls["get_source"] / n, SENT[-1]
_orig = sentry_sdk.utils.get_lines_from_file
def linecache_first(filename, lineno, max_length=None, loader=None, module=None):
# Prefer the cached linecache copy; only fall back to the loader (zipimport,
# frozen apps, ...) when linecache has nothing for this file.
if linecache.getlines(filename):
loader = None
return _orig(filename, lineno, max_length, loader, module)
emit() # warm up
ms_a, gs_a, ev_a = bench()
sentry_sdk.utils.get_lines_from_file = linecache_first
ms_b, gs_b, ev_b = bench()
print(f"sentry-sdk {sentry_sdk.VERSION}, Python {sys.version.split()[0]}")
print(f"stock: {ms_a:.3f} ms/event, loader.get_source() calls/event: {gs_a:.0f}")
print(f"linecache first: {ms_b:.3f} ms/event, loader.get_source() calls/event: {gs_b:.0f}")
print("identical exception payload:", ev_a["exception"] == ev_b["exception"])
Results with sentry-sdk==2.72.0 (same code on master), Linux x86_64:
Python 3.14.8
stock: 3.693 ms/event, loader.get_source() calls/event: 11
linecache first: 2.166 ms/event, loader.get_source() calls/event: 0
identical exception payload: True
Python 3.13.16
stock: 2.937 ms/event, loader.get_source() calls/event: 11
linecache first: 2.466 ms/event, loader.get_source() calls/event: 0
identical exception payload: True
Solution Brainstorm
Read source from linecache first and only fall back to the loader when linecache has nothing for that file (zipimport, PyInstaller/frozen apps, see #812). Alternatively linecache.getlines(filename, frame.f_globals) gives the loader fallback and caching in one call. The output payload is unchanged (checked in the script).
- Lingua principale
- Python
- Stelle
- 2.2k
- Fork
- 698
- Merge medio
- 1g 3h
- PR unite (30g)
- 158
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di getsentry/sentry-python
-
fix(aws-lambda): Load handler module only once in the layerForse già presa @pabloDeputter l’ha presa 6 giorni fa. ApertaImprovement Integration: AWS Lambda Python Traces
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
getsentry/sentry-python#8079 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
fix(aws-lambda): Only set `messaging.batch.message_count` for messaging batchesForse di nuovo libera Una pull request per questa issue è stata chiusa senza essere unita. ApertaImprovement Integration: AWS Lambda Python Traces
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
getsentry/sentry-python#8078 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
fix(boto3): Set `aws.dynamodb.item_collection_metrics` scrubbing to `auto`Forse già presa @pabloDeputter l’ha presa 6 giorni fa. ApertaImprovement Integration: Boto3 Python Traces
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
getsentry/sentry-python#8074 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
`litellm`: Set request parametersForse di nuovo libera Una pull request per questa issue è stata chiusa senza essere unita. ApertaImprovement Python Spans
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
getsentry/sentry-python#7994 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
`cohere`: Set request parametersForse di nuovo libera @mohanprasath-dev l’ha presa 18 giorni fa e non c’è nessuna pull request aperta. ApertaImprovement Python Spans Waiting for: Product Owner
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
getsentry/sentry-python#7992 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di getsentry/sentry-python
Issue simili
-
area:scripts area:tests bug priority:later
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
Ikalus1988/MisakaNet#4358 ·
I maintainer di solito rispondono entro 1 giorno
-
Discovered ports from service discovery are never propagated to modules in Module.load()Forse già presa @vishalmore90 l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
OWASP/Nettacker#1828 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
aws-samples/sample-per-model-bedrock#708 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
-
[Performance] Bound resolve_api_hashes raw fallback reads to the 1 MiB region actually scannedAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
sjkim1127/Reversecore_MCP#365 ·
I maintainer di solito rispondono entro 1 giorno