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
|
try
|
||||||
{
|
{
|
||||||
ContractChecks.Run();
|
ContractChecks.Run();
|
||||||
|
RuntimeChecks.Run();
|
||||||
Console.WriteLine("PASS trajectory-planning-visualization");
|
Console.WriteLine("PASS trajectory-planning-visualization");
|
||||||
return 0;
|
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