RecordFrame: 仅内存追加, 零磁盘IO Flush: 仿真Stop时单次事务批量InsertAll BeginRecording/Discard/BufferedFrameCount 新API SimulationEngine: 移除 _frameDb 持久连接, RecordFrames默认true SimulationRunner: 移除所有 Debug.Log 每帧日志 ReplayController: 适配 ReadFrames 新API FrameDataStoreTests: 6项覆盖 录制/刷新/读取/丢弃/清理
108 lines
3.8 KiB
C#
108 lines
3.8 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using CounterDrone.Core.Models;
|
||
using SQLite;
|
||
|
||
namespace CounterDrone.Core.Simulation
|
||
{
|
||
/// <summary>帧数据存储 — 内存缓存,仿真结束时批量写入 SQLite</summary>
|
||
public class FrameDataStore
|
||
{
|
||
private readonly IPathProvider _paths;
|
||
private readonly List<SimFrameRecord> _buffer = new();
|
||
private string _taskId = string.Empty;
|
||
/// <summary>帧数据保留天数,默认 180 天(6 个月)</summary>
|
||
public int RetentionDays { get; set; } = 180;
|
||
public int BufferedFrameCount => _buffer.Count > 0
|
||
? _buffer[^1].FrameIndex + 1
|
||
: 0;
|
||
|
||
public FrameDataStore(IPathProvider paths)
|
||
{
|
||
_paths = paths;
|
||
}
|
||
|
||
/// <summary>开始录制。清空缓冲区,准备接收帧数据。</summary>
|
||
public void BeginRecording(string taskId)
|
||
{
|
||
_taskId = taskId;
|
||
_buffer.Clear();
|
||
}
|
||
|
||
/// <summary>记录一帧所有实体的快照(仅内存,不写磁盘)</summary>
|
||
public void RecordFrame(int frameIdx, float timestamp, List<EntitySnapshot> snapshots)
|
||
{
|
||
for (int i = 0; i < snapshots.Count; i++)
|
||
{
|
||
var s = snapshots[i];
|
||
_buffer.Add(new SimFrameRecord
|
||
{
|
||
TaskId = _taskId,
|
||
FrameIndex = frameIdx,
|
||
Timestamp = timestamp,
|
||
EntityId = s.EntityId,
|
||
EntityType = (int)s.EntityType,
|
||
PosX = s.PosX,
|
||
PosY = s.PosY,
|
||
PosZ = s.PosZ,
|
||
RotX = s.RotX,
|
||
RotY = s.RotY,
|
||
RotZ = s.RotZ,
|
||
StateData = s.StateData,
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>将缓冲区所有帧数据批量写入 SQLite(事务级别,单次 IO)</summary>
|
||
public void Flush()
|
||
{
|
||
if (_buffer.Count == 0) return;
|
||
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{_taskId}.db");
|
||
Directory.CreateDirectory(_paths.GetFramesDir());
|
||
using var db = new SQLiteConnection(dbPath);
|
||
db.CreateTable<SimFrameRecord>();
|
||
db.CreateIndex("SimFrameRecord", new[] { "TaskId", "FrameIndex" });
|
||
db.InsertAll(_buffer);
|
||
_buffer.Clear();
|
||
}
|
||
|
||
public void Discard() => _buffer.Clear();
|
||
|
||
public List<SimFrameRecord> ReadFrames(string taskId, int fromFrame, int toFrame)
|
||
{
|
||
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
|
||
if (!File.Exists(dbPath)) return new List<SimFrameRecord>();
|
||
using var db = new SQLiteConnection(dbPath);
|
||
return db.Table<SimFrameRecord>()
|
||
.Where(f => f.TaskId == taskId && f.FrameIndex >= fromFrame && f.FrameIndex <= toFrame)
|
||
.OrderBy(f => f.FrameIndex)
|
||
.ToList();
|
||
}
|
||
|
||
public void DeleteFrameDb(string taskId)
|
||
{
|
||
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
|
||
if (File.Exists(dbPath)) File.Delete(dbPath);
|
||
}
|
||
|
||
/// <summary>清理过期的帧数据库文件</summary>
|
||
public int CleanupExpired()
|
||
{
|
||
var dir = _paths.GetFramesDir();
|
||
if (!Directory.Exists(dir)) return 0;
|
||
var cutoff = DateTime.Now.AddDays(-RetentionDays);
|
||
var deleted = 0;
|
||
foreach (var file in Directory.GetFiles(dir, "*.db"))
|
||
{
|
||
if (File.GetCreationTime(file) < cutoff)
|
||
{
|
||
File.Delete(file);
|
||
deleted++;
|
||
}
|
||
}
|
||
return deleted;
|
||
}
|
||
}
|
||
}
|