Files
MoShiro/backend/shared/MoShiro.Measurement/Merging/ChunkFinalizer.cs
T
justme ad5efc62ff agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
2026-10-21 11:55:36 +08:00

92 lines
3.2 KiB
C#

using System.Text.Json;
using Fleet.Core;
using MoShiro.Protocol;
namespace MoShiro.Measurement;
/// <summary>Agent 单调时钟(微秒)与 UTC(毫秒)的对应关系:块头里的时钟锚点。</summary>
public readonly record struct ClockAnchor(long ListenerMonoUs, long UtcMs);
/// <summary>
/// 给合并好的流盖章:生成完整的块头(任务/测试、参数快照、端口、时钟锚点、起止时间)。
/// 起止时间由数据本身的时间戳换算得出,而不是取收到列块的时刻,所以不受拨号方交付延迟的影响。
/// </summary>
public static class ChunkFinalizer
{
public const int MaxDurationMs = 600_000;
public static RawChunk Finalize(
KindSpec spec,
RawChunk fragment,
IReadOnlyList<RawStream> streams,
NodeId agent,
ClockAnchor anchor,
IReadOnlyDictionary<string, JsonElement> resolvedParams,
int port,
ChunkId id)
{
RawChunkHeader source = fragment.Header;
long offsetUs = source.Clock.DialerOffsetUs;
long min = long.MaxValue;
long max = long.MinValue;
foreach (RawStream stream in streams)
{
if (stream.Name == "tcp_info")
{
continue; // 这个流的时钟取决于 side 列,不参与起止时间的计算
}
foreach (RawColumn column in stream.Columns)
{
if (column.Role == ColumnRole.None)
{
continue;
}
for (int i = 0; i < stream.Count; i++)
{
if (!column.IsValid(i))
{
continue;
}
long utc = ToUtcMs(column.Values[i], column.Role, anchor, offsetUs);
min = Math.Min(min, utc);
max = Math.Max(max, utc);
}
}
}
long start = min == long.MaxValue ? anchor.UtcMs - source.DurationMs : min;
int duration = (int)Math.Clamp(Math.Max(source.DurationMs, min == long.MaxValue ? 0 : max - start), 1, MaxDurationMs);
var header = new RawChunkHeader
{
ChunkId = id,
Kind = spec.Id,
TaskId = source.TaskId,
TestId = source.TestId,
Phase = source.Phase,
AgentId = agent,
DialerId = source.DialerId,
IpVersion = source.IpVersion,
Target = null,
Params = new Dictionary<string, JsonElement>(resolvedParams),
Port = port,
Direction = source.Direction,
StartUtcMs = start,
DurationMs = duration,
Partial = false,
Clock = new ChunkClock { ListenerMonoUs = anchor.ListenerMonoUs, UtcMs = anchor.UtcMs, DialerOffsetUs = offsetUs },
};
return new RawChunk(header, streams);
}
public static long ToUtcMs(long value, ColumnRole role, ClockAnchor anchor, long dialerOffsetUs)
{
long listenerUs = role == ColumnRole.TsDialer ? value + dialerOffsetUs : value;
long delta = listenerUs - anchor.ListenerMonoUs;
return anchor.UtcMs + (delta >= 0 ? delta / 1000 : -((-delta + 999) / 1000));
}
}