Co-Study4Grid / benchmarks /bench_n1_diagram_patch.py
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
6.27 kB
#!/usr/bin/env python3
# Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
# SPDX-License-Identifier: MPL-2.0
"""Profile the N-1 patch endpoint (SVG-less payload) vs the full-SVG
endpoint, end-to-end on the reference bare_env_20240828T0100Z grid.
The patch endpoint skips pypowsybl's `get_network_area_diagram` call
and the ~12 MB SVG serialisation entirely. The frontend then clones
the already-loaded N-state SVG DOM and applies the patch in-place,
avoiding the 20-28 MB transfer and re-parse on each contingency
selection.
See `docs/performance/history/svg-dom-recycling.md` for the wider
context and payload schema; this script captures the backend cost
delta alone (wire + client parse are measured in the frontend).
Usage:
python benchmarks/bench_n1_diagram_patch.py # default contingency
BENCH_CONTINGENCY='DISCO_X' python bench_n1_diagram_patch.py
"""
from __future__ import annotations
import json
import os
import time
from _bench_common import NETWORK_PATH, setup_service
CONTINGENCY = os.environ.get("BENCH_CONTINGENCY", "ARGIAL71CANTE")
RESULTS_FILE = os.environ.get(
"BENCH_RESULTS_FILE",
os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"profiling_patch_results.json",
),
)
def _payload_bytes(payload: dict) -> int:
"""Rough on-wire size of a JSON payload, pre-gzip."""
return len(json.dumps(payload, default=str).encode("utf-8"))
def _measure(fn, reps: int = 3) -> tuple[float, list[float]]:
"""Run `fn` `reps` times, return (median_ms, all_dts_ms)."""
dts: list[float] = []
for _ in range(reps):
t0 = time.perf_counter()
fn()
dts.append((time.perf_counter() - t0) * 1000)
dts_sorted = sorted(dts)
return dts_sorted[len(dts_sorted) // 2], dts
def main() -> None:
print(f"Network: {NETWORK_PATH}")
print(f"Contingency: {CONTINGENCY}")
_ns, recommender_service, dt_setup = setup_service()
print(f"Setup done in {dt_setup:.0f} ms\n")
# --- COLD full fetch (sets up N-1 variant, AC LF, overload cache) ---
print("=== Full /api/n1-diagram (COLD) ===")
t0 = time.perf_counter()
full_cold = recommender_service.get_n1_diagram(CONTINGENCY)
full_cold_ms = (time.perf_counter() - t0) * 1000
full_cold_size = _payload_bytes(full_cold)
full_cold_svg_mb = len(full_cold["svg"]) / 1_000_000
print(f" total: {full_cold_ms:>8.1f} ms")
print(f" full payload size: {full_cold_size / 1_000_000:>8.2f} MB")
print(f" SVG size: {full_cold_svg_mb:>8.2f} MB")
print(f" flow_deltas: {len(full_cold.get('flow_deltas', {}))}")
# --- WARM full fetch (variant + LF cached) — median of 3 ---
print("\n=== Full /api/n1-diagram (WARM median of 3) ===")
full_warm_ms, full_warm_all = _measure(
lambda: recommender_service.get_n1_diagram(CONTINGENCY), reps=3
)
print(f" median: {full_warm_ms:>8.1f} ms")
print(f" all runs: {[f'{d:.0f}' for d in full_warm_all]}")
# --- COLD patch (expected to reuse LF cache already populated by
# the full-fetch call above; still avoids the ~2-4 s
# `_generate_diagram` NAD call + ~12 MB SVG serialisation) ---
print("\n=== Patch /api/n1-diagram-patch (COLD) ===")
t0 = time.perf_counter()
patch_cold = recommender_service.get_n1_diagram_patch(CONTINGENCY)
patch_cold_ms = (time.perf_counter() - t0) * 1000
patch_cold_size = _payload_bytes(patch_cold)
print(f" total: {patch_cold_ms:>8.1f} ms")
print(f" patch payload: {patch_cold_size / 1_000_000:>8.3f} MB")
print(f" patchable: {patch_cold['patchable']}")
print(f" flow_deltas: {len(patch_cold.get('flow_deltas', {}))}")
print(f" absolute_flows: {sum(len(patch_cold.get('absolute_flows', {}).get(k, {})) for k in ('p1','p2','q1','q2'))}")
# --- WARM patch — median of 3 ---
print("\n=== Patch /api/n1-diagram-patch (WARM median of 3) ===")
patch_warm_ms, patch_warm_all = _measure(
lambda: recommender_service.get_n1_diagram_patch(CONTINGENCY), reps=3
)
print(f" median: {patch_warm_ms:>8.1f} ms")
print(f" all runs: {[f'{d:.0f}' for d in patch_warm_all]}")
# --- Summary ---
print(f"\n=== Summary (lower is better) ===")
print(f" FULL cold: {full_cold_ms/1000:>5.2f} s warm: {full_warm_ms/1000:>5.2f} s")
print(f" PATCH cold: {patch_cold_ms/1000:>5.2f} s warm: {patch_warm_ms/1000:>5.2f} s")
warm_savings = full_warm_ms - patch_warm_ms
cold_savings = full_cold_ms - patch_cold_ms
print(f" Δ cold: -{cold_savings/1000:>5.2f} s "
f"({-100 * cold_savings / full_cold_ms:>5.1f}%)")
print(f" Δ warm: -{warm_savings/1000:>5.2f} s "
f"({-100 * warm_savings / full_warm_ms:>5.1f}%)")
print(f" Payload reduction: "
f"{(full_warm_all[0] and full_cold_size) and full_cold_size / 1_000_000:>5.2f} MB → "
f"{patch_cold_size / 1_000_000:>5.3f} MB "
f"({100 * patch_cold_size / max(full_cold_size, 1):.1f}% of full)")
# --- Persist to JSON for the perf retrospective doc ---
out = {
"network_path": NETWORK_PATH,
"contingency": CONTINGENCY,
"full": {
"cold_ms": full_cold_ms,
"warm_ms_median": full_warm_ms,
"warm_ms_all": full_warm_all,
"payload_bytes": full_cold_size,
"svg_mb": full_cold_svg_mb,
},
"patch": {
"cold_ms": patch_cold_ms,
"warm_ms_median": patch_warm_ms,
"warm_ms_all": patch_warm_all,
"payload_bytes": patch_cold_size,
"patchable": patch_cold["patchable"],
},
"savings": {
"cold_ms": cold_savings,
"cold_pct": 100 * cold_savings / max(full_cold_ms, 1),
"warm_ms": warm_savings,
"warm_pct": 100 * warm_savings / max(full_warm_ms, 1),
"payload_ratio_pct": 100 * patch_cold_size / max(full_cold_size, 1),
},
}
with open(RESULTS_FILE, "w", encoding="utf-8") as f:
json.dump(out, f, indent=2)
print(f"\n JSON: {RESULTS_FILE}")
if __name__ == "__main__":
main()