perf: 仿真期间零字符串分配,JSON 序列化推迟到 Flush

EntitySnapshot 新增原始字段(DamageStage/Hp/CloudRadius等), CollectSnapshots 直接填值

FrameDataStore buffer 改为 FrameRecord 结构体, Flush 时统一拼 JSON

SimulationRunner 直接读 snap.CloudRadius/CloudOpacity, 移除 JsonDocument.Parse

删除 SimulationEngine 的 StringBuilder 和 Str() 方法
This commit is contained in:
tian 2026-06-15 19:18:44 +08:00
parent fb22605fa3
commit ce9d44db7e
6 changed files with 64 additions and 66 deletions

View File

@ -10,13 +10,10 @@ namespace CounterDrone.Core.Simulation
public class FrameDataStore
{
private readonly IPathProvider _paths;
private readonly List<SimFrameRecord> _buffer = new();
private readonly List<FrameRecord> _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 int BufferedFrameCount { get; private set; }
public FrameDataStore(IPathProvider paths)
{
@ -28,46 +25,66 @@ namespace CounterDrone.Core.Simulation
{
_taskId = taskId;
_buffer.Clear();
BufferedFrameCount = 0;
}
/// <summary>记录一帧所有实体的快照(仅内存,不写磁盘</summary>
/// <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
_buffer.Add(new FrameRecord
{
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,
FrameIndex = frameIdx, Timestamp = timestamp,
Snapshot = new EntitySnapshot
{
EntityId = s.EntityId, EntityType = s.EntityType,
PosX = s.PosX, PosY = s.PosY, PosZ = s.PosZ,
DamageStage = s.DamageStage, Hp = s.Hp,
PlatformStateStr = s.PlatformStateStr,
CloudRadius = s.CloudRadius, CloudOpacity = s.CloudOpacity,
CloudPhase = s.CloudPhase, CloudElapsed = s.CloudElapsed,
}
});
}
BufferedFrameCount = frameIdx + 1;
}
/// <summary>将缓冲区所有帧数据批量写入 SQLite事务级别单次 IO</summary>
/// <summary>将缓冲区序列化为 JSON 并批量写入 SQLite</summary>
public void Flush()
{
if (_buffer.Count == 0) return;
var records = new List<SimFrameRecord>(_buffer.Count);
for (int i = 0; i < _buffer.Count; i++)
{
var fr = _buffer[i];
var s = fr.Snapshot;
records.Add(new SimFrameRecord
{
TaskId = _taskId, FrameIndex = fr.FrameIndex, Timestamp = fr.Timestamp,
EntityId = s.EntityId, EntityType = (int)s.EntityType,
PosX = s.PosX, PosY = s.PosY, PosZ = s.PosZ,
StateData = s.EntityType switch
{
Models.EntityType.Drone => $"{{\"damageStage\":{s.DamageStage},\"hp\":{s.Hp:F2}}}",
Models.EntityType.Platform => $"{{\"state\":\"{s.PlatformStateStr}\"}}",
Models.EntityType.Cloud => $"{{\"radius\":{s.CloudRadius:F1},\"opacity\":{s.CloudOpacity:F2},\"phase\":{s.CloudPhase},\"elapsed\":{s.CloudElapsed:F1}}}",
_ => "{}",
},
});
}
_buffer.Clear();
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();
db.InsertAll(records);
}
public void Discard() => _buffer.Clear();
public void Discard() { _buffer.Clear(); BufferedFrameCount = 0; }
public List<SimFrameRecord> ReadFrames(string taskId, int fromFrame, int toFrame)
{
@ -104,4 +121,11 @@ namespace CounterDrone.Core.Simulation
return deleted;
}
}
internal struct FrameRecord
{
public int FrameIndex;
public float Timestamp;
public EntitySnapshot Snapshot;
}
}

View File

@ -28,7 +28,18 @@ namespace CounterDrone.Core.Simulation
public string EntityId { get; set; } = string.Empty;
public Models.EntityType EntityType { get; set; }
public float PosX, PosY, PosZ, RotX, RotY, RotZ;
public string StateData { get; set; } = "{}";
// ── 原始状态字段(引擎直接填充,前端直接读取,无需 JSON──
public int DamageStage;
public float Hp;
public string? PlatformStateStr;
public float CloudRadius;
public float CloudOpacity;
public int CloudPhase;
public float CloudElapsed;
/// <summary>仅 Flush 入库时使用,仿真期间保持 null</summary>
public string? StateData;
}
public enum DroneStatus { Flying, Destroyed, ReachedTarget, ZoneIntruded }

View File

@ -449,30 +449,20 @@ namespace CounterDrone.Core.Simulation
for (int i = 0; i < _drones.Count; i++)
{
var d = _drones[i];
int stage = (int)_damageModel.GetDamageStage(1 - d.Hp);
_snapshotPool.Add(new EntitySnapshot { EntityId = d.Id, EntityType = Models.EntityType.Drone, PosX = d.PosX, PosY = d.PosY, PosZ = d.PosZ, StateData = Str($"{{\"damageStage\":{stage},\"hp\":{d.Hp:F2}}}") });
_snapshotPool.Add(new EntitySnapshot { EntityId = d.Id, EntityType = Models.EntityType.Drone, PosX = d.PosX, PosY = d.PosY, PosZ = d.PosZ, DamageStage = (int)_damageModel.GetDamageStage(1 - d.Hp), Hp = d.Hp });
}
for (int i = 0; i < _platforms.Count; i++)
{
var p = _platforms[i];
if (p.PlatformType != Models.PlatformType.AirBased) continue;
_snapshotPool.Add(new EntitySnapshot { EntityId = p.Id, EntityType = Models.EntityType.Platform, PosX = p.PosX, PosY = p.PosY, PosZ = p.PosZ, StateData = Str($"{{\"state\":\"{p.State}\"}}") });
_snapshotPool.Add(new EntitySnapshot { EntityId = p.Id, EntityType = Models.EntityType.Platform, PosX = p.PosX, PosY = p.PosY, PosZ = p.PosZ, PlatformStateStr = p.State.ToString() });
}
for (int i = 0; i < _clouds.Count; i++)
{
var c = _clouds[i];
_snapshotPool.Add(new EntitySnapshot { EntityId = c.Id, EntityType = Models.EntityType.Cloud, PosX = c.Dispersion.Center.X, PosY = c.Dispersion.Center.Y, PosZ = c.Dispersion.Center.Z, StateData = Str($"{{\"radius\":{c.Dispersion.Radius:F1},\"opacity\":{c.Dispersion.Particles.Opacity:F2},\"phase\":{c.Dispersion.Phase},\"elapsed\":{c.Dispersion.Elapsed:F1}}}") });
_snapshotPool.Add(new EntitySnapshot { EntityId = c.Id, EntityType = Models.EntityType.Cloud, PosX = c.Dispersion.Center.X, PosY = c.Dispersion.Center.Y, PosZ = c.Dispersion.Center.Z, CloudRadius = c.Dispersion.Radius, CloudOpacity = c.Dispersion.Particles.Opacity, CloudPhase = c.Dispersion.Phase, CloudElapsed = c.Dispersion.Elapsed });
}
return _snapshotPool;
}
/// <summary>GC 友好字符串构造:复用 StringBuilder避免拼接中间字符串</summary>
private static string Str(FormattableString fs)
{
_sb.Clear();
_sb.AppendFormat(fs.Format, fs.GetArguments());
return _sb.ToString();
}
private static readonly System.Text.StringBuilder _sb = new(256);
}
}

View File

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using CounterDrone.Core;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
@ -83,11 +82,9 @@ namespace CounterDrone.Unity
if (snap.EntityType == EntityType.Cloud)
{
float r = ParseCloudRadius(snap.StateData);
float opacity = ParseCloudOpacity(snap.StateData);
go.transform.localScale = UVector3.one * r;
go.transform.localScale = UVector3.one * snap.CloudRadius;
var mat = go.GetComponent<Renderer>().material;
mat.color = new Color(1, 0, 0, opacity);
mat.color = new Color(1, 0, 0, snap.CloudOpacity);
}
}
@ -161,29 +158,5 @@ namespace CounterDrone.Unity
mat.color = new Color(0.2f, 0.5f, 1f, 0.8f); // 蓝色半透明 = 空基平台
_entityVisuals[snap.EntityId] = go;
}
private float ParseCloudOpacity(string stateData)
{
try
{
using var doc = JsonDocument.Parse(stateData);
if (doc.RootElement.TryGetProperty("opacity", out var o))
return o.GetSingle();
}
catch { }
return 0.5f;
}
private float ParseCloudRadius(string stateData)
{
try
{
using var doc = JsonDocument.Parse(stateData);
if (doc.RootElement.TryGetProperty("radius", out var r))
return r.GetSingle();
}
catch { }
return 1f;
}
}
}