From 6b67a67d7d9528a76d505ecc01806f75c807671d Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Mon, 11 Aug 2025 18:50:49 +0200 Subject: [PATCH] More alerts page filtering measurement Signed-off-by: Julius Volz --- web/ui/mantine-ui/src/pages/AlertsPage.tsx | 76 ++++++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/web/ui/mantine-ui/src/pages/AlertsPage.tsx b/web/ui/mantine-ui/src/pages/AlertsPage.tsx index 5498de5796..47ebc23410 100644 --- a/web/ui/mantine-ui/src/pages/AlertsPage.tsx +++ b/web/ui/mantine-ui/src/pages/AlertsPage.tsx @@ -20,7 +20,15 @@ import badgeClasses from "../Badge.module.css"; import panelClasses from "../Panel.module.css"; import RuleDefinition from "../components/RuleDefinition"; import { humanizeDurationRelative, now } from "../lib/formatTime"; -import { Fragment, useEffect, useMemo, useRef } from "react"; +import { + Profiler, + Fragment, + useEffect, + useMemo, + useRef, + useCallback, +} from "react"; +import type { ProfilerOnRenderCallback } from "react"; import { StateMultiSelect } from "../components/StateMultiSelect"; import { IconInfoCircle, IconSearch } from "@tabler/icons-react"; import { LabelBadges } from "../components/LabelBadges"; @@ -67,6 +75,16 @@ type AlertsPageData = { }[]; }; +// Track timing for a search render cycle +type SearchMeasure = { + id: number; + search: string; + startedAt: number; + computeEnd?: number; + elementsEnd?: number; // fallback if Profiler isn't available + elementsDuration?: number; // actual render time from React Profiler +}; + const kvSearch = new KVSearch({ shouldSort: true, indexedKeys: ["name", "labels", ["labels", /.*/]], @@ -168,9 +186,18 @@ export default function AlertsPage() { // Measure how long it takes to render the filtered list after debouncedSearch changes. const searchRenderStartRef = useRef(null); const prevDebouncedSearchRef = useRef(debouncedSearch); + const measureIdRef = useRef(0); + const measureRef = useRef(null); if (prevDebouncedSearchRef.current !== debouncedSearch) { - searchRenderStartRef.current = performance.now(); prevDebouncedSearchRef.current = debouncedSearch; + measureIdRef.current += 1; + const startedAt = performance.now(); + measureRef.current = { + id: measureIdRef.current, + search: debouncedSearch, + startedAt, + }; + searchRenderStartRef.current = startedAt; } // After commit, wait for paint to complete, then log duration. @@ -181,11 +208,23 @@ export default function AlertsPage() { let raf2 = 0; raf1 = requestAnimationFrame(() => { raf2 = requestAnimationFrame(() => { - const duration = performance.now() - start; + const paintEnd = performance.now(); + const m = measureRef.current; + const computeMs = m?.computeEnd != null ? m.computeEnd - start : NaN; + const elementsMs = + m?.elementsDuration != null + ? m.elementsDuration + : m?.elementsEnd != null && m?.computeEnd != null + ? m.elementsEnd - m.computeEnd + : NaN; + const paintMs = + m?.elementsEnd != null ? paintEnd - m.elementsEnd : NaN; + const totalMs = paintEnd - start; console.log( - `AlertsPage: filtered list render time ${duration.toFixed(2)} ms (search="${debouncedSearch}")` + `AlertsPage timings (search="${m?.search ?? ""}"): compute=${isNaN(computeMs) ? "-" : computeMs.toFixed(2)}ms, elements=${isNaN(elementsMs as number) ? "-" : (elementsMs as number).toFixed(2)}ms, paint=${isNaN(paintMs) ? "-" : paintMs.toFixed(2)}ms, total=${totalMs.toFixed(2)}ms` ); searchRenderStartRef.current = null; + measureRef.current = null; }); }); return () => { @@ -213,6 +252,12 @@ export default function AlertsPage() { [data, stateFilter, debouncedSearch] ); + // Mark compute end when the filtered data is ready for rendering + if (measureRef.current && searchRenderStartRef.current != null) { + // We consider compute finished after alertsPageData has been recomputed + measureRef.current.computeEnd = performance.now(); + } + const shownGroups = useMemo( () => showEmptyGroups @@ -429,6 +474,25 @@ export default function AlertsPage() { [currentPageGroups, showAnnotations, setShowEmptyGroups] ); + // Capture actual render time for the list subtree using React Profiler + const onListRender = useCallback( + (_, __, actualDuration) => { + if (measureRef.current && searchRenderStartRef.current != null) { + measureRef.current.elementsDuration = actualDuration; + } + }, + [] + ); + + // Mark elements end after creating React elements for the current view (fallback when Profiler duration is unavailable) + if ( + measureRef.current && + measureRef.current.computeEnd && + searchRenderStartRef.current != null + ) { + measureRef.current.elementsEnd = performance.now(); + } + return ( @@ -485,7 +549,9 @@ export default function AlertsPage() { onChange={setActivePage} hideWithOnePage /> - {renderedPageItems} + + {renderedPageItems} + ); }