perf(M01): 요소·표 저장도 흔들리는 로직만 검사 — 「어느 로직이 무엇을 보는지」 지도 (일감 33)
- 인력 한 줄 저장 7.0초 → 0.9초 · 자재품목 7.2초 → 2.0초 · 소요량 표 1.7초 · 자재품목 저장 뒤 목록 2.5초 → 0.45초 · 켠 뒤 첫 목록 3.4초 → 1.6초 - 로직 줄을 셀 때 `Master.get` 을 덧옷(`_Watch`)으로 감싸 그 줄이 본 자리를 모음 — 키 · 「ID:원문번호」 · 없는 키를 물은 것까지 · 재료 고르기 줄은 자재품목 묶음 통째 - 저장은 그 지도로 흔들리는 줄만 검사(고친 키 · 고치기 전후 원문번호 · 지운 줄) · 요소를 고친 저장은 연결·준용 검사를 그대로 돌림 - 저장 뒤 목록은 흔들린 자리를 알려 받아(`stale(names, marks)`) 그 줄만 다시 셈 · 다른 창이 고친 파일이면 예전처럼 통째로 다시 셈 - 자재품목을 「구분」 으로 미리 갈라 둠(`master_material.by_class`) — 고르기 후보를 3만 줄에서 훑지 않음 · 가른 차례는 파일 차례 그대로라 고르는 줄은 안 바뀜 - 전수 확인 — 요소·표 파일 90 개마다 줄을 통째로 비우고 로직 1,354 줄을 다시 검사, 달라진 줄이 모두 지도 안(지도 밖 0) - 시험 `test_m01_cache.py` 에 셋 더함(열둘) — 쓰이는 요소를 지우면 저장이 막힘 · 요소를 고쳐 저장한 뒤 막힘 표시가 줄마다 검사한 것과 같음 · 인력 줄을 비운 전수 대조 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
자국(파일 시각·크기)이 바뀐 파일만 다시 읽고, 로직 막힘도 미리 세어 둠 —
|
||||
목록은 검사를 돌리지 않고 세어 둔 값을 씀. 고치는 길(`calc` · `text` · `save`)은
|
||||
사본을 만지므로 그대로 `cm.load` 로 새로 읽음.
|
||||
저장 전 검사(`check_saved`)도 여기 — 세어 둔 부르기 그림으로 고친 줄 둘레만 봄(`narrow`).
|
||||
저장 전 검사(`check_saved`)도 여기 — 어느 로직이 어느 요소·표·로직을 보는지 세어 두고
|
||||
그것으로 저장이 흔드는 줄만 검사함(`narrow`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,12 +20,14 @@ SCRIPTS = Path(__file__).resolve().parents[1] / "resources/master_data/scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
import check_master as cm # noqa: E402
|
||||
import master_material as mm # noqa: E402 (자재품목 테이블ID — 재료 고르기는 그 묶음을 훑음)
|
||||
|
||||
_FRESH = threading.RLock()
|
||||
_READ: dict[str, tuple[tuple, dict]] = {} # 파일 이름 → (자국, 읽은 내용)
|
||||
_INDEX: dict = {} # 폴더 · 자국 · files · whole
|
||||
_BLOCK: dict[str, tuple[tuple, dict]] = {} # 로직 파일 → (자국, {키: (막힘 까닭, 부르는 로직)})
|
||||
_BLOCK: dict[str, tuple[tuple, dict]] = {} # 로직 파일 → (자국, {키: `_check` 가 센 것})
|
||||
_BASE: dict[str, tuple] = {} # 막힘을 셀 때 바탕이던, 로직 아닌 파일의 자국
|
||||
_MARKED: dict[str, set] = {} # 우리가 고친 요소·표 파일 → 흔들린 자리(`narrow` 가 고른 것)
|
||||
|
||||
|
||||
def _stamps(folder: Path) -> dict[str, tuple]:
|
||||
@@ -43,6 +46,36 @@ def _logic_files(files: dict[str, dict]):
|
||||
yield name, data
|
||||
|
||||
|
||||
class _Watch:
|
||||
"""`Master.get` 을 감싸 그 로직 줄이 본 요소·표를 적어 둠 — 저장 때 검사를 좁히는 지도."""
|
||||
|
||||
def __init__(self, whole):
|
||||
self._whole, self.seen = whole, set()
|
||||
|
||||
def get(self, ref):
|
||||
self.seen.add(str(ref))
|
||||
return self._whole.get(ref)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._whole, name)
|
||||
|
||||
def __setattr__(self, name, value): # 속에 적어 둠 — 미리 갈라 둔 것을 줄마다 다시 안 짜게
|
||||
if name in ("_whole", "seen"):
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
setattr(self._whole, name, value)
|
||||
|
||||
|
||||
def _check(whole, row: dict) -> tuple[list[str], set, set]:
|
||||
"""(막힘 까닭, 부르는 로직, 본 요소·표) — 본 것은 키 · 「ID:원문번호」 ·
|
||||
재료 고르기가 있으면 자재품목 묶음 통째(「#MT」 · 후보를 그 안에서 고름)."""
|
||||
watch = _Watch(whole)
|
||||
why, calls = cm.mf.check_logic(watch, row)
|
||||
if any(isinstance(h.get("요소"), dict) for h in row.get("호표") or []):
|
||||
watch.seen.add("#" + mm.MARKET)
|
||||
return why, calls, watch.seen
|
||||
|
||||
|
||||
def forget() -> None:
|
||||
"""읽어 둔 것을 버림 — 파일을 고친 자리(저장)에서 부름."""
|
||||
with _FRESH:
|
||||
@@ -50,17 +83,19 @@ def forget() -> None:
|
||||
_INDEX.clear()
|
||||
_BLOCK.clear()
|
||||
_BASE.clear()
|
||||
_MARKED.clear()
|
||||
|
||||
|
||||
def stale(names) -> None:
|
||||
def stale(names, marks=()) -> None:
|
||||
"""그 파일만 다시 읽게 함 — 저장처럼 스스로 고친 자리에서 부름
|
||||
(자국은 시각이 같은 틱이면 못 가릴 수 있으므로 고친 쪽이 알려 줌)."""
|
||||
(자국은 시각이 같은 틱이면 못 가릴 수 있으므로 고친 쪽이 알려 줌).
|
||||
`marks` = 요소·표에서 흔들린 자리 — 그 자리를 보던 로직 줄만 다시 셈."""
|
||||
with _FRESH:
|
||||
for name in names:
|
||||
was = _READ.pop(name, (None, {}))[1]
|
||||
_BLOCK.pop(name, None)
|
||||
if was.get("그룹") != "로직":
|
||||
_BASE.clear() # 요소·표가 바뀌면 모든 로직을 다시 셈
|
||||
_MARKED.setdefault(name, set()).update(marks)
|
||||
_INDEX.pop("자국", None)
|
||||
|
||||
|
||||
@@ -90,10 +125,20 @@ def blocks(folder: Path) -> dict[str, list[str]]:
|
||||
files, whole = loaded(folder)
|
||||
stamps = _INDEX["자국"]
|
||||
base = {n: s for n, s in stamps.items() if files[n].get("그룹") != "로직"}
|
||||
marks = None # 요소·표에서 흔들린 자리 — None 이면 무엇이 바뀐지 모름(통째로 다시)
|
||||
if _BASE != base: # 인력 · 재료 · 표가 바뀌면 바탕이 달라짐
|
||||
_BLOCK.clear()
|
||||
off = {n for n in set(_BASE) | set(base) if _BASE.get(n) != base.get(n)}
|
||||
marks = set()
|
||||
for name in off:
|
||||
if name not in _MARKED: # 다른 창이 고친 파일 — 어디가 흔들렸는지 모름
|
||||
marks = None
|
||||
break
|
||||
marks |= _MARKED[name]
|
||||
if marks is None:
|
||||
_BLOCK.clear()
|
||||
_BASE.clear()
|
||||
_BASE.update(base)
|
||||
_MARKED.clear()
|
||||
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]: # 파일째 사라진 것
|
||||
@@ -101,45 +146,64 @@ def blocks(folder: Path) -> dict[str, list[str]]:
|
||||
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 []}
|
||||
got = {str(r.get("키")): _check(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): # 다른 파일에서 그 로직을 부르던 줄
|
||||
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()}
|
||||
shaken = [
|
||||
k
|
||||
for k, (_, calls, seen) in got.items()
|
||||
if calls & moved or (marks and seen & marks)
|
||||
]
|
||||
for key in shaken:
|
||||
got[key] = _check(whole, rows[key])
|
||||
return {key: why for _, got in _BLOCK.values() for key, (why, *_) in got.items()}
|
||||
|
||||
|
||||
def graph(folder: Path) -> dict[str, set]:
|
||||
"""로직 키마다 부르는 로직 키 — 미리 세어 둔 것(저장 때 검사를 좁히는 데 씀)."""
|
||||
def counted(folder: Path) -> dict[str, tuple]:
|
||||
"""로직 키마다 (막힘 까닭, 부르는 로직, 본 요소·표) — 미리 세어 둔 것."""
|
||||
with _FRESH:
|
||||
blocks(folder)
|
||||
return {key: calls for _, got in _BLOCK.values() for key, (_, calls) in got.items()}
|
||||
return {key: got for _, rows in _BLOCK.values() for key, got in rows.items()}
|
||||
|
||||
|
||||
def _rows_of(data: dict) -> list:
|
||||
return data.get("표") or data.get("줄") or []
|
||||
|
||||
|
||||
def narrow(folder: Path, batch: list[dict], before: dict, after: dict):
|
||||
"""(검사할 로직 키, 안 보는 줄이 부르는 키) — 저장이 건드린 줄과 그 줄을 부르던 줄만.
|
||||
"""(검사할 로직 키, 안 보는 줄이 부르는 키, 요소도 고쳤는지) — 저장이 흔드는 줄만.
|
||||
|
||||
로직 밖 파일(요소 · 표)이 섞이면 어느 로직이 흔들릴지 모르므로 None — 전부 검사.
|
||||
로직 줄은 고친 줄과 **그 줄을 부르던 줄** · 요소·표 줄은 그 줄을 **보던 로직**
|
||||
(키 · 「ID:원문번호」 · 자재품목은 묶음 통째 — `_Watch` 가 적어 둔 지도).
|
||||
"""
|
||||
if any(after[part["file"]].get("그룹") != "로직" for part in batch):
|
||||
return None
|
||||
calls, touched, gone = graph(folder), set(), set()
|
||||
counts = counted(folder)
|
||||
calls = {key: got[1] for key, got in counts.items()}
|
||||
touched, gone, marks = set(), set(), set()
|
||||
for part in batch:
|
||||
was, now = [
|
||||
{str(r.get("키")) for r in side[part["file"]].get("줄") or []}
|
||||
for side in (before, after)
|
||||
]
|
||||
touched |= now - was # 더한 줄(키는 저장 때 대장이 냄)
|
||||
gone |= was - now
|
||||
touched |= {str(c.get("key")) for c in part.get("changes") or [] if c.get("op") != "add"}
|
||||
name = part["file"]
|
||||
was, now = ({str(r.get("키")): r for r in _rows_of(side[name])} for side in (before, after))
|
||||
keys = (set(now) - set(was)) | {
|
||||
str(c.get("key")) for c in part.get("changes") or [] if c.get("op") != "add"
|
||||
}
|
||||
if after[name].get("그룹") == "로직":
|
||||
touched |= keys
|
||||
gone |= set(was) - set(now)
|
||||
continue
|
||||
for key in keys | (set(was) - set(now)): # 지운 줄도 — 가리키던 자리가 끊김
|
||||
marks |= {key, "#" + key[:2]}
|
||||
for side in (was, now):
|
||||
number = (side.get(key) or {}).get("원문번호")
|
||||
if number:
|
||||
marks.add(f"{key[:2]}:{number}")
|
||||
only = touched | {key for key, called in calls.items() if called & touched}
|
||||
return only, {key: called for key, called in calls.items() if key not in gone}
|
||||
only |= {key for key, got in counts.items() if got[2] & marks}
|
||||
return only, {key: called for key, called in calls.items() if key not in gone}, marks
|
||||
|
||||
|
||||
def check_saved(files: dict, changed: list[str], book: dict, narrowed=None) -> list[str]:
|
||||
@@ -150,7 +214,10 @@ def check_saved(files: dict, changed: list[str], book: dict, narrowed=None) -> l
|
||||
"""
|
||||
whole = cm.mf.Master(files)
|
||||
out = [x for name in changed for x in cm.check_form(name, files[name])]
|
||||
return out + cm.check_logics(files, whole, book, narrowed) + cm.mcb.check_all(files, whole)
|
||||
out += cm.check_logics(files, whole, book, narrowed and narrowed[:2])
|
||||
if narrowed and narrowed[2]: # 요소·표를 고쳤으면 연결 · 준용도 봄(로직 좁힘과 따로)
|
||||
out += cm.check_links(whole)
|
||||
return out + cm.mcb.check_all(files, whole)
|
||||
|
||||
|
||||
def warm(folder: Path) -> threading.Thread:
|
||||
|
||||
Reference in New Issue
Block a user