/** * 仪表盘遥测的纯逻辑:滑动窗口和按入站聚合的吞吐差值。 * * 采集本身(开流、轮询)在 web/telemetry.ts 的单例里做;这里只回答 * 「两次连接快照之间每一路走了多少字节」这类纯问题,让 bun test 够得着。 */ import type { ConnectionItem } from './connections.ts' /** 流量大图保留多久的历史。切走页面数据还在,是这个窗口在兜底。 */ export const TRAFFIC_WINDOW_MS = 30 * 60_000 /** 每路吞吐走势保留多久。活跃判定也用同一个窗口。 */ export const ROUTE_WINDOW_MS = 5 * 60_000 /** 连接快照的轮询间隔。 */ export const ROUTE_POLL_MS = 3_000 /** * 「活跃」的门槛:最近 5 分钟走过的字节数。 * 取 256 KB 是为了滤掉心跳、DNS 这类背景流量——它们在 5 分钟里通常只有几十 KB, * 而任何真实使用(网页、更新检查)轻松超过这个数。 */ export const ACTIVE_BYTES = 256 * 1024 /** 一个采样点。up/down 是这个采样间隔里走过的字节数(不是速率),ms 是实际间隔。 */ export interface RouteSample { t: number ms: number up: number down: number } /** 流量曲线的一个点。up/down 是内核报的每秒字节数。 */ export interface TrafficPoint { t: number up: number down: number } /** 追加一个点并把窗口外的老点挤掉。窗口按时间算,不按条数——采样间隔不保证均匀。 */ export function pushWindow(list: T[], point: T, windowMs: number): T[] { const cutoff = point.t - windowMs const start = list.findIndex((p) => p.t >= cutoff) return [...(start === -1 ? [] : list.slice(start)), point] } /** 上次快照里每条连接的累计字节,按连接 id 记。 */ export type ConnTotals = Map /** * 两次连接快照之间,每一路入站走了多少字节。 * * 按连接逐条求差再按入站聚合,而不是先按入站求和再求差:连接关闭后会从快照里 * 消失,直接对总和求差会算出负数。代价是连接关闭前最后一段流量丢失——对走势图 * 来说可以接受。新出现的连接把它的累计全部记入本次间隔(它就是这段时间里产生的)。 * 同一条 id 的累计变小说明内核重启过计数,按全新连接处理。 */ export function routeDeltas( prev: ConnTotals, conns: ConnectionItem[], ): { deltas: Record; next: ConnTotals } { const deltas: Record = {} const next: ConnTotals = new Map() for (const conn of conns) { const key = conn.metadata.inboundName || conn.metadata.inboundPort const last = prev.get(conn.id) const fresh = !last || last.up > conn.upload || last.down > conn.download const up = fresh ? conn.upload : conn.upload - last.up const down = fresh ? conn.download : conn.download - last.down const entry = (deltas[key] ??= { up: 0, down: 0 }) entry.up += up entry.down += down next.set(conn.id, { up: conn.upload, down: conn.download }) } return { deltas, next } } /** 窗口内一共走了多少字节(上下行合计)。活跃判定用。 */ export function windowBytes(samples: RouteSample[], since: number): number { let total = 0 for (const s of samples) { if (s.t >= since) total += s.up + s.down } return total } /** 最近一个采样的速率(字节每秒)。没有采样时是 null——「没数据」和 0 不是一回事。 */ export function lastRate(samples: RouteSample[]): { up: number; down: number } | null { const last = samples.at(-1) if (!last || last.ms <= 0) return null return { up: (last.up / last.ms) * 1000, down: (last.down / last.ms) * 1000 } }