Cover daemon perf: plan - #621
Merged
Merged
Conversation
Single-file plan because the surface is small (codex/librarian/ covers/ — 6 files, ~440 LOC). The dominant finding is F3: parallelize the image pipeline via concurrent.futures. ProcessPoolExecutor. Eleven ranked findings: - F1: Pre-fetch DB paths in the parent. Drops N serial SELECTs to 1, and removes Django from the worker subprocess in F3. - F2: Skip-existing pre-filter in the parent. Drops dispatch overhead for already-cached covers. - F3: ProcessPoolExecutor for the image pipeline. Workers return bytes; parent writes to disk + updates status via as_completed. Headline win — embarrassingly parallel workload across CPU cores, today serial on one thread. - F4: Lazy + persistent pool lifecycle. Cold-start cost amortized across batches; one pool per cover-thread instance. - F5: CODEX_COVER_WORKERS setting. Default min(os.cpu_count(), 8); env var override for memory-tight installs. - F6: Single-pk inline fast path — speculative, defer. - F7: purge_cover_paths parallelization — skip (unlinks are cheap). - F8: Stale "Called from views/cover" docstring on create_cover_from_path. Cleanup. - F9: Empty @DataClass decorators on CoverTask / CoverRemoveAllTask / CoverRemoveOrphansTask / CoverCreateAllTask. Cosmetic. - F10: Status update batching — speculative, defer. - F11: Memory ceiling guidance. ~100 MB per worker × 8 = 800 MB worst case. Documented. Suggested ordering: F1 + F2 (pure refactor, no thread changes) -> F8 + F9 (cleanup) -> F3 + F4 + F5 (the big change). Pre- work first so F3 lands with the smallest unit-of-risk diff. Risks called out: comicbox + PIL subprocess safety (unverified but expected fine), COMICBOX_CONFIG import cost in workers, loguru cross-process logging, pool shutdown on hard kill, RAM ceiling under heavy worker count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced Apr 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Plan for
codex/librarian/covers/. Single-file plan because thesurface is small (6 files, ~440 LOC) — no meta split needed.
Pure planning deliverable, no code changes.
Eleven ranked findings
ProcessPoolExecutorfor the image pipelineCODEX_COVER_WORKERSsettingpurge_cover_pathsparallelizationcreate_cover_from_pathdocstring@dataclassdecorators on cover tasksHeadline finding
F3 — the cover thread today serializes a CPU-bound image
pipeline through a single worker:
Each iteration runs
comicbox.get_cover_page+ PILImage.thumbnail(LANCZOS, reducing_gap=3.0)+save("WEBP", method=6). On a multi-core box, 7 cores sit idle.Plan recommends
concurrent.futures.ProcessPoolExecutor(notthreads — PIL releases the GIL inconsistently across encoder
paths). Worker function is top-level (picklable), accepts
pre-resolved
(pk, db_path, cover_path_str, custom)tuples,returns
(pk, thumb_bytes, error_msg). Parent usesas_completedto stream results, writes to disk via theexisting atomic-replace
save_cover_to_cache, updates statusper completion.
F1 (pre-fetch DB paths) and F2 (skip-existing pre-filter) are
called out separately because they need to land first — F3's
worker subprocess shouldn't carry a Django connection, and the
batched path fetch + skip-existing filter remove that
dependency.
Suggested ordering
F3 builds on it.
creation, settings knob. Single PR; the unit of risk is the
subprocess introduction.
Risks flagged
comicbox+ PIL subprocess safety (expected fine, verify viasmoke test against a populated dev DB).
COMICBOX_CONFIGimport cost in workers — pass the dict as aworker arg vs. re-importing Django settings per subprocess.
for the parent to log, sidesteps the issue.
ProcessPoolExecutoratexithandler should cover it; signal handler if not.
Documented; honor
CODEX_COVER_WORKERSfor memory-tightinstalls.
References
tasks/coverd-perf/00-plan.mdcodex/librarian/scribe/importer/create/comics.py:173— mainCoverCreateTaskproducer (post-import batches)codex/views/browser/cover.py:104— the 202-pollper-request producer (single pk per task)
🤖 Generated with Claude Code