增加可选性能埋点并完成物性效率评估
This commit is contained in:
1 parent
4e0b9fd8cc
commit
57b459bc72
21 files changed
+2029
-179
No files matched your search
@@ -0,0 +1,262 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import statistics
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from math import ceil
|
||||
from pathlib import Path
|
||||
from time import perf_counter_ns, process_time_ns
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _named_value(value: str, *, option: str) -> tuple[str, str]:
|
||||
name, separator, target = value.partition("=")
|
||||
if not separator or not name.strip() or not target.strip():
|
||||
raise ValueError(
|
||||
f"{option} must use NAME=VALUE syntax, received {value!r}."
|
||||
)
|
||||
return name.strip(), target.strip()
|
||||
|
||||
|
||||
def _percentile(values: list[float], percentile: float) -> float:
|
||||
ordered = sorted(values)
|
||||
index = max(0, min(len(ordered) - 1, ceil(percentile * len(ordered)) - 1))
|
||||
return ordered[index]
|
||||
|
||||
|
||||
def _duration_summary(values: list[float]) -> dict[str, object]:
|
||||
return {
|
||||
"samplesMs": values,
|
||||
"minimumMs": min(values),
|
||||
"medianMs": statistics.median(values),
|
||||
"p95Ms": _percentile(values, 0.95),
|
||||
"maximumMs": max(values),
|
||||
}
|
||||
|
||||
|
||||
def _load_factory_xml(specification: str) -> bytes:
|
||||
module_name, separator, member_name = specification.partition(":")
|
||||
if not separator or not module_name or not member_name:
|
||||
raise ValueError(
|
||||
"Factory specifications must use module.path:callable syntax."
|
||||
)
|
||||
factory = getattr(importlib.import_module(module_name), member_name)
|
||||
value = factory()
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
|
||||
from app.main import build_reactflow_system_xml
|
||||
|
||||
return build_reactflow_system_xml(value)
|
||||
|
||||
|
||||
def _clear_property_caches() -> None:
|
||||
from app.simulation.components.amesim.media.mediums import (
|
||||
AmesimHeliumPengRobinsonMedium,
|
||||
)
|
||||
|
||||
for method_name in (
|
||||
"temperature_from_pressure_enthalpy",
|
||||
"properties_from_mU",
|
||||
):
|
||||
method = getattr(AmesimHeliumPengRobinsonMedium, method_name)
|
||||
cache_clear = getattr(method, "cache_clear", None)
|
||||
if cache_clear is not None:
|
||||
cache_clear()
|
||||
|
||||
|
||||
def _serialize_result_event(result: dict[str, object]) -> bytes:
|
||||
"""Render the final NDJSON payload shape used by the streaming endpoint."""
|
||||
|
||||
status = str(result.get("status", "completed"))
|
||||
event = {
|
||||
"event": "result",
|
||||
"progress": 100 if status == "completed" else 0,
|
||||
"phase": status,
|
||||
"message": "仿真完成" if status == "completed" else "仿真任务结束",
|
||||
"simulatedTime": result.get("simulatedUntil"),
|
||||
"totalTime": result.get("requestedStopTime"),
|
||||
"result": result,
|
||||
}
|
||||
return (
|
||||
json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _run_case(
|
||||
name: str,
|
||||
xml_bytes: bytes,
|
||||
*,
|
||||
warmups: int,
|
||||
runs: int,
|
||||
cancellable_path: bool,
|
||||
clear_property_cache: bool,
|
||||
allow_failures: bool,
|
||||
) -> dict[str, object]:
|
||||
from app.main import run_system_xml_simulation
|
||||
|
||||
cancel_check = (lambda: False) if cancellable_path else None
|
||||
for _ in range(warmups):
|
||||
if clear_property_cache:
|
||||
_clear_property_caches()
|
||||
result = run_system_xml_simulation(xml_bytes, cancel_check=cancel_check)
|
||||
if not bool(result.get("success")) and not allow_failures:
|
||||
raise RuntimeError(f"Warmup for {name!r} failed: {result.get('message')}")
|
||||
|
||||
wall_samples_ms: list[float] = []
|
||||
cpu_samples_ms: list[float] = []
|
||||
serialization_samples_ms: list[float] = []
|
||||
serialized_sizes: list[int] = []
|
||||
profiles: list[dict[str, object]] = []
|
||||
final_result: dict[str, object] | None = None
|
||||
for _ in range(runs):
|
||||
if clear_property_cache:
|
||||
_clear_property_caches()
|
||||
wall_start = perf_counter_ns()
|
||||
cpu_start = process_time_ns()
|
||||
result = run_system_xml_simulation(xml_bytes, cancel_check=cancel_check)
|
||||
cpu_samples_ms.append((process_time_ns() - cpu_start) / 1_000_000.0)
|
||||
wall_samples_ms.append((perf_counter_ns() - wall_start) / 1_000_000.0)
|
||||
if not bool(result.get("success")) and not allow_failures:
|
||||
raise RuntimeError(f"Benchmark for {name!r} failed: {result.get('message')}")
|
||||
diagnostics = result.get("diagnostics")
|
||||
if isinstance(diagnostics, dict):
|
||||
performance = diagnostics.get("performance")
|
||||
if isinstance(performance, dict):
|
||||
profiles.append(performance)
|
||||
serialization_start = perf_counter_ns()
|
||||
serialized_event = _serialize_result_event(result)
|
||||
serialization_samples_ms.append(
|
||||
(perf_counter_ns() - serialization_start) / 1_000_000.0
|
||||
)
|
||||
serialized_sizes.append(len(serialized_event))
|
||||
final_result = result
|
||||
|
||||
assert final_result is not None
|
||||
return {
|
||||
"name": name,
|
||||
"success": bool(final_result.get("success")),
|
||||
"message": final_result.get("message"),
|
||||
"inputBytes": len(xml_bytes),
|
||||
"inputSha256": hashlib.sha256(xml_bytes).hexdigest(),
|
||||
"status": final_result.get("status"),
|
||||
"simulatedUntil": final_result.get("simulatedUntil"),
|
||||
"requestedStopTime": final_result.get("requestedStopTime"),
|
||||
"wall": _duration_summary(wall_samples_ms),
|
||||
"cpu": _duration_summary(cpu_samples_ms),
|
||||
"resultSerialization": _duration_summary(serialization_samples_ms),
|
||||
"resultEventBytes": serialized_sizes,
|
||||
"performanceRuns": profiles,
|
||||
}
|
||||
|
||||
|
||||
def _parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Benchmark the real System XML simulation path with optional profiling."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=("off", "standard", "audit"),
|
||||
default="audit",
|
||||
help="Instrumentation depth selected before importing the simulation modules.",
|
||||
)
|
||||
parser.add_argument("--warmups", type=int, default=1)
|
||||
parser.add_argument("--runs", type=int, default=5)
|
||||
parser.add_argument(
|
||||
"--xml",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="NAME=PATH",
|
||||
help="Add an XML file benchmark case.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--factory",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="NAME=MODULE:CALLABLE",
|
||||
help="Add a zero-argument factory returning XML or ReactFlowProjectPayload.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--direct-path",
|
||||
action="store_true",
|
||||
help="Do not pass a cancel callback; use the one-shot SciPy path when eligible.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cold-property-cache",
|
||||
action="store_true",
|
||||
help="Clear the two helium property LRU caches before every warmup and measured run.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-failures",
|
||||
action="store_true",
|
||||
help="Record failed simulation runs instead of aborting the benchmark.",
|
||||
)
|
||||
parser.add_argument("--output", type=Path)
|
||||
arguments = parser.parse_args(argv)
|
||||
if arguments.warmups < 0:
|
||||
parser.error("--warmups must not be negative.")
|
||||
if arguments.runs <= 0:
|
||||
parser.error("--runs must be positive.")
|
||||
if not arguments.xml and not arguments.factory:
|
||||
parser.error("At least one --xml or --factory case is required.")
|
||||
return arguments
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
arguments = _parse_arguments(argv)
|
||||
os.environ["SIMULATIONAPP_PROFILE"] = arguments.mode
|
||||
|
||||
cases: list[tuple[str, bytes]] = []
|
||||
for raw_case in arguments.xml:
|
||||
name, raw_path = _named_value(raw_case, option="--xml")
|
||||
cases.append((name, Path(raw_path).read_bytes()))
|
||||
for raw_case in arguments.factory:
|
||||
name, specification = _named_value(raw_case, option="--factory")
|
||||
cases.append((name, _load_factory_xml(specification)))
|
||||
|
||||
report: dict[str, Any] = {
|
||||
"generatedAt": datetime.now(UTC).isoformat(),
|
||||
"profileMode": arguments.mode,
|
||||
"cancellableSolverPath": not arguments.direct_path,
|
||||
"coldPropertyCache": bool(arguments.cold_property_cache),
|
||||
"allowFailures": bool(arguments.allow_failures),
|
||||
"warmups": arguments.warmups,
|
||||
"runs": arguments.runs,
|
||||
"runtime": {
|
||||
"python": sys.version,
|
||||
"platform": platform.platform(),
|
||||
"processor": platform.processor(),
|
||||
},
|
||||
"cases": [
|
||||
_run_case(
|
||||
name,
|
||||
xml_bytes,
|
||||
warmups=arguments.warmups,
|
||||
runs=arguments.runs,
|
||||
cancellable_path=not arguments.direct_path,
|
||||
clear_property_cache=arguments.cold_property_cache,
|
||||
allow_failures=arguments.allow_failures,
|
||||
)
|
||||
for name, xml_bytes in cases
|
||||
],
|
||||
}
|
||||
text = json.dumps(report, ensure_ascii=False, indent=2)
|
||||
if arguments.output is not None:
|
||||
arguments.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
arguments.output.write_text(text + "\n", encoding="utf-8")
|
||||
print(f"Performance report written to {arguments.output.resolve()}")
|
||||
else:
|
||||
print(text)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user