From 703417dfe31a4298cb1c4062acb5ff7f581a3c5a Mon Sep 17 00:00:00 2001 From: Skylar Date: Fri, 27 Sep 2024 09:48:56 -0600 Subject: [PATCH] fix: fix gradual memory leak / perf issue with cytoscape (#56) --- kontrol-frontend/src/contexts/ApiContext.tsx | 9 +-- .../src/pages/TrafficConfiguration.tsx | 68 +++++++++++-------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/kontrol-frontend/src/contexts/ApiContext.tsx b/kontrol-frontend/src/contexts/ApiContext.tsx index 09676b9..38a0e7c 100644 --- a/kontrol-frontend/src/contexts/ApiContext.tsx +++ b/kontrol-frontend/src/contexts/ApiContext.tsx @@ -31,7 +31,6 @@ export interface ApiContextType { getFlows: () => Promise; getTemplates: () => Promise; getTopology: () => Promise; - loading: boolean; postFlowCreate: ( b: RequestBody<"/tenant/{uuid}/flow/create", "post">, ) => Promise; @@ -50,7 +49,6 @@ const defaultContextValue: ApiContextType = { getTopology: async () => { return { nodes: [], edges: [] }; }, - loading: false, postFlowCreate: async () => ({ "flow-id": "", "access-entry": [], @@ -72,15 +70,13 @@ export const ApiContextProvider = ({ children }: PropsWithChildren) => { ); const uuid = match?.params.uuid; - const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [templates, setTemplates] = useState([]); - // boilerplate loading state, error handling for any API call + // boilerplate error handling for any API call const handleApiCall = useCallback(async function ( pendingRequest: Promise<{ data?: T }>, // Api fetch promise ): Promise { - setLoading(true); try { const response = await pendingRequest; if (response.data == null) { @@ -91,8 +87,6 @@ export const ApiContextProvider = ({ children }: PropsWithChildren) => { console.error("Failed to fetch route:", error); setError((error as Error).message); throw error; - } finally { - setLoading(false); } }, []); @@ -198,7 +192,6 @@ export const ApiContextProvider = ({ children }: PropsWithChildren) => { getFlows, getTemplates, getTopology, - loading, postFlowCreate, postTemplateCreate, templates, diff --git a/kontrol-frontend/src/pages/TrafficConfiguration.tsx b/kontrol-frontend/src/pages/TrafficConfiguration.tsx index 2305a4a..09ad598 100644 --- a/kontrol-frontend/src/pages/TrafficConfiguration.tsx +++ b/kontrol-frontend/src/pages/TrafficConfiguration.tsx @@ -12,40 +12,48 @@ const Page = () => { const prevResponse = useRef(); const { getTopology } = useApi(); const { refetchFlows, flowVisibility } = useFlowsContext(); + const timerRef = useRef(null); - useEffect(() => { - const fetchElems = async () => { - const response = await getTopology(); - const filtered = { - ...response, - nodes: response.nodes.map((node) => { - return { - ...node, - versions: node.versions?.filter((version) => { - return flowVisibility[version.flowId] === true; - }), - }; - }), - }; - const newElems = utils.normalizeData(filtered); - - // dont update react state if the API response is identical to the previous one - // This avoids unnecessary re-renders - if (JSON.stringify(newElems) === prevResponse.current) { - return; - } - prevResponse.current = JSON.stringify(newElems); - setElems(newElems); - // re-fetch flows if topology changes - refetchFlows(); + const fetchElems = async () => { + const response = await getTopology(); + const filtered = { + ...response, + nodes: response.nodes.map((node) => { + return { + ...node, + versions: node.versions?.filter((version) => { + return flowVisibility[version.flowId] === true; + }), + }; + }), }; + const newElems = utils.normalizeData(filtered); + + // dont update react state if the API response is identical to the previous one + // This avoids unnecessary re-renders + if (JSON.stringify(newElems) === prevResponse.current) { + return; + } + prevResponse.current = JSON.stringify(newElems); + setElems(newElems); + // re-fetch flows if topology changes + refetchFlows(); + }; + + const startPolling = () => { + timerRef.current = setInterval(fetchElems, pollingIntervalSeconds * 1000); + }; - // Continuously fetch elements - const intervalId = setInterval(fetchElems, pollingIntervalSeconds * 1000); - fetchElems(); - return () => clearInterval(intervalId); + const stopPolling = () => { + clearInterval(timerRef.current!); + timerRef.current = null; + }; + + useEffect(() => { + startPolling(); + return stopPolling; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [flowVisibility]); + }, []); return (