Files
Aislo/M01_MasterData/M01_MasterData_Store_Cache.py
T
eomsangdonandClaude Opus 5 3c6e68231c perf(M01): 로직·조합 목록이 마스터를 한 번만 읽고 막힘은 미리 세어 둠 (일감 32)
- `_Store_Cache` 신설 — 자국(시각·크기)이 바뀐 파일만 다시 읽음 · 목록은 검사를 돌리지 않고 세어 둔 막힘을 씀
- 요소·표가 바뀌면 모든 로직을, 로직 파일이 바뀌면 그 파일과 그 로직을 부르던 줄만 다시 셈
- 저장 뒤에는 고친 파일을 바로 다시 읽게 알려 줌 — 저장 직후 목록 2.5초 → 0.06초
- `/logics` 둘째 번부터 2.5초 → 0.02초 · `/combos` 0.3초 → 0.01초 · 켠 뒤 첫 번만 3.4초
- 시험 `test_m01_cache.py` 다섯 — 세어 둔 막힘이 줄마다 검사한 값과 같음 · 거름 그대로 · 저장 직후 비침

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
2026-09-22 02:17:49 +09:00

115 lines
4.9 KiB
Python

"""M01 마스터 한 번 읽어 두기 — 목록 길이 요청마다 마스터를 새로 읽지 않게.
`M01_MasterData_Store.loaded` · `.blocks` 가 씀(읽기 전용 길만).
자국(파일 시각·크기)이 바뀐 파일만 다시 읽고, 로직 막힘도 미리 세어 둠 —
목록은 검사를 돌리지 않고 세어 둔 값을 씀. 고치는 길(`calc` · `text` · `save`)은
사본을 만지므로 그대로 `cm.load` 로 새로 읽음.
"""
from __future__ import annotations
import json
import sys
import threading
from decimal import Decimal
from pathlib import Path
SCRIPTS = Path(__file__).resolve().parents[1] / "resources/master_data/scripts"
sys.path.insert(0, str(SCRIPTS))
import check_master as cm # noqa: E402
_FRESH = threading.RLock()
_READ: dict[str, tuple[tuple, dict]] = {} # 파일 이름 → (자국, 읽은 내용)
_INDEX: dict = {} # 폴더 · 자국 · files · whole
_BLOCK: dict[str, tuple[tuple, dict]] = {} # 로직 파일 → (자국, {키: (막힘 까닭, 부르는 로직)})
_BASE: dict[str, tuple] = {} # 막힘을 셀 때 바탕이던, 로직 아닌 파일의 자국
def _stamps(folder: Path) -> dict[str, tuple]:
"""첫 층 JSON 자국 — {이름: (시각, 크기)} · 밑줄 파일은 `cm.load` 가 안 읽으므로 뺌."""
out = {}
for path in sorted(folder.glob("*.json")):
if not path.name.startswith("_"):
stat = path.stat()
out[path.name] = (stat.st_mtime_ns, stat.st_size)
return out
def _logic_files(files: dict[str, dict]):
for name, data in files.items():
if data.get("그룹") == "로직":
yield name, data
def forget() -> None:
"""읽어 둔 것을 버림 — 파일을 고친 자리(저장)에서 부름."""
with _FRESH:
_READ.clear()
_INDEX.clear()
_BLOCK.clear()
_BASE.clear()
def stale(names) -> None:
"""그 파일만 다시 읽게 함 — 저장처럼 스스로 고친 자리에서 부름
(자국은 시각이 같은 틱이면 못 가릴 수 있으므로 고친 쪽이 알려 줌)."""
with _FRESH:
for name in names:
was = _READ.pop(name, (None, {}))[1]
_BLOCK.pop(name, None)
if was.get("그룹") != "로직":
_BASE.clear() # 요소·표가 바뀌면 모든 로직을 다시 셈
_INDEX.pop("자국", None)
def loaded(folder: Path) -> tuple[dict[str, dict], object]:
"""(파일 묶음, 색인) — **읽기 전용**. 고칠 것이면 `cm.load` 로 새로 읽을 것."""
with _FRESH:
if _INDEX.get("폴더") != folder: # 시험은 사본 폴더로 바꿈
forget()
_INDEX["폴더"] = folder
now = _stamps(folder)
for gone in [name for name in _READ if name not in now]:
del _READ[gone]
for name, stamp in now.items():
if _READ.get(name, (None,))[0] != stamp:
raw = (folder / name).read_bytes()
_READ[name] = (stamp, json.loads(raw, parse_float=Decimal, parse_int=Decimal))
if _INDEX.get("자국") != now:
files = {name: _READ[name][1] for name in now}
_INDEX.update(자국=now, files=files, whole=cm.mf.Master(files))
return _INDEX["files"], _INDEX["whole"]
def blocks(folder: Path) -> dict[str, list[str]]:
"""로직 키마다 막힘 까닭 — 미리 세어 둔 값. 요소 파일이 바뀌면 모든 로직을,
로직 파일은 바뀐 그 파일과 그 로직을 부르던 줄만 다시 셈."""
with _FRESH:
files, whole = loaded(folder)
stamps = _INDEX["자국"]
base = {n: s for n, s in stamps.items() if files[n].get("그룹") != "로직"}
if _BASE != base: # 인력 · 재료 · 표가 바뀌면 바탕이 달라짐
_BLOCK.clear()
_BASE.clear()
_BASE.update(base)
moved, fresh = set(), set() # 바뀐 로직 파일의 키(옛것 · 새것)
names = {name for name, _ in _logic_files(files)}
for gone in [name for name in _BLOCK if name not in names]: # 파일째 사라진 것
moved |= set(_BLOCK.pop(gone)[1])
for name, data in _logic_files(files):
if _BLOCK.get(name, (None,))[0] == stamps[name]:
continue
got = {str(r.get("키")): cm.mf.check_logic(whole, r) for r in data.get("줄") or []}
moved |= set(_BLOCK.get(name, (None, {}))[1]) | set(got)
_BLOCK[name] = (stamps[name], got)
fresh.add(name)
for name, data in _logic_files(files): # 다른 파일에서 그 로직을 부르던 줄
if name in fresh:
continue
got = _BLOCK[name][1]
rows = {str(r.get("키")): r for r in data.get("줄") or []}
for key in [k for k, (_, calls) in got.items() if calls & moved]:
got[key] = cm.mf.check_logic(whole, rows[key])
return {key: why for _, got in _BLOCK.values() for key, (why, _) in got.items()}