Spaces:
Sleeping
Sleeping
| #!/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() | |