302 lines
9.9 KiB
C++
302 lines
9.9 KiB
C++
#include "DebugDataSaver.h"
|
||
#include "LaserDataLoader.h"
|
||
#include "VrLog.h"
|
||
#include "VrError.h"
|
||
#include <QCoreApplication>
|
||
#include <QDir>
|
||
#include <QDirIterator>
|
||
#include <QFile>
|
||
#include <QFileInfo>
|
||
#include <QDateTime>
|
||
#include <QString>
|
||
#include <cstring>
|
||
|
||
namespace {
|
||
// 获取当前应用名(不含扩展名),用于构造 debug 子目录前缀
|
||
QString AppName()
|
||
{
|
||
QString name = QFileInfo(QCoreApplication::applicationFilePath()).baseName();
|
||
if (name.isEmpty()) name = QStringLiteral("app");
|
||
return name;
|
||
}
|
||
|
||
// 子目录前缀:{appName}_PointCloud_,仅匹配此前缀的子目录会被统计/清理
|
||
QString SubDirPrefix()
|
||
{
|
||
return AppName() + QStringLiteral("_PointCloud_");
|
||
}
|
||
}
|
||
|
||
DebugDataSaver::DebugDataSaver(int maxThreads)
|
||
: m_maxThreads(std::max(1, std::min(maxThreads, 4)))
|
||
, m_running(true)
|
||
{
|
||
for (int i = 0; i < m_maxThreads; ++i) {
|
||
m_workers.emplace_back(&DebugDataSaver::WorkerThread, this);
|
||
}
|
||
LOG_INFO("[DebugDataSaver] 已启动 %d 个存储线程\n", m_maxThreads);
|
||
}
|
||
|
||
DebugDataSaver::~DebugDataSaver()
|
||
{
|
||
Stop();
|
||
}
|
||
|
||
void DebugDataSaver::Stop()
|
||
{
|
||
if (!m_running.exchange(false)) {
|
||
return;
|
||
}
|
||
|
||
m_queueCondition.notify_all();
|
||
|
||
for (auto& worker : m_workers) {
|
||
if (worker.joinable()) {
|
||
worker.join();
|
||
}
|
||
}
|
||
m_workers.clear();
|
||
|
||
// 释放队列中未处理的任务
|
||
while (!m_taskQueue.empty()) {
|
||
FreeData(m_taskQueue.front().data);
|
||
m_taskQueue.pop();
|
||
}
|
||
|
||
LOG_INFO("[DebugDataSaver] 已停止\n");
|
||
}
|
||
|
||
void DebugDataSaver::SaveAsync(const std::string& basePath, const std::string& timestamp, const std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& data, const std::string& deviceName)
|
||
{
|
||
if (data.empty() || !m_running) return;
|
||
|
||
SaveTask task;
|
||
task.deviceName = deviceName;
|
||
task.filePath = GenerateSavePath(basePath, timestamp, deviceName);
|
||
task.data = DeepCopyData(data);
|
||
|
||
// 在入队前清理超限文件,避免任务堆积时磁盘瞬间打满
|
||
EnsureStorageLimit(basePath);
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lock(m_queueMutex);
|
||
m_taskQueue.push(std::move(task));
|
||
}
|
||
m_queueCondition.notify_one();
|
||
}
|
||
|
||
void DebugDataSaver::WorkerThread()
|
||
{
|
||
LaserDataLoader dataLoader;
|
||
|
||
while (true) {
|
||
SaveTask task;
|
||
{
|
||
std::unique_lock<std::mutex> lock(m_queueMutex);
|
||
m_queueCondition.wait(lock, [this] {
|
||
return !m_taskQueue.empty() || !m_running;
|
||
});
|
||
|
||
if (!m_running && m_taskQueue.empty()) {
|
||
break;
|
||
}
|
||
if (m_taskQueue.empty()) {
|
||
continue;
|
||
}
|
||
|
||
task = std::move(m_taskQueue.front());
|
||
m_taskQueue.pop();
|
||
}
|
||
|
||
int lineNum = static_cast<int>(task.data.size());
|
||
float scanSpeed = 0.0f;
|
||
int maxTimeStamp = 0;
|
||
int clockPerSecond = 0;
|
||
|
||
int result = dataLoader.SaveLaserScanData(task.filePath, task.data, lineNum, scanSpeed, maxTimeStamp, clockPerSecond);
|
||
|
||
if (result == SUCCESS) {
|
||
LOG_INFO("[DebugDataSaver] 保存成功: %s (%d行)\n", task.filePath.c_str(), lineNum);
|
||
} else {
|
||
LOG_ERROR("[DebugDataSaver] 保存失败: %s, 错误: %s\n", task.filePath.c_str(), dataLoader.GetLastError().c_str());
|
||
}
|
||
|
||
FreeData(task.data);
|
||
}
|
||
}
|
||
|
||
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>> DebugDataSaver::DeepCopyData(
|
||
const std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& src)
|
||
{
|
||
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>> dst;
|
||
dst.reserve(src.size());
|
||
|
||
for (const auto& item : src) {
|
||
EVzResultDataType dataType = item.first;
|
||
const SVzLaserLineData& srcLine = item.second;
|
||
|
||
SVzLaserLineData dstLine;
|
||
memset(&dstLine, 0, sizeof(SVzLaserLineData));
|
||
|
||
if (srcLine.nPointCount > 0) {
|
||
if (dataType == keResultDataType_Position) {
|
||
dstLine.p3DPoint = new SVzNL3DPosition[srcLine.nPointCount];
|
||
if (srcLine.p3DPoint) {
|
||
memcpy(dstLine.p3DPoint, srcLine.p3DPoint, sizeof(SVzNL3DPosition) * srcLine.nPointCount);
|
||
}
|
||
dstLine.p2DPoint = new SVzNL2DPosition[srcLine.nPointCount];
|
||
if (srcLine.p2DPoint) {
|
||
memcpy(dstLine.p2DPoint, srcLine.p2DPoint, sizeof(SVzNL2DPosition) * srcLine.nPointCount);
|
||
}
|
||
} else if (dataType == keResultDataType_PointXYZRGBA) {
|
||
dstLine.p3DPoint = new SVzNLPointXYZRGBA[srcLine.nPointCount];
|
||
if (srcLine.p3DPoint) {
|
||
memcpy(dstLine.p3DPoint, srcLine.p3DPoint, sizeof(SVzNLPointXYZRGBA) * srcLine.nPointCount);
|
||
}
|
||
dstLine.p2DPoint = new SVzNL2DLRPoint[srcLine.nPointCount];
|
||
if (srcLine.p2DPoint) {
|
||
memcpy(dstLine.p2DPoint, srcLine.p2DPoint, sizeof(SVzNL2DLRPoint) * srcLine.nPointCount);
|
||
}
|
||
}
|
||
}
|
||
|
||
dstLine.nPointCount = srcLine.nPointCount;
|
||
dstLine.llTimeStamp = srcLine.llTimeStamp;
|
||
dstLine.llFrameIdx = srcLine.llFrameIdx;
|
||
dstLine.nEncodeNo = srcLine.nEncodeNo;
|
||
dstLine.fSwingAngle = srcLine.fSwingAngle;
|
||
dstLine.bEndOnceScan = srcLine.bEndOnceScan;
|
||
|
||
dst.push_back(std::make_pair(dataType, dstLine));
|
||
}
|
||
|
||
return dst;
|
||
}
|
||
|
||
void DebugDataSaver::FreeData(std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& data)
|
||
{
|
||
LaserDataLoader loader;
|
||
loader.FreeLaserScanData(data);
|
||
data.clear();
|
||
}
|
||
|
||
std::string DebugDataSaver::GenerateSavePath(const std::string& basePath, const std::string& timestamp, const std::string& deviceName)
|
||
{
|
||
// 子目录命名:{appName}_PointCloud_YYYYMMDD(时间戳前 8 位为日期)
|
||
QString subDir = SubDirPrefix() + QString::fromStdString(timestamp.substr(0, 8));
|
||
QString fullPath = QString::fromStdString(basePath) + "/" + subDir;
|
||
|
||
QDir dir(fullPath);
|
||
if (!dir.exists()) {
|
||
dir.mkpath(".");
|
||
}
|
||
|
||
// 文件名:{设备名}_{日期}_{时间}.txt(timestamp 形如 YYYYMMDD_HHmmsszzz)
|
||
// 设备名缺省时退化为 pointcloud
|
||
QString name = deviceName.empty() ? QStringLiteral("pointcloud")
|
||
: QString::fromStdString(deviceName);
|
||
QString fileName = QString("%1/%2_%3.txt").arg(fullPath)
|
||
.arg(name)
|
||
.arg(QString::fromStdString(timestamp));
|
||
|
||
return fileName.toStdString();
|
||
}
|
||
|
||
void DebugDataSaver::EnsureStorageLimit(const std::string& basePath)
|
||
{
|
||
std::lock_guard<std::mutex> lock(m_diskSpaceMutex);
|
||
|
||
const int64_t limit = m_maxStorageSize.load();
|
||
if (limit <= 0) return;
|
||
|
||
int64_t totalSize = ComputeDirSize(basePath);
|
||
const QString prefix = SubDirPrefix();
|
||
|
||
// 循环删除最早的文件,直到目录总大小 <= limit
|
||
while (totalSize > limit) {
|
||
std::string oldest = FindOldestFile(basePath);
|
||
if (oldest.empty()) {
|
||
LOG_WARNING("[DebugDataSaver] 目录大小 %lld 超过上限 %lld,但无可删除文件\n",
|
||
static_cast<long long>(totalSize), static_cast<long long>(limit));
|
||
break;
|
||
}
|
||
|
||
QFileInfo info(QString::fromStdString(oldest));
|
||
qint64 fileSize = info.size();
|
||
QString parentPath = info.absolutePath();
|
||
|
||
if (!QFile::remove(QString::fromStdString(oldest))) {
|
||
LOG_ERROR("[DebugDataSaver] 删除失败: %s\n", oldest.c_str());
|
||
break;
|
||
}
|
||
|
||
LOG_INFO("[DebugDataSaver] 磁盘超限,删除最早文件: %s (%lld 字节)\n",
|
||
oldest.c_str(), static_cast<long long>(fileSize));
|
||
totalSize -= fileSize;
|
||
|
||
// 父目录若已为空且名字匹配 {appName}_PointCloud_* 前缀,连同目录一起回收
|
||
QDir parentDir(parentPath);
|
||
const QString parentName = parentDir.dirName();
|
||
if (parentName.startsWith(prefix) && parentDir.isEmpty()) {
|
||
parentDir.cdUp();
|
||
if (parentDir.rmdir(parentName)) {
|
||
LOG_INFO("[DebugDataSaver] 已移除空目录: %s\n", parentPath.toStdString().c_str());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
int64_t DebugDataSaver::ComputeDirSize(const std::string& rootPath)
|
||
{
|
||
QString root = QString::fromStdString(rootPath);
|
||
QDir rootDir(root);
|
||
if (!rootDir.exists()) return 0;
|
||
|
||
const QString prefix = SubDirPrefix();
|
||
int64_t total = 0;
|
||
|
||
// 只统计匹配 {appName}_debug_* 模式的子目录,避免误判 basePath 下其他内容
|
||
const QStringList subDirs = rootDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||
for (const QString& name : subDirs) {
|
||
if (!name.startsWith(prefix)) continue;
|
||
QString subPath = rootDir.absoluteFilePath(name);
|
||
QDirIterator it(subPath, QStringList() << "*.txt", QDir::Files, QDirIterator::Subdirectories);
|
||
while (it.hasNext()) {
|
||
it.next();
|
||
total += it.fileInfo().size();
|
||
}
|
||
}
|
||
return total;
|
||
}
|
||
|
||
std::string DebugDataSaver::FindOldestFile(const std::string& rootPath)
|
||
{
|
||
QString root = QString::fromStdString(rootPath);
|
||
QDir rootDir(root);
|
||
if (!rootDir.exists()) return std::string();
|
||
|
||
const QString prefix = SubDirPrefix();
|
||
QString oldestPath;
|
||
QDateTime oldestTime;
|
||
bool first = true;
|
||
|
||
const QStringList subDirs = rootDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||
for (const QString& name : subDirs) {
|
||
if (!name.startsWith(prefix)) continue;
|
||
QString subPath = rootDir.absoluteFilePath(name);
|
||
QDirIterator it(subPath, QStringList() << "*.txt", QDir::Files, QDirIterator::Subdirectories);
|
||
while (it.hasNext()) {
|
||
it.next();
|
||
QFileInfo info = it.fileInfo();
|
||
QDateTime mtime = info.lastModified();
|
||
if (first || mtime < oldestTime) {
|
||
oldestTime = mtime;
|
||
oldestPath = info.absoluteFilePath();
|
||
first = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
return oldestPath.toStdString();
|
||
}
|