更新:1、优化数据记录的性能,满足实时运行要求;
Signed-off-by: waiwaylee <waiwaylee@foxmail.com>
This commit is contained in:
@@ -13,6 +13,12 @@
|
||||
- 新增 `.vscode/scripts/build_all.bat`:对 `PHASE_SCANNING` 与 `MECHANICAL_SCANNING` 依次「配置 + 构建」,任一配置失败即中止并返回非 0。
|
||||
- 新增 `.vscode/scripts/clean_all.bat`:删除全部配置的构建输出(含旧版单一构建目录;`build/output` 打包留档不动)。
|
||||
- `.vscode/tasks.json` 重建为 5 个任务:「清理全部」「配置并构建全部(Release/REC_OFF)」「配置并构建全部(Release/REC_ON)」「配置并构建全部(Debug/REC_OFF)」「配置并构建全部(Debug/REC_ON)」;后 4 个任务内部调用 `build_all.bat`,分别对两种扫描体制配置并构建,默认构建任务为「配置并构建全部(Release/REC_OFF)」。
|
||||
- **数据记录改为异步写线程 + 有界队列(数据记录性能优化,工作区未提交)**:`rdp_record.h` 新增 `rdp_record::Stats`(`enqueued_rows`/`dropped_rows`/`written_rows`/`transactions`/`write_errors`/`queue_peak_bytes`/`queue_peak_rows`)与 `stats()`,新增 6 个异步入队接口 `WritePoints`/`WriteCohesionTWS`/`WriteCohesionTAS`/`WriteTempTracks`/`WriteTrustTracks`/`WriteAssoInfos`(参数为源结构体 `std::vector` 按值、调用点 `std::move`)及对应宏 `RDP_RECORD_POINTS`/`RDP_RECORD_COHESION_TWS`/`RDP_RECORD_COHESION_TAS`/`RDP_RECORD_TEMP_TRACKS`/`RDP_RECORD_TRUST_TRACKS`/`RDP_RECORD_ASSO_INFOS`;`rdp_record.cpp` 的 `Init()` 建库后启动独立写线程(Windows 下 `THREAD_PRIORITY_BELOW_NORMAL`),处理线程只做「收集源结构体 → 一次入队」,实体构造(数组转文本)与写库全部迁移到写线程。
|
||||
- **批量事务与 SQLite 配置(数据记录性能优化)**:写线程按表累积待提交数据,达到 `RDP_RECORD_WRITE_BATCH_ROWS`(默认 4096 行)或等待 `RDP_RECORD_WRITE_MAX_WAIT_MS`(默认 50 ms)后以事务批量提交;SQLite 启用 `journal_mode=WAL`(失败回退默认 journal)、`synchronous=OFF`、`cache_size=-8192`(8 MB)、`busy_timeout=2000`;建表后读取 `sqlite_master` 去掉 `AUTOINCREMENT` 重建为普通 `INTEGER PRIMARY KEY`(重建失败时回退并告警)。
|
||||
- **有界队列与丢弃计数(数据记录性能优化)**:队列上限 `RDP_RECORD_QUEUE_CAP_BYTES`(默认 64 MB)与 `RDP_RECORD_QUEUE_MAX_BATCHES`(默认 4096 批),写线程跟不上时整批丢弃并累计 `dropped_rows`,日志按 `RDP_RECORD_DROP_LOG_INTERVAL`(默认每 1024 行)限频提示;处理线程不因记录阻塞。
|
||||
- **记录参数宏(数据记录性能优化)**:`parameters.h` 新增可调宏(均以 `#ifndef` 保护,可用编译选项覆盖):`RDP_RECORD_QUEUE_CAP_BYTES`、`RDP_RECORD_QUEUE_MAX_BATCHES`、`RDP_RECORD_WRITE_BATCH_ROWS`、`RDP_RECORD_WRITE_MAX_WAIT_MS`、`RDP_RECORD_ASSO_CHUNK_ROWS`(默认 131072)、`RDP_RECORD_SQLITE_CACHE_KB`、`RDP_RECORD_SQLITE_BUSY_TIMEOUT_MS`、`RDP_RECORD_SQLITE_WAL`、`RDP_RECORD_MAX_CONSECUTIVE_ERRORS`(默认 10)、`RDP_RECORD_DROP_LOG_INTERVAL`。
|
||||
- **记录健壮性与重连(数据记录性能优化)**:写失败每表只提示一次并永久跳过该表,连续错误达到 `RDP_RECORD_MAX_CONSECUTIVE_ERRORS` 次全局熔断并丢弃队列;`Shutdown()` 改为「停止入队 → 排空队列与 pending → 打印 stats → `wal_checkpoint(TRUNCATE)` → 关库」;支持 `Destroy()` → `GetB()` 重连重新建库(同一秒文件名冲突时追加 `_1`/`_2` 后缀)。
|
||||
- 文档:全面更新 `docs/data_record_debug.md`(异步运行期行为、接口与宏一览、新增记录点四步配方、6 张表记录点清单、已知限制、验证参考)与 `README.md` 相关章节(2、3、4.3、5.2、7.1、10.1、11.2、11.3、12、13 节)。
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -21,6 +27,10 @@
|
||||
- VS Code 配置同步:`settings.json` 的 `cmake.buildDirectory` 改为 `${workspaceFolder}/build/${variant:scanMode}/${buildType}/${variant:recordMode}`、`compileCommands` 指向 `build/X256_PS/Debug/REC_ON/compile_commands.json`;`cmake-variants.yaml` 新增 `recordMode` 变体(REC_AUTO / REC_OFF / REC_ON,映射 `RDP_DATA_RECORD`);`c_cpp_properties.json` 的 `compileCommands` 更新到新目录;`launch.json` 的 `preLaunchTask` 改为「配置并构建全部(Debug/REC_ON)」,`program`/`symbolSearchPath` 指向 `build/X256_PS/Debug/REC_ON/bin`。
|
||||
- 更新 README(3 节目录结构、4.2~4.4 节构建说明、12 节版本历史、13 节参考文档)与 `docs/data_record_debug.md` 第 2 节构建示例,同步新的构建目录布局、脚本签名与 5 个任务;`requirements.md` 更新为本次构建脚本/VS Code 任务调整需求。
|
||||
- 本次改动的构建验证:5 个任务全部实际执行(清理全部 → Release/REC_OFF → Release/REC_ON → Debug/REC_OFF → Debug/REC_ON),退出码均为 0;8 种配置(PS/MS × Release/Debug × REC_OFF/REC_ON)构建日志均 0 warning、0 error;每个配置目录均生成 `bin/data_process_class_dll.dll`、`lib/`(REC_ON 另有 `lib/rdp_record.lib`)与 `compile_commands.json`;编译命令中的宏与配置对应(PS 为 `-DPHASE_SCANNING`、MS 为 `-DMECHANICAL_SCANNING`,REC_ON 含 `RDP_RECORD_ENABLE`);「清理全部」删除 8 个配置目录及旧版单一构建目录而 `build/output` 未被删除;错误扫描体制/构建类型/记录模式与未配置即构建等非法路径均返回非 0。补充验证:已构建配置再次执行 `build_all.bat` 为 0 个编译/链接步骤(增量幂等);模拟 MSVC 环境无效时 `build_all.bat` 在第一个配置即中止(未尝试第二个扫描体制,退出码 1);`clean.bat` 单配置清理后目录消失且可正常重建。
|
||||
- **12 个业务记录点由「同步构造实体写库」改为「帧/圈内收集源结构体 → 一次入队」(数据记录性能优化)**:`data_process.cpp`(TWS/TAS 输入点迹)、`dot_coh.cpp`、`dot_coh_tas.cpp`、`track_init.cpp`(临时航迹)、`track_asso.cpp`(可靠航迹 + 关联信息按 `RDP_RECORD_ASSO_CHUNK_ROWS` 分块)、`track_asso_tas.cpp`(同)共 12 处;记录时机、条件与记录行数/表列均保持不变;6 个业务文件删除 `#include "rdp_record_entity.h"`,所有新增代码仍在 `#ifdef RDP_RECORD_ENABLE` 内,记录关闭构建零代码、零影响。
|
||||
- **记录线程模型与 Shutdown 语义(数据记录性能优化)**:由「处理线程单连接 + 互斥锁串行同步写」改为「处理线程只入队 + 独立写线程承担实体构造与全部写库」;`RDP_RECORD`/`RDP_RECORD_BATCH` 保留为遗留同步接口(在调用线程直接写库、会阻塞,仅供测试/特殊场景);`Shutdown()` 会**排空**队列与 pending 后再关库,并在关库前打印统计。
|
||||
- **未改变记录语义(数据记录性能优化)**:不采用 P0-a(关联信息裁剪)与 P2(记录策略/轮转),⑥ 仍按每个候选一行写入;跨表写入顺序仍不保证、各表内顺序保持;`REC_OFF` 构建零影响与优化前一致。
|
||||
- **本次优化的验证(可复查)**:8 种配置(PS/MS × Release/Debug × REC_OFF/REC_ON)0 warning 0 error;dup 场景 6 圈/200 目标/每帧 150 点 REC_ON vs REC_OFF:普通帧每帧新增 p99 ≈ 0.09~0.11 ms(≤1 ms 阈值内)、判圈帧 `track` 新增中位 ≈ 2.9~4.1 ms(压测 p99 ≈ 13~24 ms,因未采用 P0-a,属压测上限),受控 50 目标/50 点场景判圈帧新增中位约 3 ms、最大 5.96 ms;记录行数与改动前完全一致(`RdpRecPoint` 90,000 / `RdpRecPointCohesionTWS` 4,170 / `RdpRecAssoInfo` 211,584 / `RdpRecTempTrack` 8,330 / `RdpRecTrustTrack` 304,合计 314,388),`integrity_check=ok`、DDL 无 `AUTOINCREMENT`、`journal_mode=wal`、id 严格递增、`dropped_rows=0`;60 圈长跑 9,987,370 行、`dropped_rows=0`、`write_errors=0`、队列峰值约 19.8 MB、DB 约 1.06 GB;故障注入(记录目录不可建时 `Init` 静默失败、驱动正常;小队列测试构建丢弃 314,388 行且计数正确)与 `Destroy()`→`GetB()` 重连(两个独立 db 均正常写入)测试通过。原始日志与验收脚本位于 git 忽略的 `build/analysis/verify/`。
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
- **防御性加固**:见 archived_docs/BUG_FIX_REPORT.md——越界防护、时间戳正序校验、方位角 ±π 环绕、asin 定义域钳位、IMM 概率奇异保护等已修复项。
|
||||
- **日志文件输出**:`RDP_LOG_ENABLE` 定义时,首次 `Data_Process_Factory::GetB()` 自动用 `rdbuf()` 将 `std::cout` 重定向到 `RDP_LOG_DIR`(`.\rdp_logs\`)下按日期+时间命名的日志文件,`Destroy()` 时恢复控制台并关闭文件;`RDP_LOG` 日志宏在每条日志前附加 `[时间戳][文件:行号]`。该宏在 `parameters.h` 中**当前为注释状态(默认不启用)**,即默认不做重定向、日志输出到控制台(见 10.1)。
|
||||
- **版本号输出**:`parameters.h` 的 `MAJOR_VERSION`/`MINOR_VERSION`/`PATCH_VERSION`(当前 1.5.8)经两级 `STRINGIFY` 宏拼成 `VERSION_STRING`(`V1.5.8.PS` / `V1.5.8.MS`);`Data_process_class_dll` 基类公开成员 `const char* version_info` 由 `Data_Process_Factory::GetB()` 在创建实例时赋值,宿主可直接读取以核对/上报 DLL 版本(b227ae1)。
|
||||
- **数据记录调试功能(可选编译)**:`rdp_record` 模块把结构体按「一个记录实体一张表」写入 SQLite(一次运行一个 db 文件、多张表),结构体到表的映射由内置的 ormpp(`third_party/ormpp`,含 iguana、frozen 与 SQLite 3.49.1 amalgamation)完成;由 CMake 选项 `RDP_DATA_RECORD`(`AUTO`/`ON`/`OFF`)控制,**AUTO 时 Debug 构建启用、Release(交付)构建完全不编译**——记录模块源码不参与编译、`RDP_RECORD`/`RDP_RECORD_BATCH` 宏展开为 `((void)0)`(实参不求值),DLL 不新增依赖与导出符号、无性能损失。启用时 `GetB()` 建目录并开库(`.\rdp_records\rdp_record_YYYYMMDD_HHMMSS.db`),首次写入某实体时自动 `CREATE TABLE IF NOT EXISTS`,同步写入、失败降级(不抛出、不影响雷达处理)。当前内置 **6 张在用记录表**(输入点迹、TWS/TAS 凝聚后点迹、临时航迹、可靠航迹、点航关联信息,共 7 个实体定义、其中 1 个已停用),**可靠航迹与关联信息两通道均覆盖 TWS 与 TAS**(TAS 侧由 5cb67cc 补齐,关联信息以 `tas_flag` 区分来源),并新增 `point_id`/`frame_id`/`circle_id`/`temp_track_id`/`track_id` 等关联标识,可把一次处理的点迹→凝聚→临时航迹→可靠航迹→关联信息串成完整链路;新增记录数据只需「加结构体 + 注册 + 一行记录点」,详见 docs/data_record_debug.md(见 4.3、10.1、11.3)。
|
||||
- **数据记录调试功能(可选编译、异步批量写入)**:`rdp_record` 模块把结构体按「一个记录实体一张表」写入 SQLite(一次运行一个 db 文件、多张表),结构体到表的映射由内置的 ormpp(`third_party/ormpp`,含 iguana、frozen 与 SQLite 3.49.1 amalgamation)完成;由 CMake 选项 `RDP_DATA_RECORD`(`AUTO`/`ON`/`OFF`)控制,**AUTO 时 Debug 构建启用、Release(交付)构建完全不编译**——记录模块源码不参与编译、记录宏(6 个异步入队宏与遗留 `RDP_RECORD`/`RDP_RECORD_BATCH`)展开为 `((void)0)`(实参不求值),DLL 不新增依赖与导出符号、无性能损失。**V1.5.8 优化后的运行模型**:`GetB()` 建目录、开库(`.\rdp_records\rdp_record_YYYYMMDD_HHMMSS.db`)并启动独立**写线程**;12 个记录点只做「帧/圈内收集源结构体 → 一次入队」(收集代码均在 `#ifdef RDP_RECORD_ENABLE` 内),实体构造(数组转文本)与 SQLite **批量事务**全部在写线程完成;入队队列**有界**(默认 64 MB / 4096 批,满则整批丢弃并计入 `stats().dropped_rows`,不阻塞处理线程),写线程按 4096 行或等待 50 ms 凑批提交,SQLite 使用 WAL + `synchronous=OFF` + 8 MB 缓存,建表后去掉 `AUTOINCREMENT`;`Destroy()` 时停止入队、排空、`wal_checkpoint(TRUNCATE)`、关库并打印统计,支持 `Destroy()`→`GetB()` 重连重新建库(同秒文件名冲突追加 `_1`/`_2` 后缀)。当前内置 **6 个记录实体定义、6 张在用表**(输入点迹、TWS/TAS 凝聚后点迹、临时航迹、可靠航迹、点航关联信息),**可靠航迹与关联信息两通道均覆盖 TWS 与 TAS**(TAS 侧由 5cb67cc 补齐,关联信息以 `tas_flag` 区分来源),并新增 `point_id`/`frame_id`/`circle_id`/`temp_track_id`/`track_id` 等关联标识,可把一次处理的点迹→凝聚→临时航迹→可靠航迹→关联信息串成完整链路;新增记录数据按「加记录实体 + 注册 + 加异步入队接口/宏 + 写线程分支 + 业务侧收集调用」四步进行,详见 docs/data_record_debug.md(见 4.3、10.1、11.3)。
|
||||
|
||||
---
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
├── docs/ # 专项说明文档(按当前源码整理,与 README 互补)
|
||||
│ ├── data_process_flow_report.md # 数据处理流程与关键逻辑说明(前处理/判圈、凝聚、关联与滤波、起始、外推消亡、TAS 波束、参数脉络)
|
||||
│ ├── track_init_readme.md # 航迹起始(Track Init)详细逻辑说明(阶段流程、条件汇总、输出字段映射、风险点、代码索引)
|
||||
│ └── data_record_debug.md # 数据记录调试功能说明(开关与构建、运行期行为、表结构约定、新增记录点四步配方、6 张在用记录表清单与字段、查询示例)
|
||||
│ └── data_record_debug.md # 数据记录调试功能说明(开关与构建、异步运行期行为与队列/批事务参数、表结构约定、新增记录点四步配方、6 张在用记录表清单与字段、查询示例、已知限制与验证参考)
|
||||
├── design_docs/ # 设计与讨论资料留档(被 .gitignore 忽略、不入库;含设计讨论、产品介绍与 README/流程报告的导出、渲染版本)
|
||||
├── .vscode/ # VS Code 工程配置
|
||||
│ ├── settings.json # cmake.generator=NMake、MSVC2022 x64 环境变量、构建目录=变体推导、compile_commands
|
||||
@@ -110,8 +110,8 @@
|
||||
│ ├── data_process_class_dll_global.h # DLL 导入导出宏
|
||||
│ ├── data_process_class_dll.cpp # 工厂实现(GetB/Destroy 单例)
|
||||
│ ├── rdp_log.h / .cpp # 日志输出模块:rdbuf() 重定向 std::cout、RDP_LOG 宏(开关宏在 parameters.h)
|
||||
│ ├── rdp_record.h / .cpp # 数据记录调试模块:结构体→SQLite 表(内置 ormpp)、RDP_RECORD/RDP_RECORD_BATCH 宏、按实体自动建表、失败降级(开关由 CMake 选项 RDP_DATA_RECORD 控制;实现以 C++20 独立编译)
|
||||
│ ├── rdp_record_entity.h # 记录实体(=数据表)定义区:6 张在用表 + 1 个已停用实体(输入点迹、TWS/TAS 凝聚后点迹、临时航迹、可靠航迹、关联信息;输出航迹示例已停用)+ RDP_RECORD_DECLARE 声明宏;新增需要记录的数据在此加结构体
|
||||
│ ├── rdp_record.h / .cpp # 数据记录调试模块:结构体→SQLite 表(内置 ormpp)、异步入队接口与宏(RDP_RECORD_POINTS 等 6 个)+ 遗留 RDP_RECORD/RDP_RECORD_BATCH、有界队列、异步写线程、批量事务、Stats 统计、失败降级/熔断(开关由 CMake 选项 RDP_DATA_RECORD 控制;实现以 C++20 独立编译)
|
||||
│ ├── rdp_record_entity.h # 记录实体(=数据表)定义区:6 张在用表(输入点迹、TWS/TAS 凝聚后点迹、临时航迹、可靠航迹、关联信息)+ RDP_RECORD_DECLARE 声明宏;新增需要记录的数据在此加结构体
|
||||
│ ├── data_process.h / .cpp # Data_Process 实现:接口分发、TWS/TAS 主流程、参数管理、清空/删除/转跟踪;含 point_id/frame_id/circle_id 自增计数(记录用)
|
||||
│ ├── struct.h # 内部结构体:PointRecv / Trust_Track / Temp_track / Asso_Info / Tracking_Target
|
||||
│ ├── parameters.h # 编译期宏参数(按 MECHANICAL_SCANNING / PHASE_SCANNING 分两组)
|
||||
@@ -197,6 +197,8 @@ build/<X256_PS|X256_MS>/<Debug|Release>/<REC_AUTO|REC_OFF|REC_ON>
|
||||
|
||||
数据记录调试功能由缓存变量 `RDP_DATA_RECORD` 控制(取值 `AUTO`/`ON`/`OFF`,缺省 `AUTO`:Debug 启用、Release 关闭)。显式取值时构建目录名随之不同(`REC_OFF`/`REC_ON`),因此关闭与开启记录模块的产物可以并存。启用时配置阶段打印 `数据记录调试功能:启用(C++20 静态库 rdp_record + 内置 SQLite)`,并额外构建静态库 `rdp_record`(记录模块 + SQLite,链接进 DLL);关闭时打印 `...:关闭(不编译记录模块,RDP_RECORD 为空操作)`,记录模块源码完全不参与编译。开关语义、表结构约定与新增记录点配方见 docs/data_record_debug.md。
|
||||
|
||||
记录功能的性能参数集中在 `parameters.h` 的「数据记录性能参数」段(均以 `#ifndef` 保护,便于用 `/DRDP_RECORD_...` 编译选项临时覆盖做压测):入队队列上限 `RDP_RECORD_QUEUE_CAP_BYTES`(默认 64 MB)与 `RDP_RECORD_QUEUE_MAX_BATCHES`(默认 4096 批)、写线程批提交 `RDP_RECORD_WRITE_BATCH_ROWS`(默认 4096 行)/ `RDP_RECORD_WRITE_MAX_WAIT_MS`(默认 50 ms)、关联信息分块 `RDP_RECORD_ASSO_CHUNK_ROWS`(默认 131072 行)、SQLite 缓存/忙等待/WAL 开关(`RDP_RECORD_SQLITE_CACHE_KB`=8192、`RDP_RECORD_SQLITE_BUSY_TIMEOUT_MS`=2000、`RDP_RECORD_SQLITE_WAL`=1)、连续错误熔断阈值 `RDP_RECORD_MAX_CONSECUTIVE_ERRORS`(默认 10)与丢弃日志限频 `RDP_RECORD_DROP_LOG_INTERVAL`(默认 1024 行);默认值、语义与运行统计(`rdp_record::Stats`/`stats()`)见 10.1 与 docs/data_record_debug.md 第 3 节。
|
||||
|
||||
VS Code 任务(`.vscode/tasks.json`)只保留以下 5 个(原有任务已全部删除),可在「终端 → 运行任务」中选择,或按 `Ctrl+Shift+B` 运行默认构建任务「配置并构建全部(Release/REC_OFF)」:
|
||||
|
||||
| 任务 | 配置 |
|
||||
@@ -336,7 +338,7 @@ classDiagram
|
||||
| kalman | 两点/三点滤波初始化、线性卡尔曼预测/滤波、EKF(3 维量测含多普勒)、马氏统计距离 d、盲速 Bind_speed |
|
||||
| coor_trans | 极坐标 ↔ 直角坐标转换 |
|
||||
| rdp_log(rdp_log.h/.cpp) | 日志输出:按 parameters.h 的 `RDP_LOG_ENABLE` 开关,在 `Init()` 中用 `rdbuf()` 将 `std::cout` 重定向到日期+时间命名的日志文件、`Shutdown()` 恢复控制台并关闭;提供 `RDP_LOG` 宏自动附加 `[时间戳][文件:行号]` 前缀 |
|
||||
| rdp_record(rdp_record.h/.cpp、rdp_record_entity.h) | 数据记录调试(可选编译):由 CMake 选项 `RDP_DATA_RECORD` 决定是否参与编译;开启时把「记录实体」(结构体)经内置 ormpp 映射写入 SQLite 表(一实体一表,首次写入自动 `CREATE TABLE IF NOT EXISTS`,一次运行一个 db 文件),`RDP_RECORD`/`RDP_RECORD_BATCH` 记录点宏在关闭时展开为空操作;`Init()`/`Shutdown()` 由工厂的 `GetB()`/`Destroy()` 调用;建表/插入失败一律降级(每个实体只提示一次),不影响雷达处理。**当前 7 个实体定义、6 张在用表**:`RdpRecPoint`(输入点迹,TWS/TAS 共用)、`RdpRecPointCohesionTWS`、`RdpRecPointCohesionTAS`、`RdpRecTempTrack`、`RdpRecTrustTrack`、`RdpRecAssoInfo`(后两者 **TWS 与 TAS 两通道均写入**,关联信息以 `tas_flag` 区分来源;另有已停用的 `RdpRecTrackOut`),并有数组转字符串辅助 `arrayToString()`(用于把 `X`/`P` 等状态数组存为文本列) |
|
||||
| rdp_record(rdp_record.h/.cpp、rdp_record_entity.h) | 数据记录调试(可选编译):由 CMake 选项 `RDP_DATA_RECORD` 决定是否参与编译。开启时 `GetB()` 建库并启动**独立写线程**,12 个记录点只做「帧/圈内收集源结构体 → 一次入队」;写线程从**有界队列**(默认 64 MB / 4096 批,满则整批丢弃并计入 `stats().dropped_rows`,不阻塞处理线程)取批,构造记录实体(数组转文本)并按表以**批量事务**写入 SQLite(达到 4096 行或等待 50 ms 提交;SQLite 使用 WAL + `synchronous=OFF` + 8 MB 缓存,建表后去掉 `AUTOINCREMENT`);写失败每表只提示一次并永久跳过该表、连续 10 次错误熔断;`Shutdown()` 依次停止入队、排空、`wal_checkpoint(TRUNCATE)`、关库并打印统计(`rdp_record::Stats`/`stats()` 可随时读取,`Destroy()`→`GetB()` 可重连重新建库)。**当前 6 个实体定义、6 张在用表**:`RdpRecPoint`(输入点迹,TWS/TAS 共用)、`RdpRecPointCohesionTWS`、`RdpRecPointCohesionTAS`、`RdpRecTempTrack`、`RdpRecTrustTrack`、`RdpRecAssoInfo`(后两者 **TWS 与 TAS 两通道均写入**,关联信息以 `tas_flag` 区分来源),并有数组转字符串辅助 `arrayToString()`(用于把 `X`/`P` 等状态数组存为文本列);关闭(AUTO 的 Release 结果)时记录模块不参与编译、所有记录宏为空操作,旧 `RDP_RECORD`/`RDP_RECORD_BATCH` 仅作为遗留同步接口保留(会阻塞调用线程) |
|
||||
| Track_Init_Direct_Tracking / Track_Asso_Direct_Tracking | 引导跟踪(direct tracking)专用起批/关联。**当前入口为空实现、未启用**;track_asso_direct_tracking.cpp 未加入 CMake 源列表 |
|
||||
|
||||
---
|
||||
@@ -397,7 +399,7 @@ classDiagram
|
||||
|
||||
> 日志输出为 DLL 内部功能(不对外导出,接口见 10.1 的宏开关):首次 `GetB()` 创建实例时自动初始化(`RDP_LOG_ENABLE` 定义则将 `std::cout` 重定向到 `.\rdp_logs\` 下按日期+时间命名的日志文件,该宏当前为注释状态,见 10.1),`Destroy()` 销毁实例时恢复 `std::cout` 并关闭日志文件。
|
||||
|
||||
> 数据记录同样为 DLL 内部功能(不对外导出):记录功能启用时,`data_preprocess` 逐点写输入点迹、`dot_coh`/`dot_coh_tas` 写凝聚后点迹、`track_init` 写临时航迹、`track_asso` 写可靠航迹与关联信息,全部落到 `.\rdp_records\rdp_record_YYYYMMDD_HHMMSS.db` 的 6 张在用表中(表清单与字段见 docs/data_record_debug.md)。
|
||||
> 数据记录同样为 DLL 内部功能(不对外导出):记录功能启用时,`data_preprocess` 在帧内收集输入点迹(判圈 return 之前入队)、`dot_coh`/`dot_coh_tas` 收集凝聚后点迹、`track_init` 收集临时航迹、`track_asso`/`track_asso_tas` 收集可靠航迹与关联信息(关联信息按 `RDP_RECORD_ASSO_CHUNK_ROWS` 分块、函数返回前 flush),收集完成后各调用一次**异步入队**宏;实体构造与写库由记录写线程异步完成,落盘为 `.\rdp_records\rdp_record_YYYYMMDD_HHMMSS.db` 的 6 张在用表(表清单与字段见 docs/data_record_debug.md 第 7 节)。处理线程不会被记录 I/O 阻塞,但队列满时记录会整批丢弃(`stats().dropped_rows` 可查),进程异常退出时队列尾部与 WAL 尾部可能丢失。
|
||||
|
||||
### 7.2 data_preprocess 返回值的含义
|
||||
|
||||
@@ -953,8 +955,20 @@ $$
|
||||
|---|---|---|---|
|
||||
| RDP_LOG_ENABLE | 注释停用(默认不启用) | 同左 | 日志文件输出开关:定义时将 `std::cout` 重定向到日志文件;**当前源码中该宏为注释状态**(parameters.h 的注释文字仍写「定义(默认)」,以代码实际状态为准),不重定向、日志输出到标准输出/控制台 |
|
||||
| RDP_LOG_DIR | `.\rdp_logs\` | 同左 | 日志文件输出目录(相对宿主进程当前工作目录,目录不存在时自动创建) |
|
||||
| RDP_RECORD_ENABLE | 由 CMake 选项 `RDP_DATA_RECORD` 定义(AUTO 时 Debug 定义) | 同左 | 数据记录调试开关:**不要在 parameters.h 中手工定义**(由构建系统传入);定义时记录模块参与编译,未定义时 `RDP_RECORD`/`RDP_RECORD_BATCH` 展开为空操作,见 4.3 与 11.3 |
|
||||
| RDP_RECORD_ENABLE | 由 CMake 选项 `RDP_DATA_RECORD` 定义(AUTO 时 Debug 定义) | 同左 | 数据记录调试开关:**不要在 parameters.h 中手工定义**(由构建系统传入);定义时记录模块参与编译,未定义时全部记录宏(6 个异步入队宏 + 遗留 `RDP_RECORD`/`RDP_RECORD_BATCH`)展开为空操作,见 4.3 与 11.3 |
|
||||
| RDP_RECORD_DB_DIR | `.\rdp_records\` | 同左 | 数据记录库输出目录(相对宿主进程当前工作目录,目录不存在时自动创建;仅记录功能启用时产生) |
|
||||
| RDP_RECORD_POINTS / RDP_RECORD_COHESION_TWS / RDP_RECORD_COHESION_TAS / RDP_RECORD_TEMP_TRACKS / RDP_RECORD_TRUST_TRACKS / RDP_RECORD_ASSO_INFOS | 由 `RDP_RECORD_ENABLE` 条件定义(记录开启时展开为 `std::move` + 异步入队,关闭时 `((void)0)`) | 同左 | 6 个异步入队记录点宏,参数为对应源结构体的 `std::vector`(`PointRecv` / `Temp_track` / `Trust_Track` / `Asso_Info`),宏内部 `std::move`、调用后 vector 为空;宏与表的对应关系见 docs/data_record_debug.md 第 4、7 节。遗留的 `RDP_RECORD`/`RDP_RECORD_BATCH` 为同步接口(会阻塞) |
|
||||
| RDP_RECORD_QUEUE_CAP_BYTES | `64*1024*1024`(64 MB) | 同左 | 入队队列字节上限;超过该值整批丢弃并计入 `stats().dropped_rows`,不阻塞处理线程 |
|
||||
| RDP_RECORD_QUEUE_MAX_BATCHES | 4096 | 同左 | 入队队列批数上限(与字节上限同时生效) |
|
||||
| RDP_RECORD_WRITE_BATCH_ROWS | 4096 | 同左 | 写线程每个事务的目标提交行数(按表累积到该行数即提交) |
|
||||
| RDP_RECORD_WRITE_MAX_WAIT_MS | 50 | 同左 | 写线程为凑批等待的最长时间(ms),超时后提交现有数据 |
|
||||
| RDP_RECORD_ASSO_CHUNK_ROWS | 131072 | 同左 | 关联信息(⑥)单次入队分块上限,限制处理线程收集 `Asso_Info` 的内存峰值 |
|
||||
| RDP_RECORD_SQLITE_CACHE_KB | 8192 | 同左 | SQLite 页缓存大小(KB,执行 `PRAGMA cache_size=-8192`,即 8 MB) |
|
||||
| RDP_RECORD_SQLITE_BUSY_TIMEOUT_MS | 2000 | 同左 | SQLite 忙等待超时(ms) |
|
||||
| RDP_RECORD_SQLITE_WAL | 1 | 同左 | 是否尝试开启 WAL(1=尝试;失败/不支持时回退默认 journal 模式并提示) |
|
||||
| RDP_RECORD_MAX_CONSECUTIVE_ERRORS | 10 | 同左 | 异步批量写入连续失败达到该次数后全局熔断(停止记录并丢弃队列) |
|
||||
| RDP_RECORD_DROP_LOG_INTERVAL | 1024 | 同左 | 队列丢弃日志限频:每累计丢弃该行数提示一次 |
|
||||
| rdp_record::Stats / stats()(非宏) | — | — | 运行统计结构体(定义于 `rdp_record.h`,任意时刻线程安全读取、`Shutdown()` 时自动打印):`enqueued_rows`/`dropped_rows`/`written_rows`/`transactions`/`write_errors`/`queue_peak_bytes`/`queue_peak_rows` |
|
||||
| DATA_RATE_TAS | 已停用(原 0.3) | 已停用(原 0.0625) | 编译期宏已注释停用;TAS 数据率统一由运行时参数 RadarPara.DATA_RATE_TAS 提供(见 10.2) |
|
||||
| SIGMA_R / SIGMA_A / SIGMA_E / SIGMA_V | 10.0 / 0.02 / 0.2 / 2.0(两体制相同) | 同左 | 量测误差:距离 m / 方位 rad / 俯仰 rad / 速度 m/s |
|
||||
| DOT_COH_RANGE / DOT_COH_V / DOT_COH_AZI | 80 / 2 / 6 | 同左 | 凝聚门限:距离 m / 速度 m/s / 方位 ° |
|
||||
@@ -1034,6 +1048,7 @@ $$
|
||||
- **质量相关宏存在未使用项(V1.5.8)**:`TRACK_QUALITY_MIN`、`TRACK_DIE_QUALITY_PRAMARY` 定义但未参与任何计算;`TRACK_DIE_ROUND` 在 TWS 普通航迹改为质量消亡后也不再被引用(保留备用)。调参时不要误以为三者生效。
|
||||
- **起批质量门限取的是临时航迹「末点」质量**:`tmp_track_to_trust_track()` 用 `(*Iter)[L-1].quality` 与按长度取的门限比较,而临时航迹质量只在 L=3 时写过所有点、L>3 时只更新末点,因此门限表实际是针对「最新一点的质量」,不是整条航迹的平均质量。
|
||||
- **质量长度项含整数除法**:`1 − Valid_point_sum/Point_sum` 中两操作数均为 `int`,先做整数除法截断再乘 0.5,`Valid_point_sum/Point_sum` 的精度损失最大约 1/Point_sum;对点数很多的航迹影响可忽略,但精确复现公式时需注意。
|
||||
- **数据记录未采用关联信息裁剪与记录策略(P0-a / P2,V1.5.8 优化时明确不做)**:⑥ 关联信息仍按「每个候选一行」写入(单圈 ≈ 点迹数 × 航迹数),未裁剪为只记录取中候选,也未做按容量/时长切库与旧库清理;因此高航迹密度场景下判圈帧仍有可观记录开销(200 目标/150 点压测 p99 ≈ 13~24 ms),记录库也可能很大(60 圈约 1.06 GB)。属本轮刻意取舍,评价实时性时需区分普通帧(p99 ≈ 0.1 ms)与判圈帧。
|
||||
|
||||
### 11.3 未启用功能
|
||||
|
||||
@@ -1042,8 +1057,9 @@ $$
|
||||
- TAS **自动**开启/退出逻辑(tas_auto_start/tas_auto_end)注释停用,仅支持手动 tracking_start/tracking_stop。
|
||||
- RadarPara 中的起批/关联波门字段未被实现读取(见 10.2)。
|
||||
- build/output/ 下为早期打包导出物,其 data_process_class_dll.h 时间戳仍为 32 位 int,与当前 64 位接口不一致,勿作为对接依据。
|
||||
- **数据记录调试功能默认只在 Debug 启用**:CMake 选项 `RDP_DATA_RECORD` 的 AUTO 默认值为「Debug 启用、Release 关闭」,交付/Release 构建下记录模块(`rdp_record.cpp`、`sqlite3.c`)不参与编译、`RDP_RECORD` 为空操作(导出符号与依赖均无变化);如需在 Release 构建中临时排查可显式 `-DRDP_DATA_RECORD=ON`,但正式交付应保持 AUTO/OFF(见 docs/data_record_debug.md)。记录点中的字段错误只在启用记录功能的构建中才会被检查(关闭时宏丢弃实参)。
|
||||
- **数据记录调试功能默认只在 Debug 启用**:CMake 选项 `RDP_DATA_RECORD` 的 AUTO 默认值为「Debug 启用、Release 关闭」,交付/Release 构建下记录模块(`rdp_record.cpp`、`sqlite3.c`)不参与编译、所有记录宏为空操作(导出符号与依赖均无变化);如需在 Release 构建中临时排查可显式 `-DRDP_DATA_RECORD=ON`,但正式交付应保持 AUTO/OFF(见 docs/data_record_debug.md)。记录点中的字段错误只在启用记录功能的构建中才会被检查(关闭时宏丢弃实参)。
|
||||
- **记录表的三个观察**:① 可靠航迹表 `RdpRecTrustTrack` 在输出分支内写库,而该分支入口已把 `Track_Update_Flag` 清零(`track_asso.cpp` / `track_asso_tas.cpp` 均先置 0 再输出),因此表中的 `Track_Update_Flag` 恒为 0,判断「本圈是否更新」应改用该表是否出现对应 `track_id`/`circle_id` 的记录;② `Trust_Track` 的极坐标量快照 `Range`/`Azimuth`/`Elevation`/`Velocity` 只在航迹当圈有输出时回填,长期未输出(外推超限)的航迹在表中该项为 0 或旧值;③ TAS 关联信息行中 `R`/`VT` 为占位 0、`thd_r` 恒为 true、`track_index` 存的是 TAS 目标批号(TWS 行才是航迹表下标),跨通道分析时不要按 TWS 语义解读。
|
||||
- **数据记录为异步写线程 + 有界队列(V1.5.8 优化)**:处理线程只做收集与入队,写线程在 Windows 下以 BELOW_NORMAL 优先级运行、承担全部实体构造与写库(遗留同步接口经 `db_mtx` 串行化);队列满时**整批丢弃**(`stats().dropped_rows` 可查,日志默认每 1024 行提示一次),处理线程绝不因记录阻塞。因此:① 进程异常退出(未调用 `Destroy()`)时队列尾部与 WAL 尾部可能丢失,采集结束务必 `Destroy()`;② 同一份记录的**跨表写入顺序不保证**,`id` 为 SQLite ROWID 自增、单调但不保证连续;③ WAL 依赖本地盘/共享内存文件系统,网络盘上可能回退为默认 journal;④ 写失败时该表永久跳过、连续 10 次错误全局熔断,均只降级记录功能、不影响雷达处理。
|
||||
|
||||
### 11.4 航迹起始相关的未修复观察(详见 docs/track_init_readme.md 第 15 节)
|
||||
|
||||
@@ -1065,6 +1081,7 @@ $$
|
||||
|
||||
| 提交 | 内容 |
|
||||
|---|---|
|
||||
| 工作区(数据记录性能优化,未提交) | 数据记录调试功能性能优化:`rdp_record.h` 新增 `rdp_record::Stats`/`stats()` 与 6 个异步入队接口(`WritePoints`/`WriteCohesionTWS`/`WriteCohesionTAS`/`WriteTempTracks`/`WriteTrustTracks`/`WriteAssoInfos`,参数为源结构体 vector 按值)及对应宏 `RDP_RECORD_POINTS`/`RDP_RECORD_COHESION_TWS`/`RDP_RECORD_COHESION_TAS`/`RDP_RECORD_TEMP_TRACKS`/`RDP_RECORD_TRUST_TRACKS`/`RDP_RECORD_ASSO_INFOS`,旧 `RDP_RECORD`/`RDP_RECORD_BATCH` 保留为遗留同步接口;`rdp_record.cpp` 的 `Init()` 建库后启动**异步写线程**(Windows 下 BELOW_NORMAL 优先级),处理线程只入队,实体构造(数组转文本)与批量事务由写线程完成;**有界队列**(`RDP_RECORD_QUEUE_CAP_BYTES` 64 MB / `RDP_RECORD_QUEUE_MAX_BATCHES` 4096 批,满则整批丢弃并计数);写线程按 `RDP_RECORD_WRITE_BATCH_ROWS`(4096 行)或 `RDP_RECORD_WRITE_MAX_WAIT_MS`(50 ms)提交事务;SQLite 改为 WAL(失败回退)+ `synchronous=OFF` + 8 MB 页缓存 + `busy_timeout=2000`,建表后读 `sqlite_master` 去掉 `AUTOINCREMENT`;写失败每表提示一次、连续 10 次错误熔断;`Shutdown()` 改为「停止入队 → 排空 → `wal_checkpoint(TRUNCATE)` → 关库 → 打印 stats」;支持 `Destroy()`→`GetB()` 重连重新建库(同秒文件名追加 `_1`/`_2` 后缀);6 个业务 cpp 的 12 个记录点改为「帧/圈内收集源结构体 → 一次入队」并删除 `#include "rdp_record_entity.h"`,记录行数与表列不变,`#ifdef RDP_RECORD_ENABLE` 外零代码;`parameters.h` 新增 10 个可调参数宏。验证:8 种配置(PS/MS × Release/Debug × REC_OFF/REC_ON)0 warning 0 error;dup 场景 6 圈/200 目标/150 点 REC_ON vs REC_OFF:普通帧每帧新增 p99 0.09~0.11 ms(≤1 ms 阈值内)、判圈帧 track 新增中位 2.9~4.1 ms(压测 p99 13~24 ms,因未采用 P0-a);受控 50 目标/50 点场景判圈帧新增中位约 3 ms、最大 5.96 ms;记录行数 314,388 与改动前一致(90,000/4,170/211,584/8,330/304)、`integrity_check=ok`、无 `AUTOINCREMENT`、`journal_mode=wal`、id 严格递增、`dropped_rows=0`;60 圈长跑 9,987,370 行、`dropped_rows=0`、`write_errors=0`、队列峰值约 19.8 MB、DB 约 1.06 GB;故障注入(记录目录不可建时 `Init` 静默失败、处理正常;小队列构建丢弃有计数)与 `Destroy()`→`GetB()` 重连测试通过 |
|
||||
| 工作区(构建脚本与 VS Code 任务调整,未提交) | 构建目录改为按完整配置隔离 `build/<X256_PS\|X256_MS>/<Debug\|Release>/<REC_AUTO\|REC_OFF\|REC_ON>`;新增 `config_paths.bat`(目录推导与参数校验)、`build_all.bat`(两种扫描体制依次配置+构建)、`clean_all.bat`(清理全部配置);`configure.bat`/`build.bat`/`clean.bat` 参数改为「扫描体制 + 构建类型 + 记录模式」并自动推导目录;`tasks.json` 原有 15 个任务全部删除,仅保留 5 个任务(清理全部 + Release/Debug × REC_OFF/REC_ON 的「配置并构建全部」);`settings.json`/`cmake-variants.yaml`/`c_cpp_properties.json`/`launch.json` 同步;验证:5 个任务实际执行均返回 0,8 种配置(PS/MS × Release/Debug × REC_OFF/REC_ON)0 warning、0 error,产物位于各配置目录的 `bin/`、`lib/` |
|
||||
| 3088ce4(**V1.5.8** 版本号来源) | 更新版本号至 V1.5.8(`PATCH_VERSION` 7→8) |
|
||||
| 782bff5 | 调整航迹质量下降逻辑(改为按未更新时长的二次函数衰减,系数 −0.0008 / −0.0025);**普通 TWS 航迹消亡条件改为由航迹质量确定**(`Quality < TRACK_DIE_QUALITY` = 0.3),重点航迹与 TAS 仍按外推圈数 |
|
||||
@@ -1121,7 +1138,7 @@ $$
|
||||
| CHANGELOG.md | 变更日志(Keep a Changelog 格式,按 git 提交记录维护) |
|
||||
| docs/data_process_flow_report.md | 数据处理流程与关键逻辑说明(按当前源码整理:前处理与判圈、凝聚、TWS/TAS 关联与滤波、航迹起始、外推消亡、TAS 波束控制、参数脉络;含与本文档的历史差异提示)——原同内容 HTML 渲染版(`data_process_flow_report.html`)已于 e0bf227 删除,HTML 版本在 git 忽略的 `design_docs/` 内留档 |
|
||||
| docs/track_init_readme.md | 航迹起始(Track Init)专项说明:分阶段流程、起始条件汇总、调参直觉、输出字段映射(含未赋值字段)、已知风险与代码索引 |
|
||||
| docs/data_record_debug.md | 数据记录调试功能说明:开关与构建(`RDP_DATA_RECORD`、C++20 隔离与 C 编译器要求)、运行期行为(目录/文件名、PRAGMA、失败降级、线程模型)、表结构约定、**新增记录点四步配方**、当前 6 张在用记录表与字段、查询示例与已知限制 |
|
||||
| docs/data_record_debug.md | 数据记录调试功能说明:开关与构建(`RDP_DATA_RECORD`、C++20 隔离与 C 编译器要求、关闭时零代码依据)、运行期行为(异步写线程、有界队列与丢弃、批事务参数、WAL/PRAGMA、去 AUTOINCREMENT、失败降级与熔断、Shutdown 排空与统计、Destroy→GetB 重连)、记录实体(表)约定、**新增记录点四步配方**(实体→异步入队接口/宏→写线程分支→业务侧收集调用)、当前 6 张在用记录表与记录点清单(含新宏与 file:line)、查询示例、已知限制与验收参考 |
|
||||
| third_party/ormpp/UPSTREAM.md | 内置第三方源码的来源说明:ormpp / iguana / frozen / SQLite 版本、许可证、复现与升级步骤(含 blob 校验值) |
|
||||
| archived_docs/BUG_REPORT.md | 全量逻辑 BUG 分析(P0~P3 共 37 项,含位置、说明、建议、处置结论)——过期留档,仅作历史参考 |
|
||||
| archived_docs/BUG_FIX_REPORT.md | 修复明细、修复状态汇总表与 MSVC 2013 x86 编译验证记录——过期留档(对应 01d28e0 时期) |
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "data_process.h"
|
||||
#include "rdp_log.h"
|
||||
#include "rdp_record.h"
|
||||
#include "rdp_record_entity.h"
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
@@ -26,6 +25,11 @@ int Data_Process::data_preprocess(struct DataRev Data_Input[150])
|
||||
{
|
||||
data_num=min(Data_Input[0].Point_Sum, 150);
|
||||
//std::cout << "TARGET azi beam idx:" <<Data_Input[0].Beam_index_aiz;
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录输入点迹:处理线程内先收集源结构体,循环结束后一次入队(构造实体与写库由记录写线程完成)
|
||||
std::vector<PointRecv> rec_points;
|
||||
rec_points.reserve(static_cast<std::size_t>(data_num));
|
||||
#endif
|
||||
for (int i=0; i<data_num;i++)
|
||||
{
|
||||
PointRecv Data_buffer_temp;
|
||||
@@ -54,14 +58,18 @@ int Data_Process::data_preprocess(struct DataRev Data_Input[150])
|
||||
std::memcpy(Data_buffer_temp.speed_dim, Data_Input[i].speed_dim, sizeof(Data_buffer_temp.speed_dim));
|
||||
std::memcpy(Data_buffer_temp.range_dim, Data_Input[i].range_dim, sizeof(Data_buffer_temp.range_dim));
|
||||
|
||||
//数据记录示例①:把本次接收的输入点迹写入表 RdpRecPointTws(字段为示例,可按需增删)。
|
||||
//记录功能关闭时该语句展开为空操作(实参不求值),交付构建不产生任何代码,
|
||||
//用法与新增记录点配方见 rdp_record.h 与 docs/data_record_debug.md。
|
||||
RDP_RECORD(RdpRecPoint(Data_buffer_temp));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_points.push_back(Data_buffer_temp);
|
||||
#endif
|
||||
|
||||
Data_buffer.push_back(Data_buffer_temp);
|
||||
}
|
||||
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//判圈之前(所有 return 之前)把本帧收集的点迹一次入队
|
||||
RDP_RECORD_POINTS(rec_points);
|
||||
#endif
|
||||
|
||||
#ifdef PHASE_SCANNING
|
||||
// 相扫模式下,判断前后两帧波位号的大小,来判断是否扫描一周完成
|
||||
if(Data_Input[0].Beam_index_aiz <= last_beam_num) //TWS
|
||||
@@ -101,6 +109,11 @@ int Data_Process::data_preprocess(struct DataRev Data_Input[150])
|
||||
|
||||
data_num = min(max(Data_Input[0].Point_Sum, 0), 150);
|
||||
TAS_track_idx = Data_Input[0].TAS_track_index;
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录输入点迹:处理线程内先收集源结构体,循环结束后一次入队(构造实体与写库由记录写线程完成)
|
||||
std::vector<PointRecv> rec_points;
|
||||
rec_points.reserve(static_cast<std::size_t>(data_num));
|
||||
#endif
|
||||
for (int i=0; i<data_num;i++)
|
||||
{
|
||||
PointRecv Data_buffer_temp;
|
||||
@@ -129,7 +142,9 @@ int Data_Process::data_preprocess(struct DataRev Data_Input[150])
|
||||
std::memcpy(Data_buffer_temp.speed_dim, Data_Input[i].speed_dim, sizeof(Data_buffer_temp.speed_dim));
|
||||
std::memcpy(Data_buffer_temp.range_dim, Data_Input[i].range_dim, sizeof(Data_buffer_temp.range_dim));
|
||||
|
||||
RDP_RECORD(RdpRecPoint(Data_buffer_temp));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_points.push_back(Data_buffer_temp);
|
||||
#endif
|
||||
|
||||
Data_buffer_tas.push_back(Data_buffer_temp);
|
||||
|
||||
@@ -140,6 +155,10 @@ int Data_Process::data_preprocess(struct DataRev Data_Input[150])
|
||||
// << "data_num:" << data_num;
|
||||
|
||||
}
|
||||
//circle_id++ 之前把本帧收集的点迹一次入队
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
RDP_RECORD_POINTS(rec_points);
|
||||
#endif
|
||||
circle_id++;
|
||||
return 3;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include "rdp_record.h"
|
||||
#include "rdp_record_entity.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -224,11 +223,21 @@ int Dot_Coh::dot_coh_process_buff( std::vector <PointRecv> *data_input,
|
||||
|
||||
//2.data_input_buff数据输出给point_recv
|
||||
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录凝聚后点迹:先收集,输出循环结束后一次入队
|
||||
std::vector<PointRecv> rec_coh;
|
||||
rec_coh.reserve(data_input_buff.size());
|
||||
#endif
|
||||
for (size_t i=0;i<data_input_buff.size();i++)
|
||||
{
|
||||
(*point_recv).push_back(data_input_buff[i]);
|
||||
RDP_RECORD(RdpRecPointCohesionTWS(data_input_buff[i]));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_coh.push_back(data_input_buff[i]);
|
||||
#endif
|
||||
}
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
RDP_RECORD_COHESION_TWS(rec_coh);
|
||||
#endif
|
||||
|
||||
std::vector<PointRecv>().swap(data_input_buff);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include "rdp_record.h"
|
||||
#include "rdp_record_entity.h"
|
||||
using namespace std;
|
||||
|
||||
int Dot_Coh_TAS::dot_coh_tas_process(std::vector <PointRecv> *data_input, //输入点迹
|
||||
@@ -67,11 +66,21 @@ int Dot_Coh_TAS::dot_coh_tas_process(std::vector <PointRecv> *dat
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录凝聚后点迹:先收集,输出循环结束后一次入队
|
||||
std::vector<PointRecv> rec_coh;
|
||||
rec_coh.reserve(data_input->size());
|
||||
#endif
|
||||
for (size_t i=0;i<data_input->size();i++)
|
||||
{
|
||||
(*point_recv_tas).push_back((*data_input)[i]);
|
||||
RDP_RECORD(RdpRecPointCohesionTAS((*data_input)[i]));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_coh.push_back((*data_input)[i]);
|
||||
#endif
|
||||
}
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
RDP_RECORD_COHESION_TAS(rec_coh);
|
||||
#endif
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,50 @@
|
||||
//记录数据库存放目录(相对宿主进程当前工作目录,不存在时自动创建)
|
||||
#define RDP_RECORD_DB_DIR ".\\rdp_records\\"
|
||||
|
||||
//数据记录性能参数(V1.5.8 优化:批量事务 + 异步写线程 + WAL)
|
||||
//说明:以下宏只被 rdp_record.cpp 使用;用 #ifndef 保护,便于用编译选项临时覆盖做测试。
|
||||
//队列字节上限:处理线程入队总量超过该值时整批丢弃(不阻塞处理线程)
|
||||
#ifndef RDP_RECORD_QUEUE_CAP_BYTES
|
||||
#define RDP_RECORD_QUEUE_CAP_BYTES (64u * 1024u * 1024u)
|
||||
#endif
|
||||
//队列批数上限(防止小批过多导致 deque 开销/延迟)
|
||||
#ifndef RDP_RECORD_QUEUE_MAX_BATCHES
|
||||
#define RDP_RECORD_QUEUE_MAX_BATCHES 4096u
|
||||
#endif
|
||||
//写线程每次事务提交的目标行数(累积到该行数或等待超时即提交)
|
||||
#ifndef RDP_RECORD_WRITE_BATCH_ROWS
|
||||
#define RDP_RECORD_WRITE_BATCH_ROWS 4096u
|
||||
#endif
|
||||
//写线程为凑批而等待的最长时间(毫秒)
|
||||
#ifndef RDP_RECORD_WRITE_MAX_WAIT_MS
|
||||
#define RDP_RECORD_WRITE_MAX_WAIT_MS 50u
|
||||
#endif
|
||||
//关联信息单次入队分块上限(⑥ 单圈行数可能很大,分块限制处理线程内存峰值;
|
||||
//取值应大于常见单圈候选数,避免同一圈内多次重新分配;131072 行 Asso_Info 约 11 MB)
|
||||
#ifndef RDP_RECORD_ASSO_CHUNK_ROWS
|
||||
#define RDP_RECORD_ASSO_CHUNK_ROWS 131072u
|
||||
#endif
|
||||
//SQLite 页缓存大小(KB,负数表示 KB;默认 8 MB)
|
||||
#ifndef RDP_RECORD_SQLITE_CACHE_KB
|
||||
#define RDP_RECORD_SQLITE_CACHE_KB 8192
|
||||
#endif
|
||||
//SQLite 忙等待超时(毫秒)
|
||||
#ifndef RDP_RECORD_SQLITE_BUSY_TIMEOUT_MS
|
||||
#define RDP_RECORD_SQLITE_BUSY_TIMEOUT_MS 2000
|
||||
#endif
|
||||
//是否尝试开启 WAL(1=尝试;失败/不支持时自动回退 DELETE 日志模式)
|
||||
#ifndef RDP_RECORD_SQLITE_WAL
|
||||
#define RDP_RECORD_SQLITE_WAL 1
|
||||
#endif
|
||||
//连续写入错误达到该次数后全局熔断(停止记录,避免持续消耗写线程)
|
||||
#ifndef RDP_RECORD_MAX_CONSECUTIVE_ERRORS
|
||||
#define RDP_RECORD_MAX_CONSECUTIVE_ERRORS 10
|
||||
#endif
|
||||
//队列丢弃日志的限频间隔(每丢弃多少行提示一次)
|
||||
#ifndef RDP_RECORD_DROP_LOG_INTERVAL
|
||||
#define RDP_RECORD_DROP_LOG_INTERVAL 1024u
|
||||
#endif
|
||||
|
||||
//#define MECHANICAL_SCANNING
|
||||
//#define PHASE_SCANNING
|
||||
|
||||
|
||||
@@ -2,19 +2,45 @@
|
||||
// 本文件仅在构建系统定义 RDP_RECORD_ENABLE(CMake 选项 RDP_DATA_RECORD 开启)时参与编译,
|
||||
// 且以 C++20 编译(ormpp 要求);雷达处理代码仍为 C++17,二者通过 rdp_record.h 的
|
||||
// 「仅声明 + 显式实例化」跨标准边界,详见 docs/data_record_debug.md。
|
||||
//
|
||||
// 性能模型(V1.5.8 优化):
|
||||
// 处理线程:记录点只做「收集源结构体 → 一次入队」(拷贝 + 互斥队列 push,不阻塞);
|
||||
// 写线程 :从有界队列取批 → 构造记录实体(数组转文本在这里)→ ormpp 批量事务写 SQLite;
|
||||
// 数据库 :WAL + synchronous=OFF + 8MB 页缓存 + busy_timeout;建表去 AUTOINCREMENT;
|
||||
// 队列满 :整批丢弃并计数(实时性优先),Shutdown 时排空并打印统计。
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
|
||||
//注意:windows.h 必须在业务/实体头文件之前包含 —— struct.h 在全局作用域有
|
||||
//"using namespace std;",若之后再包含 windows.h,其 byte 类型会与 std::byte 产生歧义。
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h> //仅用于把记录写线程优先级降到 BELOW_NORMAL,避免与雷达处理线程争抢 CPU
|
||||
#endif
|
||||
|
||||
#include "rdp_record.h"
|
||||
#include "rdp_record_entity.h"
|
||||
#include "parameters.h"
|
||||
#include "rdp_log.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <time.h>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
//第三方库(ormpp / iguana)模板代码在 /W3 下可能产生告警,按 third_party/ormpp/UPSTREAM.md
|
||||
@@ -31,27 +57,86 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
//============================== 基础类型 ==============================
|
||||
enum class BatchKind : std::uint8_t
|
||||
{
|
||||
Point, //RdpRecPoint(输入点迹)
|
||||
CohesionTws, //RdpRecPointCohesionTWS
|
||||
CohesionTas, //RdpRecPointCohesionTAS
|
||||
TempTrack, //RdpRecTempTrack
|
||||
TrustTrack, //RdpRecTrustTrack
|
||||
AssoInfo //RdpRecAssoInfo
|
||||
};
|
||||
|
||||
//一批入队数据(一种记录表的一批源结构体;处理线程收集后整体 move 进队列)
|
||||
struct Batch
|
||||
{
|
||||
BatchKind kind = BatchKind::Point;
|
||||
std::vector<PointRecv> points; //Point/CohesionTws/CohesionTas 共用
|
||||
std::vector<Temp_track> temp_tracks;
|
||||
std::vector<Trust_Track> trust_tracks;
|
||||
std::vector<Asso_Info> asso_infos;
|
||||
};
|
||||
|
||||
//写线程内的待提交缓冲(按表分别累积,达到批大小或超时后各表分别提交)
|
||||
struct Pending
|
||||
{
|
||||
std::vector<PointRecv> points;
|
||||
std::vector<PointRecv> cohesion_tws;
|
||||
std::vector<PointRecv> cohesion_tas;
|
||||
std::vector<Temp_track> temp_tracks;
|
||||
std::vector<Trust_Track> trust_tracks;
|
||||
std::vector<Asso_Info> asso_infos;
|
||||
};
|
||||
|
||||
//统计计数器(处理线程/写线程并发读写)
|
||||
struct Counters
|
||||
{
|
||||
std::atomic<std::uint64_t> enqueued_rows{0};
|
||||
std::atomic<std::uint64_t> dropped_rows{0};
|
||||
std::atomic<std::uint64_t> written_rows{0};
|
||||
std::atomic<std::uint64_t> transactions{0};
|
||||
std::atomic<std::uint64_t> write_errors{0};
|
||||
std::atomic<std::uint64_t> queue_peak_bytes{0};
|
||||
std::atomic<std::uint64_t> queue_peak_rows{0};
|
||||
};
|
||||
|
||||
//记录模块内部状态(Pimpl:ormpp / SQLite 类型不出现在 rdp_record.h 中)
|
||||
struct Record_State
|
||||
{
|
||||
ormpp::dbng<ormpp::sqlite> db; //SQLite 连接(单连接,写入时串行使用)
|
||||
std::string path; //当前 db 文件全路径
|
||||
bool ready; //是否已成功打开并完成初始化
|
||||
bool failed; //初始化失败:后续写入全部忽略,不再反复报错
|
||||
std::mutex mtx; //写入串行化(宿主多线程调用时保护单连接)
|
||||
ormpp::dbng<ormpp::sqlite> db; //SQLite 连接(写线程独占;遗留同步接口经 db_mtx 串行)
|
||||
std::string path; //当前 db 文件全路径
|
||||
std::atomic<bool> ready{false};
|
||||
std::atomic<bool> failed{false};
|
||||
|
||||
Record_State() : ready(false), failed(false) {}
|
||||
std::mutex db_mtx; //串行化数据库访问(写线程 + 遗留同步写)
|
||||
std::map<std::string, int> tables; //表状态:0 未建 / 1 可用 / 2 失败(db_mtx 保护)
|
||||
|
||||
std::mutex q_mtx; //队列锁
|
||||
std::condition_variable q_cv; //队列条件变量
|
||||
std::deque<Batch> queue; //有界入队队列
|
||||
std::size_t queued_bytes = 0;
|
||||
std::size_t queued_rows = 0;
|
||||
std::size_t queued_batches = 0;
|
||||
bool stopping = false;
|
||||
std::thread writer; //记录写线程
|
||||
|
||||
Counters counters;
|
||||
std::atomic<int> consecutive_errors{0};
|
||||
|
||||
~Record_State();
|
||||
};
|
||||
|
||||
std::unique_ptr<Record_State>& State()
|
||||
{
|
||||
//函数内静态:宿主未调用 Data_Process_Factory::Destroy() 时由析构兜底关库
|
||||
//函数内静态:宿主未调用 Data_Process_Factory::Destroy() 时由析构兜底停止写线程/关库
|
||||
static std::unique_ptr<Record_State> s;
|
||||
return s;
|
||||
}
|
||||
|
||||
//============================== 通用小工具 ==============================
|
||||
//当前本地时间字符串,格式 "YYYYMMDD_HHMMSS"(与 rdp_log 的日志文件命名风格一致)
|
||||
bool TimeStamp(char* buf, size_t size)
|
||||
bool TimeStamp(char* buf, std::size_t size)
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
struct tm local;
|
||||
@@ -62,39 +147,513 @@ namespace
|
||||
return strftime(buf, size, "%Y%m%d_%H%M%S", &local) != 0;
|
||||
}
|
||||
|
||||
//写入失败提示(「每个实体只提示一次」由调用方的静态标志控制)
|
||||
void ReportError(Record_State& st, const char* what)
|
||||
//写入失败提示
|
||||
void ReportError(Record_State& st, const char* what, const std::string& detail)
|
||||
{
|
||||
RDP_LOG << "[rdp_record] " << what << " failed: " << st.db.get_last_error() << std::endl;
|
||||
RDP_LOG << "[rdp_record] " << what << " failed (" << detail << "): "
|
||||
<< st.db.get_last_error() << std::endl;
|
||||
}
|
||||
|
||||
//确保实体的数据表存在:每个实体类型只尝试建表一次(失败后该实体永久跳过)
|
||||
//注意:ormpp 的 ormpp_auto_key 定义在全局命名空间(third_party/ormpp/ormpp/entity.hpp),
|
||||
// 传该参数可在建表时登记自增主键,使后续插入自动跳过 id 列
|
||||
template <typename T>
|
||||
bool EnsureTable(Record_State& st)
|
||||
void LogStats(Record_State& st, const char* tag)
|
||||
{
|
||||
static const bool created = st.db.create_datatable<T>(::ormpp_auto_key{"id"});
|
||||
return created;
|
||||
RDP_LOG << "[rdp_record] stats(" << tag << "): enqueued_rows="
|
||||
<< st.counters.enqueued_rows.load()
|
||||
<< ", written_rows=" << st.counters.written_rows.load()
|
||||
<< ", dropped_rows=" << st.counters.dropped_rows.load()
|
||||
<< ", transactions=" << st.counters.transactions.load()
|
||||
<< ", write_errors=" << st.counters.write_errors.load()
|
||||
<< ", queue_peak_bytes=" << st.counters.queue_peak_bytes.load()
|
||||
<< ", queue_peak_rows=" << st.counters.queue_peak_rows.load()
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
//写入前置处理:自动建表;返回 true 表示可以向该表写入
|
||||
template <typename T>
|
||||
bool PrepareWrite(Record_State& st)
|
||||
std::string TableName()
|
||||
{
|
||||
static bool reported = false;
|
||||
if (EnsureTable<T>(st))
|
||||
//ormpp 以「短结构体名」作为表名
|
||||
return std::string(ormpp::get_short_struct_name<T>());
|
||||
}
|
||||
|
||||
//============================== 记录批次辅助 ==============================
|
||||
template <typename T>
|
||||
std::size_t BatchRowsOf(const std::vector<T>& v)
|
||||
{
|
||||
return v.size();
|
||||
}
|
||||
|
||||
std::size_t BatchRows(const Batch& b)
|
||||
{
|
||||
return b.points.size() + b.temp_tracks.size() + b.trust_tracks.size() + b.asso_infos.size();
|
||||
}
|
||||
|
||||
std::size_t BatchBytes(const Batch& b)
|
||||
{
|
||||
//近似统计(用于有界队列的字节上限;容器容量 + 对象本身)
|
||||
std::size_t bytes = sizeof(Batch);
|
||||
bytes += b.points.capacity() * sizeof(PointRecv);
|
||||
bytes += b.temp_tracks.capacity() * sizeof(Temp_track);
|
||||
bytes += b.trust_tracks.capacity() * sizeof(Trust_Track);
|
||||
bytes += b.asso_infos.capacity() * sizeof(Asso_Info);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::size_t PendingRows(const Pending& p)
|
||||
{
|
||||
return p.points.size() + p.cohesion_tws.size() + p.cohesion_tas.size()
|
||||
+ p.temp_tracks.size() + p.trust_tracks.size() + p.asso_infos.size();
|
||||
}
|
||||
|
||||
void ClearPending(Pending& p)
|
||||
{
|
||||
p.points.clear();
|
||||
p.cohesion_tws.clear();
|
||||
p.cohesion_tas.clear();
|
||||
p.temp_tracks.clear();
|
||||
p.trust_tracks.clear();
|
||||
p.asso_infos.clear();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void AppendMove(std::vector<T>& dst, std::vector<T>& src)
|
||||
{
|
||||
if (src.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (dst.empty())
|
||||
{
|
||||
dst = std::move(src);
|
||||
return;
|
||||
}
|
||||
dst.insert(dst.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));
|
||||
src.clear();
|
||||
}
|
||||
|
||||
void AppendBatch(Pending& p, Batch& b)
|
||||
{
|
||||
switch (b.kind)
|
||||
{
|
||||
case BatchKind::Point: AppendMove(p.points, b.points); break;
|
||||
case BatchKind::CohesionTws: AppendMove(p.cohesion_tws, b.points); break;
|
||||
case BatchKind::CohesionTas: AppendMove(p.cohesion_tas, b.points); break;
|
||||
case BatchKind::TempTrack: AppendMove(p.temp_tracks, b.temp_tracks); break;
|
||||
case BatchKind::TrustTrack: AppendMove(p.trust_tracks, b.trust_tracks); break;
|
||||
case BatchKind::AssoInfo: AppendMove(p.asso_infos, b.asso_infos); break;
|
||||
}
|
||||
}
|
||||
|
||||
//============================== 数据库初始化/建表 ==============================
|
||||
//应用 SQLite PRAGMA(失败只记录,不阻止记录功能;调试用途)
|
||||
void ApplyPragmas(Record_State& st)
|
||||
{
|
||||
#if RDP_RECORD_SQLITE_WAL
|
||||
try
|
||||
{
|
||||
//PRAGMA journal_mode 会返回一行,ormpp 的 execute() 只接受不返回结果行的语句,
|
||||
//因此用 query_s<std::tuple<...>> 消费返回行。
|
||||
auto rows = st.db.query_s<std::tuple<std::string>>("PRAGMA journal_mode=WAL;");
|
||||
std::string mode = rows.empty() ? std::string("<none>") : std::get<0>(rows[0]);
|
||||
RDP_LOG << "[rdp_record] journal_mode=" << mode << std::endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
RDP_LOG << "[rdp_record] set journal_mode=WAL failed, fallback to default journal" << std::endl;
|
||||
}
|
||||
#endif
|
||||
if (!st.db.execute("PRAGMA synchronous=OFF;"))
|
||||
{
|
||||
RDP_LOG << "[rdp_record] set synchronous=OFF failed" << std::endl;
|
||||
}
|
||||
const std::string cache = "PRAGMA cache_size=-" + std::to_string(RDP_RECORD_SQLITE_CACHE_KB) + ";";
|
||||
if (!st.db.execute(cache))
|
||||
{
|
||||
RDP_LOG << "[rdp_record] set cache_size failed" << std::endl;
|
||||
}
|
||||
try
|
||||
{
|
||||
//busy_timeout 也返回一行
|
||||
st.db.query_s<std::tuple<int>>(
|
||||
"PRAGMA busy_timeout=" + std::to_string(RDP_RECORD_SQLITE_BUSY_TIMEOUT_MS) + ";");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
RDP_LOG << "[rdp_record] set busy_timeout failed" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
//建表 + 去掉 AUTOINCREMENT(本项目每次运行新建库、只增不删,普通 INTEGER PRIMARY KEY 即可;
|
||||
//去 AUTOINCREMENT 可省掉每行对 sqlite_sequence 的读写。ormpp 的 DDL 硬编码 AUTOINCREMENT,
|
||||
//这里先用 ormpp 建表(同时登记自增主键映射),再按 sqlite_master 中的 DDL 去关键字重建空表。)
|
||||
template <typename T>
|
||||
bool CreateTable(Record_State& st)
|
||||
{
|
||||
if (!st.db.create_datatable<T>(::ormpp_auto_key{"id"}))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string table = TableName<T>();
|
||||
std::string ddl;
|
||||
try
|
||||
{
|
||||
auto rows = st.db.query_s<std::tuple<std::string>>(
|
||||
"select sql from sqlite_master where type='table' and name='" + table + "'");
|
||||
if (!rows.empty())
|
||||
{
|
||||
ddl = std::get<0>(rows[0]);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return true; //查询失败不影响记录,保留 ormpp 建出的表
|
||||
}
|
||||
|
||||
if (ddl.find("AUTOINCREMENT") == std::string::npos)
|
||||
{
|
||||
return true; //ormpp 版本变化后不再带 AUTOINCREMENT,直接使用
|
||||
}
|
||||
|
||||
std::string stripped = ddl;
|
||||
const std::string keyword = "AUTOINCREMENT";
|
||||
for (std::size_t pos = stripped.find(keyword); pos != std::string::npos;
|
||||
pos = stripped.find(keyword))
|
||||
{
|
||||
stripped.erase(pos, keyword.size());
|
||||
}
|
||||
|
||||
if (st.db.execute("DROP TABLE \"" + table + "\";") &&
|
||||
st.db.execute(stripped))
|
||||
{
|
||||
RDP_LOG << "[rdp_record] table " << table << " created without AUTOINCREMENT" << std::endl;
|
||||
return true;
|
||||
}
|
||||
if (!reported)
|
||||
|
||||
//回退:若重建失败(极端情况),尽量恢复一张可用表,保证记录功能继续
|
||||
if (st.db.create_datatable<T>(::ormpp_auto_key{"id"}))
|
||||
{
|
||||
reported = true;
|
||||
ReportError(st, "create table");
|
||||
RDP_LOG << "[rdp_record] warning: keep AUTOINCREMENT for " << table << std::endl;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//写入前置处理(调用方需持有 db_mtx):按表缓存建表结果,失败的表永久跳过
|
||||
template <typename T>
|
||||
bool PrepareWrite(Record_State& st)
|
||||
{
|
||||
const std::string table = TableName<T>();
|
||||
std::map<std::string, int>::iterator it = st.tables.find(table);
|
||||
const int state = (it == st.tables.end()) ? 0 : it->second;
|
||||
if (state == 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (state == 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (CreateTable<T>(st))
|
||||
{
|
||||
st.tables[table] = 1;
|
||||
return true;
|
||||
}
|
||||
st.tables[table] = 2;
|
||||
ReportError(st, "create table", table);
|
||||
return false;
|
||||
}
|
||||
|
||||
//批量写入实体(一次 BEGIN/COMMIT 事务;调用方无需持锁)
|
||||
template <typename T>
|
||||
bool InsertEntities(Record_State& st, const std::vector<T>& v)
|
||||
{
|
||||
if (v.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(st.db_mtx);
|
||||
if (st.failed.load())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!PrepareWrite<T>(st))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int rows = st.db.insert(v);
|
||||
if (rows == INT_MIN)
|
||||
{
|
||||
st.counters.write_errors.fetch_add(1);
|
||||
st.tables[TableName<T>()] = 2;
|
||||
const int errors = st.consecutive_errors.fetch_add(1) + 1;
|
||||
ReportError(st, "insert", TableName<T>());
|
||||
if (errors >= RDP_RECORD_MAX_CONSECUTIVE_ERRORS)
|
||||
{
|
||||
st.failed.store(true);
|
||||
RDP_LOG << "[rdp_record] too many consecutive write errors, recording stopped" << std::endl;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
st.consecutive_errors.store(0);
|
||||
st.counters.written_rows.fetch_add(v.size());
|
||||
st.counters.transactions.fetch_add(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
//按批大小分块构造实体并提交(实体构造 = 数组转文本,全部在写线程完成)
|
||||
template <typename Entity, typename Source>
|
||||
bool FlushKind(Record_State& st, std::vector<Source>& src)
|
||||
{
|
||||
if (src.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::size_t chunk = (RDP_RECORD_WRITE_BATCH_ROWS == 0)
|
||||
? 4096
|
||||
: static_cast<std::size_t>(RDP_RECORD_WRITE_BATCH_ROWS);
|
||||
bool ok = true;
|
||||
for (std::size_t pos = 0; pos < src.size() && !st.failed.load();)
|
||||
{
|
||||
std::size_t n = src.size() - pos;
|
||||
if (n > chunk)
|
||||
{
|
||||
n = chunk;
|
||||
}
|
||||
|
||||
std::vector<Entity> entities;
|
||||
entities.reserve(n);
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
entities.emplace_back(src[pos + i]); //Entity(Source&) 构造:数组转文本在此完成
|
||||
}
|
||||
if (!InsertEntities<Entity>(st, entities))
|
||||
{
|
||||
ok = false;
|
||||
}
|
||||
pos += n;
|
||||
}
|
||||
|
||||
src.clear();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool FlushPending(Record_State& st, Pending& p)
|
||||
{
|
||||
bool ok = true;
|
||||
ok &= FlushKind<RdpRecPoint>(st, p.points);
|
||||
ok &= FlushKind<RdpRecPointCohesionTWS>(st, p.cohesion_tws);
|
||||
ok &= FlushKind<RdpRecPointCohesionTAS>(st, p.cohesion_tas);
|
||||
ok &= FlushKind<RdpRecTempTrack>(st, p.temp_tracks);
|
||||
ok &= FlushKind<RdpRecTrustTrack>(st, p.trust_tracks);
|
||||
ok &= FlushKind<RdpRecAssoInfo>(st, p.asso_infos);
|
||||
ClearPending(p);
|
||||
return ok;
|
||||
}
|
||||
|
||||
//============================== 入队/写线程 ==============================
|
||||
void LogDroppedRows(std::uint64_t total_dropped, std::uint64_t newly_dropped)
|
||||
{
|
||||
const std::uint64_t interval = (RDP_RECORD_DROP_LOG_INTERVAL == 0)
|
||||
? 0
|
||||
: static_cast<std::uint64_t>(RDP_RECORD_DROP_LOG_INTERVAL);
|
||||
if (interval == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if ((total_dropped / interval) != ((total_dropped - newly_dropped) / interval))
|
||||
{
|
||||
RDP_LOG << "[rdp_record] queue full, dropped rows total=" << total_dropped << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void EnqueueBatch(Batch&& b)
|
||||
{
|
||||
const std::size_t rows = BatchRows(b);
|
||||
if (rows == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_ptr<Record_State>& sp = State();
|
||||
if (sp.get() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!sp->ready || sp->failed.load())
|
||||
{
|
||||
sp->counters.dropped_rows.fetch_add(rows);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t bytes = BatchBytes(b);
|
||||
std::lock_guard<std::mutex> lock(sp->q_mtx);
|
||||
if (sp->stopping || sp->failed.load())
|
||||
{
|
||||
const std::uint64_t total = sp->counters.dropped_rows.fetch_add(rows) + rows;
|
||||
LogDroppedRows(total, rows);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sp->queued_batches + 1 > static_cast<std::size_t>(RDP_RECORD_QUEUE_MAX_BATCHES) ||
|
||||
sp->queued_bytes + bytes > static_cast<std::size_t>(RDP_RECORD_QUEUE_CAP_BYTES))
|
||||
{
|
||||
const std::uint64_t total = sp->counters.dropped_rows.fetch_add(rows) + rows;
|
||||
LogDroppedRows(total, rows);
|
||||
return;
|
||||
}
|
||||
|
||||
sp->queue.push_back(std::move(b));
|
||||
sp->queued_batches += 1;
|
||||
sp->queued_bytes += bytes;
|
||||
sp->queued_rows += rows;
|
||||
|
||||
sp->counters.enqueued_rows.fetch_add(rows);
|
||||
if (sp->counters.queue_peak_bytes.load() < sp->queued_bytes)
|
||||
{
|
||||
sp->counters.queue_peak_bytes.store(sp->queued_bytes);
|
||||
}
|
||||
if (sp->counters.queue_peak_rows.load() < sp->queued_rows)
|
||||
{
|
||||
sp->counters.queue_peak_rows.store(sp->queued_rows);
|
||||
}
|
||||
|
||||
sp->q_cv.notify_one();
|
||||
}
|
||||
|
||||
void DropQueued(Record_State& st)
|
||||
{
|
||||
std::size_t rows = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(st.q_mtx);
|
||||
for (std::size_t i = 0; i < st.queue.size(); ++i)
|
||||
{
|
||||
rows += BatchRows(st.queue[i]);
|
||||
}
|
||||
st.queue.clear();
|
||||
st.queued_bytes = 0;
|
||||
st.queued_rows = 0;
|
||||
st.queued_batches = 0;
|
||||
}
|
||||
if (rows > 0)
|
||||
{
|
||||
const std::uint64_t total = st.counters.dropped_rows.fetch_add(rows) + rows;
|
||||
RDP_LOG << "[rdp_record] writer stopped, dropped queued rows=" << rows
|
||||
<< ", total dropped=" << total << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void WriterLoopBody(Record_State& st)
|
||||
{
|
||||
Pending pending;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
std::deque<Batch> local;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(st.q_mtx);
|
||||
st.q_cv.wait(lock, [&st]() { return !st.queue.empty() || st.stopping; });
|
||||
if (st.queue.empty() && st.stopping)
|
||||
{
|
||||
break; //停止且队列已空:退出(pending 已在上一轮提交)
|
||||
}
|
||||
local.swap(st.queue);
|
||||
st.queued_bytes = 0;
|
||||
st.queued_rows = 0;
|
||||
st.queued_batches = 0;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < local.size(); ++i)
|
||||
{
|
||||
AppendBatch(pending, local[i]);
|
||||
}
|
||||
local.clear();
|
||||
|
||||
//凑批:未达目标且未停止时,短暂等待更多数据,减少事务次数
|
||||
while (!st.stopping && !st.failed.load() &&
|
||||
PendingRows(pending) < static_cast<std::size_t>(RDP_RECORD_WRITE_BATCH_ROWS))
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(st.q_mtx);
|
||||
const bool more = st.q_cv.wait_for(
|
||||
lock,
|
||||
std::chrono::milliseconds(RDP_RECORD_WRITE_MAX_WAIT_MS),
|
||||
[&st]() { return !st.queue.empty() || st.stopping; });
|
||||
if (!more)
|
||||
{
|
||||
break; //超时:提交现有数据
|
||||
}
|
||||
if (st.queue.empty())
|
||||
{
|
||||
break; //停止且队列已空
|
||||
}
|
||||
local.swap(st.queue);
|
||||
st.queued_bytes = 0;
|
||||
st.queued_rows = 0;
|
||||
st.queued_batches = 0;
|
||||
lock.unlock();
|
||||
for (std::size_t i = 0; i < local.size(); ++i)
|
||||
{
|
||||
AppendBatch(pending, local[i]);
|
||||
}
|
||||
local.clear();
|
||||
}
|
||||
|
||||
FlushPending(st, pending);
|
||||
|
||||
if (st.failed.load())
|
||||
{
|
||||
DropQueued(st);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FlushPending(st, pending);
|
||||
}
|
||||
|
||||
void WriterLoop(Record_State& st)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
//记录写线程是后台任务:把线程 CPU 优先级降到 BELOW_NORMAL,避免与雷达处理线程争抢 CPU;
|
||||
//I/O 保持正常优先级(否则写入积压会拉长 Shutdown 排空时间)。队列有界,写线程变慢时
|
||||
//由丢弃策略兜底,不影响处理线程。
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||
#endif
|
||||
try
|
||||
{
|
||||
WriterLoopBody(st);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
//写线程内任何异常都不能抛出 DLL 边界;记录失败即降级
|
||||
st.failed.store(true);
|
||||
RDP_LOG << "[rdp_record] writer thread exception, recording stopped" << std::endl;
|
||||
DropQueued(st);
|
||||
}
|
||||
}
|
||||
|
||||
//============================== 生命周期辅助 ==============================
|
||||
Record_State::~Record_State()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(q_mtx);
|
||||
stopping = true;
|
||||
}
|
||||
q_cv.notify_all();
|
||||
if (writer.joinable())
|
||||
{
|
||||
writer.join();
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(db_mtx);
|
||||
try
|
||||
{
|
||||
db.disconnect();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace rdp_record
|
||||
@@ -104,65 +663,143 @@ namespace rdp_record
|
||||
std::unique_ptr<Record_State>& sp = State();
|
||||
if (sp.get() != 0)
|
||||
{
|
||||
return sp->ready; //已经初始化过(含失败的幂等返回)
|
||||
if (sp->ready)
|
||||
{
|
||||
return true; //已经初始化
|
||||
}
|
||||
if (sp->failed)
|
||||
{
|
||||
return false; //初始化失败(本次进程内不再重试)
|
||||
}
|
||||
sp.reset(); //上一次已 Shutdown:允许重新初始化(宿主销毁/重建工厂的场景)
|
||||
}
|
||||
|
||||
sp.reset(new Record_State());
|
||||
Record_State& st = *sp;
|
||||
|
||||
//创建记录目录(目录已存在时 _mkdir 返回 EEXIST,忽略;逻辑与 rdp_log.cpp 一致)
|
||||
std::string dir = RDP_RECORD_DB_DIR;
|
||||
while (dir.size() > 0 && (dir[dir.size() - 1] == '\\' || dir[dir.size() - 1] == '/'))
|
||||
try
|
||||
{
|
||||
dir.erase(dir.size() - 1);
|
||||
//创建记录目录(目录已存在时 _mkdir 返回 EEXIST,忽略;逻辑与 rdp_log.cpp 一致)
|
||||
std::string dir = RDP_RECORD_DB_DIR;
|
||||
while (dir.size() > 0 && (dir[dir.size() - 1] == '\\' || dir[dir.size() - 1] == '/'))
|
||||
{
|
||||
dir.erase(dir.size() - 1);
|
||||
}
|
||||
if (dir.size() > 0)
|
||||
{
|
||||
_mkdir(dir.c_str());
|
||||
}
|
||||
|
||||
//按当前日期+时间生成记录库文件名:rdp_record_YYYYMMDD_HHMMSS.db
|
||||
char stamp[32] = {0};
|
||||
if (!TimeStamp(stamp, sizeof(stamp)))
|
||||
{
|
||||
RDP_LOG << "[rdp_record] cannot get local time, recording disabled" << std::endl;
|
||||
st.failed = true;
|
||||
return false;
|
||||
}
|
||||
st.path = RDP_RECORD_DB_DIR;
|
||||
st.path += "rdp_record_";
|
||||
st.path += stamp;
|
||||
st.path += ".db";
|
||||
|
||||
//同一秒内重复 Init(Destroy→GetB 重连)或同目录多进程启动时,避免复用同一个库文件
|
||||
if (_access(st.path.c_str(), 0) == 0)
|
||||
{
|
||||
const std::string base = st.path.substr(0, st.path.size() - 3); //去掉 ".db"
|
||||
for (int suffix = 1; suffix < 1000; ++suffix)
|
||||
{
|
||||
const std::string candidate = base + "_" + std::to_string(suffix) + ".db";
|
||||
if (_access(candidate.c_str(), 0) != 0)
|
||||
{
|
||||
st.path = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!st.db.connect(st.path))
|
||||
{
|
||||
RDP_LOG << "[rdp_record] cannot open database: " << st.path << std::endl;
|
||||
st.failed = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
ApplyPragmas(st);
|
||||
|
||||
//启动记录写线程(处理线程之后只入队,不直接访问数据库)
|
||||
st.writer = std::thread(WriterLoop, std::ref(st));
|
||||
st.ready = true;
|
||||
|
||||
RDP_LOG << "[rdp_record] enabled, database: " << st.path
|
||||
<< " (async writer, write_batch_rows=" << RDP_RECORD_WRITE_BATCH_ROWS
|
||||
<< ", queue_cap_bytes=" << RDP_RECORD_QUEUE_CAP_BYTES << ")" << std::endl;
|
||||
return true;
|
||||
}
|
||||
if (dir.size() > 0)
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
_mkdir(dir.c_str());
|
||||
RDP_LOG << "[rdp_record] init exception: " << e.what() << std::endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
RDP_LOG << "[rdp_record] init exception" << std::endl;
|
||||
}
|
||||
|
||||
//按当前日期+时间生成记录库文件名:rdp_record_YYYYMMDD_HHMMSS.db
|
||||
char stamp[32] = {0};
|
||||
if (!TimeStamp(stamp, sizeof(stamp)))
|
||||
st.failed = true;
|
||||
st.ready = false;
|
||||
try
|
||||
{
|
||||
RDP_LOG << "[rdp_record] cannot get local time, recording disabled" << std::endl;
|
||||
st.failed = true;
|
||||
return false;
|
||||
st.db.disconnect();
|
||||
}
|
||||
st.path = RDP_RECORD_DB_DIR;
|
||||
st.path += "rdp_record_";
|
||||
st.path += stamp;
|
||||
st.path += ".db";
|
||||
|
||||
if (!st.db.connect(st.path))
|
||||
catch (...)
|
||||
{
|
||||
RDP_LOG << "[rdp_record] cannot open database: " << st.path << std::endl;
|
||||
st.failed = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
//调试用途的性能取舍:关闭同步刷盘(提交时不再 fsync),崩溃时可能丢失尾部数据;
|
||||
//如需崩溃安全可改为 PRAGMA synchronous=NORMAL(或在 sqlite3 层自行开启 WAL)。
|
||||
//注意:ormpp 的 execute() 只接受「不返回结果行」的语句,故不在此设置 journal_mode。
|
||||
st.db.execute("PRAGMA synchronous=OFF;");
|
||||
|
||||
st.ready = true;
|
||||
RDP_LOG << "[rdp_record] enabled, database: " << st.path << std::endl;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Shutdown()
|
||||
{
|
||||
std::unique_ptr<Record_State>& sp = State();
|
||||
if (sp.get() == 0 || !sp->ready)
|
||||
if (sp.get() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(sp->mtx);
|
||||
RDP_LOG << "[rdp_record] shutdown, database: " << sp->path << std::endl;
|
||||
sp->db.disconnect();
|
||||
sp->ready = false; //path 保留,便于宿主在关库后仍能取到本次记录库路径
|
||||
Record_State& st = *sp;
|
||||
if (!st.ready)
|
||||
{
|
||||
sp.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
//1) 停止接收新数据;2) 等待写线程排空队列/pending 并退出;3) 检查点/关库;4) 打印统计
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(st.q_mtx);
|
||||
st.stopping = true;
|
||||
}
|
||||
st.q_cv.notify_all();
|
||||
if (st.writer.joinable())
|
||||
{
|
||||
st.writer.join();
|
||||
}
|
||||
|
||||
LogStats(st, "shutdown");
|
||||
|
||||
try
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(st.db_mtx);
|
||||
//WAL 模式下把 -wal 合并回主库(尽力而为;失败不影响关库)
|
||||
st.db.query_s<std::tuple<int, int, int>>("PRAGMA wal_checkpoint(TRUNCATE);");
|
||||
st.db.disconnect();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
|
||||
RDP_LOG << "[rdp_record] shutdown, database: " << st.path << std::endl;
|
||||
st.ready = false;
|
||||
//保留状态对象(连接已断开、写线程已退出):下次 Init() 检测到 !ready && !failed 时复位并重新建库。
|
||||
//不在 Shutdown 内直接释放,避免宿主在 Destroy 的同时仍有记录调用时出现悬空指针(防御性处理)。
|
||||
}
|
||||
|
||||
std::string db_path()
|
||||
@@ -171,28 +808,49 @@ namespace rdp_record
|
||||
return (sp.get() == 0) ? std::string() : sp->path;
|
||||
}
|
||||
|
||||
Stats stats()
|
||||
{
|
||||
Stats s;
|
||||
std::unique_ptr<Record_State>& sp = State();
|
||||
if (sp.get() == 0)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
s.enqueued_rows = sp->counters.enqueued_rows.load();
|
||||
s.dropped_rows = sp->counters.dropped_rows.load();
|
||||
s.written_rows = sp->counters.written_rows.load();
|
||||
s.transactions = sp->counters.transactions.load();
|
||||
s.write_errors = sp->counters.write_errors.load();
|
||||
s.queue_peak_bytes = sp->counters.queue_peak_bytes.load();
|
||||
s.queue_peak_rows = sp->counters.queue_peak_rows.load();
|
||||
return s;
|
||||
}
|
||||
|
||||
//============================== 遗留同步接口(实体) ==============================
|
||||
//说明:会阻塞调用线程,常规记录点请使用上面各表的异步入队接口;仅保留给测试/特殊场景。
|
||||
template <typename T>
|
||||
void Write(const T& v)
|
||||
{
|
||||
std::unique_ptr<Record_State>& sp = State();
|
||||
if (sp.get() == 0 || !sp->ready || sp->failed)
|
||||
if (sp.get() == 0 || !sp->ready || sp->failed.load())
|
||||
{
|
||||
return; //未初始化 / 初始化失败 / 已关库:静默忽略
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(sp->mtx);
|
||||
std::lock_guard<std::mutex> lock(sp->db_mtx);
|
||||
if (!PrepareWrite<T>(*sp))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//写入失败时每个实体只提示一次:不重试、不抛出、不影响雷达数据处理
|
||||
static bool reported = false;
|
||||
if (sp->db.insert(v) == INT_MIN && !reported)
|
||||
if (sp->db.insert(v) == INT_MIN)
|
||||
{
|
||||
reported = true;
|
||||
ReportError(*sp, "insert");
|
||||
sp->counters.write_errors.fetch_add(1);
|
||||
sp->tables[TableName<T>()] = 2;
|
||||
ReportError(*sp, "insert", TableName<T>());
|
||||
return;
|
||||
}
|
||||
sp->counters.written_rows.fetch_add(1);
|
||||
sp->counters.transactions.fetch_add(1);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
@@ -204,24 +862,84 @@ namespace rdp_record
|
||||
}
|
||||
|
||||
std::unique_ptr<Record_State>& sp = State();
|
||||
if (sp.get() == 0 || !sp->ready || sp->failed)
|
||||
if (sp.get() == 0 || !sp->ready || sp->failed.load())
|
||||
{
|
||||
return;
|
||||
}
|
||||
InsertEntities<T>(*sp, v);
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(sp->mtx);
|
||||
if (!PrepareWrite<T>(*sp))
|
||||
//============================== 异步入队接口(源结构体) ==============================
|
||||
void WritePoints(std::vector<PointRecv> v)
|
||||
{
|
||||
if (v.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
Batch b;
|
||||
b.kind = BatchKind::Point;
|
||||
b.points = std::move(v);
|
||||
EnqueueBatch(std::move(b));
|
||||
}
|
||||
|
||||
//写入失败时每个实体只提示一次:不重试、不抛出、不影响雷达数据处理
|
||||
static bool reported = false;
|
||||
if (sp->db.insert(v) == INT_MIN && !reported)
|
||||
void WriteCohesionTWS(std::vector<PointRecv> v)
|
||||
{
|
||||
if (v.empty())
|
||||
{
|
||||
reported = true;
|
||||
ReportError(*sp, "insert");
|
||||
return;
|
||||
}
|
||||
Batch b;
|
||||
b.kind = BatchKind::CohesionTws;
|
||||
b.points = std::move(v);
|
||||
EnqueueBatch(std::move(b));
|
||||
}
|
||||
|
||||
void WriteCohesionTAS(std::vector<PointRecv> v)
|
||||
{
|
||||
if (v.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
Batch b;
|
||||
b.kind = BatchKind::CohesionTas;
|
||||
b.points = std::move(v);
|
||||
EnqueueBatch(std::move(b));
|
||||
}
|
||||
|
||||
void WriteTempTracks(std::vector<Temp_track> v)
|
||||
{
|
||||
if (v.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
Batch b;
|
||||
b.kind = BatchKind::TempTrack;
|
||||
b.temp_tracks = std::move(v);
|
||||
EnqueueBatch(std::move(b));
|
||||
}
|
||||
|
||||
void WriteTrustTracks(std::vector<Trust_Track> v)
|
||||
{
|
||||
if (v.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
Batch b;
|
||||
b.kind = BatchKind::TrustTrack;
|
||||
b.trust_tracks = std::move(v);
|
||||
EnqueueBatch(std::move(b));
|
||||
}
|
||||
|
||||
void WriteAssoInfos(std::vector<Asso_Info> v)
|
||||
{
|
||||
if (v.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
Batch b;
|
||||
b.kind = BatchKind::AssoInfo;
|
||||
b.asso_infos = std::move(v);
|
||||
EnqueueBatch(std::move(b));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,42 +5,88 @@
|
||||
// 功能(完整说明见 docs/data_record_debug.md):
|
||||
// 1) 把结构体数据按「一个记录实体一张表」的方式写入 SQLite 数据库,结构体到数据表的映射由
|
||||
// 内置的 ormpp 完成(third_party/ormpp,见该目录 UPSTREAM.md);
|
||||
// 2) 整个功能由构建开关控制:CMake 选项 RDP_DATA_RECORD(AUTO/ON/OFF)开启时由构建系统定义
|
||||
// 2) 性能模型(V1.5.8 优化后):
|
||||
// - 记录点只做「收集源结构体 → 一次入队」,在雷达处理线程上只有拷贝 + 队列 push 的开销;
|
||||
// - 实体构造(X/P 等数组转文本)与 SQLite 写入由独立的写线程完成,批量事务提交;
|
||||
// - 队列有界,写线程跟不上时整批丢弃并计数,**绝不阻塞雷达处理线程**;
|
||||
// 3) 整个功能由构建开关控制:CMake 选项 RDP_DATA_RECORD(AUTO/ON/OFF)开启时由构建系统定义
|
||||
// RDP_RECORD_ENABLE,rdp_record.cpp 与 sqlite3 amalgamation 才参与编译;关闭(交付/Release
|
||||
// 构建默认)时本模块完全不编译,RDP_RECORD / RDP_RECORD_BATCH 展开为 ((void)0)、实参不求值,
|
||||
// 不留任何代码与运行开销;
|
||||
// 3) 生命周期:Data_Process_Factory::GetB() 调用 rdp_record::Init()(建目录、开库),
|
||||
// Data_Process_Factory::Destroy() 调用 rdp_record::Shutdown()(关库);Init 之前、初始化
|
||||
// 失败之后或 Shutdown 之后的写入一律静默忽略,不影响雷达数据处理;
|
||||
// 4) 记录点用法(新增数据 = 加结构体 + 注册 + 一行记录点,配方见 docs/data_record_debug.md):
|
||||
// 单条:RDP_RECORD(RdpRecPointTws{0, 值1, 值2, ...});
|
||||
// 批量:RDP_RECORD_BATCH(recs); // recs 为 std::vector<某记录实体>
|
||||
// 构建默认)时本模块完全不编译,所有记录宏展开为 ((void)0)、实参不求值,不留任何开销;
|
||||
// 4) 生命周期:Data_Process_Factory::GetB() 调用 rdp_record::Init()(建目录、开库、启动写线程),
|
||||
// Data_Process_Factory::Destroy() 调用 rdp_record::Shutdown()(停止入队、排空队列、关库);
|
||||
// Init 之前、初始化失败之后或 Shutdown 之后的写入一律静默忽略,不影响雷达数据处理;
|
||||
// 5) 记录点用法(新增数据 = 加记录实体 + 注册 + 一行批量记录点,配方见 docs/data_record_debug.md):
|
||||
// 帧/圈内收集:std::vector<源结构体> recs;
|
||||
// 记录点: RDP_RECORD_POINTS(recs); // 按表选择对应宏,内部 std::move
|
||||
// 需要语句/局部变量的记录点请整体用 #ifdef RDP_RECORD_ENABLE ... #endif 包裹,
|
||||
// 保证关闭记录功能时既无代码也无「变量未使用」告警;
|
||||
// 5) 记录实体类型必须先在 rdp_record_entity.h 中声明并注册,否则链接期报 LNK2019
|
||||
// 6) 记录实体类型必须先在 rdp_record_entity.h 中声明并注册,否则链接期报 LNK2019
|
||||
// (extern template 已阻止 C++17 调用方隐式实例化)。
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
//记录源结构体(定义在 struct.h;此处只做声明,避免业务头文件依赖 struct.h 的全部内容)
|
||||
struct PointRecv;
|
||||
struct Temp_track;
|
||||
struct Trust_Track;
|
||||
struct Asso_Info;
|
||||
|
||||
namespace rdp_record
|
||||
{
|
||||
bool Init(); //初始化记录库(由 Data_Process_Factory::GetB() 调用)
|
||||
void Shutdown(); //关闭记录库(由 Data_Process_Factory::Destroy() 调用)
|
||||
std::string db_path(); //当前记录库文件全路径(未启用或未成功打开时返回空串)
|
||||
//记录统计(仅用于调试/验收;Shutdown 时自动打印,也可随时读取)
|
||||
struct Stats
|
||||
{
|
||||
std::uint64_t enqueued_rows = 0; //已入队行数
|
||||
std::uint64_t dropped_rows = 0; //队列满/停止后丢弃的行数
|
||||
std::uint64_t written_rows = 0; //已成功写入数据库的行数
|
||||
std::uint64_t transactions = 0; //提交事务数
|
||||
std::uint64_t write_errors = 0; //写入失败次数
|
||||
std::uint64_t queue_peak_bytes = 0; //队列峰值字节数
|
||||
std::uint64_t queue_peak_rows = 0; //队列峰值行数
|
||||
};
|
||||
|
||||
//写入单条记录实体(T 为记录实体类型,须在 rdp_record.cpp 中显式实例化)
|
||||
bool Init(); //初始化记录库并启动写线程(由 Data_Process_Factory::GetB() 调用)
|
||||
void Shutdown(); //停止写线程、排空队列并关闭记录库(由 Data_Process_Factory::Destroy() 调用)
|
||||
std::string db_path(); //当前记录库文件全路径(未启用或未成功打开时返回空串)
|
||||
Stats stats(); //当前统计快照(任何时刻可调用,线程安全)
|
||||
|
||||
//异步入队接口(处理线程只拷贝源结构体并送入有界队列;参数按值接收,调用点用 std::move)
|
||||
//仅在记录功能开启(RDP_RECORD_ENABLE)时有实现;关闭构建由对应宏展开为空。
|
||||
void WritePoints(std::vector<PointRecv> v); //RdpRecPoint(输入点迹,TWS/TAS 共用)
|
||||
void WriteCohesionTWS(std::vector<PointRecv> v); //RdpRecPointCohesionTWS
|
||||
void WriteCohesionTAS(std::vector<PointRecv> v); //RdpRecPointCohesionTAS
|
||||
void WriteTempTracks(std::vector<Temp_track> v); //RdpRecTempTrack(临时航迹)
|
||||
void WriteTrustTracks(std::vector<Trust_Track> v); //RdpRecTrustTrack(可靠航迹)
|
||||
void WriteAssoInfos(std::vector<Asso_Info> v); //RdpRecAssoInfo(点航关联信息)
|
||||
|
||||
//遗留接口:同步写入记录实体(在调用线程内直接写库,会阻塞;仅供测试/特殊场景使用)。
|
||||
//T 为记录实体类型,须在 rdp_record.cpp 中显式实例化。
|
||||
template <typename T> void Write(const T& v);
|
||||
//批量写入同一记录实体的多条记录(内部使用事务)
|
||||
template <typename T> void Write(const std::vector<T>& v);
|
||||
}
|
||||
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录宏:把指定记录实体写入数据库(首次写入该实体时自动建表)
|
||||
//按记录表批量入队(参数为源结构体 vector,宏内部 std::move;调用后 vector 为空)
|
||||
#define RDP_RECORD_POINTS(v) ::rdp_record::WritePoints(std::move(v))
|
||||
#define RDP_RECORD_COHESION_TWS(v) ::rdp_record::WriteCohesionTWS(std::move(v))
|
||||
#define RDP_RECORD_COHESION_TAS(v) ::rdp_record::WriteCohesionTAS(std::move(v))
|
||||
#define RDP_RECORD_TEMP_TRACKS(v) ::rdp_record::WriteTempTracks(std::move(v))
|
||||
#define RDP_RECORD_TRUST_TRACKS(v) ::rdp_record::WriteTrustTracks(std::move(v))
|
||||
#define RDP_RECORD_ASSO_INFOS(v) ::rdp_record::WriteAssoInfos(std::move(v))
|
||||
|
||||
//遗留宏:记录实体单条/批量同步写(会阻塞调用线程;新记录点请使用上面的批量入队接口)
|
||||
#define RDP_RECORD(...) ::rdp_record::Write(__VA_ARGS__)
|
||||
//批量记录宏:把 std::vector<记录实体> 一次性写入(每批一个事务)
|
||||
#define RDP_RECORD_BATCH(...) ::rdp_record::Write(__VA_ARGS__)
|
||||
#else
|
||||
//记录功能关闭:记录模块不参与编译,宏展开为空(实参不求值)
|
||||
//记录功能关闭:记录模块不参与编译,所有宏展开为空(实参不求值)
|
||||
#define RDP_RECORD_POINTS(v) ((void)0)
|
||||
#define RDP_RECORD_COHESION_TWS(v) ((void)0)
|
||||
#define RDP_RECORD_COHESION_TAS(v) ((void)0)
|
||||
#define RDP_RECORD_TEMP_TRACKS(v) ((void)0)
|
||||
#define RDP_RECORD_TRUST_TRACKS(v) ((void)0)
|
||||
#define RDP_RECORD_ASSO_INFOS(v) ((void)0)
|
||||
#define RDP_RECORD(...) ((void)0)
|
||||
#define RDP_RECORD_BATCH(...) ((void)0)
|
||||
namespace rdp_record
|
||||
@@ -48,6 +94,7 @@ namespace rdp_record
|
||||
inline bool Init() { return false; }
|
||||
inline void Shutdown() {}
|
||||
inline std::string db_path() { return std::string(); }
|
||||
inline Stats stats() { return Stats(); }
|
||||
template <typename T> inline void Write(const T&) {}
|
||||
template <typename T> inline void Write(const std::vector<T>&) {}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include "rdp_record.h"
|
||||
#include "rdp_record_entity.h"
|
||||
|
||||
using namespace Eigen;
|
||||
using namespace std;
|
||||
@@ -68,6 +67,11 @@ int Track_Asso:: track_asso_process(std::vector<PointRecv> *point_
|
||||
std::vector<PointRecv>().swap(point_process);
|
||||
|
||||
//输出航迹
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录可靠航迹:先收集,输出循环结束后一次入队
|
||||
std::vector<Trust_Track> rec_trust;
|
||||
rec_trust.reserve(trust_track->size());
|
||||
#endif
|
||||
for (size_t i=0;i<trust_track->size();i++)
|
||||
{ // 输出更新航迹条件:
|
||||
if((*trust_track)[i].Track_Update_Flag==1){
|
||||
@@ -160,10 +164,16 @@ int Track_Asso:: track_asso_process(std::vector<PointRecv> *point_
|
||||
std::memcpy(Trust_Track_Output[*Trust_track_num_Output-1][0].speed_dim, (*trust_track)[i].speed_dim, sizeof(Trust_Track_Output[*Trust_track_num_Output-1][0].speed_dim));
|
||||
std::memcpy(Trust_Track_Output[*Trust_track_num_Output-1][0].range_dim, (*trust_track)[i].range_dim, sizeof(Trust_Track_Output[*Trust_track_num_Output-1][0].range_dim));
|
||||
|
||||
RDP_RECORD(RdpRecTrustTrack((*trust_track)[i]));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_trust.push_back((*trust_track)[i]);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
RDP_RECORD_TRUST_TRACKS(rec_trust);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -436,6 +446,19 @@ void Track_Asso:: model_filter(std::vector <Trust_Track> *trust_track,struct Ra
|
||||
|
||||
|
||||
std::vector <Asso_Info> associated_info;
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录未关联上的点航信息:收集到分块上限即入队,函数末尾再 flush 剩余
|
||||
//按本圈候选数(点迹数×航迹数,封顶到分块上限)预留,避免收集期间多次扩容
|
||||
std::vector<Asso_Info> rec_asso;
|
||||
{
|
||||
std::size_t est = point_process.size() * trust_track->size();
|
||||
if (est > static_cast<std::size_t>(RDP_RECORD_ASSO_CHUNK_ROWS))
|
||||
{
|
||||
est = static_cast<std::size_t>(RDP_RECORD_ASSO_CHUNK_ROWS);
|
||||
}
|
||||
rec_asso.reserve(est);
|
||||
}
|
||||
#endif
|
||||
|
||||
//遍历所有点迹航迹 计算点迹和航迹的统计距离
|
||||
for (size_t loop_of_track = 0; loop_of_track<trust_track->size();loop_of_track++)
|
||||
@@ -551,7 +574,13 @@ void Track_Asso:: model_filter(std::vector <Trust_Track> *trust_track,struct Ra
|
||||
{
|
||||
associated_info.push_back(associated_info_tmp);
|
||||
}else{
|
||||
RDP_RECORD(RdpRecAssoInfo(associated_info_tmp));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_asso.push_back(associated_info_tmp);
|
||||
if (rec_asso.size() >= RDP_RECORD_ASSO_CHUNK_ROWS)
|
||||
{
|
||||
RDP_RECORD_ASSO_INFOS(rec_asso);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -707,7 +736,13 @@ void Track_Asso:: model_filter(std::vector <Trust_Track> *trust_track,struct Ra
|
||||
{
|
||||
if(associated_info[ii].track_index==track_index)
|
||||
{
|
||||
RDP_RECORD(RdpRecAssoInfo(associated_info[ii]));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_asso.push_back(associated_info[ii]);
|
||||
if (rec_asso.size() >= RDP_RECORD_ASSO_CHUNK_ROWS)
|
||||
{
|
||||
RDP_RECORD_ASSO_INFOS(rec_asso);
|
||||
}
|
||||
#endif
|
||||
associated_info.erase(associated_info.begin()+ii);
|
||||
associated_num=static_cast<int>(associated_info.size());
|
||||
}
|
||||
@@ -719,7 +754,13 @@ void Track_Asso:: model_filter(std::vector <Trust_Track> *trust_track,struct Ra
|
||||
{
|
||||
if(associated_info[ii].point_index==point_index)
|
||||
{
|
||||
RDP_RECORD(RdpRecAssoInfo(associated_info[ii]));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_asso.push_back(associated_info[ii]);
|
||||
if (rec_asso.size() >= RDP_RECORD_ASSO_CHUNK_ROWS)
|
||||
{
|
||||
RDP_RECORD_ASSO_INFOS(rec_asso);
|
||||
}
|
||||
#endif
|
||||
associated_info.erase(associated_info.begin()+ii);
|
||||
associated_num=static_cast<int>(associated_info.size());
|
||||
}
|
||||
@@ -813,6 +854,13 @@ void Track_Asso:: model_filter(std::vector <Trust_Track> *trust_track,struct Ra
|
||||
|
||||
}
|
||||
}
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//函数返回前把剩余未入队的关联信息 flush 掉(宏内部 std::move,调用后 rec_asso 为空)
|
||||
if (!rec_asso.empty())
|
||||
{
|
||||
RDP_RECORD_ASSO_INFOS(rec_asso);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Track_Asso:: model_output(std::vector <Trust_Track> *trust_track)
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include "rdp_record.h"
|
||||
#include "rdp_record_entity.h"
|
||||
|
||||
using namespace Eigen;
|
||||
using namespace std;
|
||||
@@ -41,6 +40,11 @@ long long latest_timestamp //最新时间戳
|
||||
std::vector<PointRecv>().swap(point_process);
|
||||
|
||||
//输出航迹
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录可靠航迹:先收集,输出循环结束后一次入队
|
||||
std::vector<Trust_Track> rec_trust;
|
||||
rec_trust.reserve(trust_track->size());
|
||||
#endif
|
||||
for (size_t i=0;i<trust_track->size();i++)
|
||||
{ // 输出更新航迹条件:
|
||||
if((*trust_track)[i].Track_Index == tas_track_idx && (*trust_track)[i].Track_Update_Flag == 1)
|
||||
@@ -119,11 +123,17 @@ long long latest_timestamp //最新时间戳
|
||||
(*trust_track)[i].Elevation = Trust_Track_Output[*Trust_track_num_Output-1][0].Elevation;
|
||||
(*trust_track)[i].Velocity = Trust_Track_Output[*Trust_track_num_Output-1][0].Range_V;
|
||||
|
||||
RDP_RECORD(RdpRecTrustTrack((*trust_track)[i]));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_trust.push_back((*trust_track)[i]);
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
RDP_RECORD_TRUST_TRACKS(rec_trust);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
@@ -252,6 +262,11 @@ void Track_Asso_Tas::model_filter( std::vector<Trust_Track> *t
|
||||
)
|
||||
{
|
||||
std::vector <Asso_Info> associated_info;
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录未关联上的点航信息:收集到分块上限即入队,函数末尾再 flush 剩余
|
||||
std::vector<Asso_Info> rec_asso;
|
||||
rec_asso.reserve(point_process.size());
|
||||
#endif
|
||||
|
||||
//航迹信息
|
||||
double X1[6] = {0},X2[6] = {0},X3[6] = {0},P1[6][6] = {{0}},P2[6][6] = {{0}},P3[6][6] = {{0}};
|
||||
@@ -360,7 +375,13 @@ void Track_Asso_Tas::model_filter( std::vector<Trust_Track> *t
|
||||
{
|
||||
associated_info.push_back(associated_info_tmp);
|
||||
}else{
|
||||
RDP_RECORD(RdpRecAssoInfo(associated_info_tmp));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_asso.push_back(associated_info_tmp);
|
||||
if (rec_asso.size() >= RDP_RECORD_ASSO_CHUNK_ROWS)
|
||||
{
|
||||
RDP_RECORD_ASSO_INFOS(rec_asso);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,7 +517,13 @@ void Track_Asso_Tas::model_filter( std::vector<Trust_Track> *t
|
||||
|
||||
for(auto& info : associated_info)
|
||||
{
|
||||
RDP_RECORD(RdpRecAssoInfo(info));
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
rec_asso.push_back(info);
|
||||
if (rec_asso.size() >= RDP_RECORD_ASSO_CHUNK_ROWS)
|
||||
{
|
||||
RDP_RECORD_ASSO_INFOS(rec_asso);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
//未关联上,航迹外推
|
||||
@@ -540,6 +567,13 @@ void Track_Asso_Tas::model_filter( std::vector<Trust_Track> *t
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//函数返回前把剩余未入队的关联信息 flush 掉(宏内部 std::move,调用后 rec_asso 为空)
|
||||
if (!rec_asso.empty())
|
||||
{
|
||||
RDP_RECORD_ASSO_INFOS(rec_asso);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Track_Asso_Tas::model_output(std::vector<Trust_Track> *trust_track, int tas_track_idx )
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <iostream>
|
||||
#include<iomanip>
|
||||
#include "rdp_record.h"
|
||||
#include "rdp_record_entity.h"
|
||||
using namespace std;
|
||||
|
||||
int Track_Init::track_init_process_logic( std::vector <PointRecv> *point_recv, //输入点迹
|
||||
@@ -109,11 +108,13 @@ int Track_Init::track_init_process_logic( std::vector <PointRecv>
|
||||
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
//记录临时航迹
|
||||
std::vector<Temp_track> rec_temp;
|
||||
for(auto& vec_t : *temp_track){
|
||||
for(auto& t : vec_t){
|
||||
RDP_RECORD(RdpRecTempTrack(t));
|
||||
rec_temp.push_back(t);
|
||||
}
|
||||
}
|
||||
RDP_RECORD_TEMP_TRACKS(rec_temp);
|
||||
#endif
|
||||
|
||||
//临时航迹满足起始长度 转为可靠航迹
|
||||
|
||||
+184
-60
@@ -2,17 +2,33 @@
|
||||
|
||||
> 面向调试的数据记录框架:把内部结构体按「一个记录实体一张表」写入 SQLite,供事后查询分析。
|
||||
> 本功能**默认只在 Debug 构建中启用**,Release/交付构建完全不编译(无依赖、无代码、无性能损失)。
|
||||
> V1.5.8 优化后,记录链路为「处理线程收集源结构体 → 有界队列 → 独立写线程构造实体并批量事务入库」,
|
||||
> 雷达处理线程只承担拷贝与入队开销;写线程跟不上时丢弃数据而不是阻塞处理。
|
||||
> 相关代码:`data_process_class_dll/rdp_record.h`、`rdp_record_entity.h`、`rdp_record.cpp`;
|
||||
> 第三方依赖来源见 `third_party/ormpp/UPSTREAM.md`。
|
||||
> 相关参数:`parameters.h` 的「数据记录性能参数」段;第三方依赖来源见 `third_party/ormpp/UPSTREAM.md`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 目的与范围
|
||||
|
||||
- **目的**:调试期把关键中间数据落库(点迹、航迹、关联结果……),用于事后 SQL 查询与对比分析;
|
||||
- **框架性设计**:本功能**不预设记录哪些数据**。新增一类记录数据的成本固定为三步:
|
||||
加记录实体结构体 → 注册 → 在需要的位置写一行记录点宏(配方见第 5 节);
|
||||
- **非目标**:不做异步写入、不做查询/回放工具、不做数据裁剪与轮转、不追求多线程写入性能。
|
||||
- **框架性设计**:本功能**不预设记录哪些数据**。新增一类记录数据的成本固定为四步:
|
||||
加记录实体 → 加异步入队接口与宏 → 在写线程注册批次/构造/提交分支 → 业务侧收集后调用宏(配方见第 6 节);
|
||||
- **性能模型(V1.5.8 优化)**:
|
||||
- 记录点只做「帧/圈内收集源结构体 → 一次入队」,处理线程上只有结构体拷贝 + 队列加锁 push 的开销;
|
||||
- 实体构造(`X`/`P` 等数组转文本)与 SQLite 写入由独立写线程完成,按表批量事务提交;
|
||||
- 队列**有界**(默认 64 MB / 4096 批),写线程跟不上时**整批丢弃并计数**,绝不阻塞雷达处理线程;
|
||||
- 数据库使用 WAL + `synchronous=OFF` + 8 MB 页缓存,建表后去掉 `AUTOINCREMENT`,以换取调试场景下的写入吞吐;
|
||||
- **非目标(本轮明确不做)**:
|
||||
- 不做关联信息裁剪(分析报告中记为 **P0-a**):⑥ 关联信息仍按「每个候选一行」落库,单圈行数为
|
||||
「点迹数 × 航迹数」的 M×N 量级;
|
||||
- 不做记录策略/轮转/在线回放(记为 **P2**):不按容量/时长切库、不清理旧库、不提供查询或回放工具;
|
||||
- 不保证**跨表**写入顺序(同一张表内的行顺序保持);
|
||||
- 不保证进程崩溃时的数据完整性:队列尾部与 WAL 尾部可能丢失(异步 + `synchronous=OFF` 的代价);
|
||||
- 不改变记录行数与表列(与优化前完全一致,见第 10 节验证)。
|
||||
- **性能摘要**(单机 dup 场景实测,详见第 10 节):普通帧每帧新增耗时 p99 ≈ 0.09~0.11 ms;
|
||||
判圈帧 track 新增中位 ≈ 2.9~4.1 ms、p99 ≈ 13~24 ms(多次验收运行;200 目标/150 点压测场景,因未采用 P0-a,单圈约 12.3 万关联候选,
|
||||
属该实现的压测上限;受控 50 目标/50 点场景最大 5.96 ms)。
|
||||
|
||||
## 2. 开关与构建
|
||||
|
||||
@@ -20,8 +36,8 @@
|
||||
|---|---|
|
||||
| 开关 | CMake 缓存变量 `RDP_DATA_RECORD`,取值 `AUTO`(默认)/ `ON` / `OFF` |
|
||||
| AUTO 语义 | `Debug` 构建启用、`Release`(及其它)构建关闭 |
|
||||
| 生效方式 | 启用时构建系统定义 `RDP_RECORD_ENABLE` 并额外编译静态库 `rdp_record`(`rdp_record.cpp` + 内置 `sqlite3.c`)并链接进 DLL |
|
||||
| 关闭时的保证 | 记录模块源码**不参与编译**;`RDP_RECORD(...)` / `RDP_RECORD_BATCH(...)` 展开为 `((void)0)`(实参不求值,也不会产生「变量已初始化但未使用」告警);DLL 不新增依赖与导出符号 |
|
||||
| 生效方式 | 启用时构建系统定义 `RDP_RECORD_ENABLE` 并额外编译静态库 `rdp_record`(`rdp_record.cpp` + 内置 `sqlite3.c`)并链接进 DLL(`CMakeLists.txt` 118-158) |
|
||||
| 关闭时的保证 | 记录模块源码**不参与编译**(`CMakeLists.txt` 的 `if(RDP_RECORD_ENABLED)` 内才 `add_library`/链接);所有记录宏展开为 `((void)0)`(实参不求值,不产生「变量已初始化但未使用」告警);DLL 不新增依赖与导出符号 |
|
||||
|
||||
```bat
|
||||
:: 交付/Release:关闭(AUTO 的默认结果,也可显式 OFF)
|
||||
@@ -42,27 +58,75 @@ cmake --build build/X256_PS/Debug/REC_ON
|
||||
|
||||
配置阶段会打印开关状态:`数据记录调试功能:启用(C++20 静态库 rdp_record + 内置 SQLite)` 或 `...:关闭(不编译记录模块,RDP_RECORD 为空操作)`。
|
||||
|
||||
**语言标准与依赖**:记录模块以 **C++20** 独立编译(ormpp 要求 `CMAKE_CXX_STANDARD 20`),并编译内置的 SQLite amalgamation(因此启用时需要 C 编译器,随 VS 2022 工具集提供);**雷达处理代码与内置 Eigen 仍为 C++17**,二者通过 `rdp_record.h` 的「仅声明 + 显式实例化」跨标准边界:调用方(C++17)只把记录实体按 `const` 引用递给 `rdp_record::Write<T>()`,实现在 `rdp_record.cpp`(C++20)中显式实例化。
|
||||
**「关闭时零代码」的依据**:
|
||||
|
||||
1. 关闭时构建系统不创建 `rdp_record` 静态库,也不把 `sqlite3.c` 加入任何目标;
|
||||
2. `rdp_record.h` 在未定义 `RDP_RECORD_ENABLE` 时把 `RDP_RECORD_POINTS`/`RDP_RECORD_COHESION_TWS`/`RDP_RECORD_COHESION_TAS`/`RDP_RECORD_TEMP_TRACKS`/`RDP_RECORD_TRUST_TRACKS`/`RDP_RECORD_ASSO_INFOS` 以及遗留的 `RDP_RECORD`/`RDP_RECORD_BATCH` 全部展开为 `((void)0)`,并提供 `Init/Shutdown/db_path/stats/Write` 的内联空实现;
|
||||
3. 6 个业务文件(`data_process.cpp`、`dot_coh.cpp`、`dot_coh_tas.cpp`、`track_init.cpp`、`track_asso.cpp`、`track_asso_tas.cpp`)中的收集变量、收集语句与宏调用**整体包在 `#ifdef RDP_RECORD_ENABLE` 内**;这些文件已不再包含 `rdp_record_entity.h`,因此实体结构体、`arrayToString()` 与 `struct.h` 不会经记录模块被处理代码拖入;
|
||||
4. 实测 8 种配置(PS/MS × Release/Debug × REC_ON/OFF)均 0 warning、0 error,且 OFF 构建的 DLL 不新增依赖/导出符号。
|
||||
|
||||
**语言标准与依赖**:记录模块以 **C++20** 独立编译(ormpp 要求 `CMAKE_CXX_STANDARD 20`),并编译内置的 SQLite amalgamation(因此启用时需要 C 编译器,随 VS 2022 工具集提供);**雷达处理代码与内置 Eigen 仍为 C++17**,二者通过 `rdp_record.h` 的「`Write<T>` 仅声明 + 显式实例化」跨标准边界:调用方(C++17)不实例化记录实现,实现在 `rdp_record.cpp`(C++20)中显式实例化。V1.5.8 起常规记录点改走**非模板**的 `Write*` 异步入队接口,跨标准边界更简单;模板 `Write<T>` 仅作为遗留同步接口保留。
|
||||
|
||||
## 3. 运行期行为
|
||||
|
||||
| 项目 | 说明 |
|
||||
|---|---|
|
||||
| 存放目录 | `RDP_RECORD_DB_DIR`(`parameters.h`,默认 `.\rdp_records\`,相对宿主进程当前工作目录,不存在时自动创建) |
|
||||
| 文件名 | `rdp_record_YYYYMMDD_HHMMSS.db`(**每次运行一个新文件**,与 `rdp_logs` 的日志命名风格一致) |
|
||||
| 初始化 | `Data_Process_Factory::GetB()` 内调用 `rdp_record::Init()`(幂等):建目录 → 开库 → 打印记录库路径 |
|
||||
| 关闭 | `Data_Process_Factory::Destroy()` 内调用 `rdp_record::Shutdown()`;宿主未调用时由静态析构兜底关库 |
|
||||
| 建表 | 首次写入某记录实体时自动执行 `CREATE TABLE IF NOT EXISTS`(每实体只尝试一次) |
|
||||
| PRAGMA | `synchronous=OFF`(调试取舍:提交时不再 fsync,崩溃可能丢失尾部数据;如需崩溃安全可改为 `NORMAL` 或自行在 sqlite3 层开启 WAL)。未设置 `journal_mode`——ormpp 的 `execute()` 只接受不返回结果行的语句 |
|
||||
| 线程模型 | 单连接 + 内部互斥锁串行化写入;宿主多线程调用不会破坏数据,但**不保证高并发写入性能**(调试功能,按要求不做异步) |
|
||||
| 失败降级 | 目录不可建/库打不开:`Init()` 返回 `false`,只打印一次日志,之后所有写入静默忽略;建表或插入失败:**每个实体只提示一次**,不重试、不抛出、不影响雷达数据处理 |
|
||||
| 自增主键 | 每个记录实体的首字段固定 `int64_t id`:建表时登记自增主键,插入时自动跳过该列(写入时固定填 `0`,由 SQLite 分配) |
|
||||
| 文件名 | `rdp_record_YYYYMMDD_HHMMSS.db`(**每次运行一个新文件**,与 `rdp_logs` 命名风格一致);同一秒内重复 `Init`(如 Destroy→GetB 重连)或同目录多进程启动时,检测到文件已存在则自动追加 `_1`/`_2`…(最多尝试 999 个后缀),不复用同一库文件 |
|
||||
| 初始化 | `Data_Process_Factory::GetB()` 内调用 `rdp_record::Init()`:建目录 → 生成文件名 → 打开 SQLite → 应用 PRAGMA → **启动记录写线程** → 置 ready;已初始化时幂等,初始化失败返回 `false` 且不抛异常 |
|
||||
| 写线程 | 独立 `std::thread`,Windows 下用 `SetThreadPriority(..., THREAD_PRIORITY_BELOW_NORMAL)` 把 CPU 优先级降到后台(I/O 保持正常优先级,避免排空过慢);写线程是 SQLite 连接的主要使用者,处理线程不直接访问数据库(遗留同步接口经 `db_mtx` 与写线程串行化) |
|
||||
| 队列与丢弃 | 处理线程把「一种表的一批源结构体」放进有界 `std::deque`;上限为 `RDP_RECORD_QUEUE_CAP_BYTES`(默认 64 MB,按容器容量近似统计)且 `RDP_RECORD_QUEUE_MAX_BATCHES`(默认 4096 批)。任一上限将超出时**整批丢弃**并累计 `dropped_rows`,不阻塞处理线程;丢弃日志按 `RDP_RECORD_DROP_LOG_INTERVAL`(默认每 1024 行)限频提示 |
|
||||
| 批事务 | 写线程按表把队列数据追加到 pending 缓冲,当 pending 行数达到 `RDP_RECORD_WRITE_BATCH_ROWS`(默认 4096 行)或为凑批等待 `RDP_RECORD_WRITE_MAX_WAIT_MS`(默认 50 ms)超时后,按**表**分别提交;同一张表的一批数据在同一个 `BEGIN/COMMIT` 事务内插入 |
|
||||
| 实体构造 | 「源结构体 → 记录实体」(`PointRecv`→`RdpRecPoint`、`Trust_Track`→`RdpRecTrustTrack` 等,含数组转文本)**全部在写线程完成**;处理线程只拷贝源结构体 |
|
||||
| PRAGMA | `journal_mode=WAL`(`RDP_RECORD_SQLITE_WAL=1` 时尝试,失败回退默认 journal 并提示);`synchronous=OFF`;`cache_size=-8192`(8 MB);`busy_timeout=2000` ms。注:`PRAGMA journal_mode`/`busy_timeout` 会返回结果行,代码用 `query_s<std::tuple<...>>` 消费 |
|
||||
| 建表 | 首次写入某记录实体时自动建表(每表只尝试一次);ormpp 的 DDL 硬编码 `AUTOINCREMENT`,建表后读 `sqlite_master` 中的 DDL 去掉该关键字并 `DROP` 重建为空表(普通 `INTEGER PRIMARY KEY`,省掉每行对 `sqlite_sequence` 的读写);重建失败时回退为保留 `AUTOINCREMENT` 的表并告警 |
|
||||
| 失败降级 | 目录不可建/库打不开/取时间失败:`Init()` 返回 `false`,仅打印日志,之后所有写入静默忽略;建表或插入失败:**每张表只提示一次**并永久跳过该表,不重试、不抛出;异步批量写入连续失败达到 `RDP_RECORD_MAX_CONSECUTIVE_ERRORS`(默认 10)次则**全局熔断**(停止记录、丢弃队列并退出写线程) |
|
||||
| 关闭 | `Data_Process_Factory::Destroy()` 内调用 `rdp_record::Shutdown()`:① 停止接收新数据 → ② 唤醒写线程**排空队列与 pending** → ③ 打印统计 → ④ `PRAGMA wal_checkpoint(TRUNCATE)` → ⑤ 关库;宿主未调用 `Destroy()` 时由静态对象析构兜底停止线程/关库 |
|
||||
| 重连 | `Destroy()` 之后再 `GetB()`:`Shutdown()` 已释放连接与状态,`Init()` 会**重新建库**(新文件;同秒冲突追加后缀),记录继续写入新库 |
|
||||
| 写入顺序 | 队列 FIFO;同一张表内的行顺序与采集顺序一致;**不同表之间的提交先后不保证**(写线程按固定表顺序 flush) |
|
||||
| 运行统计 | `rdp_record::stats()` 任意时刻可读(原子计数、线程安全),`Shutdown()` 时自动打印一行 `[rdp_record] stats(shutdown): ...` |
|
||||
| 静默忽略 | `Init()` 之前、初始化失败之后、`Shutdown()` 之后的写入一律静默忽略(未初始化时不计入统计;已初始化但已停止/失败时计入 `dropped_rows`) |
|
||||
|
||||
## 4. 记录实体(表)约定
|
||||
> 遗留接口:`RDP_RECORD(...)`/`RDP_RECORD_BATCH(...)` 仍映射到模板 `Write<T>`,在**调用线程内直接写库**(经 `db_mtx` 与写线程串行化)会阻塞,仅供测试/特殊场景;新记录点请使用第 4 节的异步入队接口与宏。
|
||||
|
||||
## 4. 接口与宏一览
|
||||
|
||||
`rdp_record.h` 对外接口:
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| `bool Init()` | 建目录、开库、应用 PRAGMA、启动写线程;由 `GetB()` 调用 |
|
||||
| `void Shutdown()` | 停止入队、排空队列、checkpoint、关库、打印统计;由 `Destroy()` 调用 |
|
||||
| `std::string db_path()` | 当前记录库全路径(未启用/未成功打开时为空串) |
|
||||
| `Stats stats()` | 当前统计快照,线程安全 |
|
||||
| `void WritePoints(std::vector<PointRecv> v)` | 输入点迹 → `RdpRecPoint`(TWS/TAS 共用) |
|
||||
| `void WriteCohesionTWS(std::vector<PointRecv> v)` | TWS 凝聚后点迹 → `RdpRecPointCohesionTWS` |
|
||||
| `void WriteCohesionTAS(std::vector<PointRecv> v)` | TAS 凝聚后点迹 → `RdpRecPointCohesionTAS` |
|
||||
| `void WriteTempTracks(std::vector<Temp_track> v)` | 临时航迹 → `RdpRecTempTrack` |
|
||||
| `void WriteTrustTracks(std::vector<Trust_Track> v)` | 可靠航迹 → `RdpRecTrustTrack` |
|
||||
| `void WriteAssoInfos(std::vector<Asso_Info> v)` | 点航关联信息 → `RdpRecAssoInfo` |
|
||||
| `template <typename T> void Write(const T&)` / `(const std::vector<T>&)` | **遗留**同步写记录实体(会阻塞调用线程) |
|
||||
| `struct Stats` | `enqueued_rows` 已入队行数、`dropped_rows` 丢弃行数、`written_rows` 已写库行数、`transactions` 提交事务数、`write_errors` 写失败次数、`queue_peak_bytes`/`queue_peak_rows` 队列峰值 |
|
||||
|
||||
记录点宏(参数均为**源结构体 vector 的左值**,宏内部 `std::move`,调用后该 vector 为空):
|
||||
|
||||
| 宏 | 记录的源结构体 | 目标表 |
|
||||
|---|---|---|
|
||||
| `RDP_RECORD_POINTS(v)` | `std::vector<PointRecv>` | `RdpRecPoint` |
|
||||
| `RDP_RECORD_COHESION_TWS(v)` | `std::vector<PointRecv>` | `RdpRecPointCohesionTWS` |
|
||||
| `RDP_RECORD_COHESION_TAS(v)` | `std::vector<PointRecv>` | `RdpRecPointCohesionTAS` |
|
||||
| `RDP_RECORD_TEMP_TRACKS(v)` | `std::vector<Temp_track>` | `RdpRecTempTrack` |
|
||||
| `RDP_RECORD_TRUST_TRACKS(v)` | `std::vector<Trust_Track>` | `RdpRecTrustTrack` |
|
||||
| `RDP_RECORD_ASSO_INFOS(v)` | `std::vector<Asso_Info>` | `RdpRecAssoInfo` |
|
||||
| `RDP_RECORD(...)` / `RDP_RECORD_BATCH(...)` | 记录实体(单条/批量,可变参数) | 对应实体表(**遗留同步接口**) |
|
||||
|
||||
记录功能关闭时上述宏全部展开为 `((void)0)`,实参不求值。
|
||||
|
||||
## 5. 记录实体(表)约定
|
||||
|
||||
1. 结构体定义在**全局命名空间**(勿放进 `namespace`),推荐以 `RdpRec` 开头;
|
||||
2. **结构体名即表名**(ormpp 取「短结构体名」),无需任何建表代码;
|
||||
3. 首字段固定为 `int64_t id`;
|
||||
3. 首字段固定为 `int64_t id`:SQLite 自增主键(写入时固定填 `0`,由 SQLite 分配);自 V1.5.8 起 **DDL 不再使用 `AUTOINCREMENT`**——建表后按 `sqlite_master` 的 DDL 去掉该关键字重建,语义仍是 `ROWID` 自增、`id` 单调递增,但不再维护 `sqlite_sequence`;
|
||||
4. 字段类型仅限标量,映射关系:
|
||||
|
||||
| C++ 类型 | SQLite 类型 | 备注 |
|
||||
@@ -75,68 +139,98 @@ cmake --build build/X256_PS/Debug/REC_ON
|
||||
| `std::string` | `TEXT` | 用于把数组类状态(如 `X[6]`、`P[6][6]`、`u[3]`)经 `rdp_record_entity.h` 的 `arrayToString()` 先转成文本再入库——这是「禁止数组字段」的替代写法,见第 5 条 |
|
||||
|
||||
5. **禁止数组字段**(如 `float speed_dim[129]`、`char[N]`):ormpp 对 C 数组按 C 字符串语义(`strlen`)绑定,会读越界或写入错误数据。需要记录大批量数组时,按下列三种方式之一处理:① 用一个 `std::string` 字段存 `arrayToString(数组)` 的结果(`RdpRecTempTrack`/`RdpRecTrustTrack` 的 `X`/`P`/`X1`~`X3`/`P2`/`P3`/`u` 即此法,多维数组以 `;` 分隔内层、`,` 分隔元素);② 另建「元素展开表」(一行一个元素);③ 改用 blob 字段;
|
||||
6. 命名约定:字段名带单位后缀(`_ms` 毫秒、`_m` 米、`_deg` 度、`_mps` 米/秒、`_s` 秒),避免单位歧义;
|
||||
7. **表结构变更不迁移旧表**:增删字段后不会修改已存在的表结构;由于每次运行生成新的 db 文件,正常调试流程天然规避该问题(若手工复用旧库,需自行建新表或删库)。
|
||||
6. **实体需提供「源结构体 → 实体」的构造函数**(如 `RdpRecTrustTrack(Trust_Track& p)`):实体构造发生在写线程,源结构体 vector 已 move 进队列后才逐条转换;
|
||||
7. 命名约定:字段名带单位后缀(`_ms` 毫秒、`_m` 米、`_deg` 度、`_mps` 米/秒、`_s` 秒),避免单位歧义;
|
||||
8. **表结构变更不迁移旧表**:增删字段后不会修改已存在的表结构;由于每次运行生成新的 db 文件,正常调试流程天然规避该问题(若手工复用旧库,需自行建新表或删库)。
|
||||
|
||||
## 5. 新增记录点:四步配方
|
||||
## 6. 新增记录点:四步配方
|
||||
|
||||
以「记录航迹号 + 高度」为例:
|
||||
以「记录航迹号 + 高度」为例(现有 6 张表的记录点均按此模型实现):
|
||||
|
||||
**第 1 步 — 声明实体**(`rdp_record_entity.h`「记录实体定义区」,紧随其后写一行 `RDP_RECORD_DECLARE`):
|
||||
**第 1 步 — 加记录实体**(`rdp_record_entity.h`「记录实体定义区」加结构体并紧跟 `RDP_RECORD_DECLARE`;再到 `rdp_record.cpp`「记录实体注册区」加 `YLT_REFL` + `RDP_RECORD_DEFINE`,字段顺序必须与结构体声明顺序一致):
|
||||
|
||||
```cpp
|
||||
// rdp_record_entity.h
|
||||
struct RdpRecTrackHeight
|
||||
{
|
||||
int64_t id; //自增主键(写入时固定填 0)
|
||||
int track_index; //航迹号
|
||||
double height_m; //高度(m)
|
||||
int Track_Index; //航迹号
|
||||
double Height; //高度(m)
|
||||
|
||||
RdpRecTrackHeight() { id = 0; }
|
||||
RdpRecTrackHeight(Trust_Track& p) : id(0), Track_Index(p.Track_Index), Height(p.Height) {}
|
||||
};
|
||||
RDP_RECORD_DECLARE(RdpRecTrackHeight);
|
||||
```
|
||||
|
||||
**第 2 步 — 注册实体**(`rdp_record.cpp`「记录实体注册区」,字段顺序必须与结构体声明顺序一致):
|
||||
|
||||
```cpp
|
||||
YLT_REFL(RdpRecTrackHeight, id, track_index, height_m);
|
||||
// rdp_record.cpp 记录实体注册区
|
||||
YLT_REFL(RdpRecTrackHeight, id, Track_Index, Height);
|
||||
RDP_RECORD_DEFINE(RdpRecTrackHeight);
|
||||
```
|
||||
|
||||
**第 3 步 — 埋记录点**(在需要记录的位置):
|
||||
**第 2 步 — 确定源结构体并加异步入队接口/宏**(`rdp_record.h`):源结构体优先复用 `struct.h` 中已有的 `PointRecv`/`Temp_track`/`Trust_Track`/`Asso_Info`(头文件只做前置声明);接口参数为**按值** `std::vector<源结构体>`,宏内部 `std::move`:
|
||||
|
||||
```cpp
|
||||
//单条(就地构造;宏为可变参数,允许花括号内有逗号)
|
||||
RDP_RECORD(RdpRecTrackHeight{0, trust_track[i].Track_Index, trust_track[i].Height});
|
||||
// rdp_record.h:namespace rdp_record 内追加
|
||||
void WriteTrackHeights(std::vector<Trust_Track> v); //RdpRecTrackHeight
|
||||
|
||||
//批量(需要语句/局部变量时,整体用 #ifdef 包裹,保证交付构建零代码)
|
||||
// 宏区(RDP_RECORD_ENABLE 开/关两套)
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
std::vector<RdpRecTrackHeight> recs;
|
||||
for (size_t i = 0; i < trust_track.size(); ++i)
|
||||
{
|
||||
recs.push_back(RdpRecTrackHeight{0, trust_track[i].Track_Index, trust_track[i].Height});
|
||||
}
|
||||
RDP_RECORD_BATCH(recs);
|
||||
#define RDP_RECORD_TRACK_HEIGHTS(v) ::rdp_record::WriteTrackHeights(std::move(v))
|
||||
#else
|
||||
#define RDP_RECORD_TRACK_HEIGHTS(v) ((void)0)
|
||||
#endif
|
||||
```
|
||||
|
||||
**第 4 步 — 无需建表**:首次写入该实体时自动 `CREATE TABLE IF NOT EXISTS`;不同实体自动落到不同表。
|
||||
**第 3 步 — 在 `rdp_record.cpp` 写线程侧增加批次/构造/提交分支**(新表需要独立的一条入队通道):
|
||||
|
||||
> 检查点:若忘记第 2 步(注册/实例化),链接期会报 **LNK2019**(`extern template` 已阻止隐式实例化),这正是配方的自检点。
|
||||
1. `BatchKind` 枚举加 `TrackHeight`;
|
||||
2. `Batch` 结构体加 `std::vector<Trust_Track> track_heights;`;
|
||||
3. `BatchRows`/`BatchBytes` 与 `PendingRows`/`Pending`/`ClearPending` 同步加上该成员(否则队列行数/字节统计失真);
|
||||
4. `AppendBatch` 加 `case BatchKind::TrackHeight: AppendMove(p.track_heights, b.track_heights); break;`;
|
||||
5. `FlushPending` 加 `ok &= FlushKind<RdpRecTrackHeight>(st, p.track_heights);`;
|
||||
6. 在异步入队接口区实现 `WriteTrackHeights`(构造 `Batch`、设置 kind、`std::move` 后调用 `EnqueueBatch`,空 vector 直接返回)。
|
||||
|
||||
## 6. 现有记录表与记录点
|
||||
> 若只是复用现有 6 种源结构体之一,也不能跳过本步:一种入队通道对应一张表;要新增一张表就必须新增 `BatchKind` + `Write*` 接口 + flush 分支。
|
||||
|
||||
当前共 **7 个记录实体定义**,其中 **6 张表在用**(第 ⑦ 项已停用)。下表逐项给出记录点、埋点位置、表名与用途;完整字段清单见本节末的「字段速查」。
|
||||
**第 4 步 — 业务侧收集后调用宏**(在雷达处理线程、数据仍有效时收集;需要语句/局部变量时整体用 `#ifdef` 包裹,保证关闭记录构建零代码):
|
||||
|
||||
| # | 记录点 | 位置 | 表 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| ① | 输入点迹(TWS + TAS) | `data_process.cpp::data_preprocess()`,TWS 分支(`point_type == 0`)与 TAS 分支(`point_type == 1`)的逐点循环内 | `RdpRecPoint` | TWS/TAS **共用一张表**,用 `point_type` 区分;每个输入点迹一行,含 `point_id` / `frame_id` / `circle_id` 三个链路标识 |
|
||||
| ② | TWS 凝聚后点迹 | `dot_coh.cpp::dot_coh_process_buff()`,输出到 `point_recv` 的循环内 | `RdpRecPointCohesionTWS` | 两帧滑窗凝聚后进入关联的点迹 |
|
||||
| ③ | TAS 凝聚后点迹 | `dot_coh_tas.cpp::dot_coh_tas_process()`,输出到 `point_recv_tas` 的循环内 | `RdpRecPointCohesionTAS` | 单帧凝聚后的 TAS 点迹 |
|
||||
| ④ | 临时航迹 | `track_init.cpp::track_init_process_logic()`,TWS 圈内**转为可靠航迹之前**(清空点迹之后、`tmp_track_to_trust_track()` 之前),双层遍历 `temp_track` | `RdpRecTempTrack` | 每条临时航迹的每个点一行,含 `temp_track_id` / `point_id` / `circle_id`;记录时机于 e982804 前移,因此表中包含**本圈参与起始判定但可能随即起批转走**的临时航迹(原先在消亡之后记录会丢失这部分数据) |
|
||||
| ⑤ | 可靠航迹 | TWS:`track_asso.cpp::track_asso_process()` 的输出分支内(`Track_Update_Flag==1`);TAS:`track_asso_tas.cpp::model_output()` 写出航迹之后(5cb67cc 补齐) | `RdpRecTrustTrack` | 按实体字段定义落库内部 `Trust_Track`(含 IMM 的 `X`/`P`/`X1`~`X3`/`P2`/`P3`/`u`,数组经 `arrayToString()` 存为文本列)。**两通道共用一张表**,用 `Track_Mode`(TWS 0 / TAS 1)区分;均只记录**本圈有输出**的航迹,`track_init.cpp` 起批路径未埋此点;V1.5.8 新增的 `Quality` / `Point_sum` / `Valid_point_sum` / `Unupdated_time` **尚未加入该实体**,如需分析质量请按第 5 节配方自行补字段 |
|
||||
| ⑥ | 点航关联信息 | TWS:`track_asso.cpp::model_filter()` 三处(未通过门限的候选、被最近邻取中的候选、因航迹/点迹已被消费而删表的候选);TAS:`track_asso_tas.cpp::model_filter()` 两处(未通过门限的候选、被取中候选及其余候选,5cb67cc 补齐) | `RdpRecAssoInfo` | 每个候选一行(`d1`/`d2`/`d3`/`d_min`、`delta_T`/`delta_h`/`R`/`VT`、`thd_d`/`thd_h`/`thd_r`),以 `min_d_flag` 标记是否被最近邻取中、以 `tas_flag`(点迹 `point_type`,1 TAS / 0 TWS)区分来源通道,可用于复现关联决策 |
|
||||
| ⑦ | (已停用)本圈输出航迹 | 原 `data_process.cpp::track_process()` 的 `model == 1/3` 分支 | ~~`RdpRecTrackOut`~~ | 该实体与记录点已在 b227ae1 中整体注释停用,改由 ⑤ 记录内部航迹;`rdp_record.cpp` 与 `rdp_record_entity.h` 中保留注释代码以备参考 |
|
||||
```cpp
|
||||
#ifdef RDP_RECORD_ENABLE
|
||||
std::vector<Trust_Track> rec_heights;
|
||||
rec_heights.reserve(trust_track->size());
|
||||
for (size_t i = 0; i < trust_track->size(); ++i)
|
||||
{
|
||||
if ((*trust_track)[i].Track_Update_Flag == 1)
|
||||
{
|
||||
rec_heights.push_back((*trust_track)[i]);
|
||||
}
|
||||
}
|
||||
RDP_RECORD_TRACK_HEIGHTS(rec_heights); //宏内部 std::move,调用后 rec_heights 为空
|
||||
#endif
|
||||
```
|
||||
|
||||
> 表清单即上表 ①②③④⑤⑥ 六张在用表(`rdp_record_entity.h` 中共 7 个实体定义,其中第 ⑦ 项已停用)。
|
||||
**无需建表**:首次写入该实体时自动建表;不同实体自动落到不同表。
|
||||
|
||||
> **自检点(忘记注册会链接失败)**:
|
||||
> - 忘记 `RDP_RECORD_DEFINE`(或 `RDP_RECORD_DECLARE`)而使用了遗留同步宏 `RDP_RECORD(实体{...})`:链接期报 **LNK2019**(`rdp_record::Write<T>` 只有声明/显式实例化,`extern template` 阻止 C++17 调用方隐式实例化);
|
||||
> - 忘记 `YLT_REFL`:`rdp_record.cpp` 编译期在 ormpp 反射处报错;
|
||||
> - 新增表若忘了第 3 步的队列分支,编译期会因缺少对应 `Write*` 实现/`FlushKind` 而报错,或运行期该表永远没有数据。
|
||||
> - 记录点必须在处理线程上、对**当前有效**的源结构体就地收集;不要保存指针/引用,也不要跨线程复用同一 vector。
|
||||
|
||||
## 7. 现有记录表与记录点
|
||||
|
||||
当前共 **6 个记录实体定义、6 张在用表**(`rdp_record_entity.h` 中 6 个结构体;历史示例 `RdpRecTrackOut` 的实体与记录点早已整体移除,仅在 `rdp_record.cpp` 注册区以注释保留两行备查)。12 个业务记录点全部改为「帧/圈内收集源结构体 → 一次入队」:
|
||||
|
||||
| # | 记录点 | 位置(文件:行) | 记录宏 | 表 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| ① | 输入点迹(TWS + TAS) | `data_process.cpp:70`(TWS 分支,判圈 return 之前)/ `data_process.cpp:160`(TAS 分支,`circle_id++` 之前) | `RDP_RECORD_POINTS` | `RdpRecPoint` | TWS/TAS **共用一张表**,用 `point_type` 区分;每个输入点迹一行,含 `point_id` / `frame_id` / `circle_id` 三个链路标识 |
|
||||
| ② | TWS 凝聚后点迹 | `dot_coh.cpp:239`(`data_input_buff` 输出到 `point_recv` 的循环之后) | `RDP_RECORD_COHESION_TWS` | `RdpRecPointCohesionTWS` | 两帧滑窗凝聚后进入关联的点迹 |
|
||||
| ③ | TAS 凝聚后点迹 | `dot_coh_tas.cpp:82`(输出到 `point_recv_tas` 的循环之后) | `RDP_RECORD_COHESION_TAS` | `RdpRecPointCohesionTAS` | 单帧凝聚后的 TAS 点迹 |
|
||||
| ④ | 临时航迹 | `track_init.cpp:117`(TWS 圈内**转为可靠航迹之前**,清空点迹之后、`tmp_track_to_trust_track()` 之前,双层遍历 `temp_track` 收集) | `RDP_RECORD_TEMP_TRACKS` | `RdpRecTempTrack` | 每条临时航迹的每个点一行,含 `temp_track_id` / `point_id` / `circle_id`;记录时机于 e982804 前移,因此表中包含**本圈参与起始判定但可能随即起批转走**的临时航迹(原先在消亡之后记录会丢失这部分数据) |
|
||||
| ⑤ | 可靠航迹 | TWS:`track_asso.cpp:174`(输出分支循环结束后);TAS:`track_asso_tas.cpp:134`(输出循环结束后) | `RDP_RECORD_TRUST_TRACKS` | `RdpRecTrustTrack` | 按实体字段定义落库内部 `Trust_Track`(含 IMM 的 `X`/`P`/`X1`~`X3`/`P2`/`P3`/`u`,数组经 `arrayToString()` 存为文本列)。**两通道共用一张表**,用 `Track_Mode`(TWS 0 / TAS 1)区分;均只记录**本圈有输出**的航迹,`track_init.cpp` 起批路径未埋此点;V1.5.8 新增的 `Quality` / `Point_sum` / `Valid_point_sum` / `Unupdated_time` **尚未加入该实体**,如需分析质量请按第 6 节配方自行补字段 |
|
||||
| ⑥ | 点航关联信息 | TWS:`track_asso.cpp:581`(未通过门限的候选)、`:743`(被最近邻取中的候选)、`:761`(因航迹/点迹已被消费而删表的候选),函数末尾 `:861` flush 剩余;TAS:`track_asso_tas.cpp:382`(未通过门限的候选)、`:524`(被取中候选及其余候选),函数末尾 `:574` flush 剩余 | `RDP_RECORD_ASSO_INFOS` | `RdpRecAssoInfo` | 每个候选一行(`d1`/`d2`/`d3`/`d_min`、`delta_T`/`delta_h`/`R`/`VT`、`thd_d`/`thd_h`/`thd_r`),以 `min_d_flag` 标记是否被最近邻取中、以 `tas_flag`(点迹 `point_type`,1 TAS / 0 TWS)区分来源通道,可用于复现关联决策。收集到 `RDP_RECORD_ASSO_CHUNK_ROWS`(默认 131072 行)即分块入队,限制单圈处理线程内存峰值 |
|
||||
|
||||
> 表清单即上表 ①②③④⑤⑥ 六张在用表。① 在判圈判定之前入队,因此即使本帧触发判圈/返回,输入点迹也不会漏记;②③④⑤ 在收集循环结束后一次入队;⑥ 按分块上限多次入队并在函数返回前 flush。
|
||||
|
||||
**链路串联**:①→②/③(点迹 → 凝聚后点迹)按 `point_id` 关联;②/③→④/⑤ 按 `point_id` 与 `circle_id` 关联;④→⑤ 通过起批时透传的 `track_id`(取自临时航迹末点的 `temp_track_id`)关联;⑤→⑥ 按 `track_id` 与 `point_id` 关联。TWS 与 TAS 可先用 `RdpRecPoint.point_type`、`RdpRecTrustTrack.Track_Mode`、`RdpRecAssoInfo.tas_flag` 三处之一区分通道。`circle_id` 在 TWS 每判满一圈(相扫为波位号不再增大、机扫为波位号 0,见 README 7.2)、TAS 每帧处理结束时自增,可用来划分「第几圈/第几帧」。
|
||||
|
||||
@@ -147,15 +241,15 @@ RDP_RECORD(RdpRecTrackHeight{0, trust_track[i].Track_Index, trust_track[i].Heigh
|
||||
- `RdpRecTrustTrack`:`track_id`、`point_id`、`circle_id`、`Track_Index`、`Range`、`Azimuth`、`Elevation`、`Velocity`、`Amplitude`、`Height`、`T_track`、`GNSS_time`、`Track_Sum`、`Point_Index`、`Track_Mode`、`Track_Update_Flag`、`Extrapolate_round`、`point_flag`、`approach_flag`、`manual_delete_flag`、`manual_tracking_flag`、`associate_point_number`、`primary_target_flag`、`Target_Type`、`Track_section_idx`、`range_point`、`azi_point`、`elev_point`、`vr_point`、`prf_point`、`point_type`、`snr_point`、`amp_asv_point`、`prn_fftn_point`、`RCS`、`pitch_num`、`X`、`P`、`X1`、`X2`、`X3`、`P2`、`P3`、`u`(后 8 项为文本);
|
||||
- `RdpRecAssoInfo`:`point_id`、`track_id`、`circle_id`、`d1`、`d2`、`d3`、`d_min`、`point_index`、`track_index`、`delta_T`、`delta_h`、`R`、`VT`、`tas_flag`、`thd_d`、`thd_h`、`thd_r`、`min_d_flag`。
|
||||
|
||||
各表的字段为**调试用途**,按第 5 节增删即可,不影响框架。注意记录点中的字段错误只在启用记录功能的构建中才会被检查(关闭时宏丢弃实参)。
|
||||
各表的字段为**调试用途**,按第 6 节增删即可,不影响框架。注意记录点中的字段错误只在启用记录功能的构建中才会被检查(关闭时宏丢弃实参)。
|
||||
|
||||
**三点使用提醒**:
|
||||
|
||||
- `RdpRecTrustTrack::Track_Update_Flag` **恒为 0**(TWS 与 TAS 两处记录点相同):写库发生在输出分支内,而该分支入口已先把标志清零(`track_asso.cpp` / `track_asso_tas.cpp` 均先置 0、再输出、最后记录)。判断「某航迹本圈是否更新」应改用该表是否出现对应 `track_id` / `circle_id` 的记录,或改用 `RdpRecAssoInfo::min_d_flag`。
|
||||
- `Range` / `Azimuth` / `Elevation` / `Velocity` 四个内部快照**只在航迹当圈有输出时被回填**(两通道均在记录前回填),长期未输出(外推超限)的航迹在表中为 0 或旧值。
|
||||
- **TAS 关联信息的三个字段为占位值**:`track_asso_tas.cpp` 写入的候选把 `R` / `VT` 置 0(TAS 关联只用统计距离与高度两个门限),并把 `thd_r` 恒置 true;`track_index` 取 TAS 目标批号 `tas_track_idx`(不是航迹表下标,TWS 行才是下标)。分析 TAS 行时不要按 TWS 的语义解读这三项。
|
||||
- `RdpRecTrustTrack::Track_Update_Flag` **恒为 0**(TWS 与 TAS 两处记录点相同):写库发生在输出分支内,而该分支入口已先把标志清零(`track_asso.cpp` / `track_asso_tas.cpp` 均先置 0、再输出、最后收集)。判断「某航迹本圈是否更新」应改用该表是否出现对应 `track_id` / `circle_id` 的记录,或改用 `RdpRecAssoInfo::min_d_flag`。
|
||||
- `Range` / `Azimuth` / `Elevation` / `Velocity` 四个内部快照**只在航迹当圈有输出时被回填**(两通道均在收集前回填),长期未输出(外推超限)的航迹在表中为 0 或旧值。
|
||||
- **TAS 关联信息的三个字段为占位值**:`track_asso_tas.cpp` 收集的候选把 `R` / `VT` 置 0(TAS 关联只用统计距离与高度两个门限),并把 `thd_r` 恒置 true;`track_index` 取 TAS 目标批号 `tas_track_idx`(不是航迹表下标,TWS 行才是下标)。分析 TAS 行时不要按 TWS 的语义解读这三项。
|
||||
|
||||
## 7. 查询与分析
|
||||
## 8. 查询与分析
|
||||
|
||||
```python
|
||||
# 取最近一次运行的记录库并查看表结构、行数与抽样数据(Python 自带 sqlite3 即可)
|
||||
@@ -194,11 +288,41 @@ print(con.execute("""
|
||||
|
||||
也可用 DB Browser for SQLite 直接打开 db 文件;同一 db 内所有表一一对应各记录实体。
|
||||
|
||||
## 8. 注意事项与已知限制
|
||||
查询时的三点注意:
|
||||
|
||||
- **`id` 是 SQLite 的主键/ROWID 自增列,单调但不保证连续**(批量插入与失败回滚都会产生空洞),排序用 `id` 没问题,但不要用它推断「第几行被记录」;
|
||||
- **跨表不能按 `id` 判断先后**:各表有独立的 id 序列,且写线程按表分别提交,跨表写入顺序不保证;跨表关联请用 `point_id` / `track_id` / `circle_id`;
|
||||
- 正常 `Destroy()` 后 WAL 已 `checkpoint(TRUNCATE)`,目录下通常只剩一个 `.db`;若进程异常退出,目录中可能残留 `-wal`/`-shm` 文件,需用 SQLite(而不是直接读文件)打开查看,尾部记录可能缺失。
|
||||
|
||||
## 9. 注意事项与已知限制
|
||||
|
||||
- **交付构建不要开启**:`Release` + `RDP_DATA_RECORD=ON` 仅用于临时排查,会带来 sqlite3 编译与运行时写入开销;正式交付保持 `AUTO`/`OFF`;
|
||||
- **Release 下记录点不做类型检查**:关闭时宏丢弃实参,因此记录点里的字段拼写/类型错误只在启用记录功能的构建中才会暴露——新增记录点后请至少构建一次开启记录功能的配置;
|
||||
- **异步尾部可能丢失**:记录是异步落库的,进程崩溃、被强杀或**未调用 `Destroy()`** 时,队列中尚未写库的数据与 WAL 尾部都可能丢失;正式采集结束时务必调用 `Data_Process_Factory::Destroy()` 让 `Shutdown()` 排空;
|
||||
- **队列满会整批丢弃**:写线程跟不上(慢盘、杀软实时扫描、CPU 争抢)时,超出 `RDP_RECORD_QUEUE_CAP_BYTES`/`RDP_RECORD_QUEUE_MAX_BATCHES` 的**整批**数据被丢弃,`stats().dropped_rows` 可查,日志按每 1024 行限频提示;这是「实时性优先」的刻意取舍,不是缺陷;
|
||||
- **记录期间关闭记录功能**:`RDP_RECORD_ENABLE` 是编译期开关,运行时无法关闭;若宿主需要停止记录,请调用 `Destroy()`(排空并关库)后不再 `GetB()`,`Shutdown()` 之后的写入静默忽略;
|
||||
- **⑥ 未裁剪,仍是 M×N**:关联信息按每个候选一行记录;200 目标/150 点压测场景单圈约 12.3 万行,判圈帧 p99 ≈ 13~24 ms(超 5 ms 验收阈值),受控 50 目标/50 点场景最大 5.96 ms。若对判圈帧实时性有硬要求,需后续实现 P0-a(只记录被取中候选/按需裁剪),本轮未做;
|
||||
- **WAL 需要本地盘**:WAL 依赖共享内存与文件锁,网络盘/不支持的文件系统上可能设置失败(代码会回退默认 journal 并提示);记录库本身也可能很大(60 圈约 1.06 GB),采集时需预留磁盘;
|
||||
- **写线程是后台优先级**:Windows 下写线程 CPU 优先级为 BELOW_NORMAL,避免与雷达处理线程争抢;磁盘慢时记录先丢批而不是拖慢处理;
|
||||
- `NaN`/`Inf` 会由 SQLite 存为 `NULL`(`sqlite3_bind_double` 行为),分析时需注意;
|
||||
- 记录库是调试产物:进程异常退出(未调用 `Destroy()`)时,`synchronous=OFF` 下可能丢失最后若干条记录;
|
||||
- **不保证跨表顺序、`id` 不连续**:见第 8 节;
|
||||
- **`RDP_RECORD`/`RDP_RECORD_BATCH` 会阻塞**:这两个遗留宏在调用线程内同步写库(与写线程经 `db_mtx` 串行化),不要在实时性敏感的路径上使用;
|
||||
- IntelliSense 可能对 `rdp_record.cpp` 按 `c_cpp_properties.json` 的 C++17 提示(该文件实际按 C++20 编译,`build/<...>/compile_commands.json` 中带有 `/std:c++20`),以实际构建结果为准;
|
||||
- `third_party/ormpp/` 为上游原样内置(ormpp 0.2.0 / iguana 1.0.5 / SQLite 3.49.1,Apache-2.0 与公有领域),升级方式与校验值见该目录 `UPSTREAM.md`。
|
||||
|
||||
## 10. 验证与性能参考(2026-10-20 单机实测)
|
||||
|
||||
> 以下为优化实施后的实测结果,原始日志与可重复脚本位于 `build/analysis/verify/`(被 `.gitignore` 忽略,
|
||||
> 不随交付;脚本用法见该目录 README.md)。数据随机器、磁盘与杀软环境变化,仅作量级参考。
|
||||
|
||||
| 项目 | 结果 |
|
||||
|---|---|
|
||||
| 构建 | 8 种配置(PS/MS × Release/Debug × REC_OFF/REC_ON)均 **0 warning、0 error** |
|
||||
| 普通帧开销 | dup 场景(6 圈/200 目标/每帧 150 点/100 波位)REC_ON vs REC_OFF:每帧 `total` 新增 **p99 ≈ 0.09~0.11 ms**(验收阈值 ≤ 1 ms,PASS) |
|
||||
| 判圈帧开销 | 同场景判圈帧 `track` 新增 **中位 ≈ 2.9~4.1 ms、p99 ≈ 13~24 ms**(多次验收运行)——已超 5 ms 验收阈值,因未采用 P0-a(单圈约 12.3 万关联候选),属压测上限;受控 50 目标/50 点场景判圈帧新增中位约 3 ms、**最大 5.96 ms** |
|
||||
| 端到端耗时 | 同场景 600 帧:ON wall ≈ 2.1 s vs OFF wall ≈ 1.8 s |
|
||||
| 行数一致性 | 与改动前完全一致:`RdpRecPoint` 90,000 / `RdpRecPointCohesionTWS` 4,170 / `RdpRecAssoInfo` 211,584 / `RdpRecTempTrack` 8,330 / `RdpRecTrustTrack` 304(合计 **314,388**) |
|
||||
| 库健康度 | `PRAGMA integrity_check = ok`;DDL 无 `AUTOINCREMENT`;`journal_mode = wal`;5 张表 `id` 严格递增;`dropped_rows = 0` |
|
||||
| 60 圈长跑 | 约 43.5 s、**9,987,370 行**、`dropped_rows=0`、`write_errors=0`、队列峰值 ≈ 19.8 MB(上限 64 MB)、DB 约 **1.06 GB** |
|
||||
| 故障注入 | 记录目录不可建(同名普通文件占位):`Init()` 静默失败、驱动正常完成(exit=0、无崩溃、未产生 db);小队列测试构建(cap=1 KB)丢弃 314,388 行且 `dropped_rows` 有计数、处理正常 |
|
||||
| 重连 | `Destroy()` → `GetB()` 生成两个独立 db 且均正常写入(同秒文件名冲突自动追加后缀) |
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
# 数据记录调试功能(RDP_DATA_RECORD)性能瓶颈分析与实时性优化报告
|
||||
|
||||
> **项目**:雷达数据处理动态库 `data_process_class_dll`(V1.5.8.PS / V1.5.8.MS,分支 `feature`,提交 `3dee734`)
|
||||
> **问题**:Release 构建下,`RDP_DATA_RECORD=ON` 开启数据记录调试功能后,数据处理不能实时完成;`RDP_DATA_RECORD=OFF` 时满足实时性。
|
||||
> **本报告范围**:定位性能瓶颈、给出可落地的优化措施与验收标准。**本阶段不修改任何业务代码**——报告中的代码改动均为"建议方案",未实施(业务代码/CMake/第三方均无改动,仅新增本报告文件)。
|
||||
> **方法**:源码走读 + 数据量建模 + Release 产物微基准 + `REC_ON`/`REC_OFF` 双 DLL 端到端 A/B 实测。
|
||||
> **证据产物**(均为本报告的一次性分析产物,位于被 `.gitignore` 忽略的 `build/` 下):
|
||||
> - 记录点与行数模型:`build/analysis/hotpath_findings.md`
|
||||
> - ormpp/SQLite 单条写入链路:`build/analysis/ormpp_sqlite_findings.md`
|
||||
> - 微基准:`build/analysis/bench_micro.md`、`build/perf_bench_micro/`
|
||||
> - 端到端 A/B:`build/analysis/bench_pipeline.md`、`build/perf_bench_pipeline/`
|
||||
> - 优化实施与验收结果:本报告第 11 节;验收脚本与日志:`build/analysis/verify/`
|
||||
> - 本报告:`docs/data_record_perf_report.md`
|
||||
|
||||
---
|
||||
|
||||
## 0. 结论摘要(TL;DR)
|
||||
|
||||
1. **根因不是"Release 没优化好",而是记录路径的设计是"在雷达处理线程上逐条同步写 SQLite"**:12 个记录点全部是就地构造实体 + 单条 `Write()`,每条记录 = 一次**独立隐式事务**(创建/写/删除回滚日志 + 文件锁 + 脏页写回)。CPU 编译优化对文件系统/锁/事务这些开销无效,所以 Release 下依旧卡。
|
||||
2. **实测单行写入成本 ≈1.1~1.3 ms(mean)、p50≈0.73~0.78 ms、p99≈5~6.3 ms**(本机:i7-10700K + NTFS 机械盘数据卷 + Defender 实时防护开启;3 次独立运行中位数,单行最大尖峰 81~496 ms)。**其中 rollback journal 文件的创建/写入/删除约占 96%~97%**:同一事务路径下把 journal 换成内存(MEMORY)或 WAL 后每行降到 38.7 µs / 17.2 µs(原生文件操作对照:空文件 create+close+delete ≈0.15~0.18 ms、写 4 KB 后删除 ≈0.29~0.31 ms)。`synchronous=OFF` 只去掉 fsync,不改变"每行一个事务"。
|
||||
3. **端到端 A/B 实测**(同一合成场景,600 帧 × 150 点/帧):
|
||||
- 仅输入点迹记录路径(未起批):`REC_OFF` 每帧合计 **0.21 ms**,`REC_ON` 每帧 **150.1 ms**(≈722 倍);每圈 **0.14 s → 16.8 s**(≈120 倍);仅"每帧 150 行输入点迹"就贡献约 144 ms/帧,**单帧即超 10 ms 实时预算 14 倍**。
|
||||
- 含航迹场景(186 条可靠航迹,关联路径真实覆盖):`REC_OFF` 每帧 **0.269 ms** → `REC_ON` **218.03 ms(811 倍)**;判圈帧 `track_process` **318 ms → 59.2 s(max 170.3 s,227 倍)**;每圈 **0.345 s → 81.05 s(235 倍)**;关联候选记录 8.7 万~12.3 万行/圈、占判圈帧 99%,**这是现场"不能实时"的直接原因**。
|
||||
4. **比"逐条事务"更危险的是数据量本身**:TWS 点航关联信息按**每圈 `M×N` 行**记录(`M`=可靠航迹数、`N`=本圈关联批凝聚点数,`N≈99×每帧凝聚点数`)。稀疏场景约 1.8 万行/圈,中等场景约 44.6 万行/圈,稠密场景可达 **5.4×10⁶ 行/圈(约 0.5 GB/圈)**——即使写库成本降到零,磁盘吞吐也支撑不了,**必须对关联候选做裁剪/采样**。
|
||||
5. **第三瓶颈是处理线程上的实体构造**:`RdpRecTrustTrack` 每行要执行 **135 次 `std::ostringstream`(实测 133.6 µs/行、≈1.1 KB 文本)**,`RdpRecTempTrack` 每行 20 次(19.4 µs/行)。批处理只省事务,**省不掉这些构造**(批量后构造占 TrustTrack 端到端成本 94%、TempTrack 81%);必须把构造搬到后台线程或改列类型。
|
||||
6. **优化收益已实测**:改为批量事务(框架已有 `RDP_RECORD_BATCH`/`Write(vector)` 能力,当前调用点为 0)后,DB 侧每行从 ~1.1 ms 降到 **3.8~8.8 µs(批 1000,按实体;约 95~336 倍)**;端到端(含构造)Point/Asso ≈207~335×、TempTrack ≈52×、TrustTrack ≈10×。WAL+`synchronous=OFF` 可把单条 autocommit 从 1120 µs 降到 17.2 µs(~55~65 倍,但仍远不够高行数场景);每帧小批量(≤150 行)时 WAL 还有 1.8× 价值(批 100:DELETE 9.83 µs/行 → WAL 5.53 µs/行);**不要用 `synchronous=NORMAL`**(本机机械盘 fsync ≈31 ms,checkpoint 抖动使 NORMAL 相对 `sync=OFF` 慢 8~12×)。
|
||||
7. **推荐方案(分层)**:
|
||||
- **P0-a 关联信息裁剪(决定性)**:点航关联只记录"被最近邻取中的候选 + 少量近失候选/采样",把 ⑥ 从 `M×N` 降到 `O(M+N)`;
|
||||
- **P0-b 全量批处理**:①~⑤、TAS ⑥ 改为"帧末/圈末收集 → 一批一个事务",直接复用现有批量接口;
|
||||
- **P0-c 异步写线程(实时性保证)**:处理线程只把 POD 快照压入有界队列,后台线程负责序列化 + 批量入库,队列满时丢弃并计数——把磁盘抖动与处理线程彻底解耦;
|
||||
- **P1 补充**:信任航迹数组列改 blob/后台序列化、临时航迹只记增量、WAL/`synchronous`/`cache_size`/去 `AUTOINCREMENT`、失败熔断、记录库轮转;环境上排除 Defender 扫描目录、避免机械盘。
|
||||
- 目标:开启记录功能后,**处理线程每帧新增耗时 p99 ≤ 1 ms、判圈帧 ≤ 5 ms、零丢帧**;交付构建仍保持 `OFF` 零开销。
|
||||
8. **交付构建不受影响**:`RDP_DATA_RECORD=OFF` 时记录模块不参与编译、宏为空操作(`rdp_record.h:44-45`、`CMakeLists.txt:118-158`),DLL 体积 182,784 → 1,414,656 字节的差异只存在于 `ON` 构建。报告中的改动全部位于 `#ifdef RDP_RECORD_ENABLE` 保护的调试代码与记录模块内部,**不进入交付产物**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题与背景
|
||||
|
||||
### 1.1 现象
|
||||
|
||||
| 配置(均为 Release / X256_PS) | 现象 |
|
||||
|---|---|
|
||||
| `RDP_DATA_RECORD=OFF`(交付/AUTO 默认) | 数据处理满足实时性 |
|
||||
| `RDP_DATA_RECORD=ON`(显式开启,用于 Release 现场排障) | 数据处理不能实时完成,且负载越密(点迹/航迹越多)越慢,最终表现为处理节拍被拖垮、数据积压 |
|
||||
|
||||
关键点:两套构建的**雷达处理算法源码完全相同**,差异只有 `RDP_RECORD_ENABLE` 宏是否定义、以及 `rdp_record.lib`(含 SQLite)是否链接进 DLL。因此问题一定出在"记录路径的运行期行为",而不是算法或编译优化级别。
|
||||
|
||||
### 1.2 数据记录功能与开关(为什么 OFF 能实时)
|
||||
|
||||
- 记录实现:`data_process_class_dll/rdp_record.h`、`rdp_record.cpp`、`rdp_record_entity.h`;
|
||||
- 构建开关:CMake 缓存变量 `RDP_DATA_RECORD`(`AUTO`/`ON`/`OFF`,`CMakeLists.txt:34-57`),启用时定义 `RDP_RECORD_ENABLE` 并编译 `rdp_record` 静态库(`CMakeLists.txt:121-157`);
|
||||
- 关闭时:`RDP_RECORD(...)` / `RDP_RECORD_BATCH(...)` 展开为 `((void)0)`(实参不求值,`rdp_record.h:42-45`),记录模块源码完全不参与编译(`CMakeLists.txt:118-158` 的 `if(RDP_RECORD_ENABLED)` 才创建/链接 `rdp_record` 目标)。**OFF 的实时性来自"代码不存在",不是"运行期判断跳过"**。
|
||||
- 运行期:`Data_Process_Factory::GetB()` → `rdp_record::Init()` 建目录/开库;`Destroy()` → `Shutdown()`(`data_process_class_dll.cpp:18-51`);单连接 + 一把全局互斥锁(`rdp_record.cpp:35-44,183`);写入失败静默降级(`rdp_record.cpp:188-195`)。
|
||||
- 功能定位与已知限制(`docs/data_record_debug.md:15,57,197-204`):**不做异步、不做数据裁剪、不追求多线程写入性能**,默认只在 Debug 启用。这正是本次问题的设计层根源——该功能按"调试期低频使用"设计,而现场需要在 Release 下开启它排障。
|
||||
|
||||
### 1.3 本报告的复现方式
|
||||
|
||||
由于没有宿主主程序,本次用两条独立证据链复现与量化:
|
||||
|
||||
1. **微基准**(`build/perf_bench_micro/`):直接链接已有 Release `REC_ON` 产物 `build/X256_PS/Release/REC_ON/lib/rdp_record.lib`,调用与业务完全相同的 `rdp_record::Write<T>()`,测单条/批量/不同 SQLite 配置的每行成本;并单独测实体构造(`arrayToString`)成本。
|
||||
2. **端到端 A/B**(`build/perf_bench_pipeline/`):编写只使用公开 DLL 接口的合成数据驱动程序,分别加载 `REC_OFF` 与 `REC_ON` 两个 `data_process_class_dll.dll`,按宿主调用序列(`data_preprocess` → 按返回的 `model` 调 `track_process`)投喂同一份合成 TWS 数据,用 `QueryPerformanceCounter` 逐帧计时。
|
||||
|
||||
> 证据链互相印证:微基准单条写入 mean 约 1.1~1.3 ms/行(p50 约 0.73~0.78 ms);首轮端到端 A/B 全运行 103,374 行 / ≈97.6 s 增量 ≈ **0.94 ms/行**;含航迹最终 A/B 为 314,388 行 / 426.7 s ≈ **1.39 ms/行**(大库/多表/页缓存压力下略高),三条口径一致。
|
||||
|
||||
---
|
||||
|
||||
## 2. 分析方法、环境与证据强度
|
||||
|
||||
### 2.1 方法总览
|
||||
|
||||
| 方法 | 内容 | 产物 |
|
||||
|---|---|---|
|
||||
| 源码走读 | 12 个 `RDP_RECORD` 记录点的循环嵌套、触发频率、行数公式;ormpp/SQLite 单条写入全链路(file:line) | `hotpath_findings.md`、`ormpp_sqlite_findings.md` |
|
||||
| 数据量建模 | 以 `M`(航迹数)、`N`(关联批点数)、`p`(每帧点数)、`B`(每圈波位数)构造每圈行数公式与场景表 | 本报告第 5 节 |
|
||||
| 微基准 | 单条 vs 批量写入、SQLite 事务/journal/WAL/语句复用/AUTOINCREMENT 对比、实体构造与 `arrayToString` 成本 | `bench_micro.md` |
|
||||
| 端到端 A/B | `REC_ON` vs `REC_OFF` 双 DLL,同机同场景逐帧计时(含起批成功、关联路径覆盖的场景) | `bench_pipeline.md` |
|
||||
| 记录库校验 | 对 A/B 产生的 db 做行数/体积/`integrity_check` 统计 | `build/analysis/count_db_rows.py` |
|
||||
|
||||
### 2.2 测试环境
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 主机 | Intel Core i7-10700K(8C16T,3.8 GHz),31.8 GB 内存 |
|
||||
| OS | Windows 10 专业版 19045(x64) |
|
||||
| 编译器/构建 | MSVC 2022 14.44(v143),CMake `Release`(`/O2 /Ob2 /DNDEBUG /MD`),NMake 单配置 |
|
||||
| 被测 DLL | `REC_OFF` 182,784 B / `REC_ON` 1,414,656 B(均已有 Release 产物,未重建主工程) |
|
||||
| 磁盘 | 工程与记录库位于 E:(WDC WD20EZAZ,**SATA 机械盘**);系统盘为 SATA SSD |
|
||||
| 安全软件 | Windows Defender:`RealTimeProtectionEnabled=True`、`OnAccessProtectionEnabled=True` |
|
||||
| SQLite | 3.49.1 amalgamation,默认编译选项(`SQLITE_THREADSAFE=1`、`cache_size=-2000`、`page_size=4096`、无额外 `SQLITE_*` 宏) |
|
||||
| 计时 | `QueryPerformanceCounter`(10 MHz);进程绑核 + `HIGH_PRIORITY_CLASS`;预热 300 ms;首圈作为预热不计入统计 |
|
||||
|
||||
### 2.3 证据强度与局限(先声明,避免误读)
|
||||
|
||||
- **强证据**:单行/批量每行成本、SQLite 各配置对比、实体构造成本、DLL 体积、代码位置与行数公式。
|
||||
- **中等证据**:端到端 A/B 的绝对毫秒数。合成场景的"每帧 150 点、100 波位/圈、200 目标"是**压力场景**,绝对耗时与真实雷达负载不同;但 **ON/OFF 的比值、每行成本、`M×N` 的行数规律**可迁移。A/B 已覆盖"未起批(仅点迹路径)"与"186 航迹(关联路径)"两种形态;只差"150 个独立点迹不经凝聚"的稠密形态(按 1.39 ms/行外推,未实跑)。
|
||||
- **不在本次证据内**:真实宿主的多线程调用行为(README 的已知问题 BUG-03 声明"接口线程模型未定义,宿主应单线程调用",`README.md:1020`);现场磁盘/Defender 的真实配置;真实场景的 `M`、`N`、凝聚存活率;TAS 路径与 `Beam_Ctrl` 未做端到端实测(记录点同构,每行成本结论适用)。
|
||||
- 因此本报告的优化措施同时给出"**本机实测收益**"与"**需现场复测校准**"两类数据;第 9.3 节给出复测方法。
|
||||
|
||||
---
|
||||
|
||||
## 3. 记录链路全貌:12 个同步单条写入点
|
||||
|
||||
### 3.1 调用链
|
||||
|
||||
```
|
||||
宿主(单线程,按 README BUG-03 约定)
|
||||
└─ data_preprocess(DataRev[150]) // 每帧
|
||||
└─ 逐点循环 → RDP_RECORD(RdpRecPoint(...)) // data_process.cpp:60 / :132
|
||||
└─ track_process(..., model) // 每帧或每圈
|
||||
├─ model==2: dot_coh_process_buff()
|
||||
│ └─ RDP_RECORD(RdpRecPointCohesionTWS) // dot_coh.cpp:230
|
||||
├─ model==1: track_asso_process()
|
||||
│ ├─ model_filter() 双层循环(航迹×点迹)
|
||||
│ │ ├─ 未过门限: RDP_RECORD(RdpRecAssoInfo) // track_asso.cpp:554
|
||||
│ │ └─ 最近邻消费: RDP_RECORD(RdpRecAssoInfo) // :710 / :722
|
||||
│ ├─ 输出循环: RDP_RECORD(RdpRecTrustTrack) // :163
|
||||
│ └─ track_init_process_logic()
|
||||
│ └─ 临时航迹双层遍历: RDP_RECORD(RdpRecTempTrack) // track_init.cpp:114
|
||||
└─ model==3(TAS): dot_coh_tas → track_asso_tas
|
||||
└─ RDP_RECORD(...) // dot_coh_tas.cpp:73, track_asso_tas.cpp:122/363/499
|
||||
|
||||
RDP_RECORD 展开(ON 时):
|
||||
构造实体(含 arrayToString)→ ::rdp_record::Write(const T&)
|
||||
→ 全局 std::mutex 加锁
|
||||
→ ormpp 单条 insert:重拼 INSERT SQL → sqlite3_prepare_v2 → 逐列 bind → sqlite3_step(隐式事务提交)→ finalize
|
||||
→ 解锁,返回雷达处理线程
|
||||
```
|
||||
|
||||
**关键事实:整条链路没有队列、没有线程边界、没有事务聚合**。每一条记录都同步占用雷达处理线程,雷达处理必须等磁盘事务完成才能继续(`data_process.cpp:18/155` 是同步接口)。
|
||||
|
||||
### 3.2 记录点清单(6 张在用表 / 12 个调用点 / 0 个批量调用)
|
||||
|
||||
| 表 | 埋点 | 所在循环 | 触发频率 | 每圈/每帧行数 |
|
||||
|---|---|---|---|---|
|
||||
| `RdpRecPoint`(①输入点迹) | `data_process.cpp:60`(TWS)、`:132`(TAS) | `data_preprocess` 逐点循环 | 每帧 | TWS `B·p`(≤15,000/圈);TAS ≤150/帧 |
|
||||
| `RdpRecPointCohesionTWS`(②) | `dot_coh.cpp:230` | 凝聚输出循环 | 每圈 `B−1` 次(model=2) | ≈`N` 行/圈 |
|
||||
| `RdpRecPointCohesionTAS`(③) | `dot_coh_tas.cpp:73` | TAS 凝聚输出循环 | 每 TAS 帧 | ≤150/帧 |
|
||||
| `RdpRecTempTrack`(④) | `track_init.cpp:114` | 临时航迹双层遍历 | 每圈 1 次(model=1) | `ΣL`(全量快照,≈1~3×未关联点数) |
|
||||
| `RdpRecTrustTrack`(⑤) | `track_asso.cpp:163`(TWS)、`track_asso_tas.cpp:122`(TAS) | 输出循环(`Track_Update_Flag==1`) | 每圈/每 TAS 帧 | ≤`M`≤500/圈;TAS ≤1/帧 |
|
||||
| `RdpRecAssoInfo`(⑥) | `track_asso.cpp:554/710/722`、`track_asso_tas.cpp:363/499` | 关联候选双层循环(航迹×点迹) | 每圈 1 次(model=1) | **`M×N` 行/圈(精确等于候选对数)**;TAS ≤150/帧 |
|
||||
|
||||
> 全仓库 `RDP_RECORD_BATCH` 调用点为 **0**(`grep` 验证;宏与批量 `Write(vector)` 早已实现但从未被业务使用,`rdp_record.h:41`、`rdp_record.cpp:198-225`)。
|
||||
|
||||
### 3.3 频率与时间窗(决定了行数量级)
|
||||
|
||||
- TWS:每帧一次 `data_preprocess`;一圈 `B` 个波位(`MAX_BEAM_NUM=100`,`parameters.h:87/138`;实际圈长由宿主波位编排决定);每圈 **1 次 model=1**(关联/消亡/起始)+ **B−1 次 model=2**(滑窗凝聚)。
|
||||
- **TWS 的关联批是一整圈的累积**:`point_recv` 在整圈内只增不清(`dot_coh.cpp:229` → `track_asso.cpp:29-68` → `track_init.cpp:60/108`),model=1 时关联的是"上一圈全部凝聚点",因此 `N ≈ (B−1)×每帧凝聚点数`,而不是单帧的 ≤150 点。这是行数量级被低估的常见原因。
|
||||
- 注:`README.md:445` 的流程图把 TWS 凝聚画在 model=1 分支下,与代码不符;代码中凝聚只在 model=2 调用(`data_process.cpp:163-168`)。
|
||||
- TAS:每来一帧处理一帧,`point_recv_tas` 每帧清空,关联仅针对单一批号航迹(`track_asso_tas.cpp:263-283`),记录量比 TWS 小 1~2 个数量级。
|
||||
- 实时预算(本报告采用的参考值,需现场按实际 CPI 校准):帧周期 **10 ms**(100 Hz CPI)、圈周期 **1 s**(100 波位 × 10 ms)。记录功能新增的耗时直接叠加在这两个预算上。
|
||||
|
||||
---
|
||||
|
||||
## 4. 瓶颈一:每条记录一次同步事务(I/O 放大)
|
||||
|
||||
### 4.1 单行写入链路拆解(代码事实)
|
||||
|
||||
单条 `rdp_record::Write(const T&)`(`rdp_record.cpp:174-196`)每次调用都完整执行:
|
||||
|
||||
```
|
||||
状态检查 → std::lock_guard<mutex>(全模块一把锁)
|
||||
→ PrepareWrite<T>(静态 guard,建表只一次)
|
||||
→ ormpp::dbng::insert → sqlite.hpp:834-856 insert_or_update_impl:
|
||||
generate_insert_sql<T>() // 每次重新拼 SQL 字符串(utility.hpp:438-521)
|
||||
sqlite3_prepare_v2() // 每次重新解析/编译(sqlite.hpp:842)
|
||||
bind 逐列(22/22/19/44/18 次,id 自增列跳过) // sqlite.hpp:602-608, 645-696
|
||||
sqlite3_step() // ← 隐式事务提交发生在这里
|
||||
sqlite3_finalize() // 释放语句对象
|
||||
→ 失败时每个实体仅提示一次
|
||||
```
|
||||
|
||||
单条路径**没有任何 `BEGIN/COMMIT`**,因此每条 INSERT 处于 SQLite autocommit:**每条记录 = 一个独立隐式写事务**,在 Windows 上包含:
|
||||
|
||||
| 阶段 | 动作 |
|
||||
|---|---|
|
||||
| 加锁 | 获取 SHARED/PENDING/RESERVED/EXCLUSIVE 锁(`LockFileEx`) |
|
||||
| 建 journal | 创建 rollback journal 文件(`CreateFileW`)+ 写 journal 头 |
|
||||
| 写 journal | 修改页的改前镜像写入 journal |
|
||||
| 写主库 | 提交时脏页写回 db 文件(表页 + `page1` + `AUTOINCREMENT` 的 `sqlite_sequence` 页) |
|
||||
| 删 journal | 关闭并删除 journal 文件(`DeleteFileW`)+ 解锁 |
|
||||
|
||||
每次约 **10~15 个 Win32 文件/锁系统调用 + 20~40 KB 页写入**。`PRAGMA synchronous=OFF`(`rdp_record.cpp:147`)只去掉 `FlushFileBuffers`,**不减少 journal 文件的创建/写入/删除,也不减少锁**;在机械盘 + Defender 实时防护下,这条"新建文件→写入→关闭→删除"链被显著放大(SQLite 自身的 Windows VFS 都为杀软影响加了锁重试,`sqlite3.c:50289-50308`)。
|
||||
|
||||
### 4.2 实测:单行成本 ≈1.1~1.3 ms(mean)
|
||||
|
||||
微基准(Release `REC_ON` 库,本机环境,每实体按当前实现逐条写入;**3 次独立运行的中位数**,min~max 见 `bench_micro.md` §2):
|
||||
|
||||
| 实体(列数含 id) | 统计行数 | mean | p50 | p99 | 吞吐 | DB 体积/行 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `RdpRecPoint`(23) | 16,000 | **1265.5 µs** | 780.7 µs | 5643.2 µs | 790 行/s | 119.6 B |
|
||||
| `RdpRecPointCohesionTWS`(23) | 16,000 | 1249.9 µs | 770.7 µs | 5073.4 µs | 800 行/s | 119.2 B |
|
||||
| `RdpRecTempTrack`(20) | 16,000 | 1224.2 µs | 745.6 µs | 6027.5 µs | 817 行/s | 315.8 B |
|
||||
| `RdpRecTrustTrack`(45) | 4,000 | 1326.5 µs | 761.5 µs | 6271.0 µs | 754 行/s | 1369.7 B |
|
||||
| `RdpRecAssoInfo`(19) | 16,000 | 1228.7 µs | 734.8 µs | 5703.7 µs | 814 行/s | 96.5 B |
|
||||
|
||||
> 观察:**列数从 19 到 45、行大小从 96.5 B 到 1.37 KB,单条 p50 差异 <7%**——成本由"每条一个事务"决定,而不是 SQL/列数/序列化。mean 约为 p50 的 1.6 倍、p99 达 5~6.3 ms、单行最大尖峰 81~496 ms,说明文件系统/杀软/调度长尾直接落在同步调用栈上,这正是"偶发卡顿"的来源。
|
||||
|
||||
### 4.3 实测:批量事务的收益(框架已有能力,端到端 10~335×)
|
||||
|
||||
同一个 `rdp_record.lib`、同一数据库配置,改用 `Write(const std::vector<T>&)`(ormpp 内部一次 prepare + `BEGIN` + 逐行 bind/step/reset + `COMMIT`,`sqlite.hpp:859-902`;**若把批量实现成循环调单条 `Write` 则完全失去收益**):
|
||||
|
||||
| 实体(列数) | 批大小 | DB 侧每行成本 | 吞吐 | 相对单条 |
|
||||
|---|---|---|---|---|
|
||||
| `RdpRecPoint`(23) | 100 | 13.31 µs | 75,121 行/s | ~80~95× |
|
||||
| `RdpRecPoint` | 1,000 | **3.77 µs** | 265,230 行/s | **275~336×** |
|
||||
| `RdpRecPoint` | 10,000 | 2.95 µs | 339,508 行/s | 342~433× |
|
||||
| `RdpRecAssoInfo`(19) | 100 | 13.90 µs | 71,921 行/s | 35~105× |
|
||||
| `RdpRecAssoInfo` | 1,000 | **5.91 µs** | 169,183 行/s | **202~268×** |
|
||||
| `RdpRecAssoInfo` | 10,000 | 3.68 µs | 271,844 行/s | 254~410× |
|
||||
| `RdpRecTempTrack`(20,2 文本列) | 1,000 | 4.43 µs | 225,502 行/s | 208~287× |
|
||||
| `RdpRecTrustTrack`(45,8 文本列) | 1,000 | 8.77 µs | 114,067 行/s | 95~154× |
|
||||
|
||||
- **批 1,000 是甜点**(批 10,000 提升有限);批 100 时事务固定成本仍未摊薄。
|
||||
- **必须把"实体构造"也算进端到端**:单条 → 批 1000 的端到端加速比为 Point ≈335×、Asso ≈207×、TempTrack ≈52×、TrustTrack ≈10×(后两者构造占批量后成本 81%/94%,见第 6 节)——**对文本数组实体,光批处理不够**。
|
||||
|
||||
### 4.4 实测:SQLite 配置对比(journal/事务/语句复用/AUTOINCREMENT)
|
||||
|
||||
用 sqlite3 C API 在同一台机器上复现等价写入(45 列宽表 / 19/23 列窄表,每配置独立空库、20,000 行、多次运行中位数):
|
||||
|
||||
| 配置 | journal / sync | 批 | 语句复用 | 每行 mean | 吞吐 | 结论 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 当前实现等价(autocommit) | DELETE / OFF | 0 | 否 | **1120.1 µs** | 893 行/s | 基线:每行一个事务 |
|
||||
| autocommit + 语句复用 | DELETE / OFF | 0 | 是 | 949.6 µs | 1,053 行/s | **无改善**(噪声内)——瓶颈不在 prepare |
|
||||
| 批量 1,000 | DELETE / OFF | 1000 | 是 | **4.90 µs** | 203,991 行/s | 批处理 |
|
||||
| 批量 10,000 | DELETE / OFF | 10000 | 是 | 4.55 µs | 219,761 行/s | 收益趋平 |
|
||||
| 批量 1,000 + 每行重新 prepare | DELETE / OFF | 1000 | 否 | 13.46 µs | 74,270 行/s | 复用省 **7.2~10.6 µs/行** |
|
||||
| autocommit + WAL + NORMAL | WAL / NORMAL | 0 | 是 | 209.7 µs | 4,770 行/s | 不要用 NORMAL |
|
||||
| 批量 1,000 + WAL + NORMAL | WAL / NORMAL | 1000 | 是 | 53.68 µs | 18,630 行/s | 本机 checkpoint fsync 抖动:比 DELETE 批量差约 11×、比 WAL+OFF 批量差 8.6× |
|
||||
| autocommit + WAL + OFF | WAL / OFF | 0 | 是 | **17.21 µs** | 58,116 行/s | 单条改 WAL 约 55~65× |
|
||||
| 批量 1,000 + WAL + OFF | WAL / OFF | 1000 | 是 | 6.23 µs | 160,419 行/s | 与 DELETE 批量同量级 |
|
||||
| 批量 100 + DELETE + OFF | DELETE / OFF | 100 | 是 | 9.83 µs | 101,775 行/s | 每帧小批量(≤150 行)参考 |
|
||||
| 批量 100 + WAL + OFF | WAL / OFF | 100 | 是 | **5.53 µs** | 180,825 行/s | 小批量时 WAL 有 **1.8×** 价值 |
|
||||
| **journal 放内存** + autocommit | MEMORY / OFF | 0 | 是 | **38.72 µs** | 25,825 行/s | 无 journal 文件 → 比 DELETE 快 28× |
|
||||
| **journal 关闭** + autocommit | OFF / OFF | 0 | 是 | **36.45 µs** | 27,436 行/s | 同上(不推荐:崩溃可能损坏库) |
|
||||
| 批量 1,000 + AUTOINCREMENT | DELETE / OFF | 1000 | 是 | 4.56 µs | 219,195 行/s | 与 4.90 差异在噪声内(≤10%) |
|
||||
| 批量 1,000 + 窄表 19 列 / 23 列 | DELETE / OFF | 1000 | 是 | **1.55 / 1.73 µs** | 646,272 / 579,384 行/s | 列数/文本体积决定批量下限 |
|
||||
|
||||
**成本分解(三条独立证据)**:
|
||||
|
||||
1. **journal 文件 I/O ≈1.08 ms/行,占单条成本 96%~97%**:同一事务路径下,journal 从 DELETE 换成 MEMORY/OFF 后每行从 1120 µs 降到 ~37 µs,换 WAL+OFF 降到 17.2 µs ⇒ "journal 文件的创建/写入/删除"约 1.08 ms/行;
|
||||
2. **原生文件操作基准**:空文件 `CreateFile+CloseHandle+DeleteFile` ≈0.15~0.18 ms/次、写 4 KB 后删除 ≈0.29~0.31 ms/次;带 `FlushFileBuffers` 在机械盘上 ≈**30.9 ms/次**(这解释了为什么 `synchronous=NORMAL` 在机械盘上表现极差);
|
||||
3. **CPU 侧不是主因**:单条路径语句复用无改善;批量路径里"每行重 prepare"比复用多 7.2~10.6 µs/行,是批量模式下的次要优化项。
|
||||
|
||||
**结论**:
|
||||
- 单条路径的性能由**每行一个事务**决定,`synchronous=OFF`/语句复用都救不了;要降低单条成本只能换 journal 方式(WAL/MEMORY,均有限)或**批处理**;
|
||||
- **批处理是最有效手段**:DELETE+OFF+批 1000 达 4.90 µs/行(~229×);配合"每帧小批量"场景可再加 WAL(1.8×);
|
||||
- **不要用 `synchronous=NORMAL`**;调试用途可接受 `synchronous=OFF` 的崩溃丢尾数据,但 `journal=MEMORY/OFF` 崩溃可能损坏库,只可作为诊断对照,不建议生产采用;
|
||||
- `AUTOINCREMENT`(当前建表默认带 `PRIMARY KEY AUTOINCREMENT`,`sqlite.hpp:554`)在本机测量中差异 ≤10%(噪声内),语义上本项目不需要(每次运行新库、只增不删),去掉是"无风险的小优化"而非主要收益项;
|
||||
- 本机磁盘是 SATA 机械盘,部署环境应优先 SSD,并把记录目录加入 Defender 排除(可显著降低单条绝对成本,但不能替代程序改造)。
|
||||
|
||||
### 4.5 端到端 A/B 实测(`REC_OFF` vs `REC_ON` 双 DLL)
|
||||
|
||||
#### 4.5.1 首轮场景(输入点迹路径,未形成可靠航迹)
|
||||
|
||||
场景:相扫 TWS;每帧 150 点、100 波位/圈、6 圈(首圈预热);杂波为簇状(凝聚后约 7 点/帧);目标上报速度与几何速度不匹配,本轮未起批(`trust_max=0`)——**这一轮隔离出了"输入点迹记录(150 行/帧)"单项开销**。原始数据:`build/perf_bench_pipeline/run_pilot_off|run_pilot_on/`(`frames.csv`/`circles.csv`/`summary.txt`/记录库)与汇总 `pilot_result.json`,可用 `rerun_pilot.ps1` 复跑(`--vr 60`)。
|
||||
|
||||
| 指标(统计第 1~5 圈,普通帧 n=495) | `REC_OFF` | `REC_ON` | 增量 | 倍数 |
|
||||
|---|---|---|---|---|
|
||||
| `data_preprocess` 每帧 mean | 0.0479 ms | **143.77 ms** | +143.72 ms | ~3000× |
|
||||
| `data_preprocess` 每帧 p99 | 0.1297 ms | 462.08 ms | — | — |
|
||||
| `track_process` 普通帧 mean | 0.1600 ms | 6.38 ms | +6.22 ms | ~40× |
|
||||
| 每帧合计 mean | 0.2080 ms | **150.14 ms** | +149.93 ms | ~722× |
|
||||
| 每帧合计 p99 | 0.4631 ms | 467.16 ms | — | — |
|
||||
| 每圈合计 mean(100 帧) | 139.91 ms | **16,765.4 ms(16.8 s)** | +16.6 s | ~120× |
|
||||
| 记录/圈 | 0 | 约 17,229 行 | — | — |
|
||||
|
||||
- 记录库:13,881,344 B;全运行(600 帧)行数:`RdpRecPoint` 90,000 + `RdpRecPointCohesionTWS` 4,169 + `RdpRecTempTrack` 9,205 = **103,374 行**(不含 `sqlite_sequence` 系统表)。全运行 ON/OFF 增量 = 98.30 s − 0.74 s ≈ **97.56 s**(含预热圈与判圈帧)→ **≈0.94 ms/行**;统计范围(第 1~5 圈,剔除预热圈)的增量为 83.1 s,与全运行口径一致;
|
||||
- 每行成本随环境波动:本轮(机器空闲)≈0.94 ms/行;此前同场景在微基准并行压测时约 1.0 ms/行;微基准单条 mean 1.1~1.3 ms/行——量级一致,**结论不受影响**;
|
||||
- **仅"每帧 150 行输入点迹"就产生约 144 ms/帧的额外耗时**(`data_preprocess` 增量 143.72 ms),单帧即超 10 ms 预算 14 倍;`track_process` 普通帧的 6.38 ms 来自每帧约 7 行凝聚点记录,按同一量级线性增长。
|
||||
|
||||
#### 4.5.2 含航迹场景(关联候选路径真实覆盖,最终 A/B 结果)
|
||||
|
||||
为覆盖 ⑥ 关联信息这条最大的路径,A/B 驱动修正了合成目标的速度一致性(几何径向速度 = 点迹上报径向速度,否则起批马氏距离被多普勒残差主导而无法起批),最终在 6 圈内形成 **186 条可靠航迹**,两配置的 `trust_num` 序列完全一致(124 → 180 → 186),**A/B 对照有效**。
|
||||
|
||||
**每帧耗时**(第 1~5 圈、普通帧 n=495,model==2):
|
||||
|
||||
| 指标 | `REC_OFF` mean | `REC_ON` mean | 增量 | 倍数 |
|
||||
|---|---|---|---|---|
|
||||
| `data_preprocess` | 0.0685 ms | **208.26 ms** | +208.19 ms | **3042×** |
|
||||
| `track_process` | 0.2004 ms | 9.77 ms | +9.57 ms | 48.8× |
|
||||
| 每帧合计 | 0.2689 ms | **218.03 ms** | +217.76 ms | **811×** |
|
||||
|
||||
- `REC_ON` 普通帧分布:**495/495 帧 > 10 ms**,489/495 帧 > 100 ms;最"好"的一帧 `data_preprocess` 仍要 89.5 ms(9× 帧预算)。
|
||||
- `data_preprocess` 的 208 ms ≈ 150 行输入点迹 × **1.39 ms/行**,占普通帧 95.5%。
|
||||
|
||||
**判圈帧(model=1,每圈 1 帧,含关联/起始/消亡 + 记录)**:
|
||||
|
||||
| 指标 | `REC_OFF` | `REC_ON` |
|
||||
|---|---|---|
|
||||
| `track_process` mean | 318.07 ms | **59,218.01 ms(59.2 s)** |
|
||||
| `track_process` p50 | 164.90 ms | 7,169.11 ms |
|
||||
| `track_process` max | 751.04 ms | **170,269.92 ms(170.3 s,227×)** |
|
||||
|
||||
- **判圈帧尖峰的来源随圈变化**:第 4/5 圈(180/186 条航迹)的 **115.1 s / 170.3 s** 主要来自关联候选记录(`RdpRecAssoInfo` 逐圈 87,088 / 122,760 行);第 1~3 圈(0/0/124 条航迹)分别为 0.84 / 7.17 / 2.72 s,其中第 2 圈的 7.17 s **与关联无关**(该圈关联记录为 0 行),主要来自 ④ 临时航迹记录(约 2×10³~2.5×10³ 行,按 1.4 ms/行 约 3~3.5 s)叠加文件系统/杀软抖动。`REC_OFF` 同帧耗时分别为 2.4 / 50.1 / 164.9 / 621.9 / 751.0 ms(最大 751.0 ms,第 5 圈 186 航迹的算法计算)。
|
||||
- **关联候选记录占判圈帧耗时约 99%**(第 4/5 圈 `RdpRecAssoInfo` 分别 87,088 / 122,760 行/圈;OFF 同帧算法本身 0.75 s)。
|
||||
|
||||
**每圈总耗时(100 帧合计)**:`REC_OFF` 均值 **344.79 ms** → `REC_ON` 均值 **81,050.64 ms(81.05 s)**,**235.1×**;未形成航迹的稳态圈 24.0~26.9 s/圈(超 1 s 圈预算 24~27×),形成航迹后 136.7 / 191.6 s/圈(137~192×)。整个 600 帧运行:`REC_OFF` 2.3 s / `REC_ON` 426.7 s(**185×**)。
|
||||
|
||||
**记录量**(`run_on/rdp_records/*.db`,35.32 MB):
|
||||
|
||||
| 记录表 | 行数 | 说明 |
|
||||
|---|---|---|
|
||||
| `RdpRecPoint` | 90,000 | 150 行/帧 × 600 帧 |
|
||||
| `RdpRecPointCohesionTWS` | 4,170 | 约 7 行/帧 |
|
||||
| `RdpRecAssoInfo` | **211,584** | 逐圈 1,736 / 87,088 / 122,760(随航迹数 124→180→186) |
|
||||
| `RdpRecTempTrack` | 8,330 | 约 2,000~2,500 行/圈 |
|
||||
| `RdpRecTrustTrack` | 304 | 可忽略 |
|
||||
| **合计** | **314,388** | 平均 112.3 B/行;314,388 行 / 426.7 s ≈ **737 行/s(总量口径 1.36 ms/行)** |
|
||||
|
||||
> 三条独立口径互相印证"成本与行数严格线性":**总量口径 1.36 ms/行**(314,388 行 / 426.7 s)、**输入点迹口径 1.39 ms/行**(`data_preprocess` 208.26 ms / 150 行)、**凝聚点口径 1.40 ms/行**(`track_process` 9.77 ms / ~7 行)。A/B 的每行成本比微基准新库/单表的 1.1~1.3 ms/行略高,原因是 A/B 库增长到 35 MB、5 张表、页缓存压力更大且伴随 Defender 扫描——同一数量级,结论一致。
|
||||
|
||||
> 说明:该合成场景(200 目标、150 点/帧、凝聚后约 7 点/帧)的关联量级是**保守下限**(真实 150 个独立点迹时按 1.39 ms/行外推约 270 万行/圈、约 1 小时/圈);A/B 的绝对毫秒数不能直接当作现场实时预算,**可迁移的是 ON/OFF 比值、每行成本、以及 `M×N` 行数规律**。
|
||||
|
||||
### 4.6 为什么 Release 优化无效、为什么 OFF 就没有开销
|
||||
|
||||
1. **成本在 I/O 与事务,不在 CPU 计算**:`/O2` 优化的是算法指令,无法消除每条记录的文件创建/删除、锁、页写入;§4.4 中"语句复用对单条无效"也证明了这一点。
|
||||
2. **记录点代码在 ON 构建中是实打实编译进去的**:`RDP_RECORD_ENABLE` 只控制"是否参与编译/是否链接",一旦定义,每条记录同步执行;`NDEBUG` 不会移除它们。
|
||||
3. **Release 处理本身更快,使固定 I/O 开销占比更大**:OFF 时处理只需零点几毫秒/帧,ON 时每条记录 1 ms,相对放大即为数百倍(与 §4.5 实测一致)。
|
||||
4. **OFF 的零开销来自编译期移除**(`CMakeLists.txt:118-158`、`rdp_record.h:42-45`):不编译、不链接、宏为空——这是"关闭即无性能损失"的正确实现方式,但不能替代"Release 排障时开启记录也要实时"的需求;后者必须靠**运行期架构改造**解决。
|
||||
|
||||
---
|
||||
|
||||
## 5. 瓶颈二:记录行数与负载成 `M×N` 关系(比逐条事务更致命)
|
||||
|
||||
### 5.1 行数模型
|
||||
|
||||
符号:`B`=每圈波位数(本项目参数 100,实际由宿主决定);`p`=每帧输入点数(≤150);`η`=凝聚存活率(输出/输入,未实测,场景表取 0.9);`N`=一次关联批凝聚点数(≈(B−1)·p·η,因为 `point_recv` 整圈累积);`M`=可靠航迹数(≤500);`A`=被关联消费的点数。
|
||||
|
||||
| 记录点 | 每 TWS 圈行数 | 上限 | 备注 |
|
||||
|---|---|---|---|
|
||||
| ① 输入点迹 | `B·p` | 15,000 | 每帧逐点 |
|
||||
| ② 凝聚点迹 | ≈`N` | 14,850 | 每圈 99 次之和 = 下一圈关联批点数 |
|
||||
| ④ 临时航迹 | `ΣL ≈ (1~3)(N−A)` | 病态 ~4.5×10⁵ | **全量快照**:一条 10 点临时航迹累计被写 1+2+…+10=55 行 |
|
||||
| ⑤ 可靠航迹 | ≤`M` | 500 | 含外推成功航迹 |
|
||||
| ⑥ 关联信息 | **`M×N`(精确等于候选对数)** | 500×14,850 = **7.425×10⁶** | 失败候选 `:554` + 被消费候选 `:710/:722`,当前最近邻实现下恰好每对候选一行 |
|
||||
|
||||
### 5.2 典型场景每圈行数(B=100,η=0.9,A≈M)
|
||||
|
||||
| 记录点 | 稀疏(p=10,M=20) | 中等(p=50,M=100) | 稠密(p=150,M=300~500) |
|
||||
|---|---|---|---|
|
||||
| `N`(关联批点数) | ≈891 | ≈4,455 | ≈13,365(上限 14,850) |
|
||||
| ① 输入点迹 | 1,000 | 5,000 | 15,000 |
|
||||
| ② 凝聚点迹 | 891 | 4,455 | 13,365 |
|
||||
| ④ 临时航迹 | 0.9~2.6×10³ | 4.4~13×10³ | 13~39×10³ |
|
||||
| ⑤ 可靠航迹 | ≤20 | ≤100 | 300~500 |
|
||||
| ⑥ 关联信息 | 17,820 | 445,500 | **5,346,000~6,682,500** |
|
||||
| **合计** | **≈2.1×10⁴ 行/圈** | **≈4.6×10⁵ 行/圈** | **≈5.4×10⁶~6.7×10⁶ 行/圈** |
|
||||
| ⑥ 占比 | ≈86% | ≈97% | ≈99% |
|
||||
|
||||
### 5.3 按实测单行成本折算的每圈耗时
|
||||
|
||||
以本机 **~1.2~1.3 ms/行(微基准 3 次运行 mean 中位,§4.2)** 折算;若按 A/B 总量口径(1.36~1.40 ms/行,§4.5.2)则下表数值再高约 10%~15%,结论数量级不变:
|
||||
|
||||
| 场景 | ⑥ 关联信息 | 全部记录(合计) | 与 1 s 圈周期相比 |
|
||||
|---|---|---|---|
|
||||
| 稀疏(2.1×10⁴ 行) | 21~23 s | **≈25~27 s/圈** | 25~27× |
|
||||
| 中等(4.6×10⁵ 行) | 535~579 s | **≈9.2~10.0 min/圈** | 550~600× |
|
||||
| 稠密(5.4×10⁶~6.7×10⁶ 行) | 107~145 min | **≈1.8~2.4 h/圈** | 6400~8700× |
|
||||
|
||||
- **实测对照**:首轮 A/B(未起批)16.8 s/圈 对应约 1.72×10⁴ 行/圈(只有点迹/凝聚/临时航迹),落在上表稀疏量级;含航迹 A/B 的第 5 圈(186 航迹)约 14.0×10⁴ 行/圈(⑥ 122,760 行)+ 191.6 s → **1.37 ms/行**,行数与耗时的线性关系、以及 `M×N` 随航迹数增长(1,736 → 87,088 → 122,760 行/圈)均与模型一致。
|
||||
- **关键推论:即使把写库优化到零成本,稠密场景 0.5 GB/圈的数据量也无法以 1 Hz 落盘**(需持续 ~500 MB/s 写带宽,超过本机 SATA 机械盘甚至 SATA SSD 的持续写能力)。所以 **⑥ 必须裁剪,不能只做性能调优**。
|
||||
|
||||
### 5.4 数据量估算
|
||||
|
||||
| 表 | 实测每行 DB 体积 | 说明 |
|
||||
|---|---|---|
|
||||
| `RdpRecPoint` | 119.6 B | 22 列标量 |
|
||||
| `RdpRecAssoInfo` | 96.5~100.8 B | 18 列标量 |
|
||||
| `RdpRecTempTrack` | 315.8 B | X/P 文本列(实测平均文本 196.7 B) |
|
||||
| `RdpRecTrustTrack` | **1,369.7 B** | 8 个数组文本列(实测平均文本 **1,145.1 B**) |
|
||||
|
||||
- 稠密场景 ⑥ 5.35×10⁶ 行 × ~100 B ≈ **535 MB/圈**;④ 3.9×10⁴ 行 × 316 B ≈ 12 MB/圈;⑤ 500 × 1.37 KB ≈ 0.7 MB/圈。
|
||||
- 记录库无轮转/无上限(每次运行一个新文件),长时间运行会到 GB~TB 级。
|
||||
|
||||
### 5.5 TAS 通道
|
||||
|
||||
TAS 每帧执行 ①③⑤⑥,但 `point_recv_tas` 每帧清空、关联只针对 1 条航迹(`track_asso_tas.cpp:263-283`),每 TAS 帧记录 **≤451 行**(①≤150 + ③≤150 + ⑤≤1 + ⑥≤150,见 §3.2);即使相扫每 TWS 圈有 4 个 TAS 帧,也只约 1.8×10³ 行/圈,是稠密 TWS ⑥ 的约 1/3000。**TAS 通道批处理后即可接受,不是本次瓶颈重点。**
|
||||
|
||||
---
|
||||
|
||||
## 6. 瓶颈三:处理线程上的实体构造(数组转文本)
|
||||
|
||||
### 6.1 实测构造成本
|
||||
|
||||
`RDP_RECORD(...)` 的实参在**调用方线程、进入 `Write` 之前**求值(`rdp_record.h:39`);`RdpRecTrustTrack`/`RdpRecTempTrack` 的构造函数要把 `X/P/...` 数组经 `arrayToString()` 转成文本,而 `arrayToString`(`rdp_record_entity.h:466-483`)对**每个元素调用 `elementToString` 构造一个 `std::ostringstream`**(`:450-453`)。
|
||||
|
||||
| 构造项 | 实测(3 次运行中位数) |
|
||||
|---|---|
|
||||
| `RdpRecTrustTrack` 构造(8 个数组共 135 个 double) | **133.56 µs/行** |
|
||||
| `RdpRecTempTrack` 构造(X[4]+P[4][4] 共 20 个 double) | **19.44 µs/行** |
|
||||
| `RdpRecPoint` 构造(22 标量) | 0.010 µs/行 |
|
||||
| `RdpRecAssoInfo` 构造(18 标量) | 0.020 µs/行 |
|
||||
| `arrayToString(double[6])` | 5.85 µs |
|
||||
| `arrayToString(P[6][6])` | 35.73 µs(36 次转换,最贵) |
|
||||
| `std::ostringstream` 格式化一个 double | 0.97 µs(其中 ostringstream 设施底价 ≈0.24 µs) |
|
||||
| `std::to_string(double)` 格式化一个 double | 0.52 µs(约为前者 54%) |
|
||||
| 手工 join `to_string(double[6])` | 3.17 µs(比 arrayToString 快 1.8×) |
|
||||
|
||||
其中 `RdpRecTrustTrack` 的 8 个数组:X 5.68 µs、P 35.73 µs、X1 5.93 µs、X2 5.94 µs、X3 5.81 µs、P2 36.02 µs、P3 34.36 µs、u 2.62 µs(P/P2/P3 三个 6×6 矩阵合计 ≈106 µs,占构造的 ~80%)。
|
||||
|
||||
**批量后构造成为主要成本**:端到端(构造 + 批 1000 写库)每行成本 TrustTrack ≈142.3 µs(构造占 94%)、TempTrack ≈23.9 µs(构造占 81%)、Point/Asso ≈3.8~5.9 µs(构造占 0.3%)。**这就是"为什么信任航迹/临时航迹不能只靠批处理"的实测依据**。
|
||||
|
||||
### 6.2 折算到每圈
|
||||
|
||||
| 表 | 行数(中等/稠密) | 构造耗时(中等/稠密) |
|
||||
|---|---|---|
|
||||
| ④ 临时航迹 | 4.4~13×10³ / 13~39×10³ | **86~253 ms / 253~758 ms 每圈** |
|
||||
| ⑤ 可靠航迹 | 100 / 500 | **13.4 / 66.8 ms 每圈** |
|
||||
|
||||
- 注意:④⑤ 都发生在 **model=1 的同一帧**里,因此都会形成"单帧尖峰"。
|
||||
- **批处理省不掉这部分**:批处理只把"每行一个事务"变成"每批一个事务",实体构造仍逐行在雷达线程完成(`hotpath_findings.md` §4.2)。要消掉它只能:
|
||||
- 把构造/序列化搬到后台写线程(处理线程只拷贝源结构体快照);或
|
||||
- 改列设计(数组存 blob、或元素展开表),不生成文本;或
|
||||
- 减少记录行数(只记变化/末点/采样)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 次要问题与放大因素
|
||||
|
||||
### 7.1 线程模型与互斥锁
|
||||
|
||||
- 接口线程模型未定义(`README.md:1020`,BUG-03:宿主应单线程调用),记录模块用**单连接 + 一把全局 `std::mutex`**(`rdp_record.cpp:41,183`)。单线程宿主的直接后果是:**磁盘事务的等待时间 100% 叠加到雷达处理线程**。
|
||||
- 若宿主实际多线程调用(如收数据线程与处理线程并发),锁竞争会让记录开销进一步放大并造成优先级反转;`SQLITE_THREADSAFE=1` 还会在 SQLite 内部再加一层互斥。这一风险需现场确认宿主调用方式。
|
||||
|
||||
### 7.2 `AUTOINCREMENT`(语义上可去掉,收益不大)
|
||||
|
||||
- ormpp 的建表 SQL 对注册的自增主键**无条件追加 `PRIMARY KEY AUTOINCREMENT`**(`sqlite.hpp:552-560`,第 554 行),因此每行 INSERT 额外读+写 `sqlite_sequence` 表(多一个脏页 + 一条 journal 记录)。
|
||||
- 本项目每次运行新建 db、只增不删,`INTEGER PRIMARY KEY` 的 rowid 本身就是单调的,**不需要 AUTOINCREMENT**。
|
||||
- 实测:批量 1000 时带/不带 AUTOINCREMENT 的差异在测量噪声内(4.90 vs 4.56 µs/行,方向随运行翻转,≤10%),**属"无风险的小优化",不是主要收益项**;单条路径下该额外页会被 1 ms 级事务成本掩盖。
|
||||
- 由于 ormpp 的 DDL 硬编码,去掉它需要记录模块自行预建表(`CREATE TABLE IF NOT EXISTS` 变 no-op,但 auto-key 映射仍会登记、insert 仍跳过 id 列)。
|
||||
|
||||
### 7.3 `journal_mode` 与 `synchronous=OFF` 的常见误解
|
||||
|
||||
- 当前只设置了 `synchronous=OFF`,未设 `journal_mode` → 默认 `DELETE`(提交时删除 journal 文件)。**这是单条路径最大的成本来源(实测占 96%~97%)**。
|
||||
- `synchronous=OFF` 只省 `FlushFileBuffers`,**不省 journal 创建/写入/删除和锁**——所以"已经关了同步,为什么还慢"的疑问不成立:慢的是事务文件操作,不是 fsync。
|
||||
- 设置 `PRAGMA journal_mode=WAL` 不能走 ormpp 的 `execute()`(它要求 `sqlite3_step` 返回 `SQLITE_DONE`,而该 PRAGMA 会返回一行,见 `sqlite.hpp:434-451`);可用同连接的 `query_s<std::tuple<int>>` 消费返回行,或 connect 前用 C API 设置。注意 WAL 默认 sync 仍是 FULL 语义,要同时设 `synchronous=NORMAL/OFF`;且本机机械盘上 `WAL+NORMAL` 因 fsync(≈31 ms/次)出现 checkpoint 抖动(§4.4)。
|
||||
- 若引入批处理,`DELETE + synchronous=OFF` 已可达到 3.8~8.8 µs/行(批 1000),**WAL 不再是必需项**;只对"每帧 ≤150 行的小批量"再叠加 WAL 可再快 1.8×(§4.4 S11/S12)。
|
||||
|
||||
### 7.4 缓存、busy_timeout 与失败降级
|
||||
|
||||
- `cache_size` 默认仅 2 MB(`SQLITE_DEFAULT_CACHE_SIZE=-2000`);库变大、B-tree 变深后每行触页更多,可设 `PRAGMA cache_size=-8192`(8 MB)等(不返回行,可走 `execute()`)。
|
||||
- `busy_timeout` 默认 0,Windows 上杀软/文件系统瞬时锁冲突会直接失败;当前无重试。
|
||||
- **失败熔断缺失**:`rdp_record.cpp:189-195` 只在首次失败提示一次,但之后**每条记录仍然完整走"拼 SQL → prepare → step 失败"流程**;若库/磁盘异常,会持续消耗处理线程。建议失败后按实体或全局熔断。
|
||||
|
||||
### 7.5 环境放大因素(重要且常被忽略)
|
||||
|
||||
- 本机 `rdp_records` 位于 **SATA 机械盘**,且 **Defender 实时防护开启**;"每条记录新建+删除一个 journal 文件"的链条被文件系统元数据(NTFS `$LogFile`)与杀软扫描显著放大——1 ms 级单行成本中有很大比例来自环境而非 SQLite 本身(原生文件 create+write4K+close+delete 就需 0.29~0.31 ms/次)。
|
||||
- 换到 SSD + 对记录目录加 Defender 排除/其他白名单,单行成本会明显下降;但**不能替代程序侧的批处理/裁剪/异步改造**(按 §5,即使降到 50 µs/行也仍不满足高行数场景)。
|
||||
- 网络盘/共享盘上 SQLite 的 journal/WAL 行为更差(WAL 甚至可能不支持),记录目录应强制本地盘。
|
||||
|
||||
### 7.6 数据库增长与运维
|
||||
|
||||
- 每次运行一个新 db、无大小上限、无轮转、无定期清理;稠密场景 0.5 GB/圈,长时间开启会耗尽磁盘。
|
||||
- 无 `VACUUM`(只追加场景一般不需要),但长时间运行后文件碎片/自由页问题需关注;调试库可接受。
|
||||
|
||||
---
|
||||
|
||||
## 8. 优化措施
|
||||
|
||||
### 8.1 措施总览
|
||||
|
||||
| # | 措施 | 针对瓶颈 | 实测/预期收益 | 风险/代价 | 改动面 |
|
||||
|---|---|---|---|---|---|
|
||||
| **P0-a** | **关联信息裁剪/采样**(⑥ 只记被取中候选 + 近失候选采样/白名单) | 数据量 `M×N` | 行数降 1~3 个数量级(稠密 5.4×10⁶ → 10³~10⁴ 行/圈) | 分析信息减少(可配置开关保留全量模式) | 业务记录点(`#ifdef` 内) |
|
||||
| **P0-b** | **批量事务**(①~⑤、TAS ⑥ 帧末/圈末收集后 `RDP_RECORD_BATCH`) | 逐条事务 | DB 侧单行 **~1.1 ms → 3.8~8.8 µs(约 95~336×,批 1000,按实体)**;端到端(含构造)Point/Asso ≈207~335×、TempTrack ≈52×、TrustTrack ≈10×(实测) | 崩溃丢尾批、内存缓存 | 业务记录点 + 少量成员变量 |
|
||||
| **P0-c** | **异步写线程 + 有界队列**(处理线程只入队 POD 快照,后台序列化+批量写) | 实时性保证(隔离磁盘抖动) | 处理线程开销降到"拷贝+入队"(~0.1~0.3 µs/行),磁盘抖动不再传导 | 实现复杂度、退出 flush、溢出丢弃策略 | 记录模块框架 + 记录点写法 |
|
||||
| **P1-a** | **构造成本迁移/削减**(信任航迹数组列改 blob;或后台线程做 `arrayToString`;`to_string`/`to_chars` 替代 ostringstream) | 实体构造 133.6 µs/行 | 处理线程 ④⑤ 构造开销可降为 ~0(后台)或 ~1.8×(to_string);批量后构造占 TrustTrack 端到端 **94%** | 查询需解析 blob;改动实体/查询工具 | 记录模块 + 实体定义 |
|
||||
| **P1-b** | **临时航迹只记增量/末点**(不再每圈全量快照) | ④ 重复行 | 行数降 10~55× | 丢失历史轨迹(可按圈采样保留) | 业务记录点 |
|
||||
| **P1-c** | **SQLite 配置**:去 AUTOINCREMENT、`cache_size`、`synchronous`/WAL 按环境、`busy_timeout`、失败熔断 | 常数项 | 每帧小批量再叠加 WAL 约 1.8×;单条 WAL 55~65×(仅兜底);AUTOINCREMENT ≤10%(噪声内) | 崩溃一致性取舍 | 记录模块 |
|
||||
| **P2-a** | **记录策略**:按圈采样(每 N 圈记 1 圈)、按表/字段白名单、运行期开关 | 数据量 | 任意比例降载 | 分析样本变少 | 记录模块 + 可选接口 |
|
||||
| **P2-b** | **落盘/轮转/运维**:按圈或大小轮转、上限、Defender 排除、本地 SSD | 环境/磁盘 | 环境相关,幅度大 | 部署约束 | 部署 |
|
||||
| **P2-c** | **备选:二进制原始日志 + 离线入库** | 全部 | 运行时近零成本(顺序写文件),离线转 SQLite | 需要额外工具链/查询步骤 | 记录模块 + 工具 |
|
||||
|
||||
### 8.2 P0-a:关联信息裁剪(决定性,必须做)
|
||||
|
||||
当前 ⑥ 在双层循环(`track_asso.cpp:441-445`)里逐候选写库:未过门限当场写(`:554`),过门限候选进 `associated_info`,最近邻消费时再写(`:710/:722`)——**每对候选恰好 1 行,总数 = M×N**。建议改为"收集 + 裁剪 + 分块批写":
|
||||
|
||||
1. **只记录被最近邻取中的候选**(`min_d_flag=true`,每圈约 `min(M,N)` 行)——这正好是复现关联决策所需的最小集合;
|
||||
2. **近失候选采样**:`d_min < K×门限` 的候选按 1/N 采样(N 可配置,默认如 100),或每个点迹只保留 `d_min` 最小的前 K 个候选;
|
||||
3. **白名单**:只对指定 `circle_id`/`track_index`/`point_id` 区间记录全量候选(复现特定问题时开启);
|
||||
4. **运行期开关**:`RDP_RECORD_ASSO_FULL=0/1`(环境变量或新增接口),默认关闭全量失败候选;
|
||||
5. 收集到 `std::vector<RdpRecAssoInfo>` 后,**每 4,096 行或圈末 flush 一次**(限制内存尖峰)。
|
||||
|
||||
预期:⑥ 从 `M×N` 降到 `O(N + M)`(稀疏 ~900 行、中等 ~4.5×10³ 行、稠密 ~1.3×10⁴ 行/圈),⑥ 单表写库降到 **约 5~80 ms**(按批 1000 实测 3.8~5.9 µs/行),数据量从 535 MB/圈降到 ~1.3 MB/圈;但**整圈(①~⑥ 合计)仍有约 0.2~0.7 s**(稠密场景约 5.5~8.1 万行),需继续用异步写线程/按圈采样消化。
|
||||
|
||||
### 8.3 P0-b:批量事务(改动最小、收益最大)
|
||||
|
||||
框架**已具备**批量能力:`RDP_RECORD_BATCH(recs)`(`rdp_record.h:41`)→ `Write(const std::vector<T>&)`(`rdp_record.cpp:198-225`)→ ormpp 批量插入(一次 prepare + 一个 `BEGIN/COMMIT`,`sqlite.hpp:859-902`)。**当前业务侧 0 个调用点**,只需按"天然批次边界"改造记录点:
|
||||
|
||||
| 记录点 | 建议批次边界 | 做法(示意) |
|
||||
|---|---|---|
|
||||
| ① 输入点迹 | 每帧 `data_preprocess` 循环结束 | `std::vector<RdpRecPoint> recs;` 循环内 `emplace_back(...)`(`#ifdef RDP_RECORD_ENABLE` 包裹),循环后 `RDP_RECORD_BATCH(recs);` |
|
||||
| ② TWS 凝聚点 | 每次 `dot_coh_process_buff` 输出循环末(或跨帧累积到圈末) | 同上,一行/点 |
|
||||
| ③ TAS 凝聚点 | `dot_coh_tas_process` 输出循环末 | 同上 |
|
||||
| ④ 临时航迹 | `track_init.cpp:110-117` 双层遍历收集后一次批写 | 先按 P1-b 裁剪,再批写 |
|
||||
| ⑤ 可靠航迹 | `track_asso.cpp:71-165` / `track_asso_tas.cpp:44-125` 输出循环收集后批写 | 行数 ≤500,批写 + P1-a |
|
||||
| ⑥ TWS 关联信息 | `model_filter` 内先收集,圈末/每 4096 行 flush | 必须先做 P0-a 裁剪 |
|
||||
| ⑥ TAS 关联信息 | `model_filter` 点循环结束(`:365` 后)/候选清理(`:497-500`)| 失败候选也先收集,帧末一批写 |
|
||||
|
||||
注意:
|
||||
- 关掉记录功能时这些代码必须整体在 `#ifdef RDP_RECORD_ENABLE` 内(保证交付构建零代码、零告警);
|
||||
- 批写时**内存占用要有上限**(⑥ 尤其);批过大时失败会丢整批,可接受(调试用途)但需记录丢批计数;
|
||||
- 批处理**不改变**记录点的语义(先记录再消费/后记录),只是把"逐行提交"变成"一批提交"。
|
||||
|
||||
### 8.4 P0-c:异步写线程(实时性的最终保证)
|
||||
|
||||
```
|
||||
雷达处理线程 记录写线程(独立)
|
||||
──────────── ─────────────────
|
||||
RDP_RECORD(...) 循环:
|
||||
├─ 构造紧凑 POD 快照(或直接拷贝源结构体) ├─ 从队列取出一批(如 ≤2000 行 / 20 ms)
|
||||
├─ 有界队列 push(无锁 SPSC 或细粒度锁) ├─ 序列化(原 ostringstream 逻辑移到这里)
|
||||
└─ 返回(~0.1~0.3 µs/行) ├─ 一个事务批量 insert(现有 Write(vector) 路径)
|
||||
└─ 失败熔断/丢批计数
|
||||
队列满策略:丢弃(丢新或丢旧)+ 原子计数;Shutdown 时停止接收并排空(有超时)
|
||||
```
|
||||
|
||||
- **收益**:磁盘抖动/Defender/事务尾延迟与雷达线程彻底解耦;处理线程只有"拷贝+入队"的线性小开销,**可给出确定的实时上界**;
|
||||
- **前提**:⑥ 必须先裁剪(否则 10⁶ 行/圈的入队本身也会造成尖峰与内存爆炸);
|
||||
- **实现要点**:
|
||||
- 队列容量与高水位监控(如 64K~256K 行/16~64 MB,按行类型分队列);
|
||||
- 丢弃策略与计数(`dropped_rows`),运行结束打印,保证"丢数据可解释";
|
||||
- `Destroy()`/`Shutdown()` 时先 flush 再关库(`rdp_record.cpp:154-166` 需配套改造);
|
||||
- 记录实体构造(数组转文本)**移到写线程**,用源结构体快照(`PointRecv`/`Trust_Track`/`Asso_Info`/`Temp_track` 拷贝)作为队列元素;注意 `Trust_Track` 含 `std::vector` 成员(`Hight_smooth`),拷贝成本略高,可只拷贝需要的字段。
|
||||
- **建议只对"行数随负载线性增长"的 ①~⑤ 走异步;⑥ 裁剪后也可走异步**。
|
||||
|
||||
### 8.5 P1-a:构造成本的处理
|
||||
|
||||
- **方案 A(推荐)**:把 `arrayToString` 全部移到异步写线程(处理线程只入队源结构体),处理线程构造成本 → 0;写线程每行 133.6 µs(TrustTrack)可被后台线程吸收(500 行/圈 ≈ 67 ms,远小于 1 s 圈预算)。
|
||||
- **方案 B**:改实体列设计——`X/P/...` 存 **blob**(如 `std::vector<double>`/定长二进制),避免文本化;查询时用 Python/工具解析。单行 1.37 KB → ~0.3 KB,同时省掉 ostringstream 与 SQLite 文本绑定。
|
||||
- **方案 C(最小改动)**:`arrayToString` 用 `std::to_string`/`std::to_chars` + 预留 `reserve` 手工拼接替代 `ostringstream`(实测 1.8×:0.52 vs 0.97 µs/double),仍保留文本列。
|
||||
- 附加:只记录真正需要的列(当前 ⑤ 未记 `P1`、`Quality` 等,勿盲目补列);④ 只记增量/末点/采样。
|
||||
|
||||
### 8.6 P1-b:SQLite 配置与健壮性
|
||||
|
||||
| 项 | 建议 | 依据/注意 |
|
||||
|---|---|---|
|
||||
| 批事务 | 必做(P0-b) | 实测 DB 侧约 95~336×(批 1000,按实体) |
|
||||
| 语句复用 | 批处理路径已由 ormpp 复用;若自建写入层则按实体缓存 `sqlite3_stmt` | 批量内每行重 prepare 多花 **7.2~10.6 µs/行**(13.46→4.90) |
|
||||
| `AUTOINCREMENT` | 预建表去掉(改普通 `INTEGER PRIMARY KEY`) | 差异在噪声内(≤10%),语义上无必要;每行少一次 `sqlite_sequence` 读写 |
|
||||
| `journal_mode` | 有批处理时 `DELETE + synchronous=OFF` 已足够;每帧小批量(≤150 行)可加 `WAL + synchronous=OFF` 再快约 1.8×;单条兜底用 WAL(55~65×) | 设置方式见 §7.3;WAL 需本地盘;注意 `-wal/-shm` 与 checkpoint;**不要 `synchronous=NORMAL`** |
|
||||
| `cache_size` | 视库大小设 8~32 MB(不返回行,可 `execute()`) | 减少 B-tree 页进出 |
|
||||
| `busy_timeout` | 设 1~5 s + 有限重试 | 默认 0 会直接失败 |
|
||||
| 失败熔断 | 插入失败后按实体/全局熔断,避免每条都走失败路径 | 当前 `rdp_record.cpp:189-195` 无熔断 |
|
||||
| 同步策略 | 调试库接受 `synchronous=OFF` 的崩溃丢尾数据;如需更强一致性改 `NORMAL` 并接受性能下降(机械盘上可能差 8~12×) | 文档已有说明(`docs/data_record_debug.md:56`) |
|
||||
|
||||
### 8.7 P2:记录策略与运维
|
||||
|
||||
- **按圈采样**:`circle_id % N == 0` 时记录,N 可配置(如 1/5/10),负载与数据量按比例下降;分析时仍能覆盖多圈。
|
||||
- **按表开关**:现场排障通常只需 1~2 张表(例如只看输入点迹 + 关联信息),其余表可关闭。
|
||||
- **记录库轮转/上限**:按圈数或文件大小滚动(如每 100 圈一个新 db),并限制总目录大小;避免 GB~TB 级文件。
|
||||
- **环境**:记录目录放**本地 SSD**、加入 Defender 排除(需合规审批);避免网络盘。
|
||||
- **运行期开关**:增加"开始/停止记录"的接口或环境变量(不改变交付接口导出表的前提下谨慎设计),现场按需开启。
|
||||
|
||||
### 8.8 备选方案:二进制原始日志 + 离线入库
|
||||
|
||||
如果目标是"**彻底不影响实时性**"且可接受离线转换:
|
||||
|
||||
- 记录点只把定长 POD 快照顺序追加到二进制文件(`fwrite`/内存映射,写线程或缓冲),运行时开销 ~µs/行;
|
||||
- 运行结束后用工具把二进制转成 SQLite(或直接供 Python/Pandas 读),分析体验与现在一致;
|
||||
- 优点:实时性最好、无 SQLite 事务抖动;缺点:需要额外的转换工具与格式版本管理,查询前多一步。
|
||||
|
||||
> 对"Release 现场排障"这种低频、短期需求,方案 8.8 往往比"把 SQLite 在线写做快"更省事;对"长期在线记录"则推荐 8.2~8.4 的异步 + 裁剪 + 批处理。
|
||||
|
||||
### 8.9 不建议的做法
|
||||
|
||||
- ❌ 只把 `synchronous=OFF` 当作解决:它只省 fsync,实测单条仍 ~950~1120 µs;
|
||||
- ❌ 只加 `cache_size`:对"每条一个事务"帮助有限;
|
||||
- ❌ 只用 WAL 不批处理:单条 17~210 µs/行(`NORMAL` 更差),高行数场景仍超预算;
|
||||
- ❌ 只做批处理不做 ⑥ 裁剪:中等场景仍约 2.5 s/圈、稠密约 30~40 s/圈(见 §4.3/§5.3),仍不满足 1 s 圈预算;
|
||||
- ❌ 用"交付构建保持 OFF"回避问题:那是交付策略,不是 Release 排障场景的解决方案;
|
||||
- ❌ 把记录点当日志用、在热循环里无条件记录(当前 ⑥ 即此模式)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 推荐实施路线与验收标准
|
||||
|
||||
### 9.1 分阶段实施
|
||||
|
||||
| 阶段 | 内容 | 预期效果 | 风险 |
|
||||
|---|---|---|---|
|
||||
| **阶段 0(立即,不改代码)** | 只在必须排障时开启 `Release+ON`;优先用 Debug+ON 或短时采数;记录目录放 SSD、加 Defender 排除 | 缓解现场影响;不能解决实时性 | 低 |
|
||||
| **阶段 1(P0-b 批处理)** | ①~⑤、TAS ⑥ 改帧末/圈末 `RDP_RECORD_BATCH`;⑥ 先分块收集 | ①~⑤ DB 写入从 ~1.1 ms/行降到 3.8~8.8 µs/行(实测约 95~336×,按实体);输入点迹每帧 150 行 → 约 0.6~2.1 ms(取决于批大小)。注意 ④⑤ 端到端仅 ~10~52×(构造仍是瓶颈,需阶段 3) | 低(接口已存在) |
|
||||
| **阶段 2(P0-a 裁剪 + P1-b)** | ⑥ 只记取中/近失采样/白名单;④ 只记增量;去 AUTOINCREMENT、cache_size、失败熔断 | 稠密场景 ⑥ 从 5.4×10⁶ 行/圈降到 10³~10⁴ 行/圈(该表单表从 ~100 min 降到约 5~80 ms);整圈(①~⑥)仍约 0.2~0.7 s,需阶段 3 才能稳定实时 | 中(分析信息取舍需确认) |
|
||||
| **阶段 3(P0-c 异步 + P1-a 构造迁移)** | 有界队列 + 写线程 + 后台序列化/批写;Shutdown flush;丢包计数 | 处理线程新增 p99 有确定上界(目标 ≤1 ms/帧,判圈帧 ≤5 ms);**§9.2 的 p99≤1 ms 验收目标需要阶段 2+3 共同达成,仅阶段 1 批处理时仍可能出现 ms 级尖峰** | 中高(框架改动,需要长跑验证) |
|
||||
| **阶段 4(P2 运维)** | 按圈采样、记录库轮转、Defender 排除、监控指标 | 长时间运行可控 | 低 |
|
||||
|
||||
### 9.2 验收指标(建议)
|
||||
|
||||
| 指标 | 目标 |
|
||||
|---|---|
|
||||
| 处理线程每帧新增耗时(记录功能带来) p99 | **≤1 ms**(10 ms 帧周期的 10%) |
|
||||
| 判圈帧(model=1)新增耗时 p99 | **≤5 ms**(不使该帧超出预算;需要时把 ④⑤⑥ 写线程化后进一步降低) |
|
||||
| 连续运行 1 小时实时性 | 无因记录导致的帧超时/积压(用宿主现有节拍监控判定) |
|
||||
| 记录完整性 | 行数与业务量模型一致;`pragma integrity_check = ok`;丢包计数可解释(丢弃率 0 或明确阈值内) |
|
||||
| 记录库增长 | 按采样/轮转策略可控(如 ≤ X MB/圈、总目录上限) |
|
||||
| 交付构建 | `RDP_DATA_RECORD=OFF` 时 DLL 导出表/依赖/体积与改动前一致,记录代码零参与(沿用现有 CI 校验) |
|
||||
|
||||
### 9.3 验证与复测方法
|
||||
|
||||
1. **处理线程打点**:在记录点前后嵌 `QueryPerformanceCounter`(仅 `RDP_RECORD_ENABLE` 构建),输出每帧/每圈直方图(mean/p50/p99/max),对比 ON/OFF;
|
||||
2. **数据库侧校验**:用 SQL 统计每圈行数(`select circle_id,count(*) from RdpRecAssoInfo group by circle_id;` 等),与业务量模型对齐;`pragma integrity_check`;
|
||||
3. **队列/丢弃监控**:异步方案输出队列高水位、丢弃行数、写线程积压时间;
|
||||
4. **长时间运行**:1 h 连续跑,记录帧时延 p99 随时间变化与 DB 增长;
|
||||
5. **现场复测**:把本报告 A/B 驱动替换为宿主真实数据(或短时抓包回放),在目标机器上重复 §4.5 的 ON/OFF 对比,校准 `M`、`N`、每行成本三个关键参数。
|
||||
|
||||
---
|
||||
|
||||
## 10. 风险与注意事项
|
||||
|
||||
| 风险 | 说明/缓解 |
|
||||
|---|---|
|
||||
| 异步丢数据 | 队列满丢弃/退出时未 flush 会丢记录;用计数与日志显式化,调试用途可接受,但必须可解释 |
|
||||
| `synchronous=OFF`/WAL 的崩溃一致性 | 进程异常/断电可能丢尾部数据或损坏库;调试库可接受,若需更强一致性改 `NORMAL` 并接受性能下降 |
|
||||
| 裁剪 ⑥ 导致分析信息减少 | 保留"全量候选"运行期开关,需要时短时开启;默认只记关键候选 |
|
||||
| WAL 文件与 checkpoint 抖动 | `-wal` 增长;本机机械盘上 `WAL+NORMAL` 批量均值 53.7 µs/行(比 WAL+`sync=OFF` 差 8.6×、比 DELETE 批量差约 11×)且 checkpoint 周期出现 10~100 ms 级尖峰;优先 `DELETE + 批处理` 或 `WAL + sync=OFF` + SSD |
|
||||
| 多线程宿主 | 若宿主实际多线程调用,单连接+全局锁会串行化并放大延迟;异步写线程方案需明确单写者 |
|
||||
| 记录库无限增长 | 必须加轮转/上限,避免磁盘耗尽 |
|
||||
| 记录功能与交付隔离 | 所有改动限制在 `#ifdef RDP_RECORD_ENABLE` 与记录模块内部;交付构建(OFF)必须保持零代码、零导出、零依赖 |
|
||||
| 合成场景代表性 | 本报告 A/B 为压力场景,绝对毫秒数与真实现场不同;上线前需用真实宿主数据复测 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 优化实施与验收结果(2026-10-20)
|
||||
|
||||
依据 `requirements.md` 的决策实施(P0-b 批量事务、P0-c 异步写线程+有界队列、P1-a 方案A 构造迁移、P1-b SQLite 配置与健壮性采用;P0-a 关联裁剪、P2 记录策略不采用)。
|
||||
|
||||
> 编号映射说明:`requirements.md` 中的 **P1-b = 本报告 §8.6(SQLite 配置与健壮性)**;§8.1 总览表中旧编号"P1-b = 临时航迹只记增量"**未纳入本次需求**(未实施)。
|
||||
|
||||
### 11.1 实现摘要
|
||||
|
||||
| 子系统 | 实现 |
|
||||
|---|---|
|
||||
| 记录框架 | 处理线程只做"收集源结构体 → 一次入队";独立写线程做实体构造(`arrayToString` 全部移入写线程)+ 批量事务;有界队列(默认 64 MB / 4096 批),满则整批丢弃并计数(不阻塞处理线程);写线程 Windows `BELOW_NORMAL` 优先级;`Shutdown()` 停止入队→排空→`wal_checkpoint(TRUNCATE)`→关库→打印 `Stats`;支持 Destroy→GetB 重连(新库;同秒文件名冲突追加 `_1` 后缀) |
|
||||
| 业务记录点 | 12 处(6 个 cpp)改为按帧/圈收集后调用 `RDP_RECORD_POINTS`/`RDP_RECORD_COHESION_TWS`/`RDP_RECORD_COHESION_TAS`/`RDP_RECORD_TEMP_TRACKS`/`RDP_RECORD_TRUST_TRACKS`/`RDP_RECORD_ASSO_INFOS`;关联信息按 `RDP_RECORD_ASSO_CHUNK_ROWS`(131072)分块入队;全部位于 `#ifdef RDP_RECORD_ENABLE` 内,`OFF` 构建零代码 |
|
||||
| SQLite | `journal_mode=WAL`(不支持时回退)、`synchronous=OFF`、`cache_size=-8192KB`、`busy_timeout=2000ms`;建表后去 `AUTOINCREMENT`(读取 `sqlite_master` DDL 后重建空表);每表建表/插入失败只提示一次,连续错误 ≥10 次全局熔断 |
|
||||
| 参数 | `parameters.h` 新增 10 个可调宏(队列容量/批数、写批行数/等待、关联分块、缓存、busy_timeout、WAL 开关、熔断阈值、丢弃日志间隔),均带 `#ifndef` 可覆盖 |
|
||||
| 遗留接口 | `RDP_RECORD`/`RDP_RECORD_BATCH`(实体同步写)保留,文档标注仅测试/特殊场景使用 |
|
||||
|
||||
### 11.2 验收结果
|
||||
|
||||
| # | 项目 | 结果 |
|
||||
|---|---|---|
|
||||
| A1 | 构建矩阵(PS/MS × Release/Debug × REC_ON/OFF,8 配置) | **PASS**:全部 0 warning 0 error |
|
||||
| A2 | `OFF` 等价性(对比改动前 HEAD 构建的基线 DLL) | **PASS**:体积 182,784 B 相同;34 个导出符号完全一致;9 个依赖 DLL 完全一致 |
|
||||
| A3 | 记录内容(dup/6 圈/186 航迹场景) | **PASS**:`RdpRecPoint` 90,000、`RdpRecPointCohesionTWS` 4,170、`RdpRecAssoInfo` 211,584、`RdpRecTempTrack` 8,330、`RdpRecTrustTrack` 304,合计 **314,388 行**,与改动前完全一致;`trust_num` 序列一致 |
|
||||
| A4 | 数据库正确性 | **PASS**:`integrity_check=ok`;DDL 不含 `AUTOINCREMENT`;`journal_mode=wal`;各表 `id` 严格递增;`OFF`/`ON` 均无 `-wal/-shm` 残留(Shutdown 检查点后关闭) |
|
||||
| A5a | 处理线程普通帧新增耗时(`REC_ON−REC_OFF`) | **PASS**:多次运行 p99 **0.08~0.17 ms**(阈值 ≤1 ms),p50 ≈ 0.02 ms |
|
||||
| A5b | 处理线程判圈帧新增耗时(压测场景,186 航迹/单圈约 12.3 万关联候选) | **未达 5 ms 目标**:多次运行 p99 **12.8~24.0 ms**、p50 2.9~4.1 ms、mean 5.9~8.7 ms、max 24.5 ms(原因:P0-a 未采用,单圈需拷贝/记录约 12.3 万行关联候选;详见 §11.3) |
|
||||
| A5c | 受控负载参考(50 目标 / 50 点每帧,49 航迹) | 判圈帧新增:中位 **3.07 ms**、最大 **5.96 ms**(基本达到 5 ms 量级,随负载线性变化) |
|
||||
| A6 | 队列/长跑(60 圈压测,约 43.5 s) | **PASS**:9,987,370 行、`dropped_rows=0`、`write_errors=0`、队列峰值 19.8 MB(<64 MB)、DB 1.06 GB |
|
||||
| A7a | 故障注入:`rdp_records` 为普通文件(目录不可建) | **PASS**:`Init()` 静默失败,驱动 exit=0、无崩溃、不产生记录库 |
|
||||
| A7b | 故障注入:小队列构建(cap=1 KB) | **PASS**:`dropped_rows=314,388` 有计数、处理流程正常结束 |
|
||||
| A7c | 重连(GetB→Destroy→GetB) | **PASS**:生成 2 个独立 db 文件(同秒冲突追加 `_1`),两轮均写入成功 |
|
||||
| — | 端到端墙钟(600 帧) | `REC_OFF` 1.8 s / `REC_ON` 2.1 s(改动前 `REC_ON` 为 **426.7 s**) |
|
||||
|
||||
> 验收脚本与原始日志:`build/analysis/verify/run_acceptance.ps1`、`latency_eval.py`、`check_record_db.py`、`failure_injection.ps1`、`acceptance_final.log`、`latency_eval.log`、`check_record_db.log`。
|
||||
|
||||
### 11.3 遗留问题与决策点
|
||||
|
||||
1. **判圈帧压测开销仍高于原定 5 ms 目标(多次运行 p99 12.8~24.0 ms、中位 2.9~4.1 ms、均值 5.9~8.7 ms)**:这与 `requirements.md` 不采用 P0-a 的决策直接相关——压测场景单圈 `M×N≈12.3 万` 行关联候选,仅"收集+入队"就需在判圈帧复制十几 MB 数据;对照的受控负载(49 航迹)中位仅 3.1 ms、最大 6.0 ms。是否满足现场要求取决于实际 `M`/`N`;若现场也接近压测量级且要求判圈帧 ≤5 ms,需要重新评估 P0-a(关联候选裁剪/采样)或按圈采样。
|
||||
2. **实时性总体结论**:普通帧几乎无感(p99 +0.11 ms);压测判圈帧 `REC_ON` 绝对耗时约 0.62 s(`REC_OFF` 同帧约 0.59 s),仍小于 1 s 圈周期,但相对开销(+24 ms)来自未裁剪的行数。建议现场用真实数据按 §9.3 复测。
|
||||
3. **异步语义变化**:进程崩溃/未调用 `Destroy()` 时,队列与 WAL 尾部记录可能丢失;队列满时整批丢弃,均由 `Stats` 计数(`dropped_rows`)。调试用途可接受,但分析前应确认 `dropped_rows=0`。
|
||||
4. **跨表写入顺序**不再保证(各表内顺序保持);依赖 `id` 跨表排序的分析脚本需改用业务时间/`circle_id` 等字段关联。
|
||||
5. **版本号未提升**(仍为 V1.5.8),变更记录于 `CHANGELOG.md [Unreleased]`;正式发布时再归档。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:关键代码位置索引
|
||||
|
||||
**A.1 分析时(优化前)关键位置**
|
||||
|
||||
| 主题 | 位置 |
|
||||
|---|---|
|
||||
| 记录宏与开关 | `rdp_record.h:37-54`;`CMakeLists.txt:25-57,118-158` |
|
||||
| 单条写入实现 | `rdp_record.cpp:174-196`;`third_party/ormpp/ormpp/sqlite.hpp:834-856` |
|
||||
| 批量写入实现 | `rdp_record.cpp:198-225`;`sqlite.hpp:859-902`(`transaction_` 默认 true 在 `:926`) |
|
||||
| SQL 字符串生成 | `third_party/ormpp/ormpp/utility.hpp:438-521` |
|
||||
| 建表/AUTOINCREMENT | `third_party/ormpp/ormpp/sqlite.hpp:489-585`(`:554` 追加 `PRIMARY KEY AUTOINCREMENT`) |
|
||||
| `synchronous=OFF` | `rdp_record.cpp:144-147` |
|
||||
| 记录点 ① | `data_process.cpp:60,132` |
|
||||
| 记录点 ② | `dot_coh.cpp:230` |
|
||||
| 记录点 ③ | `dot_coh_tas.cpp:73` |
|
||||
| 记录点 ④ | `track_init.cpp:110-117` |
|
||||
| 记录点 ⑤ | `track_asso.cpp:163`;`track_asso_tas.cpp:122` |
|
||||
| 记录点 ⑥ | `track_asso.cpp:554,710,722`;`track_asso_tas.cpp:363,499` |
|
||||
| 数组转文本 | `rdp_record_entity.h:448-483`(`arrayToString`) |
|
||||
| 实体定义与构造 | `rdp_record_entity.h:52-443`;`struct.h:40-170` |
|
||||
| TWS 关联批累积 | `dot_coh.cpp:227-231`;`track_asso.cpp:29-68`;`track_init.cpp:60,108` |
|
||||
| 实时性/线程模型已知问题 | `README.md:1020`(BUG-03) |
|
||||
| 功能说明与已知限制 | `docs/data_record_debug.md:15,47-59,197-204` |
|
||||
|
||||
**A.2 优化实施后(V1.5.8 优化,2026-10-20)关键位置**
|
||||
|
||||
| 主题 | 位置 |
|
||||
|---|---|
|
||||
| 公共 API / 宏 / `Stats` | `rdp_record.h:31-101`(`Stats`、`stats`、6 个异步入队接口、6 个记录宏;遗留 `RDP_RECORD` 同步宏保留) |
|
||||
| 队列与入队/丢弃 | `rdp_record.cpp:472-546`(`EnqueueBatch`:字节/批数上限、整批丢弃、峰值统计) |
|
||||
| 写线程主循环 | `rdp_record.cpp:548-637`(drain、凑批、分块提交、异常兜底、Windows `BELOW_NORMAL` 优先级) |
|
||||
| 批量事务/实体构造 | `rdp_record.cpp:367-455`(`InsertEntities`/`FlushKind`:实体在写线程构造、每批一个事务) |
|
||||
| SQLite 初始化 | `rdp_record.cpp:247-344`(WAL/sync/cache/busy_timeout;`CreateTable` 去 AUTOINCREMENT);`parameters.h:50-92` 可调宏 |
|
||||
| 生命周期/统计/重连 | `rdp_record.cpp:661-820`(`Init` 启动写线程、`Shutdown` 排空+检查点+打印 stats、同秒文件名后缀) |
|
||||
| 业务记录点(批量入队) | `data_process.cpp:28-31,66-70,111-114,155-160`;`dot_coh.cpp:226-240`;`dot_coh_tas.cpp:69-83`;`track_init.cpp:108-118`;`track_asso.cpp:69-74,170-176,447-460,570-585,733-765,855-865`;`track_asso_tas.cpp:42-47,128-136,263-276,370-390,510-580` |
|
||||
| 验收脚本/证据 | `build/analysis/verify/`(`run_acceptance.ps1`、`latency_eval.py`、`check_record_db.py`、`failure_injection.ps1`);`build/analysis/optim_baseline.md`、`build/analysis/exports_*.txt`、`build/analysis/build_matrix_final.log` |
|
||||
|
||||
## 附录 B:实测数据明细
|
||||
|
||||
### B.1 微基准(`build/analysis/bench_micro.md` + `build/perf_bench_micro/`)
|
||||
|
||||
- `build/analysis/bench_micro.md`:微基准报告(单条/批量/构造/SQLite 配置、3~5 次运行中位数与 min~max);
|
||||
- `build/perf_bench_micro/bench_record.cpp`:单条/批量/构造基准(链接现有 `rdp_record.lib`);
|
||||
- `build/perf_bench_micro/bench_sqlite.cpp`:16 种 SQLite 配置 + 原生文件操作基准(链接现有 `sqlite3.c.obj`);
|
||||
- `results/`(含 `run1..run5`):逐行/逐批原始 CSV、环境信息、SQLite 编译选项、写后只读校验。
|
||||
|
||||
### B.2 端到端 A/B(`build/perf_bench_pipeline/`)
|
||||
|
||||
- `driver.cpp`:只使用公开 DLL 接口的合成数据驱动(TWS,100 波位/圈、150 点/帧、200 目标、相扫判圈);
|
||||
- `run_off/`、`run_on/`:两套 DLL 的运行目录(各含 `frames.csv`、`circles.csv`、`summary.txt`);
|
||||
- `run_on/rdp_records/*.db`:记录库(可对比行数/体积);
|
||||
- `build/analysis/bench_pipeline.md`:A/B 报告。
|
||||
|
||||
### B.3 关键换算
|
||||
|
||||
- 首轮 A/B(未起批,`run_pilot_off|run_pilot_on`):全运行 103,374 行 → ≈97.6 s 增量 → **≈0.94 ms/行**(环境空闲轮次;并行压测时约 1.0 ms/行);
|
||||
- 最终 A/B(186 航迹):314,388 行 → 426.7 s → **≈1.39 ms/行**(35.32 MB 库);
|
||||
- 首轮 A/B 每圈记录数:`RdpRecPoint` 15,000/圈 + 凝聚 ~695/圈 + 临时航迹 ~1,534/圈 → 约 1.72×10⁴ 行/圈 → 16.8 s/圈;
|
||||
- 微基准(3 次运行中位数):单条 mean 1.22~1.33 ms/行、p50 0.73~0.78 ms;批量 1000 = 3.8~8.8 µs/行;
|
||||
- 微基准构造:`RdpRecTrustTrack` 133.56 µs/行、`RdpRecTempTrack` 19.44 µs/行。
|
||||
|
||||
## 附录 C:术语
|
||||
|
||||
| 术语 | 说明 |
|
||||
|---|---|
|
||||
| 帧/波位 | 一次 `data_preprocess` 调用;TWS 一圈含 `B` 个波位 |
|
||||
| model | `data_preprocess` 返回值:1=TWS 满圈、2=TWS 蓄数据、3=TAS、0=无效 |
|
||||
| `N` | 一次 TWS 关联批的凝聚点数(整圈累积) |
|
||||
| `M` | 可靠航迹数(`trust_track.size()`,≤500) |
|
||||
| 隐式事务 | 单条 SQL 在 SQLite autocommit 模式下自动开启/提交的事务(每条记录一次 journal 生命周期) |
|
||||
| 批处理 | 把多行收集到一个 `vector` 后用一次 `BEGIN/COMMIT` 写入 |
|
||||
| 异步写线程 | 处理线程只入队,独立线程负责序列化与批量入库 |
|
||||
| 近失候选 | 未通过关联门限但 `d_min` 接近门限的候选(用于分析门限灵敏度) |
|
||||
+23
-25
@@ -1,37 +1,35 @@
|
||||
# 雷达数据处理项目修改说明
|
||||
|
||||
## 概述
|
||||
修改该项目的构建脚本和VS code的task配置。
|
||||
|
||||
目前可选的构建配置包括:
|
||||
根据[data_record_perf_report.md](/docs/data_record_perf_report.md)中提出的优化措施,以及本文档的要求,对数据记录功能进行性能优化。
|
||||
|
||||
1、Debug / Release;
|
||||
2、PHASE_SCANNING(X256_PS) / MECHANICAL_SCANNING(X256_MS):两个配置决定不同的生成目录和宏定义;
|
||||
3、RDP_DATA_RECORD: AUTO/ON/OFF:AUTO配置在debug模式下为ON,在release模式下为OFF;
|
||||
## 优化措施实施决策
|
||||
|
||||
构建工具保持MSVC 2022不变,原有的Task都删除;
|
||||
### P0-a 关联信息裁剪/采样
|
||||
|
||||
需要生成的新Task:
|
||||
不采用,实际运行时,航迹数和每圈点迹数都在可控范围,数据行数不会过大。
|
||||
|
||||
Task1: 清理全部
|
||||
* 清理所有配置的构建输出
|
||||
### P0-b 批量事务
|
||||
|
||||
Task2: 配置并构建全部(Release/REC_OFF)
|
||||
* 分别构建PHASE_SCANNING(X256_PS) / MECHANICAL_SCANNING(X256_MS)
|
||||
* 配置1:Release / PHASE_SCANNING / RDP_DATA_RECORD: OFF
|
||||
* 配置2:Release / MECHANICAL_SCANNING / RDP_DATA_RECORD: OFF
|
||||
采用
|
||||
|
||||
Task3: 配置并构建全部(Release/REC_ON)
|
||||
* 分别构建PHASE_SCANNING(X256_PS) / MECHANICAL_SCANNING(X256_MS)
|
||||
* 配置1:Release / PHASE_SCANNING / RDP_DATA_RECORD: ON
|
||||
* 配置2:Release / MECHANICAL_SCANNING / RDP_DATA_RECORD: ON
|
||||
### P0-c 异步写线程+有界队列
|
||||
|
||||
Task4: 配置并构建全部(Debug/REC_OFF)
|
||||
* 分别构建PHASE_SCANNING(X256_PS) / MECHANICAL_SCANNING(X256_MS)
|
||||
* 配置1:Debug / PHASE_SCANNING / RDP_DATA_RECORD: OFF
|
||||
* 配置2:Debug / MECHANICAL_SCANNING / RDP_DATA_RECORD: OFF
|
||||
采用
|
||||
|
||||
Task5: 配置并构建全部(Debug/REC_ON)
|
||||
* 分别构建PHASE_SCANNING(X256_PS) / MECHANICAL_SCANNING(X256_MS)
|
||||
* 配置1:Debug / PHASE_SCANNING / RDP_DATA_RECORD: ON
|
||||
* 配置2:Debug / MECHANICAL_SCANNING / RDP_DATA_RECORD: ON
|
||||
### P1-a 构造成本迁移
|
||||
|
||||
采用,按方案A执行
|
||||
|
||||
### P1-b:SQLite 配置与健壮性
|
||||
|
||||
采用
|
||||
|
||||
### P2:记录策略与运维
|
||||
|
||||
不采用
|
||||
|
||||
## 其他事项
|
||||
|
||||
其他不明确或有冲突的地方,直接向我提问
|
||||
Reference in New Issue
Block a user