From f7f7122620417041e30192535e32576fa8693857 Mon Sep 17 00:00:00 2001 From: ISA Date: Fri, 6 Jun 2025 10:21:56 +0200 Subject: [PATCH] feat: Marker-Cleanup zur Vermeidung von Memory Leaks implementiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cleanupMarkers() Utility in /utils/common/cleanupMarkers.js erstellt - Marker-Cleanup in MapComponent.js vor createAndSetDevices() integriert - createAndSetDevices.js von Cleanup-Verantwortung befreit (reine Erzeugung) - setupPOIs.js erweitert um cleanupMarkers() vor Layer-Neuerstellung - poiUtils.js und markerUtils.js angepasst: cleanupMarkers() ersetzt .remove() - Memory Leaks durch verwaiste Tooltips, Events und Marker behoben - Grundlage für wiederverwendbare Marker-Cleanup-Logik für POIs, Geräte, Linien geschaffen --- TODO.md | 21 +++++++ components/mainComponent/MapComponent.js | 64 ++++++++++---------- config/appVersion.js | 2 +- hooks/useAreaMarkersLayer.js | 3 +- utils/common/cleanupMarkers.js | 25 ++++++++ utils/devices/createAndSetDevices.js | 1 + utils/markerUtils.js | 22 +++++-- utils/poiUtils.js | 11 ++-- utils/polylines/cleanupPolylinesForMemory.js | 29 +++++++++ utils/setupPOIs.js | 37 +++++++---- 10 files changed, 160 insertions(+), 55 deletions(-) create mode 100644 utils/common/cleanupMarkers.js create mode 100644 utils/polylines/cleanupPolylinesForMemory.js diff --git a/TODO.md b/TODO.md index 5d6e88719..62e5571fd 100644 --- a/TODO.md +++ b/TODO.md @@ -22,3 +22,24 @@ - [x] TODO: Möglichkeit bevor in Gitea hochgeladen, .env.local anpassen, vielleicht mit husky Wenn git push genutzt wird soll für Produktionsumgebung angepasst werden, Vorschlag ---> .env.local und .env.production für Entwicklungsumgebung und Produktionsumgebung automatische Switch + +## 🧹 Memory Leaks prüfen + +- [ ] **MapComponent.js** - [ ] `setInterval(...)` (1x) - [ ] `setTimeout(...)` (2x) - [ ] + `window.xyz = ...` (4x) – globale Variablen - [ ] `map.on(...)` (2x) - [ ] + `addEventListener(...)` (1x) - 📌 Problematisch, wenn `clearInterval`, `clearTimeout`, + `map.off(...)` oder `removeEventListener(...)` nicht im useEffect-Cleanup gemacht werden. → + Speicher kann anwachsen, besonders bei Hot-Reload oder Navigation im iFrame. + +- [ ] **useAreaMarkersLayer.js** - [ ] `setInterval(...)` - [ ] `addEventListener(...)` - 📌 Auch + hier muss geprüft werden, ob beim Unmounting der Komponente `clearInterval()` und + `removeEventListener()` aufgerufen wird. + +- [ ] **AddPOIModal.js** - [ ] `setTimeout(...)` - 📌 Prüfen, ob der Timeout vor unmount gecleart + wird (z. B. bei schnellem Öffnen und Schließen des Modals). + +- [ ] **MapLayersControlPanel.js** - [ ] `setTimeout(...)` + +- [ ] **useDataUpdater.js** - [ ] `setInterval(...)` - 📌 Sehr wahrscheinlich ein regelmäßiger + Polling-Mechanismus → unbedingt prüfen, ob `clearInterval()` im useEffect-Cleanup enthalten + ist. diff --git a/components/mainComponent/MapComponent.js b/components/mainComponent/MapComponent.js index 35422e420..3b518cf36 100644 --- a/components/mainComponent/MapComponent.js +++ b/components/mainComponent/MapComponent.js @@ -77,6 +77,8 @@ import { updateAreaThunk } from "@/redux/thunks/database/area/updateAreaThunk"; import useDynamicDeviceLayers from "@/hooks/useDynamicDeviceLayers.js"; import useDataUpdater from "@/hooks/useDataUpdater"; +import { cleanupPolylinesForMemory } from "@/utils/polylines/cleanupPolylinesForMemory"; +import { cleanupMarkers } from "@/utils/common/cleanupMarkers"; //----------------------------------------------------------------------------------------------------- const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { //------------------------------- @@ -299,9 +301,13 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { useEffect(() => { if (!map) return; - // Entferne alte Marker und Polylinien - markers.forEach(marker => marker.remove()); - polylines.forEach(polyline => polyline.remove()); + // vorherige Marker & Polylinien vollständig bereinigen + + markers.forEach(marker => { + console.log("Marker-Typ:", marker.options); + marker.remove(); + }); + cleanupPolylinesForMemory(polylines, map); // Setze neue Marker und Polylinien mit den aktuellen Daten const { markers: newMarkers, polylines: newPolylines } = setupPolylines( @@ -358,7 +364,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { polyline.closeTooltip(); }); }); - + cleanupMarkers(markers, oms); setMarkers(newMarkers); setPolylines(newPolylines); }, [ @@ -471,14 +477,15 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { } }, [map]); //-------------------------------------------- + let timeoutId; useEffect(() => { const initializeContextMenu = () => { if (map) { map.whenReady(() => { - setTimeout(() => { + timeoutId = setTimeout(() => { if (map.contextmenu) { if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") { - //console.log("Contextmenu ist vorhanden"); + console.log("Contextmenu ist vorhanden"); } } else { console.warn("Contextmenu ist nicht verfügbar."); @@ -489,6 +496,10 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { }; initializeContextMenu(); + + return () => { + clearTimeout(timeoutId); // Aufräumen + }; }, [map]); //--------------------------------------- @@ -693,34 +704,28 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { } }, [isPolylineContextMenuOpen, countdown, countdownActive, dispatch, window.map]); //---------------------------------- - // map in window setzen für mehr Debugging - useEffect(() => { - if (map) { - window.map = map; - if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") { - console.log("✅ window.map wurde gesetzt:", window.map); - } - } - }, [map]); - //--------------------------------------- // **Fehlerbehandlung für `contextmenu`** // damit den Fehler mit contextmenu nicht angezeigt wird und überspringt wird und die Seite neu geladen wird useEffect(() => { + let timeoutId; + window.onerror = function (message, source, lineno, colno, error) { if (message.includes("Cannot read properties of null (reading 'contextmenu')")) { - console.warn("⚠️ Fehler mit `contextmenu` erkannt - Neuladen der Seite."); - setTimeout(() => { + console.warn("⚠️ Fehler mit `contextmenu` erkannt – Neuladen der Seite."); + timeoutId = setTimeout(() => { window.location.reload(); - }, 0); // **Seite nach Sekunde neu laden** - return true; // **Fehler unterdrücken, damit React ihn nicht anzeigt** + }, 0); + return true; // Fehler unterdrücken } }; return () => { - window.onerror = null; // **Fehlerbehandlung entfernen, wenn Komponente unmounted wird** + clearTimeout(timeoutId); + window.onerror = null; // Aufräumen beim Unmount }; }, []); + //------------------------------------------------ useEffect(() => { if (poiTypStatus === "succeeded" && Array.isArray(poiTypData)) { @@ -736,7 +741,6 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { } }, [poiIconsData, poiIconsStatus]); //----------------------------------------------------------------- - //----------------------------------------------------------------- useEffect(() => { if (!map) return; @@ -761,16 +765,6 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { checkOverlappingMarkers(map, allMarkers, plusRoundIcon); }, [map, markerStates, mapLayersVisibility]); - //--------------------------------------------- - // 🧠 Optional für Debugging für überlappende Markers - useEffect(() => { - if (oms) { - if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") { - console.log("📌 OMS ready:", oms); - } - window.oms = oms; // Für Debugging global - } - }, [oms]); //---------------------------------------------- useEffect(() => { @@ -780,6 +774,12 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { console.log("Production Mode aktiviert"); } }, []); + //------------------------------------------- + useEffect(() => { + return () => { + cleanupMarkers(markers, oms); + }; + }, []); //--------------------------------------------- //-------------------------------------------- return ( diff --git a/config/appVersion.js b/config/appVersion.js index a2ba3996c..4af39151f 100644 --- a/config/appVersion.js +++ b/config/appVersion.js @@ -1,2 +1,2 @@ // /config/appVersion -export const APP_VERSION = "1.1.236"; +export const APP_VERSION = "1.1.237"; diff --git a/hooks/useAreaMarkersLayer.js b/hooks/useAreaMarkersLayer.js index fb7d5eece..e295823a7 100644 --- a/hooks/useAreaMarkersLayer.js +++ b/hooks/useAreaMarkersLayer.js @@ -48,7 +48,7 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => { }; window.addEventListener("storage", handleStorageChange); - const intervalId = setInterval(updateMarkersVisibility, 500); + const intervalId = setInterval(updateMarkersVisibility, 5000); return () => { window.removeEventListener("storage", handleStorageChange); @@ -68,6 +68,7 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => { const marker = L.marker([item.x, item.y], { icon: customIcon, draggable: true, + customType: "areaMarker", }); marker.bindTooltip( diff --git a/utils/common/cleanupMarkers.js b/utils/common/cleanupMarkers.js new file mode 100644 index 000000000..1df183b33 --- /dev/null +++ b/utils/common/cleanupMarkers.js @@ -0,0 +1,25 @@ +// /utils/common/cleanupMarkers.js + +/** + * Entfernt alle Leaflet-Marker vollständig aus der Karte und aus OMS egal welche Typ sind + * @param {L.Marker[]} markers - Liste der Marker + * @param {object} oms - OverlappingMarkerSpiderfier-Instanz (optional) + */ +export const cleanupMarkers = (markers = [], oms = null) => { + markers.forEach(marker => { + // Tooltip und Popup entfernen + marker.unbindTooltip?.(); + marker.unbindPopup?.(); + + // Event-Listener entfernen (zur Sicherheit) + marker.off?.(); + + // Marker von Karte entfernen + marker.remove?.(); + + // Marker aus OMS entfernen (wenn vorhanden) + if (oms && typeof oms.removeMarker === "function") { + oms.removeMarker(marker); + } + }); +}; diff --git a/utils/devices/createAndSetDevices.js b/utils/devices/createAndSetDevices.js index 2df95a193..812191a84 100644 --- a/utils/devices/createAndSetDevices.js +++ b/utils/devices/createAndSetDevices.js @@ -7,6 +7,7 @@ import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisSt import { selectGisStationsStatusDistrict } from "@/redux/slices/webservice/gisStationsStatusDistrictSlice.js"; import { selectGisStationsMeasurements } from "@/redux/slices/webservice/gisStationsMeasurementsSlice.js"; import { addContextMenuToMarker } from "@/utils/contextMenuUtils"; +import { cleanupMarkers } from "@/utils/common/cleanupMarkers"; const determinePriority = (iconPath, priorityConfig) => { for (let priority of priorityConfig) { diff --git a/utils/markerUtils.js b/utils/markerUtils.js index 114a6de27..1f1bd0f9c 100644 --- a/utils/markerUtils.js +++ b/utils/markerUtils.js @@ -5,8 +5,9 @@ import circleIcon from "../components/CircleIcon"; import { store } from "../redux/store"; import { updatePolylineCoordinatesThunk } from "../redux/thunks/database/polylines/updatePolylineCoordinatesThunk"; import { redrawPolyline } from "./mapUtils"; +import { cleanupMarkers } from "@/utils/common/cleanupMarkers"; -const savePolylineRedux = (lineData) => { +const savePolylineRedux = lineData => { return store .dispatch( updatePolylineCoordinatesThunk({ @@ -16,7 +17,7 @@ const savePolylineRedux = (lineData) => { }) ) .unwrap() - .catch((error) => { + .catch(error => { console.error("Fehler beim Speichern der Linienkoordinaten:", error.message); }); }; @@ -44,18 +45,27 @@ export const insertNewMarker = (closestPoints, newPoint, lineData, map) => { }; export const removeMarker = (marker, lineData, currentZoom, currentCenter) => { - const index = lineData.coordinates.findIndex((coord) => L.latLng(coord[0], coord[1]).equals(marker.getLatLng())); + const index = lineData.coordinates.findIndex(coord => + L.latLng(coord[0], coord[1]).equals(marker.getLatLng()) + ); if (index !== -1) { lineData.coordinates.splice(index, 1); redrawPolyline(lineData); - marker.remove(); + cleanupMarkers([marker]); savePolylineRedux(lineData); // ✅ Wiederverwendung window.location.reload(); } }; -export const handleEditPoi = (marker, userRights, setCurrentPoiData, setShowPoiUpdateModal, fetchPoiData, toast) => { +export const handleEditPoi = ( + marker, + userRights, + setCurrentPoiData, + setShowPoiUpdateModal, + fetchPoiData, + toast +) => { if (!Array.isArray(userRights)) { toast.error("Benutzerrechte sind ungültig.", { position: "top-center", @@ -64,7 +74,7 @@ export const handleEditPoi = (marker, userRights, setCurrentPoiData, setShowPoiU return; } - if (!userRights.some((r) => r.IdRight === 56)) { + if (!userRights.some(r => r.IdRight === 56)) { toast.error("Benutzer hat keine Berechtigung zum Bearbeiten.", { position: "top-center", autoClose: 5000, diff --git a/utils/poiUtils.js b/utils/poiUtils.js index 08e76ee39..7f1042b01 100644 --- a/utils/poiUtils.js +++ b/utils/poiUtils.js @@ -6,9 +6,10 @@ import { redrawPolyline } from "./polylines/redrawPolyline.js"; import { store } from "../redux/store"; import { updatePolylineCoordinatesThunk } from "../redux/thunks/database/polylines/updatePolylineCoordinatesThunk"; +import { cleanupMarkers } from "@/utils/common/cleanupMarkers"; // 🧠 Zentrale Redux-Dispatch-Hilfsfunktion -const savePolylineRedux = (lineData) => { +const savePolylineRedux = lineData => { return store .dispatch( updatePolylineCoordinatesThunk({ @@ -18,7 +19,7 @@ const savePolylineRedux = (lineData) => { }) ) .unwrap() - .catch((error) => { + .catch(error => { console.error("Fehler beim Speichern der Linienkoordinaten:", error.message); }); }; @@ -49,12 +50,14 @@ export const insertNewPOI = (closestPoints, newPoint, lineData, map) => { }; export const removePOI = (marker, lineData, currentZoom, currentCenter) => { - const index = lineData.coordinates.findIndex((coord) => L.latLng(coord[0], coord[1]).equals(marker.getLatLng())); + const index = lineData.coordinates.findIndex(coord => + L.latLng(coord[0], coord[1]).equals(marker.getLatLng()) + ); if (index !== -1) { lineData.coordinates.splice(index, 1); redrawPolyline(lineData); - marker.remove(); + cleanupMarkers([marker]); savePolylineRedux(lineData); window.location.reload(); } diff --git a/utils/polylines/cleanupPolylinesForMemory.js b/utils/polylines/cleanupPolylinesForMemory.js new file mode 100644 index 000000000..b2a544d04 --- /dev/null +++ b/utils/polylines/cleanupPolylinesForMemory.js @@ -0,0 +1,29 @@ +// /utils/polylines/cleanupPolylinesForMemory.js + +/** + * Bereinigt Leaflet-Polylinien aus dem Speicher: + * - entfernt Event-Listener (off) + * - entfernt Tooltips (unbindTooltip) + * - entfernt Layer von der Karte (removeLayer) + * + * @param {L.Polyline[]} polylines - Liste der Leaflet-Polylinien + * @param {L.Map} map - Leaflet-Karteninstanz + */ +export function cleanupPolylinesForMemory(polylines = [], map) { + if (!Array.isArray(polylines)) return; + + polylines.forEach(polyline => { + try { + // Events und Tooltip entfernen + polyline.off(); + polyline.unbindTooltip?.(); + + // Von der Karte entfernen, wenn noch vorhanden + if (map && map.hasLayer(polyline)) { + map.removeLayer(polyline); + } + } catch (e) { + console.warn("Fehler beim Bereinigen einer Polyline:", e); + } + }); +} diff --git a/utils/setupPOIs.js b/utils/setupPOIs.js index 87422af4d..bb200e4bd 100644 --- a/utils/setupPOIs.js +++ b/utils/setupPOIs.js @@ -3,6 +3,7 @@ import { parsePoint } from "./geometryUtils"; import { handleEditPoi } from "./poiUtils"; import { updateLocationInDatabaseService } from "../services/database/updateLocationInDatabaseService"; import { setSelectedPoi, clearSelectedPoi } from "../redux/slices/database/pois/selectedPoiSlice"; +import { cleanupMarkers } from "@/utils/common/cleanupMarkers"; export const setupPOIs = async ( map, @@ -28,13 +29,17 @@ export const setupPOIs = async ( // ✅ Mapping vorbereiten: idPoi → icon path const iconMap = new Map(); - poiData.forEach((item) => { + poiData.forEach(item => { if (item.idPoi && item.path) { iconMap.set(item.idPoi, item.path); } }); if (map && poiLayerRef.current) { + const existingMarkers = poiLayerRef.current?.getLayers?.(); + if (existingMarkers?.length) { + cleanupMarkers(existingMarkers); + } map.removeLayer(poiLayerRef.current); poiLayerRef.current = new L.LayerGroup().addTo(map); @@ -42,11 +47,13 @@ export const setupPOIs = async ( try { const { latitude, longitude } = parsePoint(poi.position); const poiTypName = poiTypMap.get(poi.idPoiTyp) || "Unbekannt"; - const canDrag = userRights ? userRights.some((r) => r.IdRight === 56) : false; + const canDrag = userRights ? userRights.some(r => r.IdRight === 56) : false; // ✅ Icon korrekt über idPoi auflösen const iconPath = iconMap.get(poi.idPoi); - const iconUrl = iconPath ? `/img/icons/pois/${iconPath}` : "/img/icons/pois/default-icon.png"; + const iconUrl = iconPath + ? `/img/icons/pois/${iconPath}` + : "/img/icons/pois/default-icon.png"; const marker = L.marker([latitude, longitude], { icon: L.icon({ @@ -72,7 +79,15 @@ export const setupPOIs = async ( { text: "POI Bearbeiten", icon: "/img/poi-edit.png", - callback: () => handleEditPoi(marker, userRights, setCurrentPoiData, setShowPoiUpdateModal, fetchPoiData, toast), + callback: () => + handleEditPoi( + marker, + userRights, + setCurrentPoiData, + setShowPoiUpdateModal, + fetchPoiData, + toast + ), }, ], }); @@ -80,12 +95,12 @@ export const setupPOIs = async ( marker.bindTooltip( ` -
-
${poi.description || "Unbekannt"}
-
Gerät: ${deviceName || "unbekannt"}
-
Typ: ${poiTypName}
-
- `, +
+
${poi.description || "Unbekannt"}
+
Gerät: ${deviceName || "unbekannt"}
+
Typ: ${poiTypName}
+
+ `, { direction: "top", offset: [0, -10], @@ -105,7 +120,7 @@ export const setupPOIs = async ( //this.closePopup(); }); - marker.on("dragend", (e) => { + marker.on("dragend", e => { if (canDrag) { const newLat = e.target.getLatLng().lat; const newLng = e.target.getLatLng().lng;