增加可选性能埋点并完成物性效率评估
This commit is contained in:
1 parent
4e0b9fd8cc
commit
57b459bc72
21 files changed
+2029
-179
No files matched your search
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.simulation.benchmark_performance import (
|
||||
_duration_summary,
|
||||
_load_factory_xml,
|
||||
_named_value,
|
||||
_serialize_result_event,
|
||||
)
|
||||
|
||||
|
||||
def sample_xml_factory() -> bytes:
|
||||
return b"<System/>"
|
||||
|
||||
|
||||
class PerformanceBenchmarkToolTests(unittest.TestCase):
|
||||
def test_named_value_requires_an_explicit_name(self) -> None:
|
||||
self.assertEqual(
|
||||
_named_value("chain=tests.example:project", option="--factory"),
|
||||
("chain", "tests.example:project"),
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "NAME=VALUE"):
|
||||
_named_value("tests.example:project", option="--factory")
|
||||
|
||||
def test_duration_summary_reports_repeatable_order_statistics(self) -> None:
|
||||
summary = _duration_summary([5.0, 1.0, 3.0, 2.0, 4.0])
|
||||
|
||||
self.assertEqual(summary["minimumMs"], 1.0)
|
||||
self.assertEqual(summary["medianMs"], 3.0)
|
||||
self.assertEqual(summary["p95Ms"], 5.0)
|
||||
self.assertEqual(summary["maximumMs"], 5.0)
|
||||
|
||||
def test_factory_loader_accepts_a_bytes_factory(self) -> None:
|
||||
self.assertEqual(
|
||||
_load_factory_xml(
|
||||
"tests.test_performance_benchmark:sample_xml_factory"
|
||||
),
|
||||
b"<System/>",
|
||||
)
|
||||
|
||||
def test_result_event_serialization_uses_ndjson_shape(self) -> None:
|
||||
payload = _serialize_result_event(
|
||||
{
|
||||
"success": True,
|
||||
"status": "completed",
|
||||
"simulatedUntil": 1.0,
|
||||
"requestedStopTime": 1.0,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(payload.endswith(b"\n"))
|
||||
self.assertIn(b'"event":"result"', payload)
|
||||
self.assertIn(b'"result":{"success":true', payload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,141 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from app.simulation.components.amesim.media.mediums import (
|
||||
AmesimHeliumPengRobinsonMedium,
|
||||
)
|
||||
|
||||
|
||||
class PropertyPerformanceInstrumentationTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.medium = AmesimHeliumPengRobinsonMedium()
|
||||
self.medium.temperature_from_pressure_enthalpy.cache_clear()
|
||||
self.medium.properties_from_mU.cache_clear()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.medium.temperature_from_pressure_enthalpy.cache_clear()
|
||||
self.medium.properties_from_mU.cache_clear()
|
||||
|
||||
def test_temperature_iterations_are_recorded_only_on_cache_miss(self) -> None:
|
||||
pressure = 15.3e6
|
||||
temperature = 293.15
|
||||
enthalpy = self.medium.specific_enthalpy_at_pressure(
|
||||
pressure,
|
||||
temperature,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"app.simulation.components.amesim.media.mediums."
|
||||
"record_property_iterations"
|
||||
) as record_iterations:
|
||||
first = self.medium.temperature_from_pressure_enthalpy(
|
||||
pressure,
|
||||
enthalpy,
|
||||
)
|
||||
second = self.medium.temperature_from_pressure_enthalpy(
|
||||
pressure,
|
||||
enthalpy,
|
||||
)
|
||||
|
||||
self.assertEqual(second, first)
|
||||
record_iterations.assert_called_once()
|
||||
operation, iterations, converged = record_iterations.call_args.args
|
||||
self.assertEqual(operation, "temperature_from_pressure_enthalpy")
|
||||
self.assertEqual(iterations, 5)
|
||||
self.assertTrue(converged)
|
||||
|
||||
def test_state_recovery_iterations_are_recorded_only_on_cache_miss(self) -> None:
|
||||
pressure = 15.3e6
|
||||
temperature = 293.15
|
||||
volume = 0.057
|
||||
mass = self.medium.density(pressure, temperature) * volume
|
||||
internal_energy = mass * self.medium.specific_internal_energy_at_pressure(
|
||||
pressure,
|
||||
temperature,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"app.simulation.components.amesim.media.mediums."
|
||||
"record_property_iterations"
|
||||
) as record_iterations:
|
||||
first = self.medium.properties_from_mU(mass, internal_energy, volume)
|
||||
second = self.medium.properties_from_mU(mass, internal_energy, volume)
|
||||
|
||||
self.assertIs(second, first)
|
||||
record_iterations.assert_called_once()
|
||||
operation, iterations, converged = record_iterations.call_args.args
|
||||
self.assertEqual(operation, "properties_from_mU")
|
||||
self.assertEqual(iterations, 5)
|
||||
self.assertTrue(converged)
|
||||
|
||||
def test_temperature_iteration_limit_is_reported_as_not_converged(self) -> None:
|
||||
pressure = 123_456.0
|
||||
base_temperature = 300.0
|
||||
enthalpy = self.medium.specific_enthalpy(base_temperature)
|
||||
fake_fluid = Mock()
|
||||
fake_fluid.residual_specific_enthalpy.side_effect = (
|
||||
lambda _pressure, temperature: (
|
||||
self.medium.cp_ref * 10.0
|
||||
if temperature >= base_temperature
|
||||
else -self.medium.cp_ref * 10.0
|
||||
)
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(AmesimHeliumPengRobinsonMedium, "fluid", fake_fluid),
|
||||
patch(
|
||||
"app.simulation.components.amesim.media.mediums."
|
||||
"record_property_iterations"
|
||||
) as record_iterations,
|
||||
):
|
||||
self.medium.temperature_from_pressure_enthalpy(
|
||||
pressure,
|
||||
enthalpy,
|
||||
)
|
||||
|
||||
record_iterations.assert_called_once_with(
|
||||
"temperature_from_pressure_enthalpy",
|
||||
16,
|
||||
False,
|
||||
)
|
||||
|
||||
def test_state_recovery_iteration_limit_is_reported_as_not_converged(self) -> None:
|
||||
base_temperature = 300.0
|
||||
target_internal_energy = self.medium.specific_internal_energy(
|
||||
base_temperature
|
||||
)
|
||||
fake_fluid = Mock()
|
||||
fake_fluid.residual_specific_internal_energy_at_density.side_effect = (
|
||||
lambda temperature, _density: (
|
||||
self.medium.cv * 10.0
|
||||
if temperature >= base_temperature
|
||||
else -self.medium.cv * 10.0
|
||||
)
|
||||
)
|
||||
fake_fluid.pressure_from_density.return_value = 101_325.0
|
||||
fake_fluid.residual_specific_enthalpy.return_value = 0.0
|
||||
|
||||
with (
|
||||
patch.object(AmesimHeliumPengRobinsonMedium, "fluid", fake_fluid),
|
||||
patch(
|
||||
"app.simulation.components.amesim.media.mediums."
|
||||
"record_property_iterations"
|
||||
) as record_iterations,
|
||||
):
|
||||
self.medium.properties_from_mU(
|
||||
1.0,
|
||||
target_internal_energy,
|
||||
1.0,
|
||||
)
|
||||
|
||||
record_iterations.assert_called_once_with(
|
||||
"properties_from_mU",
|
||||
16,
|
||||
False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,248 @@
|
||||
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()
|
||||
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class SimulationPerformancePipelineTests(unittest.TestCase):
|
||||
def test_off_mode_does_not_change_result_diagnostics(self) -> None:
|
||||
script = textwrap.dedent(
|
||||
"""
|
||||
from app.main import (
|
||||
build_reactflow_system_xml,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from tests.test_amesim_pnl00r_xml import amesim_pnl00r_project
|
||||
|
||||
result = run_system_xml_simulation(
|
||||
build_reactflow_system_xml(amesim_pnl00r_project())
|
||||
)
|
||||
assert result["success"], result["message"]
|
||||
print("performance" in result["diagnostics"])
|
||||
"""
|
||||
)
|
||||
environment = os.environ.copy()
|
||||
environment["SIMULATIONAPP_PROFILE"] = "off"
|
||||
completed = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
cwd=PROJECT_ROOT,
|
||||
env=environment,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
self.assertEqual(completed.stdout.strip(), "False")
|
||||
|
||||
def test_standard_mode_reports_coarse_pipeline_phases_per_run(self) -> None:
|
||||
script = textwrap.dedent(
|
||||
"""
|
||||
import json
|
||||
|
||||
from app.main import (
|
||||
build_reactflow_system_xml,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from tests.test_amesim_pnl00r_xml import amesim_pnl00r_project
|
||||
|
||||
xml = build_reactflow_system_xml(amesim_pnl00r_project())
|
||||
snapshots = []
|
||||
for _ in range(2):
|
||||
result = run_system_xml_simulation(xml)
|
||||
assert result["success"], result["message"]
|
||||
performance = result["diagnostics"]["performance"]
|
||||
snapshots.append(
|
||||
{
|
||||
"mode": performance["mode"],
|
||||
"calls": {
|
||||
name: metrics["calls"]
|
||||
for name, metrics in performance["phases"].items()
|
||||
},
|
||||
"propertyCount": len(performance["properties"]),
|
||||
}
|
||||
)
|
||||
print(json.dumps(snapshots))
|
||||
"""
|
||||
)
|
||||
environment = os.environ.copy()
|
||||
environment["SIMULATIONAPP_PROFILE"] = "standard"
|
||||
completed = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
cwd=PROJECT_ROOT,
|
||||
env=environment,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
snapshots = json.loads(completed.stdout)
|
||||
required_phases = {
|
||||
"simulation.total",
|
||||
"simulation.xml_validation",
|
||||
"simulation.network_compilation",
|
||||
"simulation.system_construction",
|
||||
"simulation.sample_initialization",
|
||||
"simulation.integration",
|
||||
"simulation.postprocessing",
|
||||
"simulation.result_assembly",
|
||||
"simulation.response_assembly",
|
||||
}
|
||||
audit_only_phases = {
|
||||
"simulation.rhs",
|
||||
"simulation.closure",
|
||||
"simulation.refresh",
|
||||
"simulation.pressure_flow",
|
||||
"simulation.pneumatic_volume",
|
||||
"simulation.stream",
|
||||
"simulation.signal",
|
||||
"simulation.derivatives",
|
||||
}
|
||||
|
||||
self.assertEqual(len(snapshots), 2)
|
||||
for snapshot in snapshots:
|
||||
self.assertEqual(snapshot["mode"], "standard")
|
||||
self.assertTrue(required_phases.issubset(snapshot["calls"]))
|
||||
self.assertEqual(snapshot["calls"]["simulation.total"], 1)
|
||||
self.assertEqual(snapshot["calls"]["simulation.xml_validation"], 1)
|
||||
self.assertEqual(snapshot["calls"]["simulation.network_compilation"], 1)
|
||||
self.assertEqual(snapshot["calls"]["simulation.integration"], 1)
|
||||
self.assertEqual(snapshot["calls"]["simulation.postprocessing"], 1)
|
||||
self.assertEqual(snapshot["calls"]["simulation.result_assembly"], 1)
|
||||
self.assertEqual(snapshot["calls"]["simulation.response_assembly"], 1)
|
||||
self.assertTrue(audit_only_phases.isdisjoint(snapshot["calls"]))
|
||||
self.assertEqual(snapshot["propertyCount"], 0)
|
||||
|
||||
def test_audit_mode_reports_hot_phases_and_property_metrics(self) -> None:
|
||||
script = textwrap.dedent(
|
||||
"""
|
||||
import json
|
||||
|
||||
from app.main import (
|
||||
build_reactflow_system_xml,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from tests.test_amesim_pnl00r_xml import amesim_pnl00r_project
|
||||
|
||||
result = run_system_xml_simulation(
|
||||
build_reactflow_system_xml(amesim_pnl00r_project())
|
||||
)
|
||||
assert result["success"], result["message"]
|
||||
print(json.dumps(result["diagnostics"]["performance"]))
|
||||
"""
|
||||
)
|
||||
environment = os.environ.copy()
|
||||
environment["SIMULATIONAPP_PROFILE"] = "audit"
|
||||
completed = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
cwd=PROJECT_ROOT,
|
||||
env=environment,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
snapshot = json.loads(completed.stdout)
|
||||
|
||||
for phase in (
|
||||
"simulation.rhs",
|
||||
"simulation.closure",
|
||||
"simulation.refresh",
|
||||
"simulation.pressure_flow",
|
||||
"simulation.pneumatic_volume",
|
||||
"simulation.stream",
|
||||
"simulation.signal",
|
||||
"simulation.derivatives",
|
||||
):
|
||||
self.assertGreater(snapshot["phases"][phase]["calls"], 0)
|
||||
self.assertGreater(len(snapshot["properties"]), 0)
|
||||
self.assertGreater(snapshot["propertyOutermostNs"], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user