perf(ui,watchdog): adaptive UI polling and zero-UCI cached settings context
- Feat(ui): implement adaptive polling in runtimeUiState service (1s -> 2s during active tasks, 10s idle) - Perf(controller): cache settings snapshot in current_tick_ctx across all probe setting() calls to eliminate repeated uci_core.get_all() - Perf(controller): reuse ctx.proxy_port in probe_proxy to eliminate duplicate proxy_port() evaluation - Perf(watchdog): guard adaptive_normal_interval behind 120s check and clear tick context at tick boundary - Test: add static and behavioral assertions in tests/watchdog_adaptive_intervals.sh
This commit is contained in:
@@ -5,7 +5,9 @@ import { applyUiStateToStore } from './uiState.service';
|
||||
|
||||
const RUNTIME_UI_STATE_REFRESH_MIN_INTERVAL_MS = 500;
|
||||
const RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS = 10_000;
|
||||
const RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS = 500;
|
||||
const RUNTIME_UI_STATE_ACTIVE_INITIAL_POLL_INTERVAL_MS = 1_000;
|
||||
const RUNTIME_UI_STATE_ACTIVE_SLOW_POLL_INTERVAL_MS = 2_000;
|
||||
const RUNTIME_UI_STATE_ACTIVE_SLOW_THRESHOLD_MS = 4_000;
|
||||
// A hidden tab must not keep spawning shell commands on the router:
|
||||
// back off to one request per 60s while hidden (a force refresh on
|
||||
// visibilitychange covers the moment the tab comes back).
|
||||
@@ -20,6 +22,7 @@ let runtimeStateResumeRefreshRegistered = false;
|
||||
let runtimeStatePollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let runtimeStatePollingStarted = false;
|
||||
let runtimeStateHasRunningAction = false;
|
||||
let runningActionStartedAt = 0;
|
||||
const runtimeUiStateListeners = new Set<RuntimeUiStateListener>();
|
||||
|
||||
function isDocumentVisible() {
|
||||
@@ -40,9 +43,17 @@ function getNextPollDelay() {
|
||||
if (!isDocumentVisible()) {
|
||||
return RUNTIME_UI_STATE_HIDDEN_POLL_INTERVAL_MS;
|
||||
}
|
||||
return runtimeStateHasRunningAction
|
||||
? RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS
|
||||
: RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS;
|
||||
if (!runtimeStateHasRunningAction) {
|
||||
runningActionStartedAt = 0;
|
||||
return RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS;
|
||||
}
|
||||
if (runningActionStartedAt === 0) {
|
||||
runningActionStartedAt = Date.now();
|
||||
}
|
||||
const elapsed = Date.now() - runningActionStartedAt;
|
||||
return elapsed < RUNTIME_UI_STATE_ACTIVE_SLOW_THRESHOLD_MS
|
||||
? RUNTIME_UI_STATE_ACTIVE_INITIAL_POLL_INTERVAL_MS
|
||||
: RUNTIME_UI_STATE_ACTIVE_SLOW_POLL_INTERVAL_MS;
|
||||
}
|
||||
|
||||
function scheduleRuntimeUiStatePoll(delay = getNextPollDelay()) {
|
||||
@@ -94,6 +105,9 @@ export async function refreshRuntimeUiState({
|
||||
}
|
||||
|
||||
lastRuntimeUiStateRefreshAt = now;
|
||||
if (force) {
|
||||
runningActionStartedAt = now;
|
||||
}
|
||||
|
||||
const promise = TachyonShellMethods.getUiState()
|
||||
.then((response) => {
|
||||
@@ -104,6 +118,9 @@ export async function refreshRuntimeUiState({
|
||||
applyUiStateToStore(response.data);
|
||||
lastRuntimeUiState = response.data;
|
||||
runtimeStateHasRunningAction = hasRunningAction(response.data);
|
||||
if (!runtimeStateHasRunningAction) {
|
||||
runningActionStartedAt = 0;
|
||||
}
|
||||
notifyRuntimeUiStateListeners(response.data);
|
||||
return response.data;
|
||||
})
|
||||
|
||||
@@ -6814,7 +6814,9 @@ function applyUiStateToStore(uiState) {
|
||||
// src/tachyon/services/runtimeUiState.service.ts
|
||||
var RUNTIME_UI_STATE_REFRESH_MIN_INTERVAL_MS = 500;
|
||||
var RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS = 1e4;
|
||||
var RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS = 500;
|
||||
var RUNTIME_UI_STATE_ACTIVE_INITIAL_POLL_INTERVAL_MS = 1e3;
|
||||
var RUNTIME_UI_STATE_ACTIVE_SLOW_POLL_INTERVAL_MS = 2e3;
|
||||
var RUNTIME_UI_STATE_ACTIVE_SLOW_THRESHOLD_MS = 4e3;
|
||||
var RUNTIME_UI_STATE_HIDDEN_POLL_INTERVAL_MS = 6e4;
|
||||
var runtimeUiStateRefreshPromise = null;
|
||||
var lastRuntimeUiStateRefreshAt = 0;
|
||||
@@ -6823,6 +6825,7 @@ var runtimeStateResumeRefreshRegistered = false;
|
||||
var runtimeStatePollTimer = null;
|
||||
var runtimeStatePollingStarted = false;
|
||||
var runtimeStateHasRunningAction = false;
|
||||
var runningActionStartedAt = 0;
|
||||
var runtimeUiStateListeners = /* @__PURE__ */ new Set();
|
||||
function isDocumentVisible() {
|
||||
return typeof document === "undefined" || !document.visibilityState || document.visibilityState === "visible";
|
||||
@@ -6836,7 +6839,15 @@ function getNextPollDelay() {
|
||||
if (!isDocumentVisible()) {
|
||||
return RUNTIME_UI_STATE_HIDDEN_POLL_INTERVAL_MS;
|
||||
}
|
||||
return runtimeStateHasRunningAction ? RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS : RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS;
|
||||
if (!runtimeStateHasRunningAction) {
|
||||
runningActionStartedAt = 0;
|
||||
return RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS;
|
||||
}
|
||||
if (runningActionStartedAt === 0) {
|
||||
runningActionStartedAt = Date.now();
|
||||
}
|
||||
const elapsed = Date.now() - runningActionStartedAt;
|
||||
return elapsed < RUNTIME_UI_STATE_ACTIVE_SLOW_THRESHOLD_MS ? RUNTIME_UI_STATE_ACTIVE_INITIAL_POLL_INTERVAL_MS : RUNTIME_UI_STATE_ACTIVE_SLOW_POLL_INTERVAL_MS;
|
||||
}
|
||||
function scheduleRuntimeUiStatePoll(delay = getNextPollDelay()) {
|
||||
if (!runtimeStatePollingStarted || runtimeStatePollTimer || typeof window === "undefined") {
|
||||
@@ -6872,6 +6883,9 @@ async function refreshRuntimeUiState({
|
||||
return void 0;
|
||||
}
|
||||
lastRuntimeUiStateRefreshAt = now;
|
||||
if (force) {
|
||||
runningActionStartedAt = now;
|
||||
}
|
||||
const promise = TachyonShellMethods.getUiState().then((response) => {
|
||||
if (!response.success) {
|
||||
return void 0;
|
||||
@@ -6879,6 +6893,9 @@ async function refreshRuntimeUiState({
|
||||
applyUiStateToStore(response.data);
|
||||
lastRuntimeUiState = response.data;
|
||||
runtimeStateHasRunningAction = hasRunningAction(response.data);
|
||||
if (!runtimeStateHasRunningAction) {
|
||||
runningActionStartedAt = 0;
|
||||
}
|
||||
notifyRuntimeUiStateListeners(response.data);
|
||||
return response.data;
|
||||
}).catch((error) => {
|
||||
|
||||
@@ -91,7 +91,11 @@ function command_output_from_args(args) {
|
||||
return result.status == 0 ? result.output : "";
|
||||
}
|
||||
|
||||
let current_tick_ctx = null;
|
||||
|
||||
function settings() {
|
||||
if (current_tick_ctx && current_tick_ctx.settings)
|
||||
return current_tick_ctx.settings;
|
||||
return common.object_or_empty(uci_core.get_all(CONFIG_NAME, "settings"));
|
||||
}
|
||||
|
||||
@@ -417,7 +421,7 @@ function controller(bus, opts) {
|
||||
};
|
||||
|
||||
let self = { EV: EV, state: state };
|
||||
let current_tick_ctx = null;
|
||||
current_tick_ctx = null;
|
||||
|
||||
// Keeps the last 20 samples. Bounded so a long-lived watchdog cannot grow
|
||||
// its heap through the history array.
|
||||
@@ -509,7 +513,7 @@ function controller(bus, opts) {
|
||||
// and duplicate pid lookups.
|
||||
function create_tick_context() {
|
||||
let now = time();
|
||||
let cfg = settings();
|
||||
let cfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "settings"));
|
||||
let reload = is_reload_in_progress();
|
||||
let list_upd = is_list_update_running();
|
||||
let sb_pid = get_sing_box_pid();
|
||||
@@ -531,6 +535,8 @@ function controller(bus, opts) {
|
||||
};
|
||||
}
|
||||
self.create_tick_context = create_tick_context;
|
||||
self.set_tick_context = function(ctx) { current_tick_ctx = ctx; };
|
||||
self.clear_tick_context = function() { current_tick_ctx = null; };
|
||||
|
||||
// ── Probe: sing-box liveness ──────────────────────────────────────────────
|
||||
// The expensive /proc scan only runs once the cheap pidfile/ubus/pidof
|
||||
@@ -580,7 +586,7 @@ function controller(bus, opts) {
|
||||
let pid = ctx.singbox_pid;
|
||||
if (pid == "" || !ctx.singbox_running) return;
|
||||
|
||||
let port = proxy_port();
|
||||
let port = ctx.proxy_port;
|
||||
// No http/mixed inbound in the generated config: there is nothing to
|
||||
// measure, so the proxy must not be declared broken on a dead port.
|
||||
if (port == "") return;
|
||||
@@ -1321,6 +1327,9 @@ function controller(bus, opts) {
|
||||
self.is_list_update_running = is_list_update_running;
|
||||
self.check_tachyon_cli_running = check_tachyon_cli_running;
|
||||
self.proxy_port = proxy_port;
|
||||
self.setting = setting;
|
||||
self.settings = settings;
|
||||
self.enabled = enabled;
|
||||
|
||||
return self;
|
||||
}
|
||||
@@ -1330,6 +1339,9 @@ function module_exports() {
|
||||
controller,
|
||||
classify_log_line,
|
||||
smart_detect_extract_domain,
|
||||
setting,
|
||||
settings,
|
||||
enabled,
|
||||
EV
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2110,12 +2110,15 @@ function check_section_failover() {
|
||||
write_state_file("/var/run/tachyon_watchdog.keepalive", as_string(now), "keepalive stamp");
|
||||
}
|
||||
current_ctx = null;
|
||||
if (controller.clear_tick_context)
|
||||
controller.clear_tick_context();
|
||||
|
||||
if (now - last_fast_check >= 15) {
|
||||
last_fast_check = now;
|
||||
current_ctx = current_ctx || (controller.create_tick_context ? controller.create_tick_context() : null);
|
||||
perform_fast_checks();
|
||||
}
|
||||
if (now - last_normal_check >= controller.adaptive_normal_interval()) {
|
||||
if (now - last_normal_check >= 120 && now - last_normal_check >= controller.adaptive_normal_interval()) {
|
||||
last_normal_check = now;
|
||||
current_ctx = current_ctx || (controller.create_tick_context ? controller.create_tick_context() : null);
|
||||
perform_normal_checks();
|
||||
@@ -2125,6 +2128,9 @@ function check_section_failover() {
|
||||
current_ctx = current_ctx || (controller.create_tick_context ? controller.create_tick_context() : null);
|
||||
perform_slow_checks();
|
||||
}
|
||||
if (controller.clear_tick_context)
|
||||
controller.clear_tick_context();
|
||||
current_ctx = null;
|
||||
} catch (e) {
|
||||
log_message("Error in tick: " + as_string(e), "err");
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ grep -Fq 'controller.probe_slow(current_ctx)' "$WATCHDOG_UC" ||
|
||||
# Event controller exports and methods
|
||||
grep -Fq 'create_tick_context' "$CONTROLLER_UC" ||
|
||||
fail "event_controller.uc must define create_tick_context"
|
||||
grep -Fq 'current_tick_ctx.settings' "$CONTROLLER_UC" ||
|
||||
fail "event_controller.uc settings() must reuse current_tick_ctx.settings"
|
||||
grep -Fq 'ai_proxy_health_interval' "$CONTROLLER_UC" ||
|
||||
fail "event_controller.uc must respect ai_proxy_health_interval"
|
||||
grep -Fq 'ai_dns_interval' "$CONTROLLER_UC" ||
|
||||
@@ -171,4 +173,28 @@ print(throttled);
|
||||
')"
|
||||
assert_eq "$res" "true" "healthy dns probe is throttled within interval"
|
||||
|
||||
# Verify shared settings caching during active tick context
|
||||
res="$(run_ucode '
|
||||
let events = require("core.events");
|
||||
let ec = require("service.event_controller");
|
||||
let b = events.bus();
|
||||
let c = ec.controller(b, {});
|
||||
|
||||
c.set_tick_context({
|
||||
settings: {
|
||||
recovery_bypass: "1",
|
||||
custom_test_key: "cached_value"
|
||||
}
|
||||
});
|
||||
|
||||
let cached_val = c.setting("custom_test_key", "default");
|
||||
let is_bypassed = c.setting("recovery_bypass", "0");
|
||||
|
||||
c.clear_tick_context();
|
||||
let cleared = (c.setting("custom_test_key", "cleared_default") == "cleared_default");
|
||||
|
||||
print(cached_val + "," + is_bypassed + "," + cleared);
|
||||
')"
|
||||
assert_eq "$res" "cached_value,1,true" "settings helper uses cached tick_context settings"
|
||||
|
||||
printf 'watchdog_adaptive_intervals checks passed\n'
|
||||
|
||||
Reference in New Issue
Block a user