mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-09 13:38:05 +08:00
🦄 Sync 2026-09-17 23:34:47
This commit is contained in:
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());
|
||||
|
||||
Reference in new issue
Block a user