diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f6f08a..30b72e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index cf468cf..7558b2f 100644 --- a/README.md +++ b/README.md @@ -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/// 数据记录调试功能由缓存变量 `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///`;新增 `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 时期) | diff --git a/data_process_class_dll/data_process.cpp b/data_process_class_dll/data_process.cpp index 257b2c4..36fda32 100644 --- a/data_process_class_dll/data_process.cpp +++ b/data_process_class_dll/data_process.cpp @@ -1,7 +1,6 @@ #include "data_process.h" #include "rdp_log.h" #include "rdp_record.h" -#include "rdp_record_entity.h" #include #include #include @@ -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:" < rec_points; + rec_points.reserve(static_cast(data_num)); +#endif for (int i=0; i rec_points; + rec_points.reserve(static_cast(data_num)); +#endif for (int i=0; i #include #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 *data_input, //2.data_input_buff数据输出给point_recv +#ifdef RDP_RECORD_ENABLE + //记录凝聚后点迹:先收集,输出循环结束后一次入队 + std::vector rec_coh; + rec_coh.reserve(data_input_buff.size()); +#endif for (size_t i=0;i().swap(data_input_buff); diff --git a/data_process_class_dll/dot_coh_tas.cpp b/data_process_class_dll/dot_coh_tas.cpp index bc5ad73..e31d79a 100644 --- a/data_process_class_dll/dot_coh_tas.cpp +++ b/data_process_class_dll/dot_coh_tas.cpp @@ -3,7 +3,6 @@ #include #include #include "rdp_record.h" -#include "rdp_record_entity.h" using namespace std; int Dot_Coh_TAS::dot_coh_tas_process(std::vector *data_input, //输入点迹 @@ -67,11 +66,21 @@ int Dot_Coh_TAS::dot_coh_tas_process(std::vector *dat } } +#ifdef RDP_RECORD_ENABLE + //记录凝聚后点迹:先收集,输出循环结束后一次入队 + std::vector rec_coh; + rec_coh.reserve(data_input->size()); +#endif for (size_t i=0;isize();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; } diff --git a/data_process_class_dll/parameters.h b/data_process_class_dll/parameters.h index 8efcba5..366db40 100644 --- a/data_process_class_dll/parameters.h +++ b/data_process_class_dll/parameters.h @@ -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 diff --git a/data_process_class_dll/rdp_record.cpp b/data_process_class_dll/rdp_record.cpp index 226b83b..9877629 100644 --- a/data_process_class_dll/rdp_record.cpp +++ b/data_process_class_dll/rdp_record.cpp @@ -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 //仅用于把记录写线程优先级降到 BELOW_NORMAL,避免与雷达处理线程争抢 CPU +#endif + #include "rdp_record.h" #include "rdp_record_entity.h" #include "parameters.h" #include "rdp_log.h" +#include +#include #include +#include +#include +#include #include +#include +#include +#include #include #include #include +#include #include +#include +#include #include //第三方库(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 points; //Point/CohesionTws/CohesionTas 共用 + std::vector temp_tracks; + std::vector trust_tracks; + std::vector asso_infos; + }; + + //写线程内的待提交缓冲(按表分别累积,达到批大小或超时后各表分别提交) + struct Pending + { + std::vector points; + std::vector cohesion_tws; + std::vector cohesion_tas; + std::vector temp_tracks; + std::vector trust_tracks; + std::vector asso_infos; + }; + + //统计计数器(处理线程/写线程并发读写) + struct Counters + { + std::atomic enqueued_rows{0}; + std::atomic dropped_rows{0}; + std::atomic written_rows{0}; + std::atomic transactions{0}; + std::atomic write_errors{0}; + std::atomic queue_peak_bytes{0}; + std::atomic queue_peak_rows{0}; + }; + //记录模块内部状态(Pimpl:ormpp / SQLite 类型不出现在 rdp_record.h 中) struct Record_State { - ormpp::dbng db; //SQLite 连接(单连接,写入时串行使用) - std::string path; //当前 db 文件全路径 - bool ready; //是否已成功打开并完成初始化 - bool failed; //初始化失败:后续写入全部忽略,不再反复报错 - std::mutex mtx; //写入串行化(宿主多线程调用时保护单连接) + ormpp::dbng db; //SQLite 连接(写线程独占;遗留同步接口经 db_mtx 串行) + std::string path; //当前 db 文件全路径 + std::atomic ready{false}; + std::atomic failed{false}; - Record_State() : ready(false), failed(false) {} + std::mutex db_mtx; //串行化数据库访问(写线程 + 遗留同步写) + std::map tables; //表状态:0 未建 / 1 可用 / 2 失败(db_mtx 保护) + + std::mutex q_mtx; //队列锁 + std::condition_variable q_cv; //队列条件变量 + std::deque 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 consecutive_errors{0}; + + ~Record_State(); }; std::unique_ptr& State() { - //函数内静态:宿主未调用 Data_Process_Factory::Destroy() 时由析构兜底关库 + //函数内静态:宿主未调用 Data_Process_Factory::Destroy() 时由析构兜底停止写线程/关库 static std::unique_ptr 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 - bool EnsureTable(Record_State& st) + void LogStats(Record_State& st, const char* tag) { - static const bool created = st.db.create_datatable(::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 - bool PrepareWrite(Record_State& st) + std::string TableName() { - static bool reported = false; - if (EnsureTable(st)) + //ormpp 以「短结构体名」作为表名 + return std::string(ormpp::get_short_struct_name()); + } + + //============================== 记录批次辅助 ============================== + template + std::size_t BatchRowsOf(const std::vector& 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 + void AppendMove(std::vector& dst, std::vector& 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> 消费返回行。 + auto rows = st.db.query_s>("PRAGMA journal_mode=WAL;"); + std::string mode = rows.empty() ? std::string("") : 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>( + "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 + bool CreateTable(Record_State& st) + { + if (!st.db.create_datatable(::ormpp_auto_key{"id"})) + { + return false; + } + + const std::string table = TableName(); + std::string ddl; + try + { + auto rows = st.db.query_s>( + "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(::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 + bool PrepareWrite(Record_State& st) + { + const std::string table = TableName(); + std::map::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(st)) + { + st.tables[table] = 1; + return true; + } + st.tables[table] = 2; + ReportError(st, "create table", table); + return false; + } + + //批量写入实体(一次 BEGIN/COMMIT 事务;调用方无需持锁) + template + bool InsertEntities(Record_State& st, const std::vector& v) + { + if (v.empty()) + { + return true; + } + + std::lock_guard lock(st.db_mtx); + if (st.failed.load()) + { + return false; + } + if (!PrepareWrite(st)) + { + return false; + } + + const int rows = st.db.insert(v); + if (rows == INT_MIN) + { + st.counters.write_errors.fetch_add(1); + st.tables[TableName()] = 2; + const int errors = st.consecutive_errors.fetch_add(1) + 1; + ReportError(st, "insert", TableName()); + 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 + bool FlushKind(Record_State& st, std::vector& src) + { + if (src.empty()) + { + return true; + } + + const std::size_t chunk = (RDP_RECORD_WRITE_BATCH_ROWS == 0) + ? 4096 + : static_cast(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 entities; + entities.reserve(n); + for (std::size_t i = 0; i < n; ++i) + { + entities.emplace_back(src[pos + i]); //Entity(Source&) 构造:数组转文本在此完成 + } + if (!InsertEntities(st, entities)) + { + ok = false; + } + pos += n; + } + + src.clear(); + return ok; + } + + bool FlushPending(Record_State& st, Pending& p) + { + bool ok = true; + ok &= FlushKind(st, p.points); + ok &= FlushKind(st, p.cohesion_tws); + ok &= FlushKind(st, p.cohesion_tas); + ok &= FlushKind(st, p.temp_tracks); + ok &= FlushKind(st, p.trust_tracks); + ok &= FlushKind(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(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& 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 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(RDP_RECORD_QUEUE_MAX_BATCHES) || + sp->queued_bytes + bytes > static_cast(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 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 local; + { + std::unique_lock 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(RDP_RECORD_WRITE_BATCH_ROWS)) + { + std::unique_lock 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 lock(q_mtx); + stopping = true; + } + q_cv.notify_all(); + if (writer.joinable()) + { + writer.join(); + } + std::lock_guard lock(db_mtx); + try + { + db.disconnect(); + } + catch (...) + { + } + } } namespace rdp_record @@ -104,65 +663,143 @@ namespace rdp_record std::unique_ptr& 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& sp = State(); - if (sp.get() == 0 || !sp->ready) + if (sp.get() == 0) { return; } - std::lock_guard 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 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 lock(st.db_mtx); + //WAL 模式下把 -wal 合并回主库(尽力而为;失败不影响关库) + st.db.query_s>("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& 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 void Write(const T& v) { std::unique_ptr& sp = State(); - if (sp.get() == 0 || !sp->ready || sp->failed) + if (sp.get() == 0 || !sp->ready || sp->failed.load()) { return; //未初始化 / 初始化失败 / 已关库:静默忽略 } - std::lock_guard lock(sp->mtx); + std::lock_guard lock(sp->db_mtx); if (!PrepareWrite(*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()] = 2; + ReportError(*sp, "insert", TableName()); + return; } + sp->counters.written_rows.fetch_add(1); + sp->counters.transactions.fetch_add(1); } template @@ -204,24 +862,84 @@ namespace rdp_record } std::unique_ptr& sp = State(); - if (sp.get() == 0 || !sp->ready || sp->failed) + if (sp.get() == 0 || !sp->ready || sp->failed.load()) { return; } + InsertEntities(*sp, v); + } - std::lock_guard lock(sp->mtx); - if (!PrepareWrite(*sp)) + //============================== 异步入队接口(源结构体) ============================== + void WritePoints(std::vector 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 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 v) + { + if (v.empty()) + { + return; + } + Batch b; + b.kind = BatchKind::CohesionTas; + b.points = std::move(v); + EnqueueBatch(std::move(b)); + } + + void WriteTempTracks(std::vector 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 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 v) + { + if (v.empty()) + { + return; + } + Batch b; + b.kind = BatchKind::AssoInfo; + b.asso_infos = std::move(v); + EnqueueBatch(std::move(b)); } } diff --git a/data_process_class_dll/rdp_record.h b/data_process_class_dll/rdp_record.h index ee58128..eba2b47 100644 --- a/data_process_class_dll/rdp_record.h +++ b/data_process_class_dll/rdp_record.h @@ -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 #include +#include #include +//记录源结构体(定义在 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 v); //RdpRecPoint(输入点迹,TWS/TAS 共用) + void WriteCohesionTWS(std::vector v); //RdpRecPointCohesionTWS + void WriteCohesionTAS(std::vector v); //RdpRecPointCohesionTAS + void WriteTempTracks(std::vector v); //RdpRecTempTrack(临时航迹) + void WriteTrustTracks(std::vector v); //RdpRecTrustTrack(可靠航迹) + void WriteAssoInfos(std::vector v); //RdpRecAssoInfo(点航关联信息) + + //遗留接口:同步写入记录实体(在调用线程内直接写库,会阻塞;仅供测试/特殊场景使用)。 + //T 为记录实体类型,须在 rdp_record.cpp 中显式实例化。 template void Write(const T& v); - //批量写入同一记录实体的多条记录(内部使用事务) template void Write(const std::vector& 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 inline void Write(const T&) {} template inline void Write(const std::vector&) {} } diff --git a/data_process_class_dll/track_asso.cpp b/data_process_class_dll/track_asso.cpp index 9f83f48..c8f17ec 100644 --- a/data_process_class_dll/track_asso.cpp +++ b/data_process_class_dll/track_asso.cpp @@ -8,7 +8,6 @@ #include #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 *point_ std::vector().swap(point_process); //输出航迹 +#ifdef RDP_RECORD_ENABLE + //记录可靠航迹:先收集,输出循环结束后一次入队 + std::vector rec_trust; + rec_trust.reserve(trust_track->size()); +#endif for (size_t i=0;isize();i++) { // 输出更新航迹条件: if((*trust_track)[i].Track_Update_Flag==1){ @@ -160,10 +164,16 @@ int Track_Asso:: track_asso_process(std::vector *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,struct Ra std::vector associated_info; +#ifdef RDP_RECORD_ENABLE + //记录未关联上的点航信息:收集到分块上限即入队,函数末尾再 flush 剩余 + //按本圈候选数(点迹数×航迹数,封顶到分块上限)预留,避免收集期间多次扩容 + std::vector rec_asso; + { + std::size_t est = point_process.size() * trust_track->size(); + if (est > static_cast(RDP_RECORD_ASSO_CHUNK_ROWS)) + { + est = static_cast(RDP_RECORD_ASSO_CHUNK_ROWS); + } + rec_asso.reserve(est); + } +#endif //遍历所有点迹航迹 计算点迹和航迹的统计距离 for (size_t loop_of_track = 0; loop_of_tracksize();loop_of_track++) @@ -551,7 +574,13 @@ void Track_Asso:: model_filter(std::vector *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,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(associated_info.size()); } @@ -719,7 +754,13 @@ void Track_Asso:: model_filter(std::vector *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(associated_info.size()); } @@ -813,6 +854,13 @@ void Track_Asso:: model_filter(std::vector *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) diff --git a/data_process_class_dll/track_asso_tas.cpp b/data_process_class_dll/track_asso_tas.cpp index 128812f..49100d8 100644 --- a/data_process_class_dll/track_asso_tas.cpp +++ b/data_process_class_dll/track_asso_tas.cpp @@ -8,7 +8,6 @@ #include #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().swap(point_process); //输出航迹 +#ifdef RDP_RECORD_ENABLE + //记录可靠航迹:先收集,输出循环结束后一次入队 + std::vector rec_trust; + rec_trust.reserve(trust_track->size()); +#endif for (size_t i=0;isize();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 *t ) { std::vector associated_info; +#ifdef RDP_RECORD_ENABLE + //记录未关联上的点航信息:收集到分块上限即入队,函数末尾再 flush 剩余 + std::vector 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 *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 *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 *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, int tas_track_idx ) diff --git a/data_process_class_dll/track_init.cpp b/data_process_class_dll/track_init.cpp index 1d0bb3f..e4dfaee 100644 --- a/data_process_class_dll/track_init.cpp +++ b/data_process_class_dll/track_init.cpp @@ -7,7 +7,6 @@ #include #include #include "rdp_record.h" -#include "rdp_record_entity.h" using namespace std; int Track_Init::track_init_process_logic( std::vector *point_recv, //输入点迹 @@ -109,11 +108,13 @@ int Track_Init::track_init_process_logic( std::vector #ifdef RDP_RECORD_ENABLE //记录临时航迹 + std::vector 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 //临时航迹满足起始长度 转为可靠航迹 diff --git a/docs/data_record_debug.md b/docs/data_record_debug.md index 87f2c35..ef89c38 100644 --- a/docs/data_record_debug.md +++ b/docs/data_record_debug.md @@ -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()`,实现在 `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` 仅声明 + 显式实例化」跨标准边界:调用方(C++17)不实例化记录实现,实现在 `rdp_record.cpp`(C++20)中显式实例化。V1.5.8 起常规记录点改走**非模板**的 `Write*` 异步入队接口,跨标准边界更简单;模板 `Write` 仅作为遗留同步接口保留。 ## 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>` 消费 | +| 建表 | 首次写入某记录实体时自动建表(每表只尝试一次);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`,在**调用线程内直接写库**(经 `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 v)` | 输入点迹 → `RdpRecPoint`(TWS/TAS 共用) | +| `void WriteCohesionTWS(std::vector v)` | TWS 凝聚后点迹 → `RdpRecPointCohesionTWS` | +| `void WriteCohesionTAS(std::vector v)` | TAS 凝聚后点迹 → `RdpRecPointCohesionTAS` | +| `void WriteTempTracks(std::vector v)` | 临时航迹 → `RdpRecTempTrack` | +| `void WriteTrustTracks(std::vector v)` | 可靠航迹 → `RdpRecTrustTrack` | +| `void WriteAssoInfos(std::vector v)` | 点航关联信息 → `RdpRecAssoInfo` | +| `template void Write(const T&)` / `(const std::vector&)` | **遗留**同步写记录实体(会阻塞调用线程) | +| `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` | `RdpRecPoint` | +| `RDP_RECORD_COHESION_TWS(v)` | `std::vector` | `RdpRecPointCohesionTWS` | +| `RDP_RECORD_COHESION_TAS(v)` | `std::vector` | `RdpRecPointCohesionTAS` | +| `RDP_RECORD_TEMP_TRACKS(v)` | `std::vector` | `RdpRecTempTrack` | +| `RDP_RECORD_TRUST_TRACKS(v)` | `std::vector` | `RdpRecTrustTrack` | +| `RDP_RECORD_ASSO_INFOS(v)` | `std::vector` | `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 v); //RdpRecTrackHeight -//批量(需要语句/局部变量时,整体用 #ifdef 包裹,保证交付构建零代码) +// 宏区(RDP_RECORD_ENABLE 开/关两套) #ifdef RDP_RECORD_ENABLE - std::vector 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 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(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 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` 只有声明/显式实例化,`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 且均正常写入(同秒文件名冲突自动追加后缀) | diff --git a/docs/data_record_perf_report.md b/docs/data_record_perf_report.md new file mode 100644 index 0000000..c24d608 --- /dev/null +++ b/docs/data_record_perf_report.md @@ -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()`,测单条/批量/不同 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(全模块一把锁) +→ PrepareWrite(静态 guard,建表只一次) +→ ormpp::dbng::insert → sqlite.hpp:834-856 insert_or_update_impl: + generate_insert_sql() // 每次重新拼 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&)`(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>` 消费返回行,或 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` 后,**每 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&)`(`rdp_record.cpp:198-225`)→ ormpp 批量插入(一次 prepare + 一个 `BEGIN/COMMIT`,`sqlite.hpp:859-902`)。**当前业务侧 0 个调用点**,只需按"天然批次边界"改造记录点: + +| 记录点 | 建议批次边界 | 做法(示意) | +|---|---|---| +| ① 输入点迹 | 每帧 `data_preprocess` 循环结束 | `std::vector 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`/定长二进制),避免文本化;查询时用 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` 接近门限的候选(用于分析门限灵敏度) | diff --git a/requirements.md b/requirements.md index 96e5412..475e584 100644 --- a/requirements.md +++ b/requirements.md @@ -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 \ No newline at end of file +### P1-a 构造成本迁移 + +采用,按方案A执行 + +### P1-b:SQLite 配置与健壮性 + +采用 + +### P2:记录策略与运维 + +不采用 + +## 其他事项 + +其他不明确或有冲突的地方,直接向我提问 \ No newline at end of file