diff --git a/.jules/bolt.md b/.jules/bolt.md index 254b8d5..6eca001 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -13,3 +13,7 @@ ## 2025-02-18 - Regex Pre-compilation in Hot Paths **Learning:** Re-compiling regexes inside a frequently called function (like `latex_escape` which runs for every string) creates significant overhead. Pre-compiling them at module level yielded a ~3.2x speedup. **Action:** Always look for regex compilations inside loops or frequently called functions and move them to module level constants. + +## 2025-02-18 - API Event Loop Starvation +**Learning:** In ASGI applications like FastAPI, running heavy synchronous tasks (like PDF compilation via `pdflatex`, running large ATS regex generation, or making blocking external AI calls) blocks the main thread, leading to event loop starvation and reducing throughput dramatically. +**Action:** Always wrap heavy synchronous functions using `functools.partial` and offload them to background worker threads using `await anyio.to_thread.run_sync()`. diff --git a/api/main.py b/api/main.py index 08b7b6c..9bc1182 100644 --- a/api/main.py +++ b/api/main.py @@ -1,8 +1,10 @@ +import functools import logging import os import tempfile from pathlib import Path +import anyio import yaml from fastapi import FastAPI, HTTPException, Response, Security from fastapi.middleware.cors import CORSMiddleware @@ -171,7 +173,13 @@ async def render_pdf(request: ResumeRequest): # We output to a temp file output_pdf = temp_path / "output.pdf" - generator.generate(variant=request.variant, output_format="pdf", output_path=output_pdf) + func = functools.partial( + generator.generate, + variant=request.variant, + output_format="pdf", + output_path=output_pdf, + ) + await anyio.to_thread.run_sync(func) if not output_pdf.exists(): raise HTTPException(status_code=500, detail="PDF generation failed") @@ -215,9 +223,12 @@ async def tailor_resume(request: TailorRequest): # We pass None for yaml_path as we use direct data generator = AIGenerator(yaml_path=None, config=config) - tailored_data = generator.tailor_data( - resume_data=request.resume_data, job_description=request.job_description + func = functools.partial( + generator.tailor_data, + resume_data=request.resume_data, + job_description=request.job_description, ) + tailored_data = await anyio.to_thread.run_sync(func) return tailored_data @@ -248,7 +259,10 @@ async def ats_check(request: ATSRequest): # Generate ATS report directly from resume data generator = ATSGenerator(config=config, resume_data=request.resume_data) - report = generator.generate_report(request.job_description, request.variant) + func = functools.partial( + generator.generate_report, request.job_description, request.variant + ) + report = await anyio.to_thread.run_sync(func) # Convert to JSON-serializable format result = { @@ -300,12 +314,14 @@ async def generate_cover_letter(request: CoverLetterRequest): # Generate cover letter (always use non-interactive for API) # The provided answers will be used as context by the AI - outputs, job_details = generator.generate_non_interactive( + func = functools.partial( + generator.generate_non_interactive, job_description=request.job_description, company_name=request.company_name, variant=request.variant, output_formats=[request.format], ) + outputs, job_details = await anyio.to_thread.run_sync(func) # Return the generated content # Note: outputs["md"] contains the rendered markdown, outputs["pdf"] contains LaTeX @@ -560,7 +576,10 @@ async def render_resume_pdf(resume_id: str, variant: str = "base"): generator = TemplateGenerator(yaml_path=resume_yaml_path) output_pdf = temp_path / "output.pdf" - generator.generate(variant=variant, output_format="pdf", output_path=output_pdf) + func = functools.partial( + generator.generate, variant=variant, output_format="pdf", output_path=output_pdf + ) + await anyio.to_thread.run_sync(func) if not output_pdf.exists(): raise HTTPException(status_code=500, detail="PDF generation failed")