Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@
## 2024-07-30 - Avoid chained string replace when checking character sets
**Learning:** Using chained `.replace(a, "").replace(b, "")` to check if a string consists entirely of specific characters requires intermediate string allocations for every call. In benchmarks, using `.strip("ab")` is ~30% faster and avoids multiple allocations in the hot path.
**Action:** When checking if a string is solely composed of specific characters, use `.strip(chars)` instead of chained `.replace()` calls to improve performance.
## 2026-07-16 - [FastAPI Upload Chunk Size Optimization]
**Learning:** In FastAPI, using the default 8KB chunk size for `await file.read()` when handling large uploads (e.g. 20MB) causes excessive asynchronous loop iterations, leading to high CPU overhead and slow upload speeds due to SpooledTemporaryFile underlying mechanics.
**Action:** For endpoints that accept large file uploads, increase the chunk size (e.g., to 1MB) in the `file.read(chunk_size)` loop to drastically reduce iteration overhead while maintaining bounded memory usage.
4 changes: 3 additions & 1 deletion src/newsdom_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from .service import parse_pdf

MAX_PARSE_UPLOAD_BYTES = 20 * 1024 * 1024
UPLOAD_READ_CHUNK_BYTES = 1024 * 1024
UNSUPPORTED_MEDIA_DETAIL = "Unsupported Media Type"
PAYLOAD_TOO_LARGE_DETAIL = "Payload Too Large"
INVALID_PARSE_PARAMS_DETAIL = "Invalid parse parameters"
Expand Down Expand Up @@ -250,7 +251,8 @@ async def parse(
tmp.write(header)

bytes_read = len(header)
while chunk := await file.read(8192):
# ⚡ Bolt: Increase chunk size to 1MB to significantly reduce async loop iteration overhead
while chunk := await file.read(UPLOAD_READ_CHUNK_BYTES):
bytes_read += len(chunk)
if bytes_read > MAX_PARSE_UPLOAD_BYTES:
LOGGER.warning(
Expand Down
14 changes: 14 additions & 0 deletions tests/test_parse_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from newsdom_api import mineru_runner
from newsdom_api.main import (
MAX_PARSE_UPLOAD_BYTES,
UPLOAD_READ_CHUNK_BYTES,
app,
parse,
_validate_pdf_structure,
Expand Down Expand Up @@ -555,3 +556,16 @@ def spy_unlink(self, missing_ok=False):
# We should have unlinked exactly one file, which should be in the temp directory
assert len(unlinked_paths) == 1
assert "tmp" in unlinked_paths[0].lower() or "temp" in unlinked_paths[0].lower()


@pytest.mark.asyncio
async def test_parse_endpoint_reads_with_correct_chunk_size(monkeypatch):
upload = _ReadTrackingUpload(b"%PDF-" + (b"x" * 1024 * 1024))
monkeypatch.setattr("newsdom_api.main.parse_pdf", lambda *args, **kwargs: None)
monkeypatch.setattr("newsdom_api.main._validate_pdf_structure", lambda _: None)

await parse(upload)

assert len(upload.read_sizes) >= 2
assert upload.read_sizes[0] == 5
assert upload.read_sizes[1] == UPLOAD_READ_CHUNK_BYTES
Loading