perf(web): adapt dashboard refresh hydration
This commit is contained in:
parent
30bff9da37
commit
a071df3194
3 changed files with 502 additions and 16 deletions
|
|
@ -1,11 +1,17 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
attachDashboardRefreshLifecycle,
|
||||
collectVisibleDashboardModelIds,
|
||||
createDashboardRefreshDelay,
|
||||
createDashboardTileSnapshotStore,
|
||||
createDashboardTileBackoff,
|
||||
createDashboardRequestAborter,
|
||||
runDashboardHydrationQueue,
|
||||
runViewportAwareDashboardHydrationQueue,
|
||||
shouldPauseDashboardRefresh,
|
||||
splitDashboardHydrationItemsByVisibility,
|
||||
waitForDashboardHydrationIdle,
|
||||
type DashboardIntersectionEntry,
|
||||
} from "./dashboard-refresh";
|
||||
|
||||
describe("dashboard refresh lifecycle", () => {
|
||||
|
|
@ -113,6 +119,134 @@ describe("dashboard refresh lifecycle", () => {
|
|||
expect(maxActive).toBe(2);
|
||||
});
|
||||
|
||||
test("hydrates visible items before deferred items and waits for idle", async () => {
|
||||
const calls: string[] = [];
|
||||
const idleReleases: Array<() => void> = [];
|
||||
|
||||
const hydration = runViewportAwareDashboardHydrationQueue({
|
||||
collectVisibleModelIds: async () => new Set(["b"]),
|
||||
concurrency: 1,
|
||||
getModelId: (item) => item,
|
||||
hydrate: async (item) => {
|
||||
calls.push(item);
|
||||
},
|
||||
items: ["a", "b", "c"],
|
||||
signal: new AbortController().signal,
|
||||
waitForIdle: async () => {
|
||||
calls.push("idle");
|
||||
await new Promise<void>((resolve) => idleReleases.push(resolve));
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => calls.includes("idle"));
|
||||
expect(calls).toEqual(["b", "idle"]);
|
||||
|
||||
idleReleases.shift()?.();
|
||||
await hydration;
|
||||
expect(calls).toEqual(["b", "idle", "a", "c"]);
|
||||
});
|
||||
|
||||
test("splits visible hydration items while preserving document order", () => {
|
||||
expect(splitDashboardHydrationItemsByVisibility({
|
||||
getModelId: (item) => item.id,
|
||||
items: [{ id: "status" }, { id: "telemetry" }, { id: "service" }],
|
||||
visibleModelIds: new Set(["service", "status"]),
|
||||
})).toEqual({
|
||||
visible: [{ id: "status" }, { id: "service" }],
|
||||
deferred: [{ id: "telemetry" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("collects visible data-model-id elements with IntersectionObserver", async () => {
|
||||
const elements = [
|
||||
modelElement("status"),
|
||||
modelElement("telemetry"),
|
||||
modelElement("unrelated"),
|
||||
];
|
||||
let callback:
|
||||
| ((entries: DashboardIntersectionEntry[]) => void)
|
||||
| undefined;
|
||||
let finishObservation: (() => void) | undefined;
|
||||
let disconnected = false;
|
||||
const observed: string[] = [];
|
||||
|
||||
const visible = collectVisibleDashboardModelIds({
|
||||
createObserver: (observerCallback) => {
|
||||
callback = observerCallback;
|
||||
return {
|
||||
disconnect() {
|
||||
disconnected = true;
|
||||
},
|
||||
observe(element) {
|
||||
observed.push(element.getAttribute("data-model-id") || "");
|
||||
},
|
||||
};
|
||||
},
|
||||
documentTarget: {
|
||||
querySelectorAll: () => elements,
|
||||
},
|
||||
modelIds: ["status", "telemetry"],
|
||||
setTimeout: (handler) => {
|
||||
finishObservation = handler;
|
||||
return 1 as unknown as ReturnType<typeof setTimeout>;
|
||||
},
|
||||
clearTimeout: () => undefined,
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
|
||||
callback?.([
|
||||
{
|
||||
isIntersecting: false,
|
||||
intersectionRatio: 0,
|
||||
target: elements[0],
|
||||
},
|
||||
{
|
||||
isIntersecting: true,
|
||||
target: elements[1],
|
||||
},
|
||||
]);
|
||||
finishObservation?.();
|
||||
|
||||
expect(await visible).toEqual(new Set(["telemetry"]));
|
||||
expect(observed).toEqual(["status", "telemetry"]);
|
||||
expect(disconnected).toBe(true);
|
||||
});
|
||||
|
||||
test("waits for requestIdleCallback when available", async () => {
|
||||
let idleCallback: (() => void) | undefined;
|
||||
let cancelledIdle: number | undefined;
|
||||
const wait = waitForDashboardHydrationIdle({
|
||||
cancelIdleCallback: (handle) => {
|
||||
cancelledIdle = handle;
|
||||
},
|
||||
requestIdleCallback: (callback) => {
|
||||
idleCallback = callback;
|
||||
return 7;
|
||||
},
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
|
||||
expect(cancelledIdle).toBeUndefined();
|
||||
idleCallback?.();
|
||||
await wait;
|
||||
expect(cancelledIdle).toBe(7);
|
||||
});
|
||||
|
||||
test("jitters refresh delays and slows repeated failures", () => {
|
||||
const delay = createDashboardRefreshDelay({
|
||||
jitterRatio: 0.1,
|
||||
random: () => 1,
|
||||
});
|
||||
|
||||
expect(delay.nextDelayMs(1_000)).toBe(1_100);
|
||||
delay.recordFailure();
|
||||
expect(delay.nextDelayMs(1_000)).toBe(2_200);
|
||||
delay.recordFailure();
|
||||
expect(delay.nextDelayMs(1_000)).toBe(4_400);
|
||||
delay.recordSuccess();
|
||||
expect(delay.nextDelayMs(1_000)).toBe(1_100);
|
||||
});
|
||||
|
||||
test("backs off failed tile keys and resets after success", () => {
|
||||
const backoff = createDashboardTileBackoff();
|
||||
|
||||
|
|
@ -299,3 +433,11 @@ function createMemoryStorage(): Storage {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
function modelElement(modelId: string) {
|
||||
return {
|
||||
getAttribute(name: string) {
|
||||
return name === "data-model-id" ? modelId : null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue