Dart TCP/heartbeat, Python sustained sampling, TypeScript WS 1MB 경로의 성능 병목을 분리하고 검증 산출물을 함께 남긴다.
151 lines
No EOL
5.6 KiB
Python
151 lines
No EOL
5.6 KiB
Python
"""Tests for the stress harness helpers: LatencyReservoir and summarize(request_count).
|
|
|
|
These tests validate the bounded latency sampling introduced in the
|
|
m-performance-hotspot-optimization / python-sustained-memory-metric plan.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
import bench.stress as stress_module
|
|
from bench.stress import LatencyReservoir, SUSTAINED_MAX_LATENCY_SAMPLES, Stability
|
|
|
|
|
|
class TestLatencyReservoir:
|
|
"""Verify bounded sample count and unbounded request count tracking."""
|
|
|
|
def test_reservoir_tracks_all_count(self) -> None:
|
|
"""count must reflect the total number of add() calls, not just stored samples."""
|
|
sampler = LatencyReservoir(max_samples=10)
|
|
for i in range(500):
|
|
sampler.add(float(i))
|
|
assert sampler.count == 500
|
|
|
|
def test_reservoir_bounds_samples_to_max(self) -> None:
|
|
"""samples list must never exceed max_samples length."""
|
|
sampler = LatencyReservoir(max_samples=SUSTAINED_MAX_LATENCY_SAMPLES)
|
|
for i in range(200_000):
|
|
sampler.add(float(i))
|
|
assert len(sampler.samples) == SUSTAINED_MAX_LATENCY_SAMPLES
|
|
|
|
def test_reservoir_initial_state(self) -> None:
|
|
assert LatencyReservoir(100).count == 0
|
|
assert LatencyReservoir(100).samples == []
|
|
|
|
def test_reservoir_small_values_fully_preserved(self) -> None:
|
|
"""When inputs are within the max, all values are stored."""
|
|
sampler = LatencyReservoir(max_samples=5)
|
|
for i in range(5):
|
|
sampler.add(float(i * 10))
|
|
assert len(sampler.samples) == 5
|
|
assert sampler.count == 5
|
|
|
|
def test_reservoir_deterministic_slot_mapping(self) -> None:
|
|
"""The slot formula must be deterministic for the same count."""
|
|
sampler = LatencyReservoir(max_samples=3)
|
|
sampler.add(1.0)
|
|
sampler.add(2.0)
|
|
sampler.add(3.0)
|
|
sampler.add(4.0)
|
|
sampler2 = LatencyReservoir(max_samples=3)
|
|
for v in (1.0, 2.0, 3.0, 4.0):
|
|
sampler2.add(v)
|
|
assert sampler.samples == sampler2.samples
|
|
|
|
|
|
class TestSummarizeRequestCount:
|
|
"""Verify that summarize() actually passes request_count to emit_row().
|
|
|
|
These tests use monkeypatch to intercept emit_row() kwargs so that the
|
|
real summarize() function is exercised. A mock that reimplements the same
|
|
logic would pass even if summarize() had a bug — this would not.
|
|
"""
|
|
|
|
def test_summarize_uses_request_count_for_throughput(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""When request_count=200, emit_row must receive requests=200 and throughput=200.0."""
|
|
captured: dict = {}
|
|
|
|
def fake_emit_row(**kwargs: object) -> None:
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr(stress_module, "emit_row", fake_emit_row)
|
|
|
|
stability = Stability()
|
|
stress_module.summarize(
|
|
profile="sustained",
|
|
axis="duration=1000ms",
|
|
transport="tcp",
|
|
latencies=[1.0, 2.0, 3.0],
|
|
elapsed_ms=1000.0,
|
|
client_count=1,
|
|
stability=stability,
|
|
mem=12.3,
|
|
request_count=200,
|
|
)
|
|
|
|
assert captured.get("requests") == 200
|
|
assert pytest.approx(float(captured["throughput"])) == 200.0
|
|
# p50/p95/p99 are computed from the bounded sample [1.0, 2.0, 3.0], NOT from request_count
|
|
assert pytest.approx(float(captured["p50"])) == 2.0
|
|
assert pytest.approx(float(captured["p95"])) == 3.0
|
|
assert pytest.approx(float(captured["p99"])) == 3.0
|
|
|
|
def test_summarize_defaults_to_len_latencies(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Without request_count, emit_row must receive requests=len(latencies)."""
|
|
captured: dict = {}
|
|
|
|
def fake_emit_row(**kwargs: object) -> None:
|
|
captured.update(kwargs)
|
|
|
|
original_emit = stress_module.emit_row
|
|
monkeypatch.setattr(stress_module, "emit_row", fake_emit_row)
|
|
|
|
stability = Stability()
|
|
stress_module.summarize(
|
|
profile="roundtrip",
|
|
axis="concurrency=1",
|
|
transport="tcp",
|
|
latencies=[1.5, 2.5, 3.5],
|
|
elapsed_ms=1000.0,
|
|
client_count=1,
|
|
stability=stability,
|
|
mem=10.0,
|
|
)
|
|
|
|
assert captured.get("requests") == 3
|
|
assert pytest.approx(float(captured["throughput"])) == 3.0
|
|
|
|
def test_summarize_emits_correct_payload_and_status(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Verify that non-latency fields (payload_bytes, status, mem) are also correct."""
|
|
captured: dict = {}
|
|
|
|
def fake_emit_row(**kwargs: object) -> None:
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr(stress_module, "emit_row", fake_emit_row)
|
|
|
|
stability = Stability()
|
|
stress_module.summarize(
|
|
profile="sustained",
|
|
axis="duration=2000ms",
|
|
transport="tcp",
|
|
latencies=[5.0],
|
|
elapsed_ms=2000.0,
|
|
client_count=1,
|
|
stability=stability,
|
|
mem=45.6,
|
|
request_count=1000,
|
|
)
|
|
|
|
assert captured.get("profile") == "sustained"
|
|
assert captured.get("transport") == "tcp"
|
|
assert captured.get("client_count") == 1
|
|
assert captured.get("requests") == 1000
|
|
assert pytest.approx(float(captured["throughput"])) == 1000.0 / 2000.0 * 1000.0
|
|
assert captured.get("mem") == 45.6
|
|
assert captured.get("payload_bytes") == stress_module.test_data_payload_bytes("req-0")
|
|
assert captured.get("p50") == 5.0 # single sample => all percentiles = 5.0 |