mirror of
https://github.com/prometheus/prometheus.git
synced 2025-08-07 06:37:17 +02:00
Optimize memoization and search debouncing on /targets page (#16589)
Moving the debouncing of the search field to the parent component and then memoizing the ScrapePoolsList component prevents a lot of superfluous re-renders of the entire scrape pools list that previously got triggered immediately when you typed in the search box or even just collapsed a pool. (While the computation of what data to show was already memoized in the ScrapePoolList component, the component itself still had to re-render a lot with the same data.) Discovered this problem + verified fix using react-scan. Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
8b0d33e5b2
commit
5c06804df8
@ -19,7 +19,7 @@ import {
|
|||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { useSuspenseAPIQuery } from "../../api/api";
|
import { useSuspenseAPIQuery } from "../../api/api";
|
||||||
import { Target, TargetsResult } from "../../api/responseTypes/targets";
|
import { Target, TargetsResult } from "../../api/responseTypes/targets";
|
||||||
import React, { FC, useMemo } from "react";
|
import React, { FC, memo, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
humanizeDurationRelative,
|
humanizeDurationRelative,
|
||||||
humanizeDuration,
|
humanizeDuration,
|
||||||
@ -37,7 +37,6 @@ import CustomInfiniteScroll from "../../components/CustomInfiniteScroll";
|
|||||||
import badgeClasses from "../../Badge.module.css";
|
import badgeClasses from "../../Badge.module.css";
|
||||||
import panelClasses from "../../Panel.module.css";
|
import panelClasses from "../../Panel.module.css";
|
||||||
import TargetLabels from "./TargetLabels";
|
import TargetLabels from "./TargetLabels";
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
|
||||||
import { targetPoolDisplayLimit } from "./TargetsPage";
|
import { targetPoolDisplayLimit } from "./TargetsPage";
|
||||||
import { badgeIconStyle } from "../../styles";
|
import { badgeIconStyle } from "../../styles";
|
||||||
|
|
||||||
@ -145,12 +144,8 @@ type ScrapePoolListProp = {
|
|||||||
searchFilter: string;
|
searchFilter: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
const ScrapePoolList: FC<ScrapePoolListProp> = memo(
|
||||||
poolNames,
|
({ poolNames, selectedPool, healthFilter, searchFilter }) => {
|
||||||
selectedPool,
|
|
||||||
healthFilter,
|
|
||||||
searchFilter,
|
|
||||||
}) => {
|
|
||||||
// Based on the selected pool (if any), load the list of targets.
|
// Based on the selected pool (if any), load the list of targets.
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
@ -174,17 +169,15 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||||||
(state) => state.targetsPage
|
(state) => state.targetsPage
|
||||||
);
|
);
|
||||||
|
|
||||||
const [debouncedSearch] = useDebouncedValue<string>(searchFilter.trim(), 250);
|
|
||||||
|
|
||||||
const allPools = useMemo(
|
const allPools = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildPoolsData(
|
buildPoolsData(
|
||||||
selectedPool ? [selectedPool] : poolNames,
|
selectedPool ? [selectedPool] : poolNames,
|
||||||
activeTargets,
|
activeTargets,
|
||||||
debouncedSearch,
|
searchFilter,
|
||||||
healthFilter
|
healthFilter
|
||||||
),
|
),
|
||||||
[selectedPool, poolNames, activeTargets, debouncedSearch, healthFilter]
|
[selectedPool, poolNames, activeTargets, searchFilter, healthFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const allPoolNames = Object.keys(allPools);
|
const allPoolNames = Object.keys(allPools);
|
||||||
@ -205,8 +198,8 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||||||
title="Hiding pools with no matching targets"
|
title="Hiding pools with no matching targets"
|
||||||
icon={<IconInfoCircle />}
|
icon={<IconInfoCircle />}
|
||||||
>
|
>
|
||||||
Hiding {allPoolNames.length - shownPoolNames.length} empty pools due
|
Hiding {allPoolNames.length - shownPoolNames.length} empty pools
|
||||||
to filters or no targets.
|
due to filters or no targets.
|
||||||
<Anchor ml="md" fz="1em" onClick={() => setShowEmptyPools(true)}>
|
<Anchor ml="md" fz="1em" onClick={() => setShowEmptyPools(true)}>
|
||||||
Show empty pools
|
Show empty pools
|
||||||
</Anchor>
|
</Anchor>
|
||||||
@ -287,9 +280,12 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||||||
</Anchor>
|
</Anchor>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : pool.targets.length === 0 ? (
|
) : pool.targets.length === 0 ? (
|
||||||
<Alert title="No matching targets" icon={<IconInfoCircle />}>
|
<Alert
|
||||||
No targets in this pool match your filter criteria (omitted{" "}
|
title="No matching targets"
|
||||||
{pool.count} filtered targets).
|
icon={<IconInfoCircle />}
|
||||||
|
>
|
||||||
|
No targets in this pool match your filter criteria
|
||||||
|
(omitted {pool.count} filtered targets).
|
||||||
<Anchor
|
<Anchor
|
||||||
ml="md"
|
ml="md"
|
||||||
fz="1em"
|
fz="1em"
|
||||||
@ -348,7 +344,9 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||||||
label: { textTransform: "none" },
|
label: { textTransform: "none" },
|
||||||
}}
|
}}
|
||||||
leftSection={
|
leftSection={
|
||||||
<IconRefresh style={badgeIconStyle} />
|
<IconRefresh
|
||||||
|
style={badgeIconStyle}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{humanizeDurationRelative(
|
{humanizeDurationRelative(
|
||||||
@ -383,7 +381,9 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td valign="top">
|
<Table.Td valign="top">
|
||||||
<Badge
|
<Badge
|
||||||
className={healthBadgeClass(target.health)}
|
className={healthBadgeClass(
|
||||||
|
target.health
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{target.health}
|
{target.health}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -417,6 +417,7 @@ const ScrapePoolList: FC<ScrapePoolListProp> = ({
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default ScrapePoolList;
|
export default ScrapePoolList;
|
||||||
|
@ -30,9 +30,16 @@ import ScrapePoolList from "./ScrapePoolsList";
|
|||||||
import { useSuspenseAPIQuery } from "../../api/api";
|
import { useSuspenseAPIQuery } from "../../api/api";
|
||||||
import { ScrapePoolsResult } from "../../api/responseTypes/scrapePools";
|
import { ScrapePoolsResult } from "../../api/responseTypes/scrapePools";
|
||||||
import { expandIconStyle, inputIconStyle } from "../../styles";
|
import { expandIconStyle, inputIconStyle } from "../../styles";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
|
||||||
export const targetPoolDisplayLimit = 20;
|
export const targetPoolDisplayLimit = 20;
|
||||||
|
|
||||||
|
// Should be defined as a constant here instead of inline as a value
|
||||||
|
// to avoid unnecessary re-renders. Otherwise the empty array has
|
||||||
|
// a different reference on each render and causes subsequent memoized
|
||||||
|
// computations to re-run as long as no state filter is selected.
|
||||||
|
const emptyHealthFilter: string[] = [];
|
||||||
|
|
||||||
export default function TargetsPage() {
|
export default function TargetsPage() {
|
||||||
// Load the list of all available scrape pools.
|
// Load the list of all available scrape pools.
|
||||||
const {
|
const {
|
||||||
@ -48,12 +55,13 @@ export default function TargetsPage() {
|
|||||||
const [scrapePool, setScrapePool] = useQueryParam("pool", StringParam);
|
const [scrapePool, setScrapePool] = useQueryParam("pool", StringParam);
|
||||||
const [healthFilter, setHealthFilter] = useQueryParam(
|
const [healthFilter, setHealthFilter] = useQueryParam(
|
||||||
"health",
|
"health",
|
||||||
withDefault(ArrayParam, [])
|
withDefault(ArrayParam, emptyHealthFilter)
|
||||||
);
|
);
|
||||||
const [searchFilter, setSearchFilter] = useQueryParam(
|
const [searchFilter, setSearchFilter] = useQueryParam(
|
||||||
"search",
|
"search",
|
||||||
withDefault(StringParam, "")
|
withDefault(StringParam, "")
|
||||||
);
|
);
|
||||||
|
const [debouncedSearch] = useDebouncedValue<string>(searchFilter.trim(), 250);
|
||||||
|
|
||||||
const { collapsedPools, showLimitAlert } = useAppSelector(
|
const { collapsedPools, showLimitAlert } = useAppSelector(
|
||||||
(state) => state.targetsPage
|
(state) => state.targetsPage
|
||||||
@ -147,7 +155,7 @@ export default function TargetsPage() {
|
|||||||
poolNames={scrapePools}
|
poolNames={scrapePools}
|
||||||
selectedPool={(limited && scrapePools[0]) || scrapePool || null}
|
selectedPool={(limited && scrapePools[0]) || scrapePool || null}
|
||||||
healthFilter={healthFilter as string[]}
|
healthFilter={healthFilter as string[]}
|
||||||
searchFilter={searchFilter}
|
searchFilter={debouncedSearch}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
Loading…
Reference in New Issue
Block a user