perf: Source context re-reads the whole file via loader.get_source() for every frame
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 72/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- python
- Lĩnh vực
- backend, performance
Hướng nghiên cứu
Bắt đầu bằng cách đọc get_lines_from_file() trong sentry_sdk.utils và so sánh cách hàm này sử dụng loader.get_source() với phương án dự phòng linecache. Mã tái hiện issue đo số lần gọi loader và kiểm tra payload của exception vẫn giống hệt; hãy dùng các kiểm tra đó để xác nhận thay đổi đã hoàn tất.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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).
- Ngôn ngữ chính
- Python
- Star
- 2.2k
- Fork
- 698
- Merge trung bình
- 1 ngày 3 giờ
- Pull request đã merge (30 ngày)
- 158
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của getsentry/sentry-python
-
fix(aws-lambda): Load handler module only once in the layerCó thể đã có người làm @pabloDeputter đã nhận 6 ngày trước. Đang mởImprovement Integration: AWS Lambda Python Traces
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
getsentry/sentry-python#8079 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
fix(aws-lambda): Only set `messaging.batch.message_count` for messaging batchesCó thể làm lại được Pull request cho issue này đã bị đóng mà không được merge. Đang mởImprovement Integration: AWS Lambda Python Traces
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
getsentry/sentry-python#8078 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
fix(boto3): Set `aws.dynamodb.item_collection_metrics` scrubbing to `auto`Có thể đã có người làm @pabloDeputter đã nhận 6 ngày trước. Đang mởImprovement Integration: Boto3 Python Traces
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
getsentry/sentry-python#8074 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`litellm`: Set request parametersCó thể làm lại được Pull request cho issue này đã bị đóng mà không được merge. Đang mởImprovement Python Spans
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
getsentry/sentry-python#7994 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`cohere`: Set request parametersCó thể làm lại được @mohanprasath-dev đã nhận 19 ngày trước và không có pull request nào đang mở. Đang mởImprovement Python Spans Waiting for: Product Owner
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
getsentry/sentry-python#7992 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của getsentry/sentry-python
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
earthtojake/text-to-cad#758 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
domain:upgrades enhancement needs-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
gke-labs/kube-agents#4634 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
agardnerIT/opamp-server-py#116 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement good first issue
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
elviskahoro/sdk-python-attio#144 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày