feat(도구): 전국 미리 계산 두 PC 나눔 — 몫 · PC 별 기록 · CPU 상한 · 저전력 코어 지정 (PLAN 618-1 · 요구 507~509)
- --part A-B/N = 도엽 번호 crc32 % N 몫 · 지형 단계가 0차곡 이웃(목록 밖 · 띠 넘음 포함)까지 몫대로 셈 - 0차곡 = 남의 몫 이웃 지형이 창고에 없으면 미룸 · --defer-wait 초 뒤 다시 - 진행 기록 = _precompute_progress_<컴퓨터 이름>.json · 옛 한 벌은 성공만 읽어 이어 받음 · --status 모든 PC 합침 · 창고 실물 수 - --cpu-cap = 전체 CPU 평균이 넘으면 새 도엽 안 줌 · --e-cores --e-workers = 일꾼 앞선 몇 명만 그 논리 CPU 지정 - 죽은 잠금 치움 = 자기 몫 도엽만 · --root 시험용 창고 자리 - test_612: 지형 단계 이웃 일감 덧 · 기록 파일 이름 바뀜에 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ def fake_store(tmp_path, monkeypatch):
|
|||||||
calls: list[tuple[str, str]] = []
|
calls: list[tuple[str, str]] = []
|
||||||
fail = {"36901002"}
|
fail = {"36901002"}
|
||||||
|
|
||||||
def fake_run(step: str, no: str):
|
def fake_run(step: str, no: str, part=None):
|
||||||
calls.append((step, no))
|
calls.append((step, no))
|
||||||
if no in fail:
|
if no in fail:
|
||||||
return step, no, {"ok": False, "error": "RuntimeError: 받기 실패"}
|
return step, no, {"ok": False, "error": "RuntimeError: 받기 실패"}
|
||||||
@@ -85,8 +85,9 @@ def test_resume_skips_done_and_retries_failed(fake_store) -> None:
|
|||||||
sheets = ["36901001", "36901002", "36901003"]
|
sheets = ["36901001", "36901002", "36901003"]
|
||||||
progress = tool.load_progress()
|
progress = tool.load_progress()
|
||||||
tool.run(sheets, ["e07", "dem"], 1, progress, say=lambda _: None)
|
tool.run(sheets, ["e07", "dem"], 1, progress, say=lambda _: None)
|
||||||
assert [s for s, _ in calls] == ["dem"] * 3 + ["e07"] * 3 # 차례 고정
|
# 차례 고정 · 지형 단계는 0차곡 이웃(3x3)까지 셈(618-1)
|
||||||
saved = json.loads((root / tool.PROGRESS_NAME).read_text(encoding="utf-8"))
|
assert [s for s, n in calls if n in sheets] == ["dem"] * 3 + ["e07"] * 3
|
||||||
|
saved = json.loads(tool.progress_path().read_text(encoding="utf-8"))
|
||||||
assert saved["steps"]["dem"]["36901001"]["ok"] is True
|
assert saved["steps"]["dem"]["36901001"]["ok"] is True
|
||||||
assert saved["steps"]["dem"]["36901002"]["ok"] is False
|
assert saved["steps"]["dem"]["36901002"]["ok"] is False
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
"""PLAN 618-1 — 전국 미리 계산 두 PC 나눔(`tools/precompute_terrain.py`) 지킴 시험.
|
||||||
|
|
||||||
|
나눔 겹침 0 · 합치면 전체 · 이웃 일감 한 PC 만 · PC 별 기록 이어 받기 · CPU 상한(가짜 값) ·
|
||||||
|
0차곡 남의 몫 이웃 없으면 미룸 · 지정 코어 인자 풀기 ·
|
||||||
|
셈 함수는 가짜로 바꿔 서버 · 전국 파일 없이 돎.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from B04_PreProcess.B04_PreProcess_Engine_MapSheet import neighbors_3x3 # noqa: E402
|
||||||
|
from tools import precompute_terrain as tool # noqa: E402
|
||||||
|
|
||||||
|
SHEETS = [f"36901{i:03d}" for i in range(1, 101)] + [f"35810{i:03d}" for i in range(1, 101)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_part_and_cpus() -> None:
|
||||||
|
assert tool.parse_part("") is None
|
||||||
|
assert tool.parse_part("0-69/100") == (0, 69, 100)
|
||||||
|
assert tool.parse_part("70-99/100") == (70, 99, 100)
|
||||||
|
for bad in ("50-10/100", "0-100/100"):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
tool.parse_part(bad)
|
||||||
|
assert tool.parse_cpus("12-19") == list(range(12, 20))
|
||||||
|
assert tool.parse_cpus("1, 3-4") == [1, 3, 4]
|
||||||
|
assert tool.parse_cpus("") == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_no_overlap_and_union_is_all() -> None:
|
||||||
|
for spans in (("0-69/100", "70-99/100"), ("0-49/100", "50-99/100")):
|
||||||
|
a, b = (tool.parse_part(s) for s in spans)
|
||||||
|
mine = {no for no in SHEETS if tool.in_part(no, a)}
|
||||||
|
other = {no for no in SHEETS if tool.in_part(no, b)}
|
||||||
|
assert not mine & other and mine | other == set(SHEETS)
|
||||||
|
ja, jb = set(tool.dem_jobs(SHEETS, a)), set(tool.dem_jobs(SHEETS, b))
|
||||||
|
assert not ja & jb and ja | jb == set(tool.dem_jobs(SHEETS, None))
|
||||||
|
big = {no for no in SHEETS if tool.in_part(no, (0, 69, 100))}
|
||||||
|
assert 0.6 < len(big) / len(SHEETS) < 0.8
|
||||||
|
assert tool.in_part("36901001@5186", (0, 99, 100))
|
||||||
|
|
||||||
|
|
||||||
|
def test_dem_jobs_cover_neighbors_once() -> None:
|
||||||
|
jobs = tool.dem_jobs(["36901055"], None)
|
||||||
|
assert jobs[0] == "36901055" and set(jobs) == set(neighbors_3x3("36901055"))
|
||||||
|
# 띠 경계(128° · 5186 | 5187) 넘는 이웃 = 가운데 좌표계로 옮겨 담는 일감
|
||||||
|
west = [no for no in tool.dem_jobs(["36809001"], None) if "@" in no]
|
||||||
|
assert west and all(j.endswith(f"@{tool.epsg_of('36809001')}") for j in west)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def fake_store(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setattr(tool.store, "TERRAIN_ROOT", tmp_path)
|
||||||
|
monkeypatch.setattr(tool.store, "_gpkg_stamp", lambda: "stamp-1")
|
||||||
|
monkeypatch.setattr(tool, "PC_NAME", "LAPTOP")
|
||||||
|
monkeypatch.setattr(tool, "CPU_SAMPLE_S", 0.0)
|
||||||
|
log: list = []
|
||||||
|
|
||||||
|
def fake_run(step: str, job: str, part=None):
|
||||||
|
log.append(("run", step, job))
|
||||||
|
return step, job, {"ok": True, "s": 0.01, "fresh": True, "empty": False}
|
||||||
|
|
||||||
|
monkeypatch.setattr(tool, "run_step", fake_run)
|
||||||
|
return tmp_path, log
|
||||||
|
|
||||||
|
|
||||||
|
def test_progress_per_pc_and_legacy_resume(fake_store, monkeypatch) -> None:
|
||||||
|
root, log = fake_store
|
||||||
|
legacy = {"gpkg": "stamp-1", "cell_m": 2.0, "steps": {"dem": {"36901001": {"ok": True}}}}
|
||||||
|
(root / tool.PROGRESS_NAME).write_text(json.dumps(legacy), encoding="utf-8")
|
||||||
|
tool.run(["36901001", "36901002"], ["e07"], 1, tool.load_progress(), say=lambda _: None)
|
||||||
|
assert (root / "_precompute_progress_LAPTOP.json").is_file()
|
||||||
|
assert json.loads((root / tool.PROGRESS_NAME).read_text(encoding="utf-8")) == legacy # 안 씀
|
||||||
|
log.clear()
|
||||||
|
tool.run(["36901001", "36901002"], ["dem"], 1, tool.load_progress(), say=lambda _: None)
|
||||||
|
assert ("run", "dem", "36901001") not in log # 옛 기록 성공 = 이어 받음
|
||||||
|
|
||||||
|
monkeypatch.setattr(tool, "PC_NAME", "DESKTOP")
|
||||||
|
desk = tool.load_progress()
|
||||||
|
assert desk["steps"]["e07"] == {} # 다른 PC 기록은 run 에선 안 섞음
|
||||||
|
desk["steps"]["e07"]["36901003"] = {"ok": True, "s": 1.0, "fresh": True}
|
||||||
|
tool.save_progress(desk)
|
||||||
|
merged = tool.load_progress(every_pc=True)["steps"]["e07"]
|
||||||
|
assert {"36901001", "36901002", "36901003"} <= set(merged)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cpu_cap_holds_new_work(fake_store) -> None:
|
||||||
|
_, log = fake_store
|
||||||
|
values = iter([95, 90, 60, 80, 40])
|
||||||
|
|
||||||
|
def cpu() -> float:
|
||||||
|
value = next(values)
|
||||||
|
log.append(("cpu", value))
|
||||||
|
return value
|
||||||
|
|
||||||
|
tool.run(
|
||||||
|
["36901001", "36901002"],
|
||||||
|
["e07"],
|
||||||
|
1,
|
||||||
|
tool.load_progress(),
|
||||||
|
cpu_cap=70,
|
||||||
|
cpu=cpu,
|
||||||
|
say=lambda _: None,
|
||||||
|
)
|
||||||
|
assert [x[1] if x[0] == "cpu" else x[2] for x in log] == [
|
||||||
|
95,
|
||||||
|
90,
|
||||||
|
60,
|
||||||
|
"36901001",
|
||||||
|
80,
|
||||||
|
40,
|
||||||
|
"36901002",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_zero_defers_when_foreign_neighbor_missing(tmp_path, monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(tool.store, "TERRAIN_ROOT", tmp_path)
|
||||||
|
center = "36901055"
|
||||||
|
part = (0, 49, 100)
|
||||||
|
foreign = [no for no in neighbors_3x3(center) if not tool.in_part(no, part)]
|
||||||
|
assert foreign # 고른 가운데는 남의 몫 이웃이 있음
|
||||||
|
stored: set[str] = set()
|
||||||
|
monkeypatch.setattr(
|
||||||
|
tool.store, "_stored_ok", lambda no, e, c: {"k": 1} if no in stored else None
|
||||||
|
)
|
||||||
|
ensured: list[str] = []
|
||||||
|
monkeypatch.setattr(tool.store, "ensure_sheet_terrain", lambda no, e, c: ensured.append(no))
|
||||||
|
_, _, info = tool.run_step("zero", center, part)
|
||||||
|
assert info["ok"] is False and info["wait"] and not ensured # 셈 안 함
|
||||||
|
|
||||||
|
stored.update(foreign) # 다른 PC 가 셈 · 동기 옴
|
||||||
|
from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zero
|
||||||
|
|
||||||
|
monkeypatch.setattr(zero, "sheet_accumulation", lambda no, e, c: tmp_path / "acc.npz")
|
||||||
|
_, _, info = tool.run_step("zero", center, part)
|
||||||
|
assert info["ok"] is True and len(ensured) == 9
|
||||||
|
|
||||||
|
|
||||||
|
def test_deferred_rounds_retry_until_done(fake_store, monkeypatch) -> None:
|
||||||
|
root, log = fake_store
|
||||||
|
tries: dict[str, int] = {}
|
||||||
|
|
||||||
|
def fake_run(step: str, job: str, part=None):
|
||||||
|
tries[job] = tries.get(job, 0) + 1
|
||||||
|
if job == "36901002" and tries[job] < 3:
|
||||||
|
return step, job, {"ok": False, "wait": True, "error": "이웃 지형 없음"}
|
||||||
|
return step, job, {"ok": True, "s": 0.01, "fresh": True, "empty": False}
|
||||||
|
|
||||||
|
monkeypatch.setattr(tool, "run_step", fake_run)
|
||||||
|
monkeypatch.setattr(tool.time, "sleep", lambda s: None)
|
||||||
|
progress = tool.load_progress()
|
||||||
|
tool.run(["36901001", "36901002"], ["zero"], 1, progress, say=lambda _: None, defer_wait=1)
|
||||||
|
assert tries == {"36901001": 1, "36901002": 3}
|
||||||
|
out = tool.summary(["36901001", "36901002"], progress)["zero"]
|
||||||
|
assert out["ok"] == 2 and out["failed"] == {} and out["waiting"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_dead_only_own_part(tmp_path) -> None:
|
||||||
|
part = (0, 49, 100)
|
||||||
|
mine = next(no for no in SHEETS if tool.in_part(no, part))
|
||||||
|
other = next(no for no in SHEETS if not tool.in_part(no, part))
|
||||||
|
for no in (mine, other):
|
||||||
|
(tmp_path / no).mkdir()
|
||||||
|
(tmp_path / no / ".dem_5187_2m.lock").write_text("999999999 0", encoding="utf-8")
|
||||||
|
assert tool.clean_dead(tmp_path, part) == 1
|
||||||
|
assert (tmp_path / other / ".dem_5187_2m.lock").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_init_pins_first_e_workers(monkeypatch, tmp_path) -> None:
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
pinned: list[list[int]] = []
|
||||||
|
|
||||||
|
class FakeProc:
|
||||||
|
def nice(self, _):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cpu_affinity(self, cpus):
|
||||||
|
pinned.append(cpus)
|
||||||
|
|
||||||
|
monkeypatch.setattr(psutil, "Process", FakeProc)
|
||||||
|
monkeypatch.setattr(tool.store, "TERRAIN_ROOT", tool.store.TERRAIN_ROOT) # 끝나면 되돌림
|
||||||
|
counter = multiprocessing.Value("i", 0)
|
||||||
|
for _ in range(5):
|
||||||
|
tool.init_worker(str(tmp_path), counter, [12, 13], 3)
|
||||||
|
assert pinned == [[12, 13]] * 3 and tool.store.TERRAIN_ROOT == tmp_path
|
||||||
+282
-53
@@ -6,16 +6,23 @@
|
|||||||
--sheets 36901,36712089 도엽 번호 또는 앞자리(1:50,000 묶음) · 주면 높이 차 거름 안 씀
|
--sheets 36901,36712089 도엽 번호 또는 앞자리(1:50,000 묶음) · 주면 높이 차 거름 안 씀
|
||||||
--box lon0,lat0,lon1,lat1 도엽 가운데가 이 경위도 상자 안
|
--box lon0,lat0,lon1,lat1 도엽 가운데가 이 경위도 상자 안
|
||||||
--min-relief 150 도엽 안 높이 차(m) 이상만(산지) · --limit N 앞 N 장만
|
--min-relief 150 도엽 안 높이 차(m) 이상만(산지) · --limit N 앞 N 장만
|
||||||
--workers 4 일꾼 수 · --status 진행 기록 요약만 · --rebuild-index 도엽 목록 다시
|
--workers 4 일꾼 수 · --status 진행 기록 요약만(모든 PC) · --rebuild-index 도엽 목록 다시
|
||||||
|
--part 0-69/100 두 PC 나눔(도엽 번호 crc32 % 100 이 0~69 인 것만 · PLAN 618)
|
||||||
|
--cpu-cap 70 전체 CPU % 가 넘으면 새 도엽 안 줌 · --e-cores 12-19 --e-workers 3
|
||||||
|
일꾼 셋만 그 논리 CPU 로 지정 · --root 창고 자리(시험용 임시 폴더)
|
||||||
|
|
||||||
- 도엽 목록 = 전국 등고선 파일 꼭짓점이 닿는 도엽(0.025°) — `_precompute_sheets.json`(전국 파일
|
- 도엽 목록 = 전국 등고선 파일 꼭짓점이 닿는 도엽(0.025°) — `_precompute_sheets.json`(전국 파일
|
||||||
지문이 같으면 다시 안 셈) · 차례 = 등고선 길이 큰 것(산지)부터 · 같으면 높이 차.
|
지문이 같으면 다시 안 셈) · 차례 = 등고선 길이 큰 것(산지)부터 · 같으면 높이 차.
|
||||||
- 좌표계 = 도엽 가운데 경도의 TM 띠(`tm_epsg_for_lon` · 5185~5187) · 격자 2 m.
|
- 좌표계 = 도엽 가운데 경도의 TM 띠(`tm_epsg_for_lon` · 5185~5187) · 격자 2 m.
|
||||||
- 셈 = 공용 함수만 부름(창고 지형 · D 0차곡 누적 · E07 도엽) — 저장 열쇠가 같으면 함수가 건너뜀 ·
|
- 셈 = 공용 함수만 부름(창고 지형 · D 0차곡 누적 · E07 도엽) — 저장 열쇠가 같으면 함수가 건너뜀 ·
|
||||||
쓰기 = 함수 쪽 원자 쓰기(끊겨도 반쯤 쓴 파일 없음).
|
쓰기 = 함수 쪽 원자 쓰기(끊겨도 반쯤 쓴 파일 없음).
|
||||||
- 이어 하기 = `_precompute_progress.json`(단계 · 도엽마다 성공 · 시간) — 성공한 도엽은
|
- 이어 하기 = PC 별 `_precompute_progress_<컴퓨터 이름>.json`(단계 · 도엽마다 성공 · 시간 ·
|
||||||
다시 안 부름 · 실패는 다음에 다시 · 전국 파일이 바뀌면 기록을 새로 시작.
|
시놀로지가 두 PC 사이 옮김 → 한 파일은 한 PC 만 씀) · 옛 `_precompute_progress.json` 의 성공은
|
||||||
- 시작 때 죽은 셈의 잠금(주인 프로세스 없음) · 오래된 임시 파일을 치움 ·
|
읽어서 이어 받음(안 씀) · 성공한 도엽은 다시 안 부름 · 실패는 다음에 다시 · 전국 파일이 바뀌면
|
||||||
|
기록을 새로 시작.
|
||||||
|
- 나눔 = 지형 단계가 목록 도엽 + 0차곡이 쓸 이웃(목록 밖 · 띠 넘음 포함)을 해시 몫대로 셈 ·
|
||||||
|
0차곡은 남의 몫 이웃 지형이 창고에 없으면 미뤘다 다시(다른 PC 셈 · 시놀로지 동기 기다림).
|
||||||
|
- 시작 때 자기 몫 도엽의 죽은 셈 잠금(주인 프로세스 없음) · 오래된 임시 파일을 치움 ·
|
||||||
낮은 우선순위(BELOW_NORMAL).
|
낮은 우선순위(BELOW_NORMAL).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -24,11 +31,15 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
import zlib
|
||||||
|
from concurrent.futures import FIRST_COMPLETED, ProcessPoolExecutor, wait
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -50,7 +61,10 @@ from common_util.common_util_json import atomic_write_json # noqa: E402
|
|||||||
|
|
||||||
STEPS = ("dem", "zero", "e07")
|
STEPS = ("dem", "zero", "e07")
|
||||||
CELL_M = 2.0
|
CELL_M = 2.0
|
||||||
PROGRESS_NAME = "_precompute_progress.json"
|
PROGRESS_NAME = "_precompute_progress.json" # 옛 한 벌 기록(읽기만)
|
||||||
|
PC_NAME = platform.node() or "pc"
|
||||||
|
CPU_SAMPLE_S = 3.0 # 전체 CPU 평균 잼 · 상한 쓸 때 새 도엽 주는 간격
|
||||||
|
DEFER_WAIT_S = 600.0 # 0차곡 이웃 지형 기다림 한 바퀴
|
||||||
INDEX_NAME = "_precompute_sheets.json"
|
INDEX_NAME = "_precompute_sheets.json"
|
||||||
INDEX_CHUNK = 50_000 # 가닥 묶음(약 0.5 GB 읽기)
|
INDEX_CHUNK = 50_000 # 가닥 묶음(약 0.5 GB 읽기)
|
||||||
INDEX_STRIDE = 8 # 꼭짓점 솎음(도엽 2 km 에 비해 넉넉)
|
INDEX_STRIDE = 8 # 꼭짓점 솎음(도엽 2 km 에 비해 넉넉)
|
||||||
@@ -68,8 +82,77 @@ def below_normal() -> None:
|
|||||||
psutil.Process().nice(psutil.BELOW_NORMAL_PRIORITY_CLASS)
|
psutil.Process().nice(psutil.BELOW_NORMAL_PRIORITY_CLASS)
|
||||||
|
|
||||||
|
|
||||||
def epsg_of(sheet_no: str) -> int:
|
def epsg_of(job: str) -> int:
|
||||||
return tm_epsg_for_lon(sheet5k_center(sheet_no)[1])
|
"""도엽(또는 `도엽@좌표계` 일감)의 좌표계 — 기본 = 도엽 가운데 경도 띠."""
|
||||||
|
no, _, epsg = job.partition("@")
|
||||||
|
return int(epsg) if epsg else tm_epsg_for_lon(sheet5k_center(no)[1])
|
||||||
|
|
||||||
|
|
||||||
|
# ── 나눔 · CPU ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Part = tuple[int, int, int]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_part(text: str) -> Part | None:
|
||||||
|
"""`A-B/N` → (A, B, N) · 빈 글 = 나눔 없음."""
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
span, _, n = text.partition("/")
|
||||||
|
a, _, b = span.partition("-")
|
||||||
|
part = (int(a), int(b or a), int(n or 100))
|
||||||
|
if not 0 <= part[0] <= part[1] < part[2]:
|
||||||
|
raise ValueError(f"--part = A-B/N (0 ≤ A ≤ B < N): {text}")
|
||||||
|
return part
|
||||||
|
|
||||||
|
|
||||||
|
def in_part(job: str, part: Part | None) -> bool:
|
||||||
|
"""도엽 번호 crc32 % N 이 A~B — PC 와 실행에 상관없이 같은 몫(겹침 0)."""
|
||||||
|
if part is None:
|
||||||
|
return True
|
||||||
|
value = zlib.crc32(job.partition("@")[0].encode()) % part[2]
|
||||||
|
return part[0] <= value <= part[1]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cpus(text: str) -> list[int]:
|
||||||
|
"""`12-19` · `12,14,16-17` → 논리 CPU 번호."""
|
||||||
|
cpus: list[int] = []
|
||||||
|
for piece in filter(None, (p.strip() for p in text.split(","))):
|
||||||
|
a, _, b = piece.partition("-")
|
||||||
|
cpus.extend(range(int(a), int(b or a) + 1))
|
||||||
|
return cpus
|
||||||
|
|
||||||
|
|
||||||
|
class CpuMeter:
|
||||||
|
"""전체 CPU %(시스템 · 다른 일 포함) — 뒤 실이 `CPU_SAMPLE_S` 평균을 잼 · 부르면 마지막 값."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
self.value = psutil.cpu_percent(interval=CPU_SAMPLE_S)
|
||||||
|
|
||||||
|
def loop() -> None:
|
||||||
|
while True:
|
||||||
|
self.value = psutil.cpu_percent(interval=CPU_SAMPLE_S)
|
||||||
|
|
||||||
|
threading.Thread(target=loop, daemon=True).start()
|
||||||
|
|
||||||
|
def __call__(self) -> float:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
def init_worker(root: str, counter: Any, e_cpus: list[int], e_workers: int) -> None:
|
||||||
|
"""일꾼 시작 — 창고 자리 · 낮은 우선순위 · 앞선 `e_workers` 명은 `e_cpus` 에만(저전력 코어)."""
|
||||||
|
store.TERRAIN_ROOT = Path(root)
|
||||||
|
below_normal()
|
||||||
|
if counter is None:
|
||||||
|
return
|
||||||
|
with counter.get_lock():
|
||||||
|
slot = counter.value
|
||||||
|
counter.value += 1
|
||||||
|
if slot < e_workers:
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
psutil.Process().cpu_affinity(e_cpus)
|
||||||
|
|
||||||
|
|
||||||
# ── 도엽 목록 ────────────────────────────────────────────────────────────────
|
# ── 도엽 목록 ────────────────────────────────────────────────────────────────
|
||||||
@@ -203,9 +286,24 @@ def _mtime(path: Path) -> int | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def run_step(step: str, sheet_no: str) -> tuple[str, str, dict[str, Any]]:
|
def dem_jobs(sheets: list[str], part: Part | None) -> list[str]:
|
||||||
"""도엽 하나 · 단계 하나 — 공용 함수만 부름 · fresh = 이번에 새로 셈."""
|
"""지형 단계 일감 — 목록 도엽 뒤 0차곡이 쓸 3x3 이웃(그 가운데 도엽 좌표계) · 자기 몫만.
|
||||||
epsg = epsg_of(sheet_no)
|
|
||||||
|
이웃이 가운데와 띠가 다르면 `도엽@좌표계`(옮겨 담기) — 목록 밖 이웃도 한 PC 만 셈.
|
||||||
|
"""
|
||||||
|
jobs: dict[str, None] = {no: None for no in sheets if in_part(no, part)}
|
||||||
|
for center in sheets:
|
||||||
|
epsg = epsg_of(center)
|
||||||
|
for no in neighbors_3x3(center):
|
||||||
|
if in_part(no, part):
|
||||||
|
jobs[no if epsg == epsg_of(no) else f"{no}@{epsg}"] = None
|
||||||
|
return list(jobs)
|
||||||
|
|
||||||
|
|
||||||
|
def run_step(step: str, job: str, part: Part | None = None) -> tuple[str, str, dict[str, Any]]:
|
||||||
|
"""일감 하나 · 단계 하나 — 공용 함수만 부름 · fresh = 이번에 새로 셈 · wait = 이웃 기다림."""
|
||||||
|
sheet_no = job.partition("@")[0]
|
||||||
|
epsg = epsg_of(job)
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
try:
|
try:
|
||||||
if step == "dem":
|
if step == "dem":
|
||||||
@@ -214,7 +312,11 @@ def run_step(step: str, sheet_no: str) -> tuple[str, str, dict[str, Any]]:
|
|||||||
elif step == "zero":
|
elif step == "zero":
|
||||||
from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zero
|
from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zero
|
||||||
|
|
||||||
for no in neighbors_3x3(sheet_no): # 여유 200 m 이웃 지형(누적 열쇠가 나중에 안 바뀌게)
|
neighbors = neighbors_3x3(sheet_no)
|
||||||
|
for no in neighbors: # 남의 몫 이웃 지형이 아직 없음 = 미룸(누적 열쇠가 나중에 바뀜)
|
||||||
|
if not in_part(no, part) and store._stored_ok(no, epsg, CELL_M) is None:
|
||||||
|
return step, job, {"ok": False, "wait": True, "error": f"이웃 지형 없음 {no}"}
|
||||||
|
for no in neighbors: # 여유 200 m 이웃 지형(누적 열쇠가 나중에 안 바뀌게)
|
||||||
store.ensure_sheet_terrain(no, epsg, CELL_M)
|
store.ensure_sheet_terrain(no, epsg, CELL_M)
|
||||||
target = zero._acc_names(sheet_no, epsg, CELL_M)[0]
|
target = zero._acc_names(sheet_no, epsg, CELL_M)[0]
|
||||||
before = _mtime(target)
|
before = _mtime(target)
|
||||||
@@ -231,30 +333,55 @@ def run_step(step: str, sheet_no: str) -> tuple[str, str, dict[str, Any]]:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"모르는 단계: {step}")
|
raise ValueError(f"모르는 단계: {step}")
|
||||||
except Exception as exc: # noqa: BLE001 — 한 장이 죽어도 나머지는 감 · 기록에 까닭
|
except Exception as exc: # noqa: BLE001 — 한 장이 죽어도 나머지는 감 · 기록에 까닭
|
||||||
return step, sheet_no, {"ok": False, "error": f"{type(exc).__name__}: {exc}"[:300]}
|
return step, job, {"ok": False, "error": f"{type(exc).__name__}: {exc}"[:300]}
|
||||||
seconds = round(time.monotonic() - started, 2)
|
seconds = round(time.monotonic() - started, 2)
|
||||||
return step, sheet_no, {"ok": True, "s": seconds, "fresh": fresh, "empty": empty}
|
return step, job, {"ok": True, "s": seconds, "fresh": fresh, "empty": empty}
|
||||||
|
|
||||||
|
|
||||||
# ── 진행 기록 · 정리 ─────────────────────────────────────────────────────────
|
# ── 진행 기록 · 정리 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def load_progress() -> dict[str, Any]:
|
def progress_path() -> Path:
|
||||||
path = store.TERRAIN_ROOT / PROGRESS_NAME
|
return store.TERRAIN_ROOT / f"_precompute_progress_{PC_NAME}.json"
|
||||||
stamp = store._gpkg_stamp()
|
|
||||||
|
|
||||||
|
def progress_files() -> list[Path]:
|
||||||
|
"""모든 PC 기록(옛 한 벌 · 시놀로지 충돌 사본 포함)."""
|
||||||
|
return sorted(store.TERRAIN_ROOT.glob("_precompute_progress*.json"))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_progress(path: Path, stamp: str | None) -> dict[str, Any] | None:
|
||||||
try:
|
try:
|
||||||
saved = json.loads(path.read_text(encoding="utf-8"))
|
saved = json.loads(path.read_text(encoding="utf-8"))
|
||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
saved = None
|
return None
|
||||||
if not saved or saved.get("gpkg") != stamp or saved.get("cell_m") != CELL_M:
|
if not isinstance(saved, dict) or saved.get("gpkg") != stamp or saved.get("cell_m") != CELL_M:
|
||||||
saved = {"gpkg": stamp, "cell_m": CELL_M, "steps": {}}
|
return None
|
||||||
|
return saved
|
||||||
|
|
||||||
|
|
||||||
|
def load_progress(every_pc: bool = False) -> dict[str, Any]:
|
||||||
|
"""이 PC 기록 + 옛 한 벌의 성공(every_pc = 모든 PC 기록의 성공) — 쓰기는 이 PC 파일만."""
|
||||||
|
stamp = store._gpkg_stamp()
|
||||||
|
own_path = progress_path()
|
||||||
|
saved = _read_progress(own_path, stamp) or {"gpkg": stamp, "cell_m": CELL_M, "steps": {}}
|
||||||
for step in STEPS:
|
for step in STEPS:
|
||||||
saved["steps"].setdefault(step, {})
|
saved["steps"].setdefault(step, {})
|
||||||
|
others = progress_files() if every_pc else [store.TERRAIN_ROOT / PROGRESS_NAME]
|
||||||
|
for path in others:
|
||||||
|
other = None if path == own_path else _read_progress(path, stamp)
|
||||||
|
for step, records in (other or {}).get("steps", {}).items():
|
||||||
|
mine = saved["steps"].get(step)
|
||||||
|
if mine is None:
|
||||||
|
continue
|
||||||
|
for job, info in records.items():
|
||||||
|
if info.get("ok") and not mine.get(job, {}).get("ok"):
|
||||||
|
mine[job] = info
|
||||||
return saved
|
return saved
|
||||||
|
|
||||||
|
|
||||||
def save_progress(progress: dict[str, Any]) -> None:
|
def save_progress(progress: dict[str, Any]) -> None:
|
||||||
atomic_write_json(store.TERRAIN_ROOT / PROGRESS_NAME, progress)
|
atomic_write_json(progress_path(), progress)
|
||||||
|
|
||||||
|
|
||||||
def _alive(pid: int) -> bool:
|
def _alive(pid: int) -> bool:
|
||||||
@@ -263,15 +390,18 @@ def _alive(pid: int) -> bool:
|
|||||||
return psutil.pid_exists(pid)
|
return psutil.pid_exists(pid)
|
||||||
|
|
||||||
|
|
||||||
def clean_dead(root: Path | None = None) -> int:
|
def clean_dead(root: Path | None = None, part: Part | None = None) -> int:
|
||||||
"""주인이 죽은 잠금 · 오래된 임시 파일을 지움(끊긴 일괄 셈이 남긴 것) → 지운 수."""
|
"""자기 몫 도엽의 주인이 죽은 잠금 · 오래된 임시 파일을 지움(끊긴 일괄 셈이 남긴 것) → 지운 수.
|
||||||
|
|
||||||
|
남의 몫 도엽 = 다른 PC 잠금(이 PC 에선 주인 번호가 없어 보임)이라 안 건드림.
|
||||||
|
"""
|
||||||
root = root or store.TERRAIN_ROOT
|
root = root or store.TERRAIN_ROOT
|
||||||
removed = 0
|
removed = 0
|
||||||
if not root.is_dir():
|
if not root.is_dir():
|
||||||
return 0
|
return 0
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for folder in root.iterdir():
|
for folder in root.iterdir():
|
||||||
if not folder.is_dir():
|
if not folder.is_dir() or not in_part(folder.name, part):
|
||||||
continue
|
continue
|
||||||
for path in folder.glob(".*"):
|
for path in folder.glob(".*"):
|
||||||
try:
|
try:
|
||||||
@@ -288,47 +418,102 @@ def clean_dead(root: Path | None = None) -> int:
|
|||||||
return removed
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
def _execute(step: str, todo: list[str], workers: int, record, opts: dict[str, Any]) -> None:
|
||||||
|
"""일감 차례대로 일꾼에 줌 — 상한 쓰면 전체 CPU 가 넘는 동안 새 일 안 줌(도는 것은 그대로)."""
|
||||||
|
part, cap, cpu = opts["part"], opts["cpu_cap"], opts["cpu"]
|
||||||
|
|
||||||
|
def over() -> bool:
|
||||||
|
return cap is not None and cpu() > cap
|
||||||
|
|
||||||
|
if workers <= 1:
|
||||||
|
for job in todo:
|
||||||
|
while over():
|
||||||
|
time.sleep(CPU_SAMPLE_S)
|
||||||
|
record(run_step(step, job, part))
|
||||||
|
return
|
||||||
|
e_cpus, e_workers = opts["e_cpus"], opts["e_workers"]
|
||||||
|
counter = multiprocessing.Value("i", 0) if e_cpus and e_workers else None
|
||||||
|
initargs = (str(store.TERRAIN_ROOT), counter, e_cpus, e_workers)
|
||||||
|
with ProcessPoolExecutor(workers, initializer=init_worker, initargs=initargs) as pool:
|
||||||
|
queue = list(reversed(todo))
|
||||||
|
running: set = set()
|
||||||
|
try:
|
||||||
|
while queue or running:
|
||||||
|
while queue and len(running) < workers and not over():
|
||||||
|
running.add(pool.submit(run_step, step, queue.pop(), part))
|
||||||
|
if cap is not None:
|
||||||
|
break # 상한 쓸 때 한 번에 하나 — 다음 잼을 보고 또
|
||||||
|
if not running:
|
||||||
|
time.sleep(CPU_SAMPLE_S)
|
||||||
|
continue
|
||||||
|
timeout = CPU_SAMPLE_S if cap is not None and queue else None
|
||||||
|
finished, running = wait(running, timeout=timeout, return_when=FIRST_COMPLETED)
|
||||||
|
for future in finished:
|
||||||
|
record(future.result())
|
||||||
|
except BaseException:
|
||||||
|
pool.shutdown(wait=False, cancel_futures=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
sheets: list[str],
|
sheets: list[str],
|
||||||
steps: list[str],
|
steps: list[str],
|
||||||
workers: int,
|
workers: int,
|
||||||
progress: dict[str, Any],
|
progress: dict[str, Any],
|
||||||
say=print,
|
say=print,
|
||||||
|
*,
|
||||||
|
part: Part | None = None,
|
||||||
|
cpu_cap: float | None = None,
|
||||||
|
cpu=None,
|
||||||
|
e_cpus: list[int] | None = None,
|
||||||
|
e_workers: int = 0,
|
||||||
|
defer_wait: float = 0.0,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""단계 차례대로 · 단계 안은 일꾼 여럿 · 성공 기록된 도엽은 건너뜀 → 단계별 벽시계 초."""
|
"""단계 차례대로 · 단계 안은 일꾼 여럿 · 성공 기록된 일감은 건너뜀 → 단계별 벽시계 초.
|
||||||
|
|
||||||
|
sheets = 나누기 전 목록(이웃 몫에 씀) · 미룬 0차곡 = defer_wait 초 뒤 다시(0 = 안 기다림).
|
||||||
|
"""
|
||||||
|
if cpu_cap is not None and cpu is None:
|
||||||
|
cpu = CpuMeter()
|
||||||
|
opts = {"part": part, "cpu_cap": cpu_cap, "cpu": cpu, "e_cpus": e_cpus or []}
|
||||||
|
opts["e_workers"] = e_workers
|
||||||
walls: dict[str, float] = {}
|
walls: dict[str, float] = {}
|
||||||
for step in [s for s in STEPS if s in steps]:
|
for step in [s for s in STEPS if s in steps]:
|
||||||
done = progress["steps"][step]
|
done = progress["steps"][step]
|
||||||
todo = [no for no in sheets if not done.get(no, {}).get("ok")]
|
jobs = dem_jobs(sheets, part) if step == "dem" else [n for n in sheets if in_part(n, part)]
|
||||||
say(f"[{step}] {len(todo)} 장 (건너뜀 {len(sheets) - len(todo)})")
|
todo = [job for job in jobs if not done.get(job, {}).get("ok")]
|
||||||
|
say(f"[{step}] {len(todo)} 장 (건너뜀 {len(jobs) - len(todo)} · 몫 {len(jobs)})")
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
saved_at = started
|
saved_at = started
|
||||||
count = 0
|
count = 0
|
||||||
|
waiting: list[str] = []
|
||||||
|
|
||||||
def record(result: tuple[str, str, dict[str, Any]]) -> None:
|
def record(result: tuple[str, str, dict[str, Any]]) -> None:
|
||||||
nonlocal count, saved_at
|
nonlocal count, saved_at
|
||||||
_, no, info = result
|
_, job, info = result
|
||||||
done[no] = info
|
done[job] = info
|
||||||
count += 1
|
count += 1
|
||||||
state = f"{info['s']} s" if info["ok"] else f"실패 {info['error']}"
|
if info.get("wait"):
|
||||||
say(f"[{step}] {count}/{len(todo)} {no} {state}")
|
waiting.append(job)
|
||||||
|
state = (
|
||||||
|
f"{info['s']} s"
|
||||||
|
if info["ok"]
|
||||||
|
else f"{'기다림' if info.get('wait') else '실패'} {info['error']}"
|
||||||
|
)
|
||||||
|
say(f"[{step}] {count}/{len(todo)} {job} {state}")
|
||||||
if time.monotonic() - saved_at > SAVE_EVERY_S:
|
if time.monotonic() - saved_at > SAVE_EVERY_S:
|
||||||
save_progress(progress)
|
save_progress(progress)
|
||||||
saved_at = time.monotonic()
|
saved_at = time.monotonic()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if workers <= 1:
|
while True:
|
||||||
for no in todo:
|
_execute(step, todo, workers, record, opts)
|
||||||
record(run_step(step, no))
|
if not waiting or defer_wait <= 0:
|
||||||
else:
|
break
|
||||||
with ProcessPoolExecutor(workers, initializer=below_normal) as pool:
|
save_progress(progress)
|
||||||
futures = [pool.submit(run_step, step, no) for no in todo]
|
say(f"[{step}] 이웃 지형 기다림 {len(waiting)} 장 · {defer_wait:g} s 뒤 다시")
|
||||||
try:
|
time.sleep(defer_wait)
|
||||||
for future in as_completed(futures):
|
todo, waiting[:], count = list(waiting), [], 0
|
||||||
record(future.result())
|
|
||||||
except BaseException:
|
|
||||||
pool.shutdown(wait=False, cancel_futures=True)
|
|
||||||
raise
|
|
||||||
finally:
|
finally:
|
||||||
save_progress(progress)
|
save_progress(progress)
|
||||||
walls[step] = round(time.monotonic() - started, 1)
|
walls[step] = round(time.monotonic() - started, 1)
|
||||||
@@ -341,12 +526,14 @@ def summary(sheets: list[str], progress: dict[str, Any]) -> dict[str, Any]:
|
|||||||
for step in STEPS:
|
for step in STEPS:
|
||||||
done = progress["steps"][step]
|
done = progress["steps"][step]
|
||||||
infos = [done[no] for no in sheets if no in done]
|
infos = [done[no] for no in sheets if no in done]
|
||||||
failed = {no: done[no]["error"] for no in sheets if done.get(no, {}).get("ok") is False}
|
failed = {
|
||||||
|
no: done[no]["error"]
|
||||||
|
for no in sheets
|
||||||
|
if done.get(no, {}).get("ok") is False and not done[no].get("wait")
|
||||||
|
}
|
||||||
fresh = [i["s"] for i in infos if i.get("ok") and i.get("fresh")]
|
fresh = [i["s"] for i in infos if i.get("ok") and i.get("fresh")]
|
||||||
sizes = []
|
sizes = [] # 창고 실물(다른 PC 가 셈 · 시놀로지로 옴 포함)
|
||||||
for no in sheets:
|
for no in sheets:
|
||||||
if not done.get(no, {}).get("ok"):
|
|
||||||
continue
|
|
||||||
stem = f"{epsg_of(no)}_{CELL_M:g}m"
|
stem = f"{epsg_of(no)}_{CELL_M:g}m"
|
||||||
folder = store.TERRAIN_ROOT / no
|
folder = store.TERRAIN_ROOT / no
|
||||||
total = 0
|
total = 0
|
||||||
@@ -358,6 +545,8 @@ def summary(sheets: list[str], progress: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"ok": sum(1 for i in infos if i.get("ok")),
|
"ok": sum(1 for i in infos if i.get("ok")),
|
||||||
"fresh": len(fresh),
|
"fresh": len(fresh),
|
||||||
"failed": failed,
|
"failed": failed,
|
||||||
|
"waiting": sum(1 for no in sheets if done.get(no, {}).get("wait")),
|
||||||
|
"stored": len(sizes),
|
||||||
"s_median": round(float(np.median(fresh)), 1) if fresh else None,
|
"s_median": round(float(np.median(fresh)), 1) if fresh else None,
|
||||||
"s_mean": round(float(np.mean(fresh)), 1) if fresh else None,
|
"s_mean": round(float(np.mean(fresh)), 1) if fresh else None,
|
||||||
"s_max": round(float(np.max(fresh)), 1) if fresh else None,
|
"s_max": round(float(np.max(fresh)), 1) if fresh else None,
|
||||||
@@ -377,7 +566,22 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
parser.add_argument("--workers", type=int, default=4)
|
parser.add_argument("--workers", type=int, default=4)
|
||||||
parser.add_argument("--status", action="store_true")
|
parser.add_argument("--status", action="store_true")
|
||||||
parser.add_argument("--rebuild-index", action="store_true")
|
parser.add_argument("--rebuild-index", action="store_true")
|
||||||
|
parser.add_argument("--part", default="", help="A-B/N — 도엽 번호 crc32 %% N 이 A~B 인 것만")
|
||||||
|
parser.add_argument("--cpu-cap", type=float, default=None, help="전체 CPU %% 상한")
|
||||||
|
parser.add_argument("--e-cores", default="", help="저전력 논리 CPU(예 12-19)")
|
||||||
|
parser.add_argument("--e-workers", type=int, default=0, help="그 CPU 로 지정할 일꾼 수")
|
||||||
|
parser.add_argument("--defer-wait", type=float, default=DEFER_WAIT_S)
|
||||||
|
parser.add_argument("--root", default="", help="창고 자리(시험용 임시 폴더)")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
try:
|
||||||
|
part = parse_part(args.part)
|
||||||
|
e_cpus = parse_cpus(args.e_cores)
|
||||||
|
except ValueError as exc:
|
||||||
|
parser.error(str(exc))
|
||||||
|
if (args.e_workers and not e_cpus) or args.e_workers > args.workers:
|
||||||
|
parser.error("--e-workers 는 --e-cores 와 함께 · --workers 이하")
|
||||||
|
if args.root:
|
||||||
|
store.TERRAIN_ROOT = Path(args.root)
|
||||||
steps = [s.strip() for s in args.steps.split(",") if s.strip()]
|
steps = [s.strip() for s in args.steps.split(",") if s.strip()]
|
||||||
unknown = [s for s in steps if s not in STEPS]
|
unknown = [s for s in steps if s not in STEPS]
|
||||||
if unknown:
|
if unknown:
|
||||||
@@ -392,14 +596,39 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
sheets = pick_sheets(
|
sheets = pick_sheets(
|
||||||
index, sheets=wanted or None, box=box, min_relief=args.min_relief, limit=args.limit or None
|
index, sheets=wanted or None, box=box, min_relief=args.min_relief, limit=args.limit or None
|
||||||
)
|
)
|
||||||
print(f"도엽 {len(sheets)} 장 (목록 {len(index)} 장) · 단계 {steps} · 일꾼 {args.workers}")
|
print(
|
||||||
progress = load_progress()
|
f"도엽 {len(sheets)} 장 (목록 {len(index)} 장) · 단계 {steps} · 일꾼 {args.workers}"
|
||||||
if not args.status:
|
f" · 몫 {args.part or '전부'} · PC {PC_NAME} · 상한 {args.cpu_cap}"
|
||||||
print(f"치운 잠금 · 임시 파일 {clean_dead()}")
|
f" · 지정 CPU {e_cpus} × {args.e_workers}"
|
||||||
|
)
|
||||||
|
mine = [no for no in sheets if in_part(no, part)]
|
||||||
|
if args.status:
|
||||||
|
out = summary(mine, load_progress(every_pc=True))
|
||||||
|
stamp = store._gpkg_stamp()
|
||||||
|
out["pc"] = {}
|
||||||
|
for path in progress_files():
|
||||||
|
recs = (_read_progress(path, stamp) or {}).get("steps", {})
|
||||||
|
out["pc"][path.name] = {
|
||||||
|
step: sum(1 for i in recs.get(step, {}).values() if i.get("ok")) for step in STEPS
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
progress = load_progress()
|
||||||
|
print(f"치운 잠금 · 임시 파일 {clean_dead(part=part)}")
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
walls = run(sheets, steps, args.workers, progress)
|
walls = run(
|
||||||
|
sheets,
|
||||||
|
steps,
|
||||||
|
args.workers,
|
||||||
|
progress,
|
||||||
|
part=part,
|
||||||
|
cpu_cap=args.cpu_cap,
|
||||||
|
e_cpus=e_cpus,
|
||||||
|
e_workers=args.e_workers,
|
||||||
|
defer_wait=args.defer_wait,
|
||||||
|
)
|
||||||
print(f"벽시계 {walls} · 전체 {time.monotonic() - started:.1f} s")
|
print(f"벽시계 {walls} · 전체 {time.monotonic() - started:.1f} s")
|
||||||
print(json.dumps(summary(sheets, progress), ensure_ascii=False, indent=1))
|
out = summary(mine, progress)
|
||||||
|
print(json.dumps(out, ensure_ascii=False, indent=1))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user