feat(avoidance): 添加低开销压测日志

This commit is contained in:
JSD\13999
2026-06-13 16:42:53 +08:00
parent 49acdf93cc
commit e596d17866
4 changed files with 184 additions and 4 deletions

View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using UnityEngine;
namespace FishROV.AvoidanceBenchmark
{
public sealed class AvoidanceBenchmarkLogWriter
{
private const float SampleInterval = 0.5f;
private const int MaxSamples = 4096;
private readonly List<Sample> samples = new List<Sample>(1024);
private AvoidanceBenchmarkConfig config;
private string runName;
private float runStartTime;
private float nextSampleTime;
private bool hasRun;
public string LastExportPath { get; private set; } = "尚未导出";
public int SampleCount => samples.Count;
public void StartRun(AvoidanceBenchmarkConfig benchmarkConfig, string adapterName)
{
ExportIfNeeded();
config = benchmarkConfig.Clone();
runName = SanitizeFileName($"{DateTime.Now:yyyyMMdd_HHmmss}_{adapterName}_{config.Scenario}_{config.DimensionMode}_{config.FishCount}");
runStartTime = Time.unscaledTime;
nextSampleTime = runStartTime;
samples.Clear();
hasRun = true;
LastExportPath = "本轮尚未导出";
}
public void Record(
AvoidanceMetricsRecorder metrics,
AvoidanceCollisionProbe collisionProbe,
int agentCount)
{
if (!hasRun || Time.unscaledTime < nextSampleTime || samples.Count >= MaxSamples)
{
return;
}
nextSampleTime += SampleInterval;
samples.Add(new Sample
{
TimeSeconds = Time.unscaledTime - runStartTime,
AgentCount = agentCount,
AverageFps = metrics.AverageFps,
OnePercentLowFps = metrics.OnePercentLowFps,
SimulationMs = metrics.SimulationMs,
GcAllocKb = metrics.GcAllocBytes / 1024.0,
HorizontalOverlapPairs = collisionProbe.HorizontalOverlapPairs,
SpatialOverlapPairs = collisionProbe.SpatialOverlapPairs,
MinimumClearance = collisionProbe.MinimumClearance
});
}
public string ExportIfNeeded()
{
if (!hasRun || samples.Count == 0)
{
return LastExportPath;
}
var directory = Path.Combine(Application.persistentDataPath, "AvoidanceBenchmarkLogs");
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, runName + ".csv");
using (var writer = new StreamWriter(path, false))
{
writer.WriteLine("framework,scenario,dimension,fishCount,arenaRadius,screenWidth,screenHeight,targetFrameRate,timeSeconds,agentCount,averageFps,onePercentLowFps,simulationMs,gcAllocKb,horizontalOverlapPairs,spatialOverlapPairs,minimumClearance");
for (var i = 0; i < samples.Count; i++)
{
var sample = samples[i];
writer.Write(config.Framework);
writer.Write(',');
writer.Write(config.Scenario);
writer.Write(',');
writer.Write(config.DimensionMode);
writer.Write(',');
writer.Write(config.FishCount);
writer.Write(',');
WriteFloat(writer, config.ArenaRadius);
writer.Write(',');
writer.Write(Screen.width);
writer.Write(',');
writer.Write(Screen.height);
writer.Write(',');
writer.Write(Application.targetFrameRate);
writer.Write(',');
WriteFloat(writer, sample.TimeSeconds);
writer.Write(',');
writer.Write(sample.AgentCount);
writer.Write(',');
WriteFloat(writer, sample.AverageFps);
writer.Write(',');
WriteFloat(writer, sample.OnePercentLowFps);
writer.Write(',');
WriteDouble(writer, sample.SimulationMs);
writer.Write(',');
WriteDouble(writer, sample.GcAllocKb);
writer.Write(',');
writer.Write(sample.HorizontalOverlapPairs);
writer.Write(',');
writer.Write(sample.SpatialOverlapPairs);
writer.Write(',');
WriteFloat(writer, sample.MinimumClearance);
writer.WriteLine();
}
}
LastExportPath = path;
samples.Clear();
hasRun = false;
Debug.Log($"Avoidance benchmark log exported: {path}");
return LastExportPath;
}
private static void WriteFloat(TextWriter writer, float value)
{
writer.Write(value.ToString("0.###", CultureInfo.InvariantCulture));
}
private static void WriteDouble(TextWriter writer, double value)
{
writer.Write(value.ToString("0.###", CultureInfo.InvariantCulture));
}
private static string SanitizeFileName(string value)
{
foreach (var invalid in Path.GetInvalidFileNameChars())
{
value = value.Replace(invalid, '_');
}
return value;
}
private struct Sample
{
public float TimeSeconds;
public int AgentCount;
public float AverageFps;
public float OnePercentLowFps;
public double SimulationMs;
public double GcAllocKb;
public int HorizontalOverlapPairs;
public int SpatialOverlapPairs;
public float MinimumClearance;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9c67563022844219b17fcf6a3630062c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -17,6 +17,7 @@ namespace FishROV.AvoidanceBenchmark
private readonly AvoidanceScenarioController scenarioController = new AvoidanceScenarioController();
private readonly AvoidanceMetricsRecorder metrics = new AvoidanceMetricsRecorder();
private readonly AvoidanceCollisionProbe collisionProbe = new AvoidanceCollisionProbe();
private readonly AvoidanceBenchmarkLogWriter logWriter = new AvoidanceBenchmarkLogWriter();
private AvoidanceBenchmarkConfig config;
private IAvoidanceAdapter adapter;
private Transform agentRoot;
@@ -59,6 +60,7 @@ namespace FishROV.AvoidanceBenchmark
collisionProbe.Update(agents, config.DimensionMode, Time.unscaledDeltaTime);
metrics.EndSimulation();
logWriter.Record(metrics, collisionProbe, agents.Count);
}
private void OnGUI()
@@ -72,6 +74,7 @@ namespace FishROV.AvoidanceBenchmark
GUILayout.Label($"模拟耗时:{metrics.SimulationMs:F2} ms | GC{metrics.GcAllocBytes / 1024f:F1} KB");
GUILayout.Label($"碰撞调试XZ重叠 {collisionProbe.HorizontalOverlapPairs} 对 | 3D重叠 {collisionProbe.SpatialOverlapPairs} 对 | 最近间距 {collisionProbe.MinimumClearance:F2}");
GUILayout.Label($"帧率限制不锁帧vSync={QualitySettings.vSyncCount}targetFrameRate={Application.targetFrameRate}");
GUILayout.Label($"日志样本:{logWriter.SampleCount} | {logWriter.LastExportPath}");
GUILayout.Space(8);
DrawEnumButtons("避障方案", framework, value =>
@@ -123,12 +126,18 @@ namespace FishROV.AvoidanceBenchmark
{
ResetBenchmark();
}
if (GUILayout.Button("导出日志"))
{
logWriter.ExportIfNeeded();
}
GUILayout.EndHorizontal();
GUILayout.EndArea();
}
private void OnDestroy()
{
logWriter.ExportIfNeeded();
adapter?.Dispose();
metrics.Dispose();
}
@@ -137,6 +146,7 @@ namespace FishROV.AvoidanceBenchmark
{
adapter?.Dispose();
adapter = null;
logWriter.ExportIfNeeded();
if (agentRoot != null)
{
@@ -160,6 +170,7 @@ namespace FishROV.AvoidanceBenchmark
SpawnFish();
scenarioController.Reset(config, agents);
logWriter.StartRun(config, adapter.Name);
}
private void SpawnFish()

View File

@@ -65,14 +65,16 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
编辑器和百元机都需要日志,但日志不能拖慢性能测试。
- 日志目录:`Application.persistentDataPath/AvoidanceBenchmarkLogs/`
- 编辑器下通常在用户目录的 `AppData/LocalLow/.../AvoidanceBenchmarkLogs/`
- 真机下在 App 的 persistent data path 里,使用面板的“导出日志”按钮或结束/重置测试自动写出。
- 不要每帧 `Debug.Log`
- 不要在热路径里拼接字符串。
- 不要每帧写文件。
- 不要让某个 adapter 单独输出额外明细,导致对比不公平。
- 建议只在内存中记录结构化数值样本。
- 建议固定间隔采样,例如每 `0.5s` 或每 `30` 帧聚合一次
- 建议使用环形缓冲或预分配数组,避免压测中产生 GC
- 单轮结束后一次性导出 CSV/JSON。
- 当前实现固定每 `0.5s` 采样一次,先写入内存列表
- 单轮重置、退出或点击“导出日志”时一次性导出 CSV
- Console 只打印一行摘要,例如算法、场景、数量、平均 FPS、1% Low、平均模拟耗时、最大耗时、重叠对峰值。
百元机真机日志建议额外记录:
@@ -99,5 +101,5 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
- 使用空间哈希查找近邻,避免全量 O(n²) 扫描。
- 对每个 agent 采样多组候选速度,并用目标偏差、速度平滑、预测碰撞、当前重叠、边界越界惩罚打分。
- 它不是完整 ORCA也不保证无碰撞目的在于提供一个可读、可改、可调参的学习基线
- 它不是完整 ORCA也不保证无碰撞当前只作为自研候选的弱基线,表现明显差于 RVO2 是预期内结果
- 如果它在某些场景比 RVO2 更稳,只能说明当前启发式适合该场景,不代表算法理论上更强。