perf(web): adapt dashboard refresh hydration
This commit is contained in:
parent
30bff9da37
commit
a071df3194
3 changed files with 502 additions and 16 deletions
|
|
@ -9,11 +9,16 @@ import type {
|
|||
import type { DashboardRuntimeState } from "$lib/server/dashboard";
|
||||
import {
|
||||
attachDashboardRefreshLifecycle,
|
||||
collectVisibleDashboardModelIds,
|
||||
createDashboardRefreshDelay,
|
||||
createDashboardTileSnapshotStore,
|
||||
createDashboardTileBackoff,
|
||||
createDashboardRequestAborter,
|
||||
runDashboardHydrationQueue,
|
||||
runViewportAwareDashboardHydrationQueue,
|
||||
shouldPauseDashboardRefresh,
|
||||
waitForDashboardHydrationIdle,
|
||||
type DashboardIntersectionObserverFactory,
|
||||
type DashboardTileReference,
|
||||
type DashboardTileSnapshotStoreContext,
|
||||
type RestoredDashboardTileSnapshot,
|
||||
} from "$lib/client/dashboard-refresh";
|
||||
|
|
@ -29,12 +34,6 @@ import {
|
|||
type UiDashboardPreview,
|
||||
} from "@dimensionlab/ui";
|
||||
|
||||
type DashboardTileReference =
|
||||
| { kind: "telemetry"; id: string }
|
||||
| { kind: "service"; groupId: string; id: string }
|
||||
| { kind: "module"; id: string }
|
||||
| { kind: "status"; stripId: string; id: string };
|
||||
|
||||
type DashboardTileResponse =
|
||||
| {
|
||||
state: "ready";
|
||||
|
|
@ -55,6 +54,8 @@ type DashboardTileResponse =
|
|||
type DashboardTileHydrationResult = "aborted" | "failed" | "ready";
|
||||
|
||||
const dashboardTileHydrationConcurrency = 6;
|
||||
const dashboardFallbackRefreshIntervalMs = 30_000;
|
||||
const dashboardViewportObservationTimeoutMs = 80;
|
||||
type DashboardTileSnapshotStore = ReturnType<typeof createDashboardTileSnapshotStore>;
|
||||
|
||||
export function AppStateView({
|
||||
|
|
@ -170,8 +171,10 @@ export default function App() {
|
|||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let refreshTimer: number | undefined;
|
||||
let lastRefreshIntervalMs = dashboardFallbackRefreshIntervalMs;
|
||||
let hydrationRun = 0;
|
||||
const requestAborter = createDashboardRequestAborter();
|
||||
const refreshDelay = createDashboardRefreshDelay();
|
||||
const tileBackoff = createDashboardTileBackoff();
|
||||
const tileSnapshotStore = createDashboardTileSnapshotStore(
|
||||
getTileSnapshotStorage(),
|
||||
|
|
@ -185,12 +188,22 @@ export default function App() {
|
|||
}
|
||||
|
||||
function clearRefreshTimer() {
|
||||
if (refreshTimer) {
|
||||
window.clearInterval(refreshTimer);
|
||||
if (refreshTimer !== undefined) {
|
||||
window.clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNextRefresh(baseDelayMs: number) {
|
||||
clearRefreshTimer();
|
||||
if (cancelled || refreshPaused()) return;
|
||||
|
||||
refreshTimer = window.setTimeout(() => {
|
||||
refreshTimer = undefined;
|
||||
void loadDashboard();
|
||||
}, refreshDelay.nextDelayMs(baseDelayMs));
|
||||
}
|
||||
|
||||
function pauseRefreshes() {
|
||||
clearRefreshTimer();
|
||||
requestAborter.abortActiveRequests();
|
||||
|
|
@ -200,6 +213,7 @@ export default function App() {
|
|||
async function loadDashboard() {
|
||||
if (cancelled || refreshPaused()) return;
|
||||
|
||||
clearRefreshTimer();
|
||||
const shellSignal = requestAborter.beginShellRun();
|
||||
|
||||
try {
|
||||
|
|
@ -207,6 +221,7 @@ export default function App() {
|
|||
const nextDashboard = (await response.json()) as DashboardRuntimeState;
|
||||
|
||||
if (cancelled || shellSignal.aborted || refreshPaused()) return;
|
||||
refreshDelay.recordSuccess();
|
||||
const restored = restoreDashboardTileSnapshots(
|
||||
nextDashboard,
|
||||
nextDashboard.state === "ready"
|
||||
|
|
@ -247,22 +262,20 @@ export default function App() {
|
|||
setHydratingItemIds(new Set());
|
||||
}
|
||||
|
||||
clearRefreshTimer();
|
||||
|
||||
const refreshIntervalSeconds =
|
||||
nextDashboard.state === "ready"
|
||||
? nextDashboard.document.metadata.refreshIntervalSeconds
|
||||
: undefined;
|
||||
|
||||
if (refreshIntervalSeconds && !refreshPaused()) {
|
||||
refreshTimer = window.setInterval(
|
||||
() => void loadDashboard(),
|
||||
refreshIntervalSeconds * 1000,
|
||||
);
|
||||
lastRefreshIntervalMs = refreshIntervalSeconds * 1000;
|
||||
scheduleNextRefresh(lastRefreshIntervalMs);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isAbortError(error) && !cancelled) {
|
||||
refreshDelay.recordFailure();
|
||||
console.error("Dashboard refresh failed", error);
|
||||
scheduleNextRefresh(lastRefreshIntervalMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -273,8 +286,23 @@ export default function App() {
|
|||
signal: AbortSignal,
|
||||
snapshotContext: DashboardTileSnapshotStoreContext,
|
||||
) {
|
||||
void runDashboardHydrationQueue({
|
||||
const modelIds = tiles.map(dashboardTileModelId);
|
||||
|
||||
void runViewportAwareDashboardHydrationQueue({
|
||||
collectVisibleModelIds: async () => {
|
||||
await waitForDashboardRenderFrame(signal);
|
||||
return collectVisibleDashboardModelIds({
|
||||
clearTimeout: window.clearTimeout.bind(window),
|
||||
createObserver: getDashboardIntersectionObserverFactory(),
|
||||
documentTarget: document,
|
||||
modelIds,
|
||||
setTimeout: window.setTimeout.bind(window),
|
||||
signal,
|
||||
timeoutMs: dashboardViewportObservationTimeoutMs,
|
||||
});
|
||||
},
|
||||
concurrency: dashboardTileHydrationConcurrency,
|
||||
getModelId: dashboardTileModelId,
|
||||
hydrate: async (tile) => {
|
||||
const key = dashboardTileKey(tile);
|
||||
const result = await hydrateDashboardTile(
|
||||
|
|
@ -292,6 +320,13 @@ export default function App() {
|
|||
},
|
||||
items: tiles,
|
||||
signal,
|
||||
waitForIdle: () =>
|
||||
waitForDashboardHydrationIdle({
|
||||
...getDashboardIdleCallbacks(),
|
||||
clearTimeout: window.clearTimeout.bind(window),
|
||||
setTimeout: window.setTimeout.bind(window),
|
||||
signal,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -406,6 +441,53 @@ function getTileSnapshotStorage(): Storage | undefined {
|
|||
}
|
||||
}
|
||||
|
||||
type DashboardIdleWindow = Window & {
|
||||
cancelIdleCallback?: (handle: number) => void;
|
||||
requestIdleCallback?: (
|
||||
callback: () => void,
|
||||
options?: { timeout?: number },
|
||||
) => number;
|
||||
};
|
||||
|
||||
function getDashboardIdleCallbacks() {
|
||||
const idleWindow = window as DashboardIdleWindow;
|
||||
return {
|
||||
cancelIdleCallback: idleWindow.cancelIdleCallback?.bind(idleWindow),
|
||||
requestIdleCallback: idleWindow.requestIdleCallback?.bind(idleWindow),
|
||||
};
|
||||
}
|
||||
|
||||
function getDashboardIntersectionObserverFactory():
|
||||
| DashboardIntersectionObserverFactory
|
||||
| undefined {
|
||||
if (typeof window.IntersectionObserver === "undefined") return undefined;
|
||||
|
||||
return (callback) =>
|
||||
new window.IntersectionObserver((entries) => {
|
||||
callback(entries);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForDashboardRenderFrame(signal: AbortSignal): Promise<void> {
|
||||
if (signal.aborted) return;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
let settled = false;
|
||||
let frame: number | undefined;
|
||||
|
||||
function finish() {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (frame !== undefined) window.cancelAnimationFrame(frame);
|
||||
signal.removeEventListener("abort", finish);
|
||||
resolve();
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", finish, { once: true });
|
||||
frame = window.requestAnimationFrame(finish);
|
||||
});
|
||||
}
|
||||
|
||||
function isAbortError(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
|
|
@ -539,6 +621,11 @@ function dashboardTileKey(tile: DashboardTileReference): string {
|
|||
return `${tile.kind}:${tile.id}`;
|
||||
}
|
||||
|
||||
function dashboardTileModelId(tile: DashboardTileReference): string {
|
||||
if (tile.kind === "status") return `${tile.stripId}:${tile.id}`;
|
||||
return tile.id;
|
||||
}
|
||||
|
||||
function dashboardTileUrl(tile: DashboardTileReference): string {
|
||||
const parts = tile.kind === "status"
|
||||
? ["api", "dashboard", "tile", tile.kind, tile.stripId, tile.id]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue