feat: add bounded visualization snapshots
This commit is contained in:
@@ -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,
|
||||
"");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user