using System.Text.Json;
using Fleet.Core;
using Fleet.Link;
using MoShiro.Measurement;
using MoShiro.Protocol;
namespace MoShiro.Probe;
///
/// 估计 Agent 单调时钟 − Probe 单调时钟:每次 moshiro.clock 往返取中点,在最近 8 次里取往返最短的一次
/// (往返越短,对称性假设的误差越小)。Probe 的列块头里带着这个偏移,Backend 据此对齐两端的时间戳。
///
public sealed class ClockEstimator
{
private const int Window = 8;
private readonly object _gate = new();
private readonly Queue<(long RoundTripUs, long OffsetUs)> _samples = new();
public long OffsetUs { get; private set; }
public bool HasEstimate { get; private set; }
public void Add(long sendUs, long agentUs, long receiveUs)
{
lock (_gate)
{
_samples.Enqueue((receiveUs - sendUs, agentUs - ((sendUs + receiveUs) / 2)));
while (_samples.Count > Window)
{
_samples.Dequeue();
}
OffsetUs = _samples.MinBy(s => s.RoundTripUs).OffsetUs;
HasEstimate = true;
}
}
}
///
/// 把 Probe 一侧的行交给这个 Agent:补全块头 → 编码 → 放进会话的发件箱(通道 17,至少一次,有界)。
/// 发件箱的内容在连接断开重连之后重发;发件箱满了丢弃最旧的。
///
public sealed class FragmentShipper : IFragmentSink
{
private readonly Outbox _outbox;
private readonly NodeId _agent;
private readonly ProbeId _probe;
private readonly ClockEstimator _clock;
private readonly TimeProvider _time;
public FragmentShipper(Outbox outbox, NodeId agent, ProbeId probe, ClockEstimator clock, TimeProvider time)
{
_outbox = outbox;
_agent = agent;
_probe = probe;
_clock = clock;
_time = time;
}
public void Ship(string kind, FragmentScope scope, IReadOnlyList streams, IReadOnlyDictionary? parameters, long startUtcMs, int durationMs, int port)
{
var header = new RawChunkHeader
{
ChunkId = ChunkId.New(_time),
Kind = kind,
TaskId = scope.Task,
TestId = scope.Test,
Phase = scope.Phase,
AgentId = _agent,
ProbeId = _probe,
Port = port,
StartUtcMs = startUtcMs,
DurationMs = Math.Max(1, durationMs),
Clock = new ChunkClock { ProbeOffsetUs = _clock.OffsetUs },
};
if (parameters is not null)
{
foreach ((string key, JsonElement value) in parameters)
{
header.Params[key] = value;
}
}
_outbox.Enqueue(MoShiroChannels.ColumnBlock, header.ChunkId.Value, RawChunkCodec.Encode(new RawChunk(header, streams)));
}
}