feat: add bounded visualization snapshots

This commit is contained in:
梁薄云
2026-08-06 09:27:58 +08:00
parent 55d8e1eebf
commit ab8e4010f8
6 changed files with 269 additions and 0 deletions
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace TrajectoryPlanningVisualization;
public sealed class BoundedCycleHistory
{
private readonly object sync = new object();
private readonly int capacity;
private readonly Queue<VisualizationCycleSummary> entries = new Queue<VisualizationCycleSummary>();
private readonly HashSet<long> versions = new HashSet<long>();
public BoundedCycleHistory(int capacity)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(capacity));
}
this.capacity = capacity;
}
public void Add(VisualizationCycleSummary summary)
{
if (summary == null)
{
throw new ArgumentNullException(nameof(summary));
}
lock (sync)
{
if (!versions.Add(summary.CycleVersion))
{
return;
}
entries.Enqueue(summary);
while (entries.Count > capacity)
{
VisualizationCycleSummary removed = entries.Dequeue();
versions.Remove(removed.CycleVersion);
}
}
}
public IReadOnlyList<VisualizationCycleSummary> Snapshot()
{
lock (sync)
{
return new ReadOnlyCollection<VisualizationCycleSummary>(
new List<VisualizationCycleSummary>(entries));
}
}
}
@@ -0,0 +1,46 @@
using System;
using System.Threading;
namespace TrajectoryPlanningVisualization;
public sealed class VisualizationFrame
{
public VisualizationFrame(long version, PlanningVisualizationDynamicSnapshot snapshot)
{
Version = version;
Snapshot = snapshot ?? throw new ArgumentNullException(nameof(snapshot));
}
public long Version { get; }
public PlanningVisualizationDynamicSnapshot Snapshot { get; }
}
public sealed class LatestVisualizationFrameStore
{
private VisualizationFrame latest;
private long nextVersion;
public void Publish(PlanningVisualizationDynamicSnapshot snapshot)
{
if (snapshot == null)
{
throw new ArgumentNullException(nameof(snapshot));
}
long version = Interlocked.Increment(ref nextVersion);
Interlocked.Exchange(ref latest, new VisualizationFrame(version, snapshot));
}
public bool TryReadAfter(long version, out VisualizationFrame frame)
{
VisualizationFrame current = Volatile.Read(ref latest);
if (current == null || current.Version <= version)
{
frame = null;
return false;
}
frame = current;
return true;
}
}
@@ -0,0 +1,45 @@
using System;
namespace TrajectoryPlanningVisualization;
public sealed class PlanningVisualizationOptions
{
public int Port { get; set; } = 0;
public int RefreshRateHz { get; set; } = 10;
public int HistoryCycleLimit { get; set; } = 60;
public PlanningVisualizationOptionsSnapshot CreateValidatedSnapshot()
{
if (Port < 0 || Port > 65535)
{
throw new ArgumentOutOfRangeException(nameof(Port), "Port must be in the range 0 to 65535.");
}
if (RefreshRateHz <= 0)
{
throw new ArgumentOutOfRangeException(nameof(RefreshRateHz), "Refresh rate must be positive.");
}
if (HistoryCycleLimit <= 0)
{
throw new ArgumentOutOfRangeException(nameof(HistoryCycleLimit), "History cycle limit must be positive.");
}
return new PlanningVisualizationOptionsSnapshot(Port, RefreshRateHz, HistoryCycleLimit);
}
}
public sealed class PlanningVisualizationOptionsSnapshot
{
internal PlanningVisualizationOptionsSnapshot(int port, int refreshRateHz, int historyCycleLimit)
{
Port = port;
RefreshRateHz = refreshRateHz;
HistoryCycleLimit = historyCycleLimit;
}
public int Port { get; }
public int RefreshRateHz { get; }
public int HistoryCycleLimit { get; }
public int MaximumClients => 2;
}
@@ -0,0 +1,21 @@
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace TrajectoryPlanningVisualization;
public static class VisualizationJson
{
private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Culture = CultureInfo.InvariantCulture,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
Formatting = Formatting.None
};
public static string Serialize(object value)
{
return JsonConvert.SerializeObject(value, Settings);
}
}
@@ -9,6 +9,7 @@ internal static class Program
try
{
ContractChecks.Run();
RuntimeChecks.Run();
Console.WriteLine("PASS trajectory-planning-visualization");
return 0;
}
@@ -0,0 +1,101 @@
using System;
using System.Linq;
using TrajectoryPlanningVisualization;
namespace TrajectoryPlanningVisualizationVerificationHost;
internal static class RuntimeChecks
{
public static void Run()
{
KeepsOnlyTheLatestFrame();
KeepsBoundedDeduplicatedCycleHistory();
SerializesStableCamelCaseJson();
RejectsInvalidOptions();
}
private static void KeepsOnlyTheLatestFrame()
{
var store = new LatestVisualizationFrameStore();
store.Publish(Snap(1));
store.Publish(Snap(2));
Verification.True(store.TryReadAfter(0, out VisualizationFrame frame), "latest frame exists");
Verification.Equal(2L, frame.Snapshot.Sequence, "latest frame replaces old frame");
Verification.True(!store.TryReadAfter(frame.Version, out _), "same frame is not replayed");
}
private static void KeepsBoundedDeduplicatedCycleHistory()
{
var history = new BoundedCycleHistory(2);
history.Add(Cycle(1));
history.Add(Cycle(2));
history.Add(Cycle(3));
history.Add(Cycle(3));
Verification.Equal("2|3", string.Join("|", history.Snapshot().Select(x => x.CycleVersion)),
"history is bounded and deduplicated");
}
private static void SerializesStableCamelCaseJson()
{
string json = VisualizationJson.Serialize(Snap(2));
Verification.True(json.Contains("\"sessionStateChinese\"") && json.Contains("\"observedAtUtc\""),
"JSON uses stable camel case names");
}
private static void RejectsInvalidOptions()
{
Verification.Throws<ArgumentOutOfRangeException>(
() => new PlanningVisualizationOptions { Port = -1 }.CreateValidatedSnapshot(),
"options reject a negative port");
Verification.Throws<ArgumentOutOfRangeException>(
() => new PlanningVisualizationOptions { RefreshRateHz = 0 }.CreateValidatedSnapshot(),
"options reject a zero refresh rate");
Verification.Throws<ArgumentOutOfRangeException>(
() => new PlanningVisualizationOptions { HistoryCycleLimit = 0 }.CreateValidatedSnapshot(),
"options reject a zero history limit");
Verification.Throws<ArgumentOutOfRangeException>(
() => new PlanningVisualizationOptions { Port = 65536 }.CreateValidatedSnapshot(),
"options reject a port above the TCP range");
var snapshot = new PlanningVisualizationOptions().CreateValidatedSnapshot();
Verification.Equal(0, snapshot.Port, "default port is automatic");
Verification.Equal(10, snapshot.RefreshRateHz, "default refresh rate is 10 Hz");
Verification.Equal(60, snapshot.HistoryCycleLimit, "default history is bounded to 60 cycles");
Verification.Equal(2, snapshot.MaximumClients, "maximum clients is fixed at two");
}
private static PlanningVisualizationDynamicSnapshot Snap(long sequence)
{
return new PlanningVisualizationDynamicSnapshot(
sequence,
DateTimeOffset.Parse("2026-08-06T00:00:00+00:00").AddSeconds(sequence),
"运行中",
0,
"forward",
new VisualizationPose(1d, 2d, 0d),
Array.Empty<VisualizationPolyline>(),
Array.Empty<VisualizationMarker>(),
Array.Empty<VisualizationChart>(),
Array.Empty<VisualizationValue>(),
Cycle(sequence));
}
private static VisualizationCycleSummary Cycle(long cycleVersion)
{
return new VisualizationCycleSummary(
cycleVersion,
DateTimeOffset.Parse("2026-08-06T00:00:00+00:00").AddSeconds(cycleVersion),
"成功",
true,
2.5d,
0,
"forward",
"rolling",
"none",
null,
null,
"");
}
}