perf: Source context re-reads the whole file via loader.get_source() for every frame
Mantenedores costumam responder em até 1 dia
Avaliação
- Dificuldade
- 2/5
- Tempo estimado
- 1-3 horas
- Facilidade para iniciantes
- 72/100
- Tipo de issue
- Bug
- Clareza
- Claramente especificada
- Status de atividade
- Ativa
- Stack de tecnologia
- python
- Domínio
- backend, performance
Direção de pesquisa
Comece lendo get_lines_from_file() em sentry_sdk.utils e compare como ele usa loader.get_source() com a alternativa linecache. O código de reprodução da issue mede as chamadas ao loader e verifica se o payload da exceção permanece idêntico; use essas verificações para confirmar que a alteração foi concluída.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
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).
- Linguagem predominante
- Python
- Estrelas
- 2.2k
- Forks
- 698
- Merge médio
- 1d 3h
- PRs com merge (30d)
- 158
Preparar o ambiente
- Sem Dockerfile nem arquivo Docker Compose
- Tem um modelo de pull request
- Ler o guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de getsentry/sentry-python
-
fix(aws-lambda): Load handler module only once in the layerTalvez já em andamento @pabloDeputter assumiu há 7 dias. AbertaImprovement Integration: AWS Lambda Python Traces
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
getsentry/sentry-python#8079 · 1 comentário · 1 responsável ·
Mantenedores costumam responder em até 1 dia
-
fix(aws-lambda): Only set `messaging.batch.message_count` for messaging batchesTalvez livre de novo Um pull request para esta issue foi fechado sem ser mesclado. AbertaImprovement Integration: AWS Lambda Python Traces
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 68/100
getsentry/sentry-python#8078 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
fix(boto3): Set `aws.dynamodb.item_collection_metrics` scrubbing to `auto`Talvez já em andamento @pabloDeputter assumiu há 7 dias. AbertaImprovement Integration: Boto3 Python Traces
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 67/100
getsentry/sentry-python#8074 · 1 comentário · 1 responsável ·
Mantenedores costumam responder em até 1 dia
-
`litellm`: Set request parametersTalvez livre de novo Um pull request para esta issue foi fechado sem ser mesclado. AbertaImprovement Python Spans
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 68/100
getsentry/sentry-python#7994 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
`cohere`: Set request parametersTalvez livre de novo @mohanprasath-dev assumiu há 19 dias e não há nenhum pull request aberto. AbertaImprovement Python Spans Waiting for: Product Owner
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 65/100
getsentry/sentry-python#7992 · 2 comentários ·
Mantenedores costumam responder em até 1 dia
Todas as issues de getsentry/sentry-python
Issues semelhantes
-
Dificuldade 1/5 1-3 horas Facilidade para iniciantes 78/100
QuantConnect/Documentation#2838 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 70/100
awslabs/route53-dynamic-dns-with-lambda#67 · 1 comentário ·
-
`SubAgents` returns a child run's `DeferredToolRequests` to the parent as a successful tool resultAbertabug pkg:harness
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 70/100
pydantic/pydantic-ai#12405 ·
Mantenedores costumam responder em até 1 dia
-
good first issue help wanted
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
-
good first issue help wanted
Dificuldade 1/5 Menos de uma hora Facilidade para iniciantes 82/100