Files
SystemSimulationApp/tests/test_simulation_performance.py

249 lines
8.8 KiB
Python

from __future__ import annotations
import asyncio
from contextlib import contextmanager
from functools import lru_cache
import importlib
import os
import unittest
from unittest.mock import patch
import app.simulation.performance as performance
@contextmanager
def profiling_mode(mode: str):
previous = os.environ.get("SIMULATIONAPP_PROFILE")
os.environ["SIMULATIONAPP_PROFILE"] = mode
module = importlib.reload(performance)
try:
yield module
finally:
if previous is None:
os.environ.pop("SIMULATIONAPP_PROFILE", None)
else:
os.environ["SIMULATIONAPP_PROFILE"] = previous
importlib.reload(performance)
class SimulationPerformanceTests(unittest.TestCase):
def test_off_mode_decorators_return_original_callables(self) -> None:
with profiling_mode("off") as module:
def phase_function(value: int) -> int:
return value + 1
def property_function(value: int) -> int:
return value * 2
self.assertIs(
module.profile_phase("phase")(phase_function),
phase_function,
)
self.assertIs(
module.profile_phase("phase", minimum_mode="off")(phase_function),
phase_function,
)
self.assertIs(
module.profile_property("property")(property_function),
property_function,
)
self.assertIs(
module.profile_property(
"property",
minimum_mode="off",
)(property_function),
property_function,
)
with module.profile_run() as trace:
self.assertEqual(phase_function(2), 3)
self.assertEqual(
trace.snapshot(),
{
"mode": "off",
"phases": {},
"properties": {},
"propertyOutermostNs": 0,
},
)
def test_standard_mode_records_nested_inclusive_and_self_time(self) -> None:
with profiling_mode("standard") as module:
@module.profile_phase("inner")
def inner() -> None:
return None
@module.profile_phase("outer")
def outer() -> None:
inner()
clock = iter((0, 10, 20, 30, 50, 80))
with patch.object(module, "perf_counter_ns", side_effect=clock):
with module.profile_run() as trace:
outer()
snapshot = trace.snapshot()
self.assertEqual(
snapshot["phases"]["inner"],
{
"calls": 1,
"inclusiveNs": 10,
"selfNs": 10,
"maxNs": 10,
"errors": 0,
},
)
self.assertEqual(snapshot["phases"]["outer"]["inclusiveNs"], 40)
self.assertEqual(snapshot["phases"]["outer"]["selfNs"], 30)
self.assertEqual(
snapshot["phases"]["simulation.total"]["inclusiveNs"],
80,
)
self.assertEqual(
snapshot["phases"]["simulation.total"]["selfNs"],
40,
)
def test_errors_are_recorded_and_propagated(self) -> None:
with profiling_mode("standard") as module:
@module.profile_phase("explode")
def explode() -> None:
raise RuntimeError("expected")
with self.assertRaisesRegex(RuntimeError, "expected"):
with module.profile_run() as trace:
explode()
snapshot = trace.snapshot()
self.assertEqual(snapshot["phases"]["explode"]["errors"], 1)
self.assertEqual(
snapshot["phases"]["simulation.total"]["errors"],
1,
)
def test_manual_performance_span_uses_the_current_trace(self) -> None:
with profiling_mode("standard") as module:
clock = iter((0, 10, 20, 30))
with patch.object(module, "perf_counter_ns", side_effect=clock):
with module.profile_run() as trace:
with module.performance_span("manual"):
pass
snapshot = trace.snapshot()
self.assertEqual(snapshot["phases"]["manual"]["inclusiveNs"], 10)
self.assertEqual(snapshot["phases"]["simulation.total"]["selfNs"], 20)
def test_nested_properties_only_add_outermost_time_once(self) -> None:
with profiling_mode("standard") as module:
class TestMedium:
name = "TestMedium"
@module.profile_property("inner", minimum_mode="standard")
def inner(self) -> float:
return 1.0
@module.profile_property("outer", minimum_mode="standard")
def outer(self) -> float:
return self.inner()
medium = TestMedium()
clock = iter((0, 10, 20, 30, 50, 80))
with patch.object(module, "perf_counter_ns", side_effect=clock):
with module.profile_run() as trace:
self.assertEqual(medium.outer(), 1.0)
snapshot = trace.snapshot()
outer = snapshot["properties"]["semantic.TestMedium.outer"]
inner = snapshot["properties"]["semantic.TestMedium.inner"]
self.assertEqual(outer["inclusiveNs"], 40)
self.assertEqual(outer["selfNs"], 30)
self.assertEqual(inner["inclusiveNs"], 10)
self.assertEqual(snapshot["propertyOutermostNs"], 40)
def test_audit_resets_exact_input_shadow_and_records_iterations_and_cache(self) -> None:
with profiling_mode("audit") as module:
class TestMedium:
name = "AuditMedium"
@module.profile_property("inverse", track_cache=True)
@lru_cache(maxsize=4)
def inverse(self, value: float) -> float:
module.record_property_iterations(
"inverse",
3 if value == 1.0 else 4,
value == 1.0,
)
return value * 2.0
medium = TestMedium()
@module.profile_phase("closure", reset_property_shadow=True)
def closure() -> None:
medium.inverse(1.0)
medium.inverse(1.0)
medium.inverse(2.0)
with module.profile_run() as trace:
closure()
closure()
metric = trace.snapshot()["properties"][
"semantic.AuditMedium.inverse"
]
self.assertEqual(metric["calls"], 6)
self.assertEqual(metric["exactInputUnique"], 4)
self.assertEqual(metric["exactInputRepeats"], 2)
self.assertEqual(metric["iterationCalls"], 2)
self.assertEqual(metric["iterationTotal"], 7)
self.assertEqual(metric["iterationMax"], 4)
self.assertEqual(metric["iterationConverged"], 1)
self.assertEqual(metric["iterationNonconverged"], 1)
self.assertEqual(metric["cacheLookups"], 6)
self.assertEqual(metric["cacheHits"], 4)
self.assertEqual(metric["cacheMisses"], 2)
self.assertTrue(callable(medium.inverse.cache_clear))
self.assertTrue(callable(medium.inverse.cache_info))
self.assertTrue(callable(medium.inverse.cache_parameters))
medium.inverse.cache_clear()
self.assertEqual(medium.inverse.cache_info().currsize, 0)
def test_audit_minimum_decorator_is_absent_in_standard_mode(self) -> None:
with profiling_mode("standard") as module:
def kernel(value: float) -> float:
return value
self.assertIs(
module.profile_property(
"kernel",
layer="kernel",
minimum_mode="audit",
)(kernel),
kernel,
)
def test_async_profile_runs_are_context_isolated(self) -> None:
with profiling_mode("standard") as module:
@module.profile_phase("work")
async def work() -> None:
await asyncio.sleep(0)
async def one_run() -> dict[str, object]:
with module.profile_run() as trace:
await work()
return trace.snapshot()
async def exercise() -> list[dict[str, object]]:
return await asyncio.gather(one_run(), one_run())
snapshots = asyncio.run(exercise())
self.assertEqual(
[snapshot["phases"]["work"]["calls"] for snapshot in snapshots],
[1, 1],
)
if __name__ == "__main__":
unittest.main()