Compare commits
3
Commits
af385cf688
...
4e7a8c2ba3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7a8c2ba3 | ||
|
|
93abf59afb | ||
|
|
20b0f7fbd8 |
No files matched your search
@@ -85,6 +85,7 @@ public abstract class BlockBase : IBlock
|
||||
}
|
||||
|
||||
double max = Math.Max(Spec.Int("rate_max_hz"), Spec.Int("rate_min_hz"));
|
||||
ReportStart(max);
|
||||
return new AdaptiveRate(new AdaptiveRateOptions
|
||||
{
|
||||
MinHz = Spec.Int("rate_min_hz"),
|
||||
@@ -95,6 +96,22 @@ public abstract class BlockBase : IBlock
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自适应开始:记一条「start → watch」(从最高频率起步)。审计据此知道这个任务从什么时候起在自适应,
|
||||
/// 没有这条之前的时段是「没有记录」,不是某个状态。
|
||||
/// </summary>
|
||||
protected void ReportStart(double hz) =>
|
||||
Env.OnRateChange?.Invoke(new RateChange
|
||||
{
|
||||
AtMs = Env.Time.GetUtcNow().ToUnixTimeMilliseconds(),
|
||||
Task = Spec.Scope.Task?.Value,
|
||||
Kind = Spec.KindId,
|
||||
From = "start",
|
||||
To = "watch",
|
||||
Hz = Math.Round(hz, 3),
|
||||
Reason = "start",
|
||||
});
|
||||
|
||||
private void LogTransition(ProbeTransition change)
|
||||
{
|
||||
Env.OnRateChange?.Invoke(new RateChange
|
||||
|
||||
@@ -143,6 +143,7 @@ public sealed class HttpDialer : BlockBase
|
||||
}
|
||||
|
||||
double max = Math.Max(Spec.Int("rate_max_per_min"), Spec.Int("rate_min_per_min")) / 60.0;
|
||||
ReportStart(max);
|
||||
return new AdaptiveRate(new AdaptiveRateOptions
|
||||
{
|
||||
MinHz = Spec.Int("rate_min_per_min") / 60.0,
|
||||
|
||||
@@ -74,7 +74,8 @@ public static class AdaptiveAudit
|
||||
|
||||
/// <summary>
|
||||
/// 把按时间排好的状态变化还原成每个时间桶里各状态的秒数。一次变化之后到下一次变化之前,是变化后的状态和频率;
|
||||
/// 区间里第一次变化之前,状态是这次变化的「从」(频率不知道,不计入平均)。一种采集方法一条时间线(同一条线路上每种方法各自变化)。
|
||||
/// 第一次记录之前什么都不知道,不计入任何状态(节点启动时会记一条「start → watch」,所以每个自适应的任务从启动起就有记录;
|
||||
/// 没有这条记录的时段是节点还没升级到会上报的版本)。一种采集方法一条时间线(同一条线路上每种方法各自变化)。
|
||||
/// </summary>
|
||||
public static AdaptiveResult Summarize(IReadOnlyList<RateChange> changes, long fromMs, long toMs, long stepMs)
|
||||
{
|
||||
@@ -92,10 +93,10 @@ public static class AdaptiveAudit
|
||||
var maxLoss = new double[count];
|
||||
RateChange[] ordered = [.. group.OrderBy(c => c.AtMs)];
|
||||
|
||||
// 每一段:[开始, 结束) 内的状态;第一段在第一次变化之前,状态取它的「从」
|
||||
string state = ordered[0].From;
|
||||
double? hz = null;
|
||||
long cursor = Math.Min(ordered[0].AtMs, fromMs);
|
||||
// 每一段:[这次变化, 下一次变化) 内是变化后的状态和频率;第一次记录之前不知道,不计
|
||||
string state = ordered[0].To;
|
||||
double? hz = ordered[0].Hz;
|
||||
long cursor = ordered[0].AtMs;
|
||||
foreach (RateChange change in ordered)
|
||||
{
|
||||
AddSegment(cursor, change.AtMs, state, hz);
|
||||
|
||||
@@ -28,9 +28,10 @@ public sealed class AdaptiveAuditTests : IClassFixture<BackendFixture>
|
||||
{
|
||||
RateChange[] changes =
|
||||
[
|
||||
Change(s_t0 + (30 * Minute), "quic_ping", "watch", "quiet", 2), // 10:00–10:30 watch(频率未知),之后 quiet 2 Hz
|
||||
Change(s_t0, "quic_ping", "start", "watch", 10, "start"), // 10:00 起步:watch 10 Hz
|
||||
Change(s_t0 + (30 * Minute), "quic_ping", "watch", "quiet", 2), // 10:30 起 quiet 2 Hz
|
||||
Change(s_t0 + (90 * Minute), "quic_ping", "quiet", "active", 10, "sustained loss", 0.4), // 11:30 起 active 10 Hz
|
||||
Change(s_t0 + (30 * Minute), "tcp_ping", "quiet", "watch", 20, "rtt above baseline"),
|
||||
Change(s_t0 + (30 * Minute), "tcp_ping", "quiet", "watch", 20, "rtt above baseline"), // 这条方法 10:30 才有第一条记录
|
||||
];
|
||||
|
||||
AdaptiveResult result = AdaptiveAudit.Summarize(changes, s_t0, s_t0 + (2 * Hour), Hour);
|
||||
@@ -39,18 +40,18 @@ public sealed class AdaptiveAuditTests : IClassFixture<BackendFixture>
|
||||
Assert.Equal(2, quic.Buckets.Count);
|
||||
AdaptiveBucket first = quic.Buckets[0];
|
||||
Assert.Equal((1800, 1800, 0), ((int)first.WatchSeconds, (int)first.QuietSeconds, (int)first.ActiveSeconds)); // 10:00–10:30 watch,10:30–11:00 quiet
|
||||
Assert.Equal(2, first.AverageHz); // 只有 quiet 那半小时知道频率
|
||||
Assert.Equal(1, first.Changes);
|
||||
Assert.Equal(6, first.AverageHz); // (10 Hz × 30 min + 2 Hz × 30 min) / 60 min
|
||||
Assert.Equal(2, first.Changes);
|
||||
AdaptiveBucket second = quic.Buckets[1];
|
||||
Assert.Equal((1800, 1800), ((int)second.QuietSeconds, (int)second.ActiveSeconds)); // 11:00–11:30 quiet,11:30–12:00 active
|
||||
Assert.Equal(6, second.AverageHz); // (2 Hz × 30 min + 10 Hz × 30 min) / 60 min
|
||||
Assert.Equal((1, 0.4), (second.Changes, second.MaxLossPct));
|
||||
|
||||
AdaptiveKind tcp = Assert.Single(result.Kinds, k => k.Kind == "tcp_ping");
|
||||
Assert.Equal((1800, 1800), ((int)tcp.Buckets[0].QuietSeconds, (int)tcp.Buckets[0].WatchSeconds));
|
||||
Assert.Equal((0, 1800), ((int)tcp.Buckets[0].QuietSeconds, (int)tcp.Buckets[0].WatchSeconds)); // 10:00–10:30 没有记录,不算任何状态
|
||||
Assert.Equal(3600, (int)tcp.Buckets[1].WatchSeconds);
|
||||
|
||||
Assert.Equal(3, result.Changes.Count);
|
||||
Assert.Equal(4, result.Changes.Count);
|
||||
Assert.Equal("quic_ping", result.Changes[0].Kind); // 最新的在前
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ const STATES = [
|
||||
{ key: 'quiet', label: '平稳', level: 'ok' },
|
||||
{ key: 'watch', label: '观察', level: 'warn' },
|
||||
{ key: 'active', label: '活跃', level: 'bad' },
|
||||
{ key: 'dead', label: '不通', level: 'none' },
|
||||
{ key: 'dead', label: '不通', level: 'dead' },
|
||||
] as const
|
||||
type StateKey = (typeof STATES)[number]['key']
|
||||
|
||||
const KIND_NAMES: Record<string, string> = { quic_ping: 'QUIC 往返', tcp_ping: 'TCP 往返', tcp_connect: 'TCP 建连', http_request: 'HTTP 请求' }
|
||||
const STATE_LABEL: Record<string, string> = { quiet: '平稳', watch: '观察', active: '活跃', dead: '不通' }
|
||||
const STATE_LABEL: Record<string, string> = { start: '启动', quiet: '平稳', watch: '观察', active: '活跃', dead: '不通' }
|
||||
|
||||
const secondsOf = (b: AdaptiveBucketDto, state: StateKey) => b[`${state}_seconds` as const]
|
||||
|
||||
@@ -141,6 +141,9 @@ export function AdaptiveAudit({ lineId }: { lineId: string }) {
|
||||
<i className={`bar ${s.level}`} /> {s.label}
|
||||
</span>
|
||||
))}
|
||||
<span>
|
||||
<i className="bar none" /> 无记录
|
||||
</span>
|
||||
</div>
|
||||
{data.kinds.map((k) => (
|
||||
<Row key={k.kind} kind={k.kind} buckets={k.buckets} step={info.step} />
|
||||
|
||||
@@ -2752,3 +2752,9 @@ input {
|
||||
color: var(--app-ink-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 自适应审计里「不通」:比「没有记录」(灰、半透明)深 */
|
||||
.bar.dead {
|
||||
background: var(--app-ink-muted);
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -40,10 +40,19 @@ export function fmtOffset(ms: number): string {
|
||||
return h > 0 ? `${h}:${two(m)}${sec ? ':' + two(sec) : ''}` : `${m}:${two(sec)}`
|
||||
}
|
||||
|
||||
/** 有值的格子不到三分之一(而且至少有一个)算稀疏。 */
|
||||
/**
|
||||
* 稀疏:有值的点里,前后都是空的(孤立的)占三成以上。每 10 秒才一次的建连放在 5 秒的格子里就是隔一格有一格没有,
|
||||
* 每个点都是孤立的,连不成线,图上什么都看不见;这种序列画出点,并把点连起来。
|
||||
*/
|
||||
function isSparse(values: (number | null)[]): boolean {
|
||||
const filled = values.filter((v) => v !== null).length
|
||||
return filled > 0 && filled < values.length / 3
|
||||
let filled = 0
|
||||
let isolated = 0
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (values[i] === null) continue
|
||||
filled++
|
||||
if ((i === 0 || values[i - 1] === null) && (i === values.length - 1 || values[i + 1] === null)) isolated++
|
||||
}
|
||||
return filled > 0 && isolated / filled >= 0.3
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user