perf(web): batch dashboard tile hydration
This commit is contained in:
parent
63b008095f
commit
de87464ef1
7 changed files with 518 additions and 40 deletions
|
|
@ -52,6 +52,11 @@ type DashboardTileResponse =
|
|||
message: string;
|
||||
};
|
||||
|
||||
type DashboardTilesBatchResponse = {
|
||||
state: "ready";
|
||||
tiles: DashboardTileResponse[];
|
||||
};
|
||||
|
||||
type DashboardTileHydrationResult = "aborted" | "failed" | "ready";
|
||||
|
||||
const dashboardTileHydrationConcurrency = 6;
|
||||
|
|
@ -292,6 +297,7 @@ export default function App() {
|
|||
const modelIds = tiles.map(dashboardTileModelId);
|
||||
|
||||
void runViewportAwareDashboardHydrationQueue({
|
||||
batchSize: dashboardTileHydrationConcurrency,
|
||||
collectVisibleModelIds: async () => {
|
||||
await waitForDashboardRenderFrame(signal);
|
||||
return collectVisibleDashboardModelIds({
|
||||
|
|
@ -321,6 +327,23 @@ export default function App() {
|
|||
tileBackoff.recordFailure(key);
|
||||
}
|
||||
},
|
||||
hydrateBatch: async (batch) => {
|
||||
const results = await hydrateDashboardTileBatch(
|
||||
batch,
|
||||
run,
|
||||
signal,
|
||||
snapshotContext,
|
||||
);
|
||||
|
||||
for (const { result, tile } of results) {
|
||||
const key = dashboardTileKey(tile);
|
||||
if (result === "ready") {
|
||||
tileBackoff.recordSuccess(key);
|
||||
} else if (result === "failed") {
|
||||
tileBackoff.recordFailure(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
items: tiles,
|
||||
onAllItemsSettled: () => performanceMarks.markAllTilesSettled(),
|
||||
onVisibleItemsSettled: () => performanceMarks.markVisibleTilesReady(),
|
||||
|
|
@ -335,6 +358,69 @@ export default function App() {
|
|||
});
|
||||
}
|
||||
|
||||
async function hydrateDashboardTileBatch(
|
||||
tiles: DashboardTileReference[],
|
||||
run: number,
|
||||
signal: AbortSignal,
|
||||
snapshotContext: DashboardTileSnapshotStoreContext,
|
||||
): Promise<Array<{ result: DashboardTileHydrationResult; tile: DashboardTileReference }>> {
|
||||
try {
|
||||
const response = await fetch("/api/dashboard/tiles", {
|
||||
body: JSON.stringify({ tiles }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
signal,
|
||||
});
|
||||
const batchResponse = (await response.json()) as DashboardTilesBatchResponse;
|
||||
|
||||
if (
|
||||
cancelled ||
|
||||
signal.aborted ||
|
||||
run !== hydrationRun ||
|
||||
!response.ok ||
|
||||
batchResponse.state !== "ready"
|
||||
) {
|
||||
throw new Error("Dashboard tile batch hydration failed");
|
||||
}
|
||||
|
||||
const responsesByKey = new Map(
|
||||
batchResponse.tiles.map((tileResponse) => [
|
||||
dashboardTileKey(tileResponse.tile),
|
||||
tileResponse,
|
||||
]),
|
||||
);
|
||||
|
||||
return tiles.map((tile) => {
|
||||
const key = dashboardTileKey(tile);
|
||||
try {
|
||||
return {
|
||||
tile,
|
||||
result: applyDashboardTileHydrationResponse(
|
||||
tile,
|
||||
responsesByKey.get(key),
|
||||
run,
|
||||
signal,
|
||||
snapshotContext,
|
||||
),
|
||||
};
|
||||
} finally {
|
||||
finishDashboardTileHydration(key, run, signal);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (signal.aborted || cancelled || run !== hydrationRun) {
|
||||
return tiles.map((tile) => ({ result: "aborted", tile }));
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
tiles.map(async (tile) => ({
|
||||
tile,
|
||||
result: await hydrateDashboardTile(tile, run, signal, snapshotContext),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function hydrateDashboardTile(
|
||||
tile: DashboardTileReference,
|
||||
run: number,
|
||||
|
|
@ -347,32 +433,23 @@ export default function App() {
|
|||
const response = await fetch(dashboardTileUrl(tile), { signal });
|
||||
const tileResponse = (await response.json()) as DashboardTileResponse;
|
||||
|
||||
if (
|
||||
cancelled ||
|
||||
signal.aborted ||
|
||||
run !== hydrationRun ||
|
||||
!response.ok ||
|
||||
tileResponse.state !== "ready"
|
||||
) {
|
||||
if (!response.ok) {
|
||||
return "failed";
|
||||
}
|
||||
|
||||
const result = applyDashboardTileHydrationResponse(
|
||||
tile,
|
||||
tileResponse,
|
||||
run,
|
||||
signal,
|
||||
snapshotContext,
|
||||
);
|
||||
if (result !== "ready") {
|
||||
return signal.aborted || cancelled || run !== hydrationRun
|
||||
? "aborted"
|
||||
: "failed";
|
||||
}
|
||||
|
||||
const readyTileResponse = tileResponse;
|
||||
setDashboard((current) =>
|
||||
current?.state === "ready"
|
||||
? {
|
||||
...current,
|
||||
document: applyDashboardTile(current.document, readyTileResponse),
|
||||
}
|
||||
: current,
|
||||
);
|
||||
tileSnapshotStore.saveReadyTile({
|
||||
...snapshotContext,
|
||||
response: readyTileResponse,
|
||||
});
|
||||
performanceMarks.markFirstTileReady();
|
||||
return "ready";
|
||||
} catch (error) {
|
||||
if (!isAbortError(error) && !cancelled) {
|
||||
|
|
@ -381,13 +458,58 @@ export default function App() {
|
|||
}
|
||||
return "aborted";
|
||||
} finally {
|
||||
if (!cancelled && !signal.aborted && run === hydrationRun) {
|
||||
setHydratingItemIds((current) => {
|
||||
const next = new Set(current);
|
||||
next.delete(key);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
finishDashboardTileHydration(key, run, signal);
|
||||
}
|
||||
}
|
||||
|
||||
function applyDashboardTileHydrationResponse(
|
||||
tile: DashboardTileReference,
|
||||
tileResponse: DashboardTileResponse | undefined,
|
||||
run: number,
|
||||
signal: AbortSignal,
|
||||
snapshotContext: DashboardTileSnapshotStoreContext,
|
||||
): DashboardTileHydrationResult {
|
||||
if (
|
||||
cancelled ||
|
||||
signal.aborted ||
|
||||
run !== hydrationRun ||
|
||||
!tileResponse ||
|
||||
dashboardTileKey(tileResponse.tile) !== dashboardTileKey(tile) ||
|
||||
tileResponse.state !== "ready"
|
||||
) {
|
||||
return signal.aborted || cancelled || run !== hydrationRun
|
||||
? "aborted"
|
||||
: "failed";
|
||||
}
|
||||
|
||||
const readyTileResponse = tileResponse;
|
||||
setDashboard((current) =>
|
||||
current?.state === "ready"
|
||||
? {
|
||||
...current,
|
||||
document: applyDashboardTile(current.document, readyTileResponse),
|
||||
}
|
||||
: current,
|
||||
);
|
||||
tileSnapshotStore.saveReadyTile({
|
||||
...snapshotContext,
|
||||
response: readyTileResponse,
|
||||
});
|
||||
performanceMarks.markFirstTileReady();
|
||||
return "ready";
|
||||
}
|
||||
|
||||
function finishDashboardTileHydration(
|
||||
key: string,
|
||||
run: number,
|
||||
signal: AbortSignal,
|
||||
) {
|
||||
if (!cancelled && !signal.aborted && run === hydrationRun) {
|
||||
setHydratingItemIds((current) => {
|
||||
const next = new Set(current);
|
||||
next.delete(key);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue