perf(infra): NAS CPU 중기 2건 + 1건 보류 (CHECK_POINT 🟡)
#6 insta-lab Chromium Browser Pool — Playwright/Chromium 인스턴스를 모듈 레벨에서 보관하고 매 슬레이트마다 reuse. 카드 10장 렌더의 launch 비용 (~3초/회)이 사라짐. startup/shutdown lifecycle hook 추가. crashed/disconnected 시 lazy 재초기화. #8 realestate-lab 수집 병렬화 — collect_all과 delete_old_completed가 서로 다른 데이터 영역이라 ThreadPoolExecutor(2)로 병렬. asyncio.gather 대신 thread executor를 쓴 이유는 BackgroundScheduler+동기 함수 환경 에서 자연스럽고 추가 의존성 없기 때문. 매칭은 일관성 유지로 순차. #7 stock async — 보류. 재진단 결과 stock은 BackgroundScheduler 사용 중이라 main loop 블로킹 없음. fetch 4회는 network I/O wait가 대부분이라 to_thread도 의미 없음. 진짜 효과를 보려면 AsyncIOScheduler 전환 + aiohttp 병렬이라 큰 리팩토링. 박재오 판단 대기. CHECK_POINT.md 갱신. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,47 @@ def _render_semaphore() -> asyncio.Semaphore:
|
||||
return _RENDER_SEMAPHORE
|
||||
|
||||
|
||||
# Chromium 브라우저 풀 — 매 슬레이트마다 launch 하지 않고 1개를 살려둠.
|
||||
# (CHECK_POINT 중기-6) 카드 10장 렌더 시간 ~30% 단축 기대.
|
||||
_PLAYWRIGHT = None
|
||||
_BROWSER = None
|
||||
|
||||
|
||||
async def init_browser() -> None:
|
||||
"""앱 startup hook에서 1회 호출. 이미 살아있으면 no-op."""
|
||||
global _PLAYWRIGHT, _BROWSER
|
||||
if _BROWSER is not None and _BROWSER.is_connected():
|
||||
return
|
||||
_PLAYWRIGHT = await async_playwright().start()
|
||||
_BROWSER = await _PLAYWRIGHT.chromium.launch()
|
||||
logger.info("Chromium browser pool 초기화 완료")
|
||||
|
||||
|
||||
async def shutdown_browser() -> None:
|
||||
"""앱 shutdown hook에서 1회 호출."""
|
||||
global _PLAYWRIGHT, _BROWSER
|
||||
if _BROWSER is not None:
|
||||
try:
|
||||
await _BROWSER.close()
|
||||
except Exception:
|
||||
logger.exception("browser close 중 예외 (무시)")
|
||||
_BROWSER = None
|
||||
if _PLAYWRIGHT is not None:
|
||||
try:
|
||||
await _PLAYWRIGHT.stop()
|
||||
except Exception:
|
||||
logger.exception("playwright stop 중 예외 (무시)")
|
||||
_PLAYWRIGHT = None
|
||||
|
||||
|
||||
async def _get_browser():
|
||||
"""현재 브라우저 핸들 반환. crashed/None이면 재초기화 후 반환."""
|
||||
global _BROWSER
|
||||
if _BROWSER is None or not _BROWSER.is_connected():
|
||||
await init_browser()
|
||||
return _BROWSER
|
||||
|
||||
|
||||
def _resolve_template_dir() -> str:
|
||||
"""Prefer config CARD_TEMPLATE_DIR if it exists; else fall back to in-repo templates/."""
|
||||
if os.path.isdir(CARD_TEMPLATE_DIR):
|
||||
@@ -97,29 +138,28 @@ async def _render_slate_locked(slate_id: int, template: str) -> List[str]:
|
||||
out_dir = _slate_dir(slate_id)
|
||||
paths: List[str] = []
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
try:
|
||||
ctx = await browser.new_context(viewport={"width": 1080, "height": 1350})
|
||||
page = await ctx.new_page()
|
||||
for spec in pages:
|
||||
html_str = tmpl.render(**spec)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False, encoding="utf-8") as f:
|
||||
f.write(html_str)
|
||||
html_path = f.name
|
||||
browser = await _get_browser()
|
||||
ctx = await browser.new_context(viewport={"width": 1080, "height": 1350})
|
||||
try:
|
||||
page = await ctx.new_page()
|
||||
for spec in pages:
|
||||
html_str = tmpl.render(**spec)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False, encoding="utf-8") as f:
|
||||
f.write(html_str)
|
||||
html_path = f.name
|
||||
try:
|
||||
await page.goto(f"file://{html_path}", wait_until="networkidle")
|
||||
out_path = os.path.join(out_dir, f"{spec['page_no']:02d}.png")
|
||||
await page.screenshot(path=out_path, full_page=False, omit_background=False)
|
||||
with open(out_path, "rb") as fp:
|
||||
file_hash = hashlib.md5(fp.read()).hexdigest()
|
||||
db.add_card_asset(slate_id, spec["page_no"], out_path, file_hash)
|
||||
paths.append(out_path)
|
||||
finally:
|
||||
try:
|
||||
await page.goto(f"file://{html_path}", wait_until="networkidle")
|
||||
out_path = os.path.join(out_dir, f"{spec['page_no']:02d}.png")
|
||||
await page.screenshot(path=out_path, full_page=False, omit_background=False)
|
||||
with open(out_path, "rb") as fp:
|
||||
file_hash = hashlib.md5(fp.read()).hexdigest()
|
||||
db.add_card_asset(slate_id, spec["page_no"], out_path, file_hash)
|
||||
paths.append(out_path)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(html_path)
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
await browser.close()
|
||||
os.unlink(html_path)
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
await ctx.close()
|
||||
return paths
|
||||
|
||||
@@ -31,9 +31,16 @@ app.add_middleware(
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
async def on_startup():
|
||||
os.makedirs(INSTA_DATA_PATH, exist_ok=True)
|
||||
db.init_db()
|
||||
# Chromium browser pool 초기화 (CHECK_POINT 중기-6)
|
||||
await card_renderer.init_browser()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def on_shutdown():
|
||||
await card_renderer.shutdown_browser()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
|
||||
Reference in New Issue
Block a user