Newer
Older
SmTIAS-LightSim / scripts / profile_sweep_step.py
"""スイープ 1 条件の所要時間の内訳を測る(高速化の当たりを付けるための計測).

sm-render / sm-evaluate をサブプロセスで呼ぶ現行方式と,同じ処理をプロセス内で
直接呼ぶ方式の各段階(起動・import・PLY 書き出し・シーン読込・レンダ・評価・図・遮蔽検証)
を秒で出す.

実行方法:
    uv run python scripts/profile_sweep_step.py \
        configs/_tmp_led_placement/scene_sphere_r170_th090_board_n1.yaml
"""

from __future__ import annotations

import argparse
import os
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))

ROOT = Path(__file__).resolve().parents[1]
EVAL_YAML = ROOT / "configs" / "eval_baseline_dng_target_shape.yaml"


class Timer:
    """段階ごとの経過秒を記録する."""

    def __init__(self) -> None:
        self.rows: list[tuple[str, float]] = []
        self._t = time.perf_counter()

    def lap(self, label: str) -> None:
        now = time.perf_counter()
        self.rows.append((label, now - self._t))
        self._t = now

    def report(self, title: str) -> None:
        print(f"\n=== {title} ===")
        for label, sec in self.rows:
            print(f"  {label:<40} {sec:8.2f} s")
        print(f"  {'合計':<40} {sum(s for _, s in self.rows):8.2f} s")


def profile_subprocess(scene: Path, out_dir: Path) -> None:
    """現行方式: uv run sm-render → uv run sm-evaluate をサブプロセスで."""
    t = Timer()
    subprocess.run(["uv", "run", "python", "-c", "pass"], cwd=ROOT, check=True,
                   capture_output=True)
    t.lap("uv run python -c pass(起動のみ)")
    subprocess.run(["uv", "run", "python", "-c", "import mitsuba"], cwd=ROOT, check=True,
                   capture_output=True)
    t.lap("uv run python -c 'import mitsuba'")
    subprocess.run(["uv", "run", "sm-render", "--scene", str(scene), "--output", str(out_dir)],
                   cwd=ROOT, check=True, capture_output=True)
    t.lap("uv run sm-render(全体)")
    subprocess.run(["uv", "run", "sm-evaluate", "--image", str(out_dir / "luminance.npy"),
                    "--config", str(EVAL_YAML), "--output", str(out_dir)],
                   cwd=ROOT, check=True, capture_output=True)
    t.lap("uv run sm-evaluate(全体)")
    t.report("現行方式(サブプロセス)")


def profile_inprocess(scene: Path, out_dir: Path, variant: str) -> None:
    """プロセス内で各段階を直接呼ぶ."""
    t = Timer()
    import numpy as np

    from sm_tias_lightsim.runtime.llvm_init import ensure_drjit_libllvm_path

    if variant.startswith("llvm_"):
        ensure_drjit_libllvm_path()
    import mitsuba as mi

    t.lap("import mitsuba")
    mi.set_variant(variant)
    t.lap(f"set_variant({variant})")

    from sm_tias_lightsim.scene.builder import build_scene
    from sm_tias_lightsim.scene.schema import load_scene_yaml

    config = load_scene_yaml(scene)
    config = config.model_copy(
        update={"render": config.render.model_copy(update={"variant": variant})}
    )
    t.lap("load_scene_yaml")
    scene_dict, tempdir = build_scene(config, project_root=scene.parent)
    t.lap("build_scene(STL 読込 + PLY 書き出し)")
    try:
        mscene = mi.load_dict(scene_dict)
        t.lap("mi.load_dict(PLY 読込 + 加速構造)")
        for i in range(2):
            img = mi.render(mscene, spp=config.render.spp)
            arr = np.array(img)
            t.lap(f"mi.render spp={config.render.spp}({i + 1} 回目)")
    finally:
        shutil.rmtree(tempdir, ignore_errors=True)
    from sm_tias_lightsim.render.output import save_luminance, save_png

    save_png(arr, out_dir / "render.png")
    save_luminance(arr, out_dir / "luminance.npy")
    t.lap("save_png + save_luminance")

    from sm_tias_lightsim.evaluation.pipeline import evaluate_image

    image = out_dir / "luminance.npy"
    evaluate_image(image, EVAL_YAML, out_dir, save_figures=True)
    t.lap("evaluate_image(図あり)")
    evaluate_image(image, EVAL_YAML, out_dir, save_figures=False)
    t.lap("evaluate_image(図なし)")

    from verify_baffle_occlusion import check_occlusion

    result = check_occlusion(scene, board_grid=41, led_count=32)
    t.lap(f"check_occlusion(variant={mi.variant()}・{result['blocked']:.0f}/{result['total']:.0f})")
    result = check_occlusion(scene, board_grid=41, led_count=32)
    t.lap("check_occlusion(2 回目)")
    t.report(f"プロセス内方式(variant={variant})")


def main() -> int:
    parser = argparse.ArgumentParser(description="スイープ 1 条件の所要時間の内訳")
    parser.add_argument("scene", type=Path)
    parser.add_argument("--variant", default=os.environ.get("SM_RENDER_VARIANT", "llvm_ad_rgb"))
    parser.add_argument("--skip-subprocess", action="store_true")
    args = parser.parse_args()
    out_dir = Path(tempfile.mkdtemp(prefix="sm_profile_"))
    try:
        if not args.skip_subprocess:
            profile_subprocess(args.scene.resolve(), out_dir)
        profile_inprocess(args.scene.resolve(), out_dir, args.variant)
    finally:
        shutil.rmtree(out_dir, ignore_errors=True)
    return 0


if __name__ == "__main__":
    sys.exit(main())