# -*- 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()