Files
github-actions[bot] c71da84035
Merge-upstream / merge (push) Canceled after 0s
💋 Sync 2026-10-11 00:16:14
2026-10-11 00:16:14 +08:00

95 lines
3.4 KiB
Ucode

'use strict';
// Чистая логика без I/O: качество линка -> веса. Тестируется на хосте.
// Внимание: в ucode int / int делится нацело, поэтому везде умножаем на 1.0.
export const DEFAULTS = {
total: 20, // сумма целых весов в цепочке (шаг 5%)
alpha: 0.3, // EWMA для задержки
drift: 0.01, // как быстро «обычная» задержка ползёт вверх
lat_free: 1.5, // до x1.5 от обычной задержки штрафа нет
loss_alpha: 0.1, // EWMA потерь: одиночный потерянный пинг почти не виден
loss_free: 5, // сглаженные потери до 5% штрафа не дают
loss_div: 50, // каждые 50% сверх порога снимают весь вес (с запасом 0.9)
hyst: 1.0 // применять, когда цель ушла от живого веса на >= 1 шаг
};
export function new_state() {
return { lat: null, base: null, loss: null };
};
// sample: { latency (мс), loss (%) } из mwan3track; st: состояние линка (меняется)
// Возвращает q в (0..1] или null, если данных нет.
export function quality(sample, st, cfg) {
cfg = cfg ?? DEFAULTS;
if (sample == null || sample.latency == null || sample.latency <= 0)
return null;
let lat = sample.latency;
st.lat = (st.lat == null) ? lat : st.lat + (lat - st.lat) * cfg.alpha;
st.base = (st.base == null || st.lat < st.base)
? st.lat
: st.base + (st.lat - st.base) * cfg.drift;
let r = (st.lat * 1.0) / st.base;
let q_lat = (r <= cfg.lat_free) ? 1 : cfg.lat_free / r;
let loss = (sample.loss ?? 0) * 1.0;
st.loss = (st.loss == null) ? loss : st.loss + (loss - st.loss) * cfg.loss_alpha;
let l = (st.loss - cfg.loss_free) / cfg.loss_div;
if (l < 0) l = 0;
let q_loss = 1 - (l > 0.9 ? 0.9 : l);
return q_lat * q_loss;
};
// members: [{ base (вес из uci), q (или null) }] -> вещественные цели в масштабе total
export function targets(members, total) {
let sum = 0, t = [];
for (let m in members)
sum += m.base * (m.q == null ? 1 : m.q);
for (let m in members)
push(t, sum > 0 ? (total * 1.0) * m.base * (m.q == null ? 1 : m.q) / sum : (total * 1.0) / length(members));
return t;
};
// Метод наибольших остатков, минимум 1 на участника
export function integerize(t, total) {
let n = length(t), w = [], r = [], s = 0;
for (let i = 0; i < n; i++) {
let f = int(t[i]);
push(w, f < 1 ? 1 : f);
push(r, t[i] - f);
s += w[i];
}
for (let g = 0; s < total && g < total; g++) {
let b = 0;
for (let i = 1; i < n; i++)
if (r[i] > r[b]) b = i;
w[b]++; r[b] = -1; s++;
}
for (let g = 0; s > total && g < total; g++) {
let b = 0;
for (let i = 1; i < n; i++)
if (w[i] > w[b]) b = i;
if (w[b] <= 1) break;
w[b]--; s--;
}
return w;
};
// cur: ширины диапазонов в живой цепочке (любой масштаб)
export function needs_apply(cur, t, total, hyst) {
hyst = hyst ?? DEFAULTS.hyst;
let s = 0;
for (let x in cur) s += x;
if (s <= 0 || length(cur) != length(t))
return true;
for (let i = 0; i < length(t); i++) {
let d = (cur[i] * 1.0) * total / s - t[i];
if ((d < 0 ? -d : d) >= hyst)
return true;
}
return false;
};