🦄 Sync 2026-09-17 23:34:47

This commit is contained in:
github-actions[bot] committed 2026-09-17 23:34:47 +08:00
1 parent 6ebed6f578
commit 5fc8a48415
29 files changed
+1816 -1317

No files matched your search

@@ -9,7 +9,7 @@
*/
return baseclass.extend({
/* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */
APP_VERSION: '0.1.42',
APP_VERSION: '0.1.43',
ROW_LIMIT_OPTIONS: [25, 50, 100, 250, 500, 1000, 2000],
DEFAULT_ROW_LIMIT: 100,
FETCH_MODE_OPTIONS: ['auto', 'manual'],
@@ -0,0 +1,131 @@
'use strict';
/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright 2025-2026 Lucas Albers <lucas.b.albers@gmail.com> */
'require baseclass';
/* Own queued render intent and the token bucket; the view owns DOM work.
* Adapters supply the epoch, paint callback, cost policy, clock and frame APIs.
* Without a frame API, schedule() paints synchronously. */
function createScheduler(options) {
const { getEpoch, render, renderCost, capacity, requestFrame, cancelFrame } = options;
if (!Number.isFinite(capacity) || capacity <= 0)
throw new TypeError('capacity must be a positive number');
const now = options.now || Date.now;
let pending = null;
let nextForce = null;
let frameId = null;
let bucket = capacity;
let bucketMs = null;
let floodSuppressed = false;
let lastRenderedRowCount = 0;
let lastRenderedHeadId = '';
let disposed = false;
function schedule(force) {
if (disposed) return;
force = !!force || nextForce !== null;
nextForce = null;
const epoch = getEpoch();
if (typeof requestFrame !== 'function') {
render(!!force);
return;
}
if (pending) {
/* Retain display changes (e.g. resolved names) even when a new epoch
* replaces the frame. The callback always reads current view state. */
pending.force = pending.force || !!force;
pending.epoch = epoch;
return;
}
pending = { epoch: epoch, force: !!force };
frameId = requestFrame(function () {
const request = pending;
pending = null;
frameId = null;
if (disposed || request.epoch !== getEpoch()) return;
/* A current request queued behind stale work gets its own frame. */
if (epoch !== request.epoch) schedule(request.force);
else render(request.force);
});
}
/* A successful decision reserves tokens, but only markRendered commits
* row identity after the view has actually painted. Forced paints bypass
* the bucket without clearing an existing high-rate warning. */
function shouldRender(rows, force, lastBatchNewIdCount) {
if (disposed) return false;
if (force) return true;
const cost = renderCost({
visibleRowCount: rows.length,
visibleHeadId: rows.length ? rows[0].id : '',
lastRenderedRowCount: lastRenderedRowCount,
lastRenderedHeadId: lastRenderedHeadId,
lastBatchNewIdCount: lastBatchNewIdCount
});
if (cost <= 0) {
floodSuppressed = false;
return false;
}
const timestamp = now();
const elapsed = bucketMs === null ? 0 : Math.max(0, timestamp - bucketMs);
bucketMs = timestamp;
bucket = Math.min(capacity, bucket + (elapsed * capacity) / 1000);
floodSuppressed = cost > bucket;
if (floodSuppressed) return false;
bucket -= cost;
return true;
}
function markRendered(rows) {
lastRenderedRowCount = rows.length;
lastRenderedHeadId = rows.length ? rows[0].id : '';
}
function resetBudget() {
bucket = capacity;
bucketMs = now();
floodSuppressed = false;
}
/* Reserve force for the next scheduled render. The caller can cancel its
* own unused reservation, never a queued frame or a newer reservation. */
function forceNextRender() {
const reservation = {};
if (!disposed) nextForce = reservation;
return function () {
if (nextForce === reservation) nextForce = null;
};
}
function dispose() {
if (disposed) return;
disposed = true;
pending = null;
nextForce = null;
if (frameId !== null && typeof cancelFrame === 'function') cancelFrame(frameId);
frameId = null;
}
return {
schedule: schedule,
shouldRender: shouldRender,
markRendered: markRendered,
resetBudget: resetBudget,
forceNextRender: forceNextRender,
isFloodSuppressed: function () {
return floodSuppressed;
},
dispose: dispose
};
}
return baseclass.extend({
create: createScheduler
});
@@ -21,6 +21,7 @@
'require fwlive.proto as proto';
'require fwlive.poll-coordinator as pollCoordinator';
'require fwlive.render-policy as renderPolicy';
'require fwlive.render-scheduler as renderScheduler';
const callFwlivePoll = rpc.declare({
object: 'fwlive',
@@ -108,6 +109,7 @@ return view.extend({
/* One-shot: first live poll after unpause merges instead of replacing. */
resumeMerge: false,
pollCoordinator: null,
renderScheduler: null,
pagehideHandler: null,
/* Layer 2 — visibility / RTT cadence / shed surfacing. */
rttStreakKind: null,
@@ -126,14 +128,8 @@ return view.extend({
summaryRowsShown: false,
summaryData: null,
resolveLoadShed: false,
renderRaf: 0,
filterInputTimer: null,
messageLayout: 'wrap',
renderBucket: constants.RENDER_CAP_PER_SEC,
renderBucketMs: 0,
floodSuppressed: false,
pendingForceRender: false,
pendingRenderEpoch: null,
/* Session-new IDs from the last applied batch; this is not buffer growth. */
lastBatchNewIdCount: 0,
showHostnames: false,
@@ -146,8 +142,6 @@ return view.extend({
resolveGeneration: 0,
lastPollError: false,
lastRulesError: null,
lastRenderedRowCount: 0,
lastRenderedHeadId: '',
followLive: true,
rulesMap: {},
firewallBackend: 'nft',
@@ -1002,7 +996,8 @@ return view.extend({
const cap = this.ingestCap();
if (this.entries.length >= cap && cap > 0) bits.push(_('buffer full'));
if (this.floodSuppressed) bits.push(_('render paused (high rate)'));
if (this.ensureRenderScheduler().isFloodSuppressed())
bits.push(_('render paused (high rate)'));
if (this.weakDevice && this.rowLimit > constants.WEAK_DEVICE_DISPLAY_ROW_CAP)
bits.push(
_('Display limited to %d rows on this device').format(
@@ -1072,6 +1067,26 @@ return view.extend({
return this.pollCoordinator;
},
ensureRenderScheduler() {
if (this.renderScheduler) return this.renderScheduler;
this.renderScheduler = renderScheduler.create({
getEpoch: () => this.currentPollEpoch(),
render: (force) => this.renderRows(force),
renderCost: renderPolicy.renderCost,
now: () => this.nowMs(),
capacity: constants.RENDER_CAP_PER_SEC,
requestFrame:
typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function'
? (cb) => window.requestAnimationFrame(cb)
: null,
cancelFrame:
typeof window !== 'undefined' && typeof window.cancelAnimationFrame === 'function'
? (id) => window.cancelAnimationFrame(id)
: null
});
return this.renderScheduler;
},
currentPollEpoch() {
return this.ensurePollCoordinator().getState().epoch;
},
@@ -1112,6 +1127,7 @@ return view.extend({
disposeView() {
if (this.pollCoordinator) this.pollCoordinator.dispose();
if (this.renderScheduler) this.renderScheduler.dispose();
this.resolveGeneration = (this.resolveGeneration || 0) + 1;
this.resolveInFlight = false;
if (this.pagehideHandler) {
@@ -1283,76 +1299,7 @@ return view.extend({
},
scheduleRenderRows(force) {
const doForce = !!force;
const epoch = this.currentPollEpoch();
if (typeof requestAnimationFrame !== 'function') {
this.renderRows(doForce);
return;
}
if (this.renderRaf) {
this.pendingForceRender = this.pendingForceRender || doForce;
this.pendingRenderEpoch = epoch;
return;
}
this.renderRaf = requestAnimationFrame(
function () {
this.renderRaf = 0;
const f = doForce || !!this.pendingForceRender;
const pendingEpoch = this.pendingRenderEpoch;
this.pendingForceRender = false;
this.pendingRenderEpoch = null;
if (epoch !== this.currentPollEpoch()) {
/* A newer epoch may have requested a render while this frame was
* queued. Drop stale paint and requeue only that current request. */
if (pendingEpoch === this.currentPollEpoch()) this.scheduleRenderRows(f);
return;
}
this.renderRows(f);
}.bind(this)
);
},
refillRenderBucket() {
const now = Date.now();
if (!this.renderBucketMs) this.renderBucketMs = now;
const elapsed = now - this.renderBucketMs;
this.renderBucketMs = now;
this.renderBucket = Math.min(
constants.RENDER_CAP_PER_SEC,
this.renderBucket + (elapsed * constants.RENDER_CAP_PER_SEC) / 1000
);
},
consumeRenderBudget(cost) {
if (cost <= 0) {
this.floodSuppressed = false;
return true;
}
this.refillRenderBucket();
if (cost <= this.renderBucket) {
this.renderBucket -= cost;
this.floodSuppressed = false;
return true;
}
this.floodSuppressed = true;
return false;
},
/** Charge by new log events per poll, not full table size (avoids false throttle at high limits). */
renderBudgetCost(rows) {
const count = rows ? rows.length : 0;
const headId = count ? rows[0].id : '';
return renderPolicy.renderCost({
visibleRowCount: count,
visibleHeadId: headId,
lastRenderedRowCount: this.lastRenderedRowCount,
lastRenderedHeadId: this.lastRenderedHeadId,
lastBatchNewIdCount: this.lastBatchNewIdCount
});
this.ensureRenderScheduler().schedule(!!force);
},
updateFloodBanner() {
@@ -1360,7 +1307,7 @@ return view.extend({
if (!el) return;
if (!el.style) el.style = { display: '' };
if (this.floodSuppressed) {
if (this.ensureRenderScheduler().isFloodSuppressed()) {
el.style.display = 'block';
el.textContent = _(
'High event rate — table refresh is throttled to protect the browser. The buffer still updates; refresh will resume automatically.'
@@ -1547,9 +1494,8 @@ return view.extend({
this.saveRowLimit();
this.updateHash(this.readFilters());
/* Reset flood throttle so Limit changes paint even during ping -A. */
this.renderBucket = constants.RENDER_CAP_PER_SEC;
this.floodSuppressed = false;
this.pendingForceRender = true;
this.ensureRenderScheduler().resetBudget();
const cancelForce = this.ensureRenderScheduler().forceNextRender();
if (!this.tablePaused) this.renderRows(true);
else this.updateStatus();
const epoch = this.currentPollEpoch();
@@ -1560,9 +1506,7 @@ return view.extend({
if (this.tablePaused) this.updateStatus();
else this.renderRows(true);
})
.finally(() => {
this.pendingForceRender = false;
});
.finally(cancelForce);
},
limitSelectOptions() {
@@ -1825,17 +1769,14 @@ return view.extend({
this.updateHash(this.readFilters());
const rows = this.filteredRows();
const cost = force ? Math.max(1, rows.length) : this.renderBudgetCost(rows);
const paint = this.ensureRenderScheduler().shouldRender(
rows,
!!force,
this.lastBatchNewIdCount
);
this.updateLoggingToolbarUi();
if (!force && cost === 0) {
this.floodSuppressed = false;
this.updateFloodBanner();
this.updateStatus(rows);
return;
}
if (!force && !this.consumeRenderBudget(cost)) {
if (!paint) {
this.updateFloodBanner();
this.updateStatus(rows);
return;
@@ -1875,15 +1816,14 @@ return view.extend({
else scroll.scrollTop = prevScroll;
}
this.lastRenderedRowCount = rows.length;
this.lastRenderedHeadId = rows.length ? rows[0].id : '';
this.ensureRenderScheduler().markRendered(rows);
if (rows.length && this.rowTintEnabled() && !this.tintProbeDone) {
const runProbe = () => {
if (!this.tintProbeDone) this.probeRowTintPaint();
};
if (typeof requestAnimationFrame === 'function')
requestAnimationFrame(() => requestAnimationFrame(runProbe));
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function')
window.requestAnimationFrame(() => window.requestAnimationFrame(runProbe));
else setTimeout(runProbe, 0);
} else if (!this.rowTintEnabled() && this.tintFallbackActive) {
this.clearTintFallback(document.querySelector('.fwlive-map'));
@@ -1997,7 +1937,7 @@ return view.extend({
* and stays behind the explicit Show rows control. */
if (this.tablePaused) this.updateStatus();
else if (this.summaryMode) this.renderSummary();
else this.scheduleRenderRows(!!this.pendingForceRender);
else this.scheduleRenderRows();
try {
await this.resolveHostnamesForEntries(this.filteredRows());