feat(avoidance): 添加低开销压测日志
This commit is contained in:
156
FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs
Normal file
156
FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c67563022844219b17fcf6a3630062c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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()
|
||||
|
||||
@@ -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 更稳,只能说明当前启发式适合该场景,不代表算法理论上更强。
|
||||
|
||||
Reference in New Issue
Block a user