Files
NeoTachyon/fe-app-tachyon/src/tachyon/services/runtimeUiState.service.ts
T
Dushnilin dceb04677e perf(luci): eliminate excessive router CPU load during LuCI session
- Optimize get_ui_state backend by reading /proc natively instead of spawning pidof
- Avoid spawning readlink and redundant ubus queries in state.uc
- Cache positive tachyon_runtime_network_configured checks for 5s
- Back off idle UI state polling interval from 3s to 10s (60s when tab is hidden)
- Back off TachyonLogWatcher interval to 30s
- Pause Dashboard and Monitoring polling when browser tab is inactive (Page Visibility API)
- Fix cleanTabId to ensure inactive tab pollers and timers unmount properly
2026-09-12 21:17:20 +03:00

174 lines
4.7 KiB
TypeScript

import { TachyonShellMethods } from '../methods';
import { Tachyon } from '../types';
import { logger } from './logger.service';
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;
// 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).
const RUNTIME_UI_STATE_HIDDEN_POLL_INTERVAL_MS = 60_000;
type RuntimeUiStateListener = (uiState: Tachyon.UiState) => void;
let runtimeUiStateRefreshPromise: Promise<Tachyon.UiState | undefined> | null =
null;
let lastRuntimeUiStateRefreshAt = 0;
let lastRuntimeUiState: Tachyon.UiState | undefined;
let runtimeStateResumeRefreshRegistered = false;
let runtimeStatePollTimer: ReturnType<typeof setTimeout> | null = null;
let runtimeStatePollingStarted = false;
let runtimeStateHasRunningAction = false;
const runtimeUiStateListeners = new Set<RuntimeUiStateListener>();
function isDocumentVisible() {
return (
typeof document === 'undefined' ||
!document.visibilityState ||
document.visibilityState === 'visible'
);
}
function hasRunningAction(uiState: Tachyon.UiState) {
return Object.values(uiState.actions).some((actions) =>
actions.some((action) => action.running),
);
}
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;
}
function scheduleRuntimeUiStatePoll(delay = getNextPollDelay()) {
if (
!runtimeStatePollingStarted ||
runtimeStatePollTimer ||
typeof window === 'undefined'
) {
return;
}
runtimeStatePollTimer = window.setTimeout(() => {
runtimeStatePollTimer = null;
void refreshRuntimeUiState()
.catch(() => undefined)
.finally(() => {
scheduleRuntimeUiStatePoll();
});
}, delay);
}
function notifyRuntimeUiStateListeners(uiState: Tachyon.UiState) {
for (const listener of runtimeUiStateListeners) {
try {
listener(uiState);
} catch (error) {
logger.error('[RUNTIME_UI_STATE]', 'listener failed', error);
}
}
}
export async function refreshRuntimeUiState({
force = false,
}: { force?: boolean } = {}): Promise<Tachyon.UiState | undefined> {
if (!isDocumentVisible()) {
return undefined;
}
if (runtimeUiStateRefreshPromise) {
return runtimeUiStateRefreshPromise;
}
const now = Date.now();
if (
!force &&
now - lastRuntimeUiStateRefreshAt < RUNTIME_UI_STATE_REFRESH_MIN_INTERVAL_MS
) {
return undefined;
}
lastRuntimeUiStateRefreshAt = now;
const promise = TachyonShellMethods.getUiState()
.then((response) => {
if (!response.success) {
return undefined;
}
applyUiStateToStore(response.data);
lastRuntimeUiState = response.data;
runtimeStateHasRunningAction = hasRunningAction(response.data);
notifyRuntimeUiStateListeners(response.data);
return response.data;
})
.catch((error) => {
logger.error('[RUNTIME_UI_STATE]', 'refresh failed', error);
return undefined;
})
.finally(() => {
if (runtimeUiStateRefreshPromise === promise) {
runtimeUiStateRefreshPromise = null;
}
});
runtimeUiStateRefreshPromise = promise;
return runtimeUiStateRefreshPromise;
}
export function subscribeRuntimeUiState(listener: RuntimeUiStateListener) {
runtimeUiStateListeners.add(listener);
if (lastRuntimeUiState) {
try {
listener(lastRuntimeUiState);
} catch (error) {
logger.error('[RUNTIME_UI_STATE]', 'listener failed', error);
}
}
return () => {
runtimeUiStateListeners.delete(listener);
};
}
export function getCachedRuntimeUiState() {
return lastRuntimeUiState;
}
export function registerRuntimeStateResumeRefresh() {
if (runtimeStateResumeRefreshRegistered || typeof window === 'undefined') {
return;
}
runtimeStateResumeRefreshRegistered = true;
const refreshOnResume = () => {
if (!isDocumentVisible()) {
return;
}
void refreshRuntimeUiState({ force: true });
};
document.addEventListener('visibilitychange', refreshOnResume);
window.addEventListener('pageshow', refreshOnResume);
window.addEventListener('focus', refreshOnResume);
}
export function startRuntimeUiStatePolling() {
if (runtimeStatePollingStarted || typeof window === 'undefined') {
return;
}
runtimeStatePollingStarted = true;
void refreshRuntimeUiState({ force: true }).finally(() => {
scheduleRuntimeUiStatePoll();
});
}