Files
iAOP/tests/perf/test_collect_nfr.py
T

137 lines
5.1 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""采集 / 总线 NFR 压测(Issue #87,对应 PRD 第 9 章)。
验证目标(PRD 5.1 / 5.2 / 9 章):
- 采集 P99 延迟 ≤ **1.8s**(600 点位 1Hz 基线);
- 丢失率 ≤ **0.02%**;
- 可用性 ≥ **99.8%**;
- 批量写入幂等去重(不丢不重)。
用合成点位字典 + 模拟驱动做规模化压测,输出 SLA 达标结论。
"""
from __future__ import annotations
# 引导加载内核模块
import tests.perf._bootstrap # noqa: F401
import os
import tempfile
import time
import unittest
from collector.engine import CollectorEngine
from collector.metrics import HealthMetrics
from collector.spool import SpoolStore
from drivers import SimulatorDriver
from point_dict.loader import Point, PointDict
from data_bus import BatchWriter, MemorySink
def _make_point_dict(n_points: int = 600) -> PointDict:
"""合成 600 点位(PRD 5.1 基线:600 点位 1Hz),分布到 12 台设备。"""
points = []
n_devices = max(1, n_points // 50) # 每台设备约 50 点位
for i in range(n_points):
dev = i % n_devices
points.append(Point(
device_id=f"CLF-{dev+1:02d}",
point_id=f"CLF-{dev+1:02d}.P{i:04d}",
name=f"测点{i}",
unit="℃",
data_type="float",
sample_rate=1000,
quality_code=True,
row_number=i + 2,
))
return PointDict(points)
class CollectionLatencyNFRTest(unittest.TestCase):
"""采集 P99 延迟 / 可用性压测。"""
def setUp(self) -> None:
self._tmp = tempfile.mkdtemp(prefix="iaop_perf_")
self.point_dict = _make_point_dict(600)
self.spool = SpoolStore(spool_dir=self._tmp, cache_limit_bytes=64 * 1024 * 1024)
self.metrics = HealthMetrics()
self.engine = CollectorEngine(
point_dict=self.point_dict,
driver_slots=[("simulator", SimulatorDriver(), [])],
spool=self.spool, metrics=self.metrics,
interval_ms=1000, max_pending=500_000,
)
def test_p99_latency_under_threshold(self) -> None:
"""压测:30 轮采集(600 点位/轮),P99 ≤ 1.8s。
规模说明:PRD 5.1 基线为「600 点位 1Hz」。本用例用 30 轮(30 秒等价)
压测单轮批处理 P99,足以验证调度引擎 + 模拟驱动 + spool 落盘链路
在目标点位规模下的时延达标性;CI 中保持可重复执行的耗时(< 30s)。
"""
rounds = 30
for _ in range(rounds):
self.engine.collect_once()
p99 = self.metrics.p99_latency()
self.assertLessEqual(p99, 1.8,
f"采集 P99 {p99:.4f}s 超过 1.8s 阈值(PRD 5.1)")
# 可用性 ≥ 99.8%
self.assertGreaterEqual(self.metrics.availability, 0.998,
f"可用性 {self.metrics.availability:.4%} < 99.8%")
# 丢失率 ≤ 0.02%
self.assertLessEqual(self.metrics.loss_rate, 0.0002,
f"丢失率 {self.metrics.loss_rate:.4%} > 0.02%")
self.assertTrue(self.metrics.meets_sla())
print(f"\n[采集压测] {rounds} 轮 × {len(self.point_dict)} 点位,"
f"P99={p99*1000:.1f}ms,可用性={self.metrics.availability:.4%},"
f"丢失率={self.metrics.loss_rate:.6%}")
class BusLosslessnessNFRTest(unittest.TestCase):
"""总线批量写入「不丢不重」压测。"""
def setUp(self) -> None:
self._tmp = tempfile.mkdtemp(prefix="iaop_bus_")
self.point_dict = _make_point_dict(600)
self.spool = SpoolStore(spool_dir=self._tmp, cache_limit_bytes=64 * 1024 * 1024)
self.metrics = HealthMetrics()
self.engine = CollectorEngine(
point_dict=self.point_dict,
driver_slots=[("simulator", SimulatorDriver(), [])],
spool=self.spool, metrics=self.metrics,
interval_ms=1000, max_pending=500_000,
)
def test_no_loss_no_dup_at_scale(self) -> None:
"""压测:10 轮 × 600 点位 = 6000 样本,落库不丢不重。"""
rounds = 10
expected_total = rounds * len(self.point_dict)
for _ in range(rounds):
self.engine.collect_once()
sink = MemorySink()
writer = BatchWriter(sink=sink, batch_size=1000, flush_interval=0.0)
accepted = 0
for sample in self.spool.pending_records():
if writer.push(sample):
accepted += 1
self.spool.ack(sample)
writer.flush()
# 不丢:落库数 = 采集数
self.assertEqual(len(sink.rows), expected_total)
self.assertEqual(accepted, expected_total)
# 不重:幂等去重生效(重放不增加)
replay_extra = 0
for sample in list(sink.rows):
if writer.push(sample):
replay_extra += 1
self.assertEqual(replay_extra, 0)
print(f"\n[总线压测] {expected_total} 样本落库,"
f"丢失 0,重复 0(不丢不重达标)")
if __name__ == "__main__":
unittest.main()