perf: Source context re-reads the whole file via loader.get_source() for every frame

Aberta Para iniciantes
#8,089 1 comentário 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 1 dia

@alexon1234 já está trabalhando nisso.

Desde 20/10/2026.

  • #8092 de @alexon1234 — aberto

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

Improvement Python
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

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de getsentry/sentry-python

Todas as issues de getsentry/sentry-python

Issues semelhantes

Mais issues de Python

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.