perf(826-1): M02 층 목록 요약 색인 — 자국 같으면 항목 파일 안 엶 · 판 · 요약 · 쓰는곳수 기억
- 새 M02_Layer_Index — 항목마다 자국(고친 때 · 크기) · 판 · 서고 요약 기억(메모리 + storage _derived/m02_layer_index) · 바뀐 항목만 다시 읽어 셈 - 기준금액 기댐 = 셈 코드 내용 · M01 폴더 · 층 곁 파일 자국 + 셈 때 읽은 참조 파일 자국(refs.SEEN) - 쓰는곳수 = 요청마다 쓰는 곳 파일 한 번 훑어 거꾸로 셈 · 참조 이름표 기억도 파생 저장본(ids.json) - 판 확인(저장 · 지움 409)은 그대로 파일 내용 · 목록 답 내용 · 차례 그대로(8004 바이트 대조) - 개인 층(102 줄) 두 번째 3.0 → 0.14 초 · 다시 뜬 뒤 첫 9.9 → 0.24 초 · 마스터 2.9 → 0.10 초 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from contextvars import ContextVar
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -125,11 +126,18 @@ def copy_ref(body: dict[str, Any], key: str) -> Ref:
|
||||
}
|
||||
|
||||
|
||||
#: 참조 파일을 읽기 직전에 부름(826-1 층 목록 색인 기댐 · `M02_Layer_Index`) — None = 안 부름.
|
||||
SEEN: ContextVar[Callable[[Path], None] | None] = ContextVar("m02_refs_seen", default=None)
|
||||
|
||||
|
||||
def read_ref(ref: Ref) -> dict[str, Any] | None:
|
||||
if is_copy(ref):
|
||||
return ref["몸"]
|
||||
try:
|
||||
got = json.loads(ref_path(ref).read_bytes())
|
||||
path = ref_path(ref)
|
||||
if (note := SEEN.get()) is not None:
|
||||
note(path)
|
||||
got = json.loads(path.read_bytes())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return got if isinstance(got, dict) else None
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""M02 층 목록 요약 색인(826-1 · 요구 804 · 조사 804 안 A) — 층 목록이 항목 파일을 다시 열지 않게.
|
||||
|
||||
항목 기록 `{자국, 판, 바탕?, 기댐?, 요약?}`(열쇠 「종류/이름」):
|
||||
자국 파일 고친 때 ns : 크기 — 같으면 기억한 판(목록 표시만 · 저장 · 지움 409 는 그대로
|
||||
`layers.version_of` 파일 내용)
|
||||
요약 서고 줄 `M02_Labor_Ref.summary`(쓰는곳수 뺌) — 자국 · 바탕 · 기댐이 모두 같을 때만 씀
|
||||
바탕 셈 코드 내용 · M01 폴더 · 층 곁 파일(내 기계 등 — 항목 폴더 · manifest 뺌) 자국 지문
|
||||
기댐 셈 때 읽은 참조 파일(SC · SK) 자국(`refs.SEEN`)
|
||||
쓰는곳수 = 요청마다 쓰는 곳 파일을 한 번 훑어 거꾸로 셈(`refs._ids_of` 기억 · 파일당 stat 한 번).
|
||||
자리 = 프로세스 메모리 + storage `_derived/m02_layer_index/`(파생 저장본 · 층마다 한 파일 ·
|
||||
참조 이름표 기억 `ids.json`) — 없거나 깨지면 다시 지음 · 지워도 됨 · 다른 PC · 옛 코드는 안 읽음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import threading
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_derived_store import derived_dir, derived_key, file_stamp, read_derived
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from config import config_system
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
KIND = "m02_layer_index"
|
||||
SHAPE = 1 # 기록 꼴이 바뀌면 올림(옛 기록 = 다시 셈)
|
||||
IDS = "ids"
|
||||
CODE_DIRS = ("M02_MasterTemplete", "M01_MasterData", "resources/master_data/scripts", "common_util")
|
||||
_LOCK = threading.RLock()
|
||||
_MEM: dict[str, dict[str, Any]] = {} # 층 열쇠 → {항목: 기록}
|
||||
_IDS_LOADED: list[bool] = []
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _code() -> str:
|
||||
"""셈 코드 내용 지문 — 프로세스에 한 번(코드가 바뀌면 서버가 다시 뜸 · 같은 코드 = 같은 값)."""
|
||||
digest = hashlib.blake2b(digest_size=16)
|
||||
for sub in CODE_DIRS:
|
||||
for path in sorted((config_system.PROJECT_ROOT / sub).glob("*.py")):
|
||||
digest.update(path.name.encode())
|
||||
digest.update(path.read_bytes())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _stamps(folder: Path, skip: tuple[str, ...] = ()) -> list[tuple[str, str | None]]:
|
||||
"""폴더 안 파일 자국 — 첫 단 이름이 `skip` · `_` · `.` 머리인 것은 뺌."""
|
||||
if not folder.is_dir():
|
||||
return []
|
||||
out = []
|
||||
for path in sorted(folder.rglob("*")):
|
||||
rel = path.relative_to(folder).parts
|
||||
if rel[0] in skip or any(p.startswith(("_", ".")) for p in rel) or not path.is_file():
|
||||
continue
|
||||
out.append(("/".join(rel), file_stamp(path)))
|
||||
return out
|
||||
|
||||
|
||||
def _ground(folder: Path) -> str:
|
||||
"""바탕 지문 — 셈 코드 · M01 폴더 · 이 층 · 지금 층 기본 자리의 곁 파일."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
skip = (*layers.KINDS, layers.MANIFEST_NAME)
|
||||
side = [_stamps(f, skip) for f in dict.fromkeys((folder, store.root()))]
|
||||
return derived_key(SHAPE, _code(), str(m01.FOLDER), _stamps(Path(m01.FOLDER)), side)
|
||||
|
||||
|
||||
# ── 기록 자리 ─────────────────────────────────────────────────────────
|
||||
def _book(folder: Path) -> tuple[str, dict[str, Any]]:
|
||||
key = derived_key(SHAPE, str(folder.resolve()))
|
||||
if key not in _MEM:
|
||||
got = read_derived(layers.storage_root(), KIND, key)
|
||||
value = got.get("value") if got else None
|
||||
_MEM[key] = value if isinstance(value, dict) else {}
|
||||
return key, _MEM[key]
|
||||
|
||||
|
||||
def _save(key: str, value: Any) -> None:
|
||||
path = derived_dir(layers.storage_root(), KIND) / f"{key}.json"
|
||||
try:
|
||||
atomic_write_json(path, {"key": key, "value": value})
|
||||
except OSError: # 파생 저장본 — 못 써도 다음에 다시 셈
|
||||
pass
|
||||
|
||||
|
||||
# ── 목록 · 판 ─────────────────────────────────────────────────────────
|
||||
def list_rows(folder: str | Path) -> list[dict[str, Any]]:
|
||||
"""`layers.list_templates` 와 같은 줄 — 판은 자국이 같으면 기억한 것(파일 안 엶)."""
|
||||
folder = Path(folder)
|
||||
with _LOCK:
|
||||
key, recs = _book(folder)
|
||||
dirty: list[str] = []
|
||||
|
||||
def version(path: Path) -> str | None:
|
||||
name = f"{path.parent.name}/{path.stem}"
|
||||
stamp = file_stamp(path) # 읽기 전 자국 — 그새 바뀌면 다음에 다시 셈
|
||||
rec = recs.get(name)
|
||||
if stamp and isinstance(rec, dict) and rec.get("자국") == stamp and "판" in rec:
|
||||
return rec["판"]
|
||||
recs[name] = {"자국": stamp, "판": layers.version_of(path)}
|
||||
dirty.append(name)
|
||||
return recs[name]["판"]
|
||||
|
||||
rows = layers.list_templates(folder, version)
|
||||
have = {f"{row['종류']}/{row['이름']}" for row in rows}
|
||||
gone = [name for name in recs if name not in have]
|
||||
for name in gone:
|
||||
del recs[name]
|
||||
if dirty or gone:
|
||||
_save(key, recs)
|
||||
return rows
|
||||
|
||||
|
||||
# ── 서고 요약 ─────────────────────────────────────────────────────────
|
||||
def _good(rec: Any, stamp: str | None, ground: str) -> bool:
|
||||
if not (stamp and isinstance(rec, dict) and rec.get("자국") == stamp):
|
||||
return False
|
||||
deps = rec.get("기댐")
|
||||
if rec.get("바탕") != ground or not isinstance(rec.get("요약"), dict):
|
||||
return False
|
||||
return isinstance(deps, dict) and all(file_stamp(Path(p)) == s for p, s in deps.items())
|
||||
|
||||
|
||||
def _fresh(folder: Path, kind: str, name: str, rec: Any, stamp: str | None, whole: Any) -> dict:
|
||||
"""그 항목만 다시 읽어 셈 — 참조는 읽기 직전 자국을 기댐에 적음."""
|
||||
from M02_MasterTemplete.M02_Labor_Expand import Expand
|
||||
|
||||
path = folder / kind / f"{name}.json"
|
||||
deps: dict[str, str | None] = {}
|
||||
|
||||
def note(p: Path) -> None:
|
||||
deps.setdefault(str(p), file_stamp(p))
|
||||
|
||||
token = refs.SEEN.set(note)
|
||||
try: # 셈마다 새 펼침 — 다른 항목 셈 기억을 나눠 쓰면 기댐이 빠짐
|
||||
out = refs.summary(folder, kind, name, Expand(whole), [])
|
||||
finally:
|
||||
refs.SEEN.reset(token)
|
||||
out.pop("쓰는곳수", None)
|
||||
same = isinstance(rec, dict) and rec.get("자국") == stamp and "판" in rec
|
||||
return {
|
||||
"자국": stamp,
|
||||
"판": rec["판"] if same else layers.version_of(path),
|
||||
"기댐": deps,
|
||||
"요약": out,
|
||||
}
|
||||
|
||||
|
||||
def summaries(folder: str | Path, rows: list[dict[str, Any]]) -> None:
|
||||
"""`refs.summaries` 와 같은 칸 · 차례 — 기록이 맞으면 파일 안 엶 · 다르면 그 항목만 셈."""
|
||||
folder = Path(folder)
|
||||
lib = [row for row in rows if row["종류"] in layers.LIBRARY_KINDS]
|
||||
if not lib:
|
||||
return
|
||||
with _LOCK:
|
||||
key, recs = _book(folder)
|
||||
ground, whole, dirty = _ground(folder), None, False
|
||||
for row in lib:
|
||||
name = f"{row['종류']}/{row['이름']}"
|
||||
path = folder / row["종류"] / f"{row['이름']}.json"
|
||||
stamp = file_stamp(path)
|
||||
if not _good(recs.get(name), stamp, ground):
|
||||
if whole is None:
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
whole = m01.loaded()[1]
|
||||
rec = _fresh(folder, row["종류"], row["이름"], recs.get(name), stamp, whole)
|
||||
recs[name] = {**rec, "바탕": ground}
|
||||
dirty = True
|
||||
row.update(recs[name]["요약"])
|
||||
if dirty:
|
||||
_save(key, recs)
|
||||
sk = [row for row in lib if row["종류"] == "sangeun" and "고친줄수" in row]
|
||||
if sk:
|
||||
uses, (layer, owner) = _uses(), refs.where(folder)
|
||||
for row in sk:
|
||||
rid = refs.ref_id({"층": layer, "주인": owner, "키": row["이름"]})
|
||||
row["쓰는곳수"] = uses.get(rid, 0)
|
||||
|
||||
|
||||
# ── 쓰는 곳 수 ────────────────────────────────────────────────────────
|
||||
def _uses() -> dict[str, int]:
|
||||
"""참조 이름표 → 담은 파일 수(`refs.users` 개수와 같음) — 쓰는 곳 파일 한 번 훑음."""
|
||||
_load_ids()
|
||||
before = {path: hit[0] for path, hit in refs._IDS.items()}
|
||||
files = [path for _what, _at, path in refs.use_files()]
|
||||
count: dict[str, int] = {}
|
||||
for path in files:
|
||||
got = refs._ids_of(path)
|
||||
for rid in got[1] if got else ():
|
||||
count[rid] = count.get(rid, 0) + 1
|
||||
if any(before.get(path) != refs._IDS[path][0] for path in files if path in refs._IDS):
|
||||
_save_ids()
|
||||
return count
|
||||
|
||||
|
||||
def _load_ids() -> None:
|
||||
"""참조 이름표 기억을 파생 저장본에서 채움 — 프로세스에 한 번 · 자국은 `_ids_of` 가 다시 봄."""
|
||||
if _IDS_LOADED:
|
||||
return
|
||||
_IDS_LOADED.append(True)
|
||||
got = read_derived(layers.storage_root(), KIND, IDS)
|
||||
for text, one in ((got or {}).get("value") or {}).items():
|
||||
try:
|
||||
mtime, size, name, ids = one
|
||||
refs._IDS.setdefault(Path(text), ((int(mtime), int(size)), (str(name), frozenset(ids))))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
|
||||
def _save_ids() -> None:
|
||||
"""지금 기억을 디스크 것에 덧씀(다른 서버 몫 유지) · 없어진 파일 뺌."""
|
||||
got = read_derived(layers.storage_root(), KIND, IDS)
|
||||
value = dict((got or {}).get("value") or {})
|
||||
for path, ((mtime, size), (name, ids)) in list(refs._IDS.items()):
|
||||
value[str(path)] = [mtime, size, name, sorted(ids)]
|
||||
value = {text: one for text, one in value.items() if Path(text).is_file()}
|
||||
_save(IDS, value)
|
||||
@@ -27,6 +27,7 @@ from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool, run_with_connection
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
from M02_MasterTemplete import M02_Layer_Index as index
|
||||
from M02_MasterTemplete import M02_Library_Flat as flat
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router
|
||||
@@ -275,14 +276,14 @@ async def list_layer(
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> dict[str, Any]:
|
||||
folder = await _layer_dir(session, layer, project_id=project_id, user_id=user_id, owner=owner)
|
||||
rows = await asyncio.to_thread(layers.list_templates, folder)
|
||||
rows = await asyncio.to_thread(index.list_rows, folder) # 판 = 자국 기억(826-1)
|
||||
manifest = await asyncio.to_thread(layers.read_manifest, folder)
|
||||
for row in rows:
|
||||
row["출처"] = manifest.get(f"{row['종류']}/{row['이름']}")
|
||||
if (row["종류"], row["이름"]) in store.IN_USE: # 화면이 지우기 확인 창 전에 막음
|
||||
row["쓰는곳"] = store.IN_USE[(row["종류"], row["이름"])]
|
||||
# 서고(50-3) — 보이는 이름 · 단위 · 기준 금액(51-4)
|
||||
await asyncio.to_thread(refs.summaries, folder, rows)
|
||||
await asyncio.to_thread(index.summaries, folder, rows) # 요약 색인(826-1)
|
||||
return {"층": layer, "양식": rows}
|
||||
|
||||
|
||||
|
||||
@@ -132,8 +132,10 @@ def _modified(path: Path) -> str:
|
||||
return utc_from_timestamp(path.stat().st_mtime)
|
||||
|
||||
|
||||
def list_templates(layer_dir: str | Path) -> list[dict[str, Any]]:
|
||||
"""층 하나의 양식 목록 `[{종류, 이름, 판, 수정일}]` — 종류 · 이름 차례."""
|
||||
def list_templates(layer_dir: str | Path, version: Any = None) -> list[dict[str, Any]]:
|
||||
"""층 하나의 양식 목록 `[{종류, 이름, 판, 수정일}]` — 종류 · 이름 차례 ·
|
||||
`version` = 판 셈(없으면 `version_of` · 층 목록은 자국 기억 `M02_Layer_Index`)."""
|
||||
version = version or version_of
|
||||
base = Path(layer_dir)
|
||||
rows: list[dict[str, Any]] = []
|
||||
for kind in KINDS:
|
||||
@@ -147,7 +149,7 @@ def list_templates(layer_dir: str | Path) -> list[dict[str, Any]]:
|
||||
{
|
||||
"종류": kind,
|
||||
"이름": path.stem,
|
||||
"판": version_of(path),
|
||||
"판": version(path),
|
||||
"수정일": _modified(path),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user