runner: 진행 상황 출력 — 프로그램별 호출 건수와 호출 하나 끝날 때마다 [i/n]·경과·예상 남은 시간 (stderr)

고객사 실측: unit 100+ 인 프로그램은 15분 넘게 아무것도 안 찍혀 멈춘 것으로 보였다.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
byeongwook.choi
2026-09-21 18:04:47 +09:00
co-authored by Claude Fable 5.1
parent c577d5cb93
commit ce459ba8f0
+18 -3
View File
@@ -17,6 +17,7 @@ import argparse
import json import json
import re import re
import sqlite3 import sqlite3
import sys
import time import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -428,18 +429,29 @@ def _llm_call(llm, system: str, prompt: str) -> dict:
return {"error": e} return {"error": e}
def _progress(i: int, n: int, t0: float, c: dict) -> None:
"""호출 하나가 끝날 때마다 한 줄 — 긴 프로그램에서 멈춘 것처럼 보이지 않게 (stderr)."""
state = "실패" if "error" in c else ("대기" if c.get("pending") else "ok")
elapsed = time.time() - t0
eta = elapsed / i * (n - i) if i else 0
print(f" [{i}/{n}] {state} {elapsed:.0f}s 경과, 약 {eta:.0f}s 남음", file=sys.stderr, flush=True)
def _run_calls_parallel(llm, calls: list[dict], concurrency: int) -> None: def _run_calls_parallel(llm, calls: list[dict], concurrency: int) -> None:
"""calls 각 원소의 'prompt' 를 호출하고 결과를 그 자리에 채운다 (in-place).""" """calls 각 원소의 'prompt' 를 호출하고 결과를 그 자리에 채운다 (in-place)."""
if not calls: if not calls:
return return
t0 = time.time()
if concurrency <= 1 or len(calls) == 1: if concurrency <= 1 or len(calls) == 1:
for c in calls: for i, c in enumerate(calls, 1):
c.update(_llm_call(llm, SYSTEM_PROMPT, c["prompt"])) c.update(_llm_call(llm, SYSTEM_PROMPT, c["prompt"]))
_progress(i, len(calls), t0, c)
return return
with ThreadPoolExecutor(max_workers=min(concurrency, len(calls))) as ex: with ThreadPoolExecutor(max_workers=min(concurrency, len(calls))) as ex:
futures = {ex.submit(_llm_call, llm, SYSTEM_PROMPT, c["prompt"]): c for c in calls} futures = {ex.submit(_llm_call, llm, SYSTEM_PROMPT, c["prompt"]): c for c in calls}
for fut in as_completed(futures): for i, fut in enumerate(as_completed(futures), 1):
futures[fut].update(fut.result()) futures[fut].update(fut.result())
_progress(i, len(futures), t0, futures[fut])
def _dedupe_plan(con: sqlite3.Connection, pending: list[sqlite3.Row]) -> tuple[list[sqlite3.Row], dict]: def _dedupe_plan(con: sqlite3.Connection, pending: list[sqlite3.Row]) -> tuple[list[sqlite3.Row], dict]:
@@ -569,7 +581,10 @@ def summarize_units(program: str | None, limit: int | None, fake: bool = False,
jobs.append((u, [dict(x) for x in prompts])) jobs.append((u, [dict(x) for x in prompts]))
# 2) LLM 호출 (병렬, DB 접근 없음) — 수정사항 10번 # 2) LLM 호출 (병렬, DB 접근 없음) — 수정사항 10번
_run_calls_parallel(llm, [c for _, calls in jobs for c in calls], conc) all_calls = [c for _, calls in jobs for c in calls]
print(f"[{p['name']}] unit {len(units)}건 → LLM 호출 {len(all_calls)}건 (동시 {conc})",
file=sys.stderr, flush=True)
_run_calls_parallel(llm, all_calls, conc)
# 3) 검증·저장 (DB 쓰기, 메인 스레드) # 3) 검증·저장 (DB 쓰기, 메인 스레드)
for u, calls in jobs: for u, calls in jobs: