From 48d02d06f5ce52c0e00b62372337686ddc16f879 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 29 Apr 2025 11:16:52 -0300 Subject: [PATCH 01/32] feat: add memory optimization features including garbage collection and performance monitoring components --- src/main/index.js | 41 ++ src/main/modules/memory-optimizer.js | 118 +++ src/main/preload.js | 186 +++-- src/renderer/components/LiveUpdates.vue | 14 +- .../components/MiniPerformanceMonitor.vue | 122 ++++ .../components/PerformanceMonitor.vue | 673 ++++++++++++++++++ src/renderer/components/Settings.vue | 16 + src/renderer/views/DatabaseView.vue | 30 + 8 files changed, 1139 insertions(+), 61 deletions(-) create mode 100644 src/main/modules/memory-optimizer.js create mode 100644 src/renderer/components/MiniPerformanceMonitor.vue create mode 100644 src/renderer/components/PerformanceMonitor.vue diff --git a/src/main/index.js b/src/main/index.js index f401d95..3872507 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -11,6 +11,7 @@ const { registerUpdaterHandlers, cleanup } = require("./modules/updater"); const { registerSettingsHandlers } = require("./modules/settings"); const { registerTabsHandlers } = require("./modules/tabs"); const { registerMonitoringHandlers, clearMonitoringConnections } = require("./modules/monitoring"); +const { initializeMemoryOptimizer, cleanupMemoryOptimizer, attemptGarbageCollection } = require("./modules/memory-optimizer"); require("./handlers"); @@ -20,6 +21,18 @@ const dbActivityConnections = new Map(); let mainWindow; +const enableGarbageCollection = () => { + try { + if (!global.gc) { + console.log("Garbage collection not exposed. For optimal performance, run with --expose-gc flag"); + } else { + console.log("Garbage collection available - memory optimization enabled"); + } + } catch (e) { + console.error("Failed to check garbage collection status:", e); + } +}; + function enhancePath() { const platform = process.platform; const sep = platform === "win32" ? ";" : ":"; @@ -66,6 +79,7 @@ function enhancePath() { app.whenReady().then(async () => { enhancePath(); + enableGarbageCollection(); registerConnectionHandlers(store); registerTableHandlers(store, dbMonitoringConnections); @@ -84,6 +98,9 @@ app.whenReady().then(async () => { registerUpdaterHandlers(mainWindow); + // Initialize memory optimization after everything is set up + initializeMemoryOptimizer(); + app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); @@ -93,6 +110,7 @@ app.whenReady().then(async () => { app.on("will-quit", async () => { cleanup(); + cleanupMemoryOptimizer(); await clearMonitoringConnections(dbMonitoringConnections, dbActivityConnections); }); @@ -104,9 +122,32 @@ app.on("window-all-closed", () => { }); app.on("before-quit", async () => { + if (global.gc) { + try { + attemptGarbageCollection(true); + } catch (e) { + console.error("Error running garbage collection before quit:", e); + } + } + + cleanupMemoryOptimizer(); await clearMonitoringConnections(dbMonitoringConnections, dbActivityConnections); }); +ipcMain.handle("trigger-garbage-collection", async () => { + if (global.gc) { + try { + attemptGarbageCollection(true); + return { success: true }; + } catch (error) { + console.error("Error triggering garbage collection:", error); + return { success: false, error: error.message }; + } + } else { + return { success: false, error: "Garbage collection not available" }; + } +}); + ipcMain.handle("set-app-badge", async (_, count) => { try { if (process.platform === "darwin" || process.platform === "linux") { diff --git a/src/main/modules/memory-optimizer.js b/src/main/modules/memory-optimizer.js new file mode 100644 index 0000000..f7d4520 --- /dev/null +++ b/src/main/modules/memory-optimizer.js @@ -0,0 +1,118 @@ +const { getMainWindow } = require("./window"); + +const MEMORY_CONFIG = { + checkIntervalMs: 60000, + warningThreshold: 300, + criticalThreshold: 500, + gcIntervalMs: 300000, + maxDatabaseOperations: 500 +}; + +let memoryCheckInterval = null; +let gcInterval = null; + +function initializeMemoryOptimizer() { + if (process.env.NODE_ENV === "development") { + console.log("[Memory Optimizer] Running in development mode - some optimizations disabled"); + } + + memoryCheckInterval = setInterval(checkMemoryUsage, MEMORY_CONFIG.checkIntervalMs); + + gcInterval = setInterval(attemptGarbageCollection, MEMORY_CONFIG.gcIntervalMs); + + setTimeout(() => { + checkMemoryUsage(); + attemptGarbageCollection(); + }, 30000); +} + +function checkMemoryUsage() { + try { + const memoryInfo = process.getProcessMemoryInfo ? process.getProcessMemoryInfo() : { private: process.memoryUsage().heapUsed / 1024 / 1024 }; + + const memoryUsageMB = Math.round(memoryInfo.private); + + if (process.env.NODE_ENV === "development") { + console.log(`[Memory] Current usage: ${memoryUsageMB}MB`); + } + + if (memoryUsageMB > MEMORY_CONFIG.criticalThreshold) { + handleCriticalMemoryUsage(memoryUsageMB); + } else if (memoryUsageMB > MEMORY_CONFIG.warningThreshold) { + handleHighMemoryUsage(memoryUsageMB); + } + } catch (error) { + console.error("[Memory Optimizer] Error checking memory usage:", error); + } +} + +function handleHighMemoryUsage(usageMB) { + console.warn(`[Memory] High memory usage detected: ${usageMB}MB`); + + attemptGarbageCollection(); + + const mainWindow = getMainWindow(); + if (mainWindow) { + mainWindow.webContents.send("memory-warning", { + usage: usageMB, + threshold: MEMORY_CONFIG.warningThreshold, + message: `Application is using ${usageMB}MB of memory. Consider closing unused tabs.` + }); + } +} + +function handleCriticalMemoryUsage(usageMB) { + console.error(`[Memory] Critical memory usage detected: ${usageMB}MB`); + + attemptGarbageCollection(true); + + const mainWindow = getMainWindow(); + if (mainWindow) { + mainWindow.webContents.send("memory-critical", { + usage: usageMB, + threshold: MEMORY_CONFIG.criticalThreshold, + message: `Application is using ${usageMB}MB of memory. Performance may be affected.` + }); + } +} + +function attemptGarbageCollection(aggressive = false) { + try { + if (global.gc) { + if (process.env.NODE_ENV === "development") { + console.log("[Memory] Running garbage collection" + (aggressive ? " (aggressive)" : "")); + } + + if (aggressive) { + global.gc(); + setTimeout(() => global.gc(), 500); + setTimeout(() => global.gc(), 1500); + } else { + global.gc(); + } + } + } catch (error) { + console.error("[Memory Optimizer] Error during garbage collection:", error); + } +} + +function cleanupMemoryOptimizer() { + if (memoryCheckInterval) { + clearInterval(memoryCheckInterval); + memoryCheckInterval = null; + } + + if (gcInterval) { + clearInterval(gcInterval); + gcInterval = null; + } + + attemptGarbageCollection(true); +} + +module.exports = { + initializeMemoryOptimizer, + attemptGarbageCollection, + cleanupMemoryOptimizer, + MEMORY_CONFIG +}; diff --git a/src/main/preload.js b/src/main/preload.js index 3669e1b..c43f943 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -10,6 +10,98 @@ const safeIpcRenderer = { } }; +const listenerManager = { + listeners: new Map(), + dynamicChannels: new Set(), + activeMonitoringConnections: new Set(), + + addListener: (channel, listener, isDynamic = false) => { + if (!listenerManager.listeners.has(channel)) { + listenerManager.listeners.set(channel, []); + } + + listenerManager.listeners.get(channel).push(listener); + + if (isDynamic) { + listenerManager.dynamicChannels.add(channel); + } + + return listener; + }, + + removeListener: (channel, listener) => { + if (!listenerManager.listeners.has(channel)) return; + + const listeners = listenerManager.listeners.get(channel); + const idx = listeners.indexOf(listener); + + if (idx > -1) { + listeners.splice(idx, 1); + + if (listeners.length === 0) { + listenerManager.listeners.delete(channel); + listenerManager.dynamicChannels.delete(channel); + } + } + }, + + removeAllListeners: (channel) => { + if (!channel) { + for (const [ch, listeners] of listenerManager.listeners.entries()) { + for (const listener of listeners) { + ipcRenderer.removeListener(ch, listener); + } + } + listenerManager.listeners.clear(); + listenerManager.dynamicChannels.clear(); + listenerManager.activeMonitoringConnections.clear(); + return; + } + + if (listenerManager.listeners.has(channel)) { + const listeners = listenerManager.listeners.get(channel); + for (const listener of listeners) { + ipcRenderer.removeListener(channel, listener); + } + listenerManager.listeners.delete(channel); + listenerManager.dynamicChannels.delete(channel); + + if (channel.startsWith("db-operation-")) { + const connectionId = channel.replace("db-operation-", ""); + listenerManager.activeMonitoringConnections.delete(connectionId); + } + } + }, + + addMonitoringConnection: (connectionId) => { + listenerManager.activeMonitoringConnections.add(connectionId); + }, + + removeMonitoringConnection: (connectionId) => { + listenerManager.activeMonitoringConnections.delete(connectionId); + }, + + getStats: () => { + return { + totalChannels: listenerManager.listeners.size, + totalListeners: Array.from(listenerManager.listeners.values()).reduce((acc, val) => acc + val.length, 0), + dynamicChannels: Array.from(listenerManager.dynamicChannels), + activeMonitoringConnections: Array.from(listenerManager.activeMonitoringConnections) + }; + }, + + cleanup: () => { + for (const [channel, listeners] of listenerManager.listeners.entries()) { + for (const listener of listeners) { + ipcRenderer.removeListener(channel, listener); + } + } + listenerManager.listeners.clear(); + listenerManager.dynamicChannels.clear(); + listenerManager.activeMonitoringConnections.clear(); + } +}; + function relayEventToDom(channel, data) { try { const event = new CustomEvent(channel, { @@ -28,13 +120,13 @@ function relayEventToDom(channel, data) { const validChannels = ["update-status", "update-available", "update-info", "autoUpdater:update-info", "autoUpdater:download-progress", "autoUpdater:download-complete", "restoration-progress"]; validChannels.forEach((channel) => { - ipcRenderer.on(channel, (event, data) => { + const relayer = (_, data) => { relayEventToDom(channel, data); - }); + }; + ipcRenderer.on(channel, relayer); + listenerManager.addListener(channel, relayer); }); -const eventListeners = new Map(); - try { contextBridge.exposeInMainWorld("electron", { ipcRenderer: { @@ -43,25 +135,13 @@ try { const wrappedFunc = (event, ...args) => func(event, ...args); ipcRenderer.on(channel, wrappedFunc); - if (!eventListeners.has(channel)) { - eventListeners.set(channel, []); - } - eventListeners.get(channel).push(wrappedFunc); - - return wrappedFunc; + return listenerManager.addListener(channel, wrappedFunc); } }, removeListener: (channel, func) => { if (validChannels.includes(channel)) { ipcRenderer.removeListener(channel, func); - - if (eventListeners.has(channel)) { - const listeners = eventListeners.get(channel); - const idx = listeners.indexOf(func); - if (idx > -1) { - listeners.splice(idx, 1); - } - } + listenerManager.removeListener(channel, func); } }, send: (channel, data) => { @@ -119,10 +199,14 @@ try { getOpenTabs: () => safeIpcRenderer.invoke("get-open-tabs"), saveOpenTabs: (tabs) => safeIpcRenderer.invoke("save-open-tabs", tabs), stopMonitoringDatabaseOperations: (channel, clearDataOnStop = false) => { + listenerManager.removeAllListeners(channel); + ipcRenderer.removeAllListeners(channel); const connectionId = channel.replace("db-operation-", ""); + listenerManager.removeMonitoringConnection(connectionId); + return ipcRenderer .invoke("stop-db-monitoring", connectionId, clearDataOnStop) .then((result) => { @@ -148,8 +232,12 @@ try { onUpdateStatus: (callback) => { const updateStatusListener = (_, data) => callback(data); ipcRenderer.on("update-status", updateStatusListener); + + listenerManager.addListener("update-status", updateStatusListener); + return () => { ipcRenderer.removeListener("update-status", updateStatusListener); + listenerManager.removeListener("update-status", updateStatusListener); }; }, getDatabaseRelationships: (connectionId) => @@ -159,13 +247,19 @@ try { executeSQLQuery: (config) => safeIpcRenderer.invoke("execute-sql-query", config), runArtisanCommand: (config) => safeIpcRenderer.invoke("run-artisan-command", config), getSingularForm: (word) => safeIpcRenderer.invoke("get-singular-form", word), + triggerGarbageCollection: () => safeIpcRenderer.invoke("trigger-garbage-collection"), listenCommandOutput: (commandId, callback) => { const channel = `command-output-${commandId}`; - ipcRenderer.on(channel, (_, data) => callback(data)); + const listener = (_, data) => callback(data); + + ipcRenderer.on(channel, listener); + listenerManager.addListener(channel, listener, true); + return channel; }, stopCommandListener: (channel) => { + listenerManager.removeAllListeners(channel); ipcRenderer.removeAllListeners(channel); }, getSettings: () => safeIpcRenderer.invoke("get-settings"), @@ -174,11 +268,14 @@ try { try { const channel = `db-operation-${connectionId}`; + listenerManager.removeAllListeners(channel); ipcRenderer.removeAllListeners(channel); - ipcRenderer.on(channel, (_, data) => { - callback(data); - }); + const listener = (_, data) => callback(data); + ipcRenderer.on(channel, listener); + listenerManager.addListener(channel, listener, true); + + listenerManager.addMonitoringConnection(connectionId); return ipcRenderer.invoke("start-db-monitoring", connectionId, clearHistory).then((result) => { return channel; @@ -203,21 +300,11 @@ try { const listener = (_, data) => callback(data); ipcRenderer.on(channel, listener); - if (!eventListeners.has(channel)) { - eventListeners.set(channel, []); - } - eventListeners.get(channel).push(listener); + listenerManager.addListener(channel, listener); return () => { ipcRenderer.removeListener(channel, listener); - - if (eventListeners.has(channel)) { - const listeners = eventListeners.get(channel); - const idx = listeners.indexOf(listener); - if (idx > -1) { - listeners.splice(idx, 1); - } - } + listenerManager.removeListener(channel, listener); }; } }, @@ -225,35 +312,30 @@ try { const listener = (_, data) => callback(data); ipcRenderer.on("restoration-progress", listener); - if (!eventListeners.has("restoration-progress")) { - eventListeners.set("restoration-progress", []); - } - eventListeners.get("restoration-progress").push(listener); + listenerManager.addListener("restoration-progress", listener); return () => { ipcRenderer.removeListener("restoration-progress", listener); - - if (eventListeners.has("restoration-progress")) { - const listeners = eventListeners.get("restoration-progress"); - const idx = listeners.indexOf(listener); - if (idx > -1) { - listeners.splice(idx, 1); - } - } + listenerManager.removeListener("restoration-progress", listener); }; }, + getListenerStats: () => { + return listenerManager.getStats(); + }, hashPassword: (password) => ipcRenderer.invoke("hashPassword", password), updatePassword: (config) => safeIpcRenderer.invoke("update-password", config) }); window.addEventListener("beforeunload", () => { - for (const [channel, listeners] of eventListeners.entries()) { - for (const listener of listeners) { - ipcRenderer.removeListener(channel, listener); - } - } - eventListeners.clear(); + listenerManager.cleanup(); }); + + if (process.env.NODE_ENV === "development") { + window.__cleanupListeners = () => { + listenerManager.cleanup(); + return "Listeners cleaned up"; + }; + } } catch (error) { console.error(error); } diff --git a/src/renderer/components/LiveUpdates.vue b/src/renderer/components/LiveUpdates.vue index 87ba81f..9ace5e5 100644 --- a/src/renderer/components/LiveUpdates.vue +++ b/src/renderer/components/LiveUpdates.vue @@ -414,16 +414,12 @@ function close() { if (connected.value) { stopMonitoring(); } else { + // Even if not connected, ensure any lingering listeners are properly cleaned up try { - if (window.api && window.api.monitorDatabaseOperations) { - window.api - .monitorDatabaseOperations(props.connectionId, () => {}, true) - .then(() => { - if (window.api.stopMonitoringDatabaseOperations) { - window.api.stopMonitoringDatabaseOperations(`db-operation-${props.connectionId}`, true); - } - }) - .catch((err) => console.error("Error clearing history:", err)); + const channelToClean = `db-operation-${props.connectionId}`; + + if (window.api && window.api.stopMonitoringDatabaseOperations) { + window.api.stopMonitoringDatabaseOperations(channelToClean, true).catch((err) => console.error("Error stopping monitoring on close:", err)); } } catch (e) { console.error("Error on cleanup:", e); diff --git a/src/renderer/components/MiniPerformanceMonitor.vue b/src/renderer/components/MiniPerformanceMonitor.vue new file mode 100644 index 0000000..fededfb --- /dev/null +++ b/src/renderer/components/MiniPerformanceMonitor.vue @@ -0,0 +1,122 @@ + + + diff --git a/src/renderer/components/PerformanceMonitor.vue b/src/renderer/components/PerformanceMonitor.vue new file mode 100644 index 0000000..be13455 --- /dev/null +++ b/src/renderer/components/PerformanceMonitor.vue @@ -0,0 +1,673 @@ + + + + + diff --git a/src/renderer/components/Settings.vue b/src/renderer/components/Settings.vue index e64a6b4..dea8b76 100644 --- a/src/renderer/components/Settings.vue +++ b/src/renderer/components/Settings.vue @@ -192,6 +192,21 @@ +
+ +

Display memory and event listeners statistics in the database view footer

+
+
+
+ +
Total tables: {{ databaseStore.tablesList.length }}
+ + isRedisAvailable.value); +const showPerformanceMonitor = ref(false); +const showFullPerformanceMonitor = ref(false); const connection = computed(() => { return connectionsStore.getConnection(connectionId.value); @@ -694,6 +710,7 @@ async function initializeConnection(skipReload = false) { onMounted(async () => { await initializeConnection(false); + loadPerformanceMonitorSetting(); }); onActivated(async () => { @@ -855,4 +872,17 @@ function handleUpdateProjectPath(newProjectPath) { showAlert(`Failed to update project path: ${error.message}`, "error"); }); } + +function toggleFullPerformanceMonitor() { + showFullPerformanceMonitor.value = !showFullPerformanceMonitor.value; +} + +async function loadPerformanceMonitorSetting() { + try { + const settings = await window.api.getSettings(); + showPerformanceMonitor.value = settings?.performanceMonitor && settings?.devMode; + } catch (error) { + console.error("Error loading performance monitor setting:", error); + } +} From aaa5b226256cf63588bc219ecc5e59aa379c89df Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 29 Apr 2025 11:17:10 -0300 Subject: [PATCH 02/32] feat: add historical listener tracking, potential memory leak detection, and max listeners configuration to improve performance monitoring --- src/main/preload.js | 151 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 146 insertions(+), 5 deletions(-) diff --git a/src/main/preload.js b/src/main/preload.js index c43f943..ffcf717 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -15,6 +15,9 @@ const listenerManager = { dynamicChannels: new Set(), activeMonitoringConnections: new Set(), + historicalCounts: [], + maxHistoryLength: 10, + addListener: (channel, listener, isDynamic = false) => { if (!listenerManager.listeners.has(channel)) { listenerManager.listeners.set(channel, []); @@ -82,11 +85,85 @@ const listenerManager = { }, getStats: () => { + const channelBreakdown = {}; + for (const [channel, listeners] of listenerManager.listeners.entries()) { + channelBreakdown[channel] = listeners.length; + } + + const eventEmitterStats = []; + + try { + if (ipcRenderer && ipcRenderer.eventNames) { + const events = ipcRenderer.eventNames(); + events.forEach((eventName) => { + const count = ipcRenderer.listenerCount(eventName); + eventEmitterStats.push({ + name: `ipcRenderer:${eventName}`, + count + }); + }); + } + + if (process && process.eventNames) { + const events = process.eventNames(); + events.forEach((eventName) => { + const count = process.listenerCount(eventName); + if (count > 0) { + eventEmitterStats.push({ + name: `process:${eventName}`, + count + }); + } + }); + } + } catch (error) { + console.error("Error getting EventEmitter stats:", error); + } + + const totalListeners = Array.from(listenerManager.listeners.values()).reduce((acc, val) => acc + val.length, 0); + + const timestamp = Date.now(); + listenerManager.historicalCounts.push({ + timestamp, + count: totalListeners, + channelCount: listenerManager.listeners.size + }); + + if (listenerManager.historicalCounts.length > listenerManager.maxHistoryLength) { + listenerManager.historicalCounts.shift(); + } + + let potentialLeaks = []; + if (listenerManager.historicalCounts.length >= 3) { + const current = listenerManager.historicalCounts[listenerManager.historicalCounts.length - 1]; + const previous = listenerManager.historicalCounts[listenerManager.historicalCounts.length - 3]; + + current.channelBreakdown = { ...channelBreakdown }; + + for (const [channel, listeners] of listenerManager.listeners.entries()) { + const currentCount = current.channelBreakdown[channel] || listeners.length; + const previousCount = previous.channelBreakdown?.[channel] || 0; + + if (previous && currentCount > 3 && listeners.length > 0) { + potentialLeaks.push({ + channel, + currentCount, + previousCount, + increased: currentCount > previousCount + }); + } + } + } + return { totalChannels: listenerManager.listeners.size, - totalListeners: Array.from(listenerManager.listeners.values()).reduce((acc, val) => acc + val.length, 0), + totalListeners, dynamicChannels: Array.from(listenerManager.dynamicChannels), - activeMonitoringConnections: Array.from(listenerManager.activeMonitoringConnections) + activeMonitoringConnections: Array.from(listenerManager.activeMonitoringConnections), + channelBreakdown, + eventEmitterStats, + history: listenerManager.historicalCounts, + potentialLeaks }; }, @@ -99,6 +176,25 @@ const listenerManager = { listenerManager.listeners.clear(); listenerManager.dynamicChannels.clear(); listenerManager.activeMonitoringConnections.clear(); + }, + + fixMaxListenersWarning: () => { + try { + if (process && process.setMaxListeners) { + process.setMaxListeners(50); + console.log("Process max listeners increased to 50"); + } + + if (ipcRenderer && ipcRenderer.setMaxListeners) { + ipcRenderer.setMaxListeners(50); + console.log("IpcRenderer max listeners increased to 50"); + } + + return true; + } catch (error) { + console.error("Error fixing max listeners warning:", error); + return false; + } } }; @@ -247,7 +343,20 @@ try { executeSQLQuery: (config) => safeIpcRenderer.invoke("execute-sql-query", config), runArtisanCommand: (config) => safeIpcRenderer.invoke("run-artisan-command", config), getSingularForm: (word) => safeIpcRenderer.invoke("get-singular-form", word), - triggerGarbageCollection: () => safeIpcRenderer.invoke("trigger-garbage-collection"), + triggerGarbageCollection: () => { + try { + // See if we can directly access the GC + if (global.gc) { + global.gc(); + return Promise.resolve({ success: true }); + } + // If not, try to use the main process GC + return safeIpcRenderer.invoke("trigger-garbage-collection"); + } catch (error) { + console.error("Error in triggering garbage collection:", error); + return Promise.resolve({ success: false, error: error.message }); + } + }, listenCommandOutput: (commandId, callback) => { const channel = `command-output-${commandId}`; @@ -322,6 +431,15 @@ try { getListenerStats: () => { return listenerManager.getStats(); }, + fixMaxListenersWarning: () => { + try { + const result = listenerManager.fixMaxListenersWarning(); + return Promise.resolve(result); + } catch (error) { + console.error("Error in fixMaxListenersWarning:", error); + return Promise.resolve(false); + } + }, hashPassword: (password) => ipcRenderer.invoke("hashPassword", password), updatePassword: (config) => safeIpcRenderer.invoke("update-password", config) }); @@ -332,8 +450,31 @@ try { if (process.env.NODE_ENV === "development") { window.__cleanupListeners = () => { - listenerManager.cleanup(); - return "Listeners cleaned up"; + try { + // First cleanup managed listeners + listenerManager.cleanup(); + + // Try to clean up any database monitoring connections + const dbConnectionPattern = /^db-operation-/; + const channels = ipcRenderer.eventNames ? ipcRenderer.eventNames() : []; + + for (const channel of channels) { + if (typeof channel === "string" && dbConnectionPattern.test(channel)) { + console.log(`Cleaning up untracked channel: ${channel}`); + ipcRenderer.removeAllListeners(channel); + } + } + + // Force garbage collection if available + if (global.gc) { + global.gc(); + } + + return "Listeners cleaned up successfully"; + } catch (err) { + console.error("Error in cleanup:", err); + return "Error cleaning up listeners: " + err.message; + } }; } } catch (error) { From ce426ed5506d827dea7381034e899242b0d0285e Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 29 Apr 2025 11:17:41 -0300 Subject: [PATCH 03/32] chore: bump version to 0.9.9 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70450df..fc06166 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "larabase", - "version": "0.9.8", + "version": "0.9.9", "description": "An Opined Database GUI for Laravel Developers", "main": "dist/main.cjs", "type": "commonjs", From ce8a6cbc47aa4e9feef6167d30d4ee5ae0c7d86a Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 29 Apr 2025 13:53:25 -0300 Subject: [PATCH 04/32] feat: enhance password hashing functionality by allowing configurable encryption rounds in the UpdatePasswordModal component --- src/main/handlers.js | 6 +++-- src/main/preload.js | 2 +- .../tabs/partials/UpdatePasswordModal.vue | 22 ++++++++++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/handlers.js b/src/main/handlers.js index 448e542..329bd90 100644 --- a/src/main/handlers.js +++ b/src/main/handlers.js @@ -3,9 +3,11 @@ const bcrypt = require("bcryptjs"); const mysql = require("mysql2/promise"); function setupHandlers() { - ipcMain.handle("hashPassword", async (_, password) => { + ipcMain.handle("hashPassword", async (_, password, rounds = 10) => { try { - const salt = await bcrypt.genSalt(10); + rounds = Math.max(4, Math.min(15, rounds)); + + const salt = await bcrypt.genSalt(rounds); const hash = await bcrypt.hash(password, salt); return { diff --git a/src/main/preload.js b/src/main/preload.js index ffcf717..99e7d4d 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -440,7 +440,7 @@ try { return Promise.resolve(false); } }, - hashPassword: (password) => ipcRenderer.invoke("hashPassword", password), + hashPassword: (password, rounds = 10) => ipcRenderer.invoke("hashPassword", password, rounds), updatePassword: (config) => safeIpcRenderer.invoke("update-password", config) }); diff --git a/src/renderer/components/tabs/partials/UpdatePasswordModal.vue b/src/renderer/components/tabs/partials/UpdatePasswordModal.vue index 4c886d1..9f69d39 100644 --- a/src/renderer/components/tabs/partials/UpdatePasswordModal.vue +++ b/src/renderer/components/tabs/partials/UpdatePasswordModal.vue @@ -50,6 +50,25 @@ Show password + +
+ +
+ +
+ +
@@ -86,6 +105,7 @@ const newPassword = ref(""); const passwordError = ref(""); const showPassword = ref(false); const isLoading = ref(false); +const rounds = ref(10); watch(newPassword, () => { passwordError.value = ""; @@ -123,7 +143,7 @@ async function updatePassword() { try { isLoading.value = true; - const result = await window.api.hashPassword(newPassword.value); + const result = await window.api.hashPassword(newPassword.value, rounds.value); if (!result.success) { throw new Error(result.message || "Failed to hash password"); From 4ebb4405153fd1aced70ec71514af429c2d30d80 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 29 Apr 2025 14:32:25 -0300 Subject: [PATCH 05/32] refactor: remove garbage collection functionality from memory optimizer and UI components to streamline memory management --- src/main/handlers.js | 2 +- src/main/index.js | 37 +------------ src/main/modules/memory-optimizer.js | 49 ++++------------- .../components/PerformanceMonitor.vue | 52 ------------------- 4 files changed, 12 insertions(+), 128 deletions(-) diff --git a/src/main/handlers.js b/src/main/handlers.js index 329bd90..4834948 100644 --- a/src/main/handlers.js +++ b/src/main/handlers.js @@ -6,7 +6,7 @@ function setupHandlers() { ipcMain.handle("hashPassword", async (_, password, rounds = 10) => { try { rounds = Math.max(4, Math.min(15, rounds)); - + const salt = await bcrypt.genSalt(rounds); const hash = await bcrypt.hash(password, salt); diff --git a/src/main/index.js b/src/main/index.js index 3872507..f2f63dd 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -11,7 +11,7 @@ const { registerUpdaterHandlers, cleanup } = require("./modules/updater"); const { registerSettingsHandlers } = require("./modules/settings"); const { registerTabsHandlers } = require("./modules/tabs"); const { registerMonitoringHandlers, clearMonitoringConnections } = require("./modules/monitoring"); -const { initializeMemoryOptimizer, cleanupMemoryOptimizer, attemptGarbageCollection } = require("./modules/memory-optimizer"); +const { initializeMemoryOptimizer, cleanupMemoryOptimizer } = require("./modules/memory-optimizer"); require("./handlers"); @@ -21,18 +21,6 @@ const dbActivityConnections = new Map(); let mainWindow; -const enableGarbageCollection = () => { - try { - if (!global.gc) { - console.log("Garbage collection not exposed. For optimal performance, run with --expose-gc flag"); - } else { - console.log("Garbage collection available - memory optimization enabled"); - } - } catch (e) { - console.error("Failed to check garbage collection status:", e); - } -}; - function enhancePath() { const platform = process.platform; const sep = platform === "win32" ? ";" : ":"; @@ -79,7 +67,6 @@ function enhancePath() { app.whenReady().then(async () => { enhancePath(); - enableGarbageCollection(); registerConnectionHandlers(store); registerTableHandlers(store, dbMonitoringConnections); @@ -122,32 +109,10 @@ app.on("window-all-closed", () => { }); app.on("before-quit", async () => { - if (global.gc) { - try { - attemptGarbageCollection(true); - } catch (e) { - console.error("Error running garbage collection before quit:", e); - } - } - cleanupMemoryOptimizer(); await clearMonitoringConnections(dbMonitoringConnections, dbActivityConnections); }); -ipcMain.handle("trigger-garbage-collection", async () => { - if (global.gc) { - try { - attemptGarbageCollection(true); - return { success: true }; - } catch (error) { - console.error("Error triggering garbage collection:", error); - return { success: false, error: error.message }; - } - } else { - return { success: false, error: "Garbage collection not available" }; - } -}); - ipcMain.handle("set-app-badge", async (_, count) => { try { if (process.platform === "darwin" || process.platform === "linux") { diff --git a/src/main/modules/memory-optimizer.js b/src/main/modules/memory-optimizer.js index f7d4520..5f5f8ea 100644 --- a/src/main/modules/memory-optimizer.js +++ b/src/main/modules/memory-optimizer.js @@ -4,12 +4,10 @@ const MEMORY_CONFIG = { checkIntervalMs: 60000, warningThreshold: 300, criticalThreshold: 500, - gcIntervalMs: 300000, maxDatabaseOperations: 500 }; let memoryCheckInterval = null; -let gcInterval = null; function initializeMemoryOptimizer() { if (process.env.NODE_ENV === "development") { @@ -18,19 +16,24 @@ function initializeMemoryOptimizer() { memoryCheckInterval = setInterval(checkMemoryUsage, MEMORY_CONFIG.checkIntervalMs); - gcInterval = setInterval(attemptGarbageCollection, MEMORY_CONFIG.gcIntervalMs); - setTimeout(() => { checkMemoryUsage(); - attemptGarbageCollection(); }, 30000); } function checkMemoryUsage() { try { - const memoryInfo = process.getProcessMemoryInfo ? process.getProcessMemoryInfo() : { private: process.memoryUsage().heapUsed / 1024 / 1024 }; + const memoryInfo = typeof process.getProcessMemoryInfo === "function" ? process.getProcessMemoryInfo() : { private: process.memoryUsage().heapUsed / 1024 / 1024 }; + + let memoryValue; + try { + memoryValue = typeof memoryInfo.private === "number" && !isNaN(memoryInfo.private) ? memoryInfo.private : process.memoryUsage().heapUsed / 1024 / 1024; + } catch (e) { + memoryValue = 0; + console.error("[Memory] Error getting memory usage details:", e); + } - const memoryUsageMB = Math.round(memoryInfo.private); + const memoryUsageMB = Math.round(memoryValue); if (process.env.NODE_ENV === "development") { console.log(`[Memory] Current usage: ${memoryUsageMB}MB`); @@ -49,8 +52,6 @@ function checkMemoryUsage() { function handleHighMemoryUsage(usageMB) { console.warn(`[Memory] High memory usage detected: ${usageMB}MB`); - attemptGarbageCollection(); - const mainWindow = getMainWindow(); if (mainWindow) { mainWindow.webContents.send("memory-warning", { @@ -64,8 +65,6 @@ function handleHighMemoryUsage(usageMB) { function handleCriticalMemoryUsage(usageMB) { console.error(`[Memory] Critical memory usage detected: ${usageMB}MB`); - attemptGarbageCollection(true); - const mainWindow = getMainWindow(); if (mainWindow) { mainWindow.webContents.send("memory-critical", { @@ -76,43 +75,15 @@ function handleCriticalMemoryUsage(usageMB) { } } -function attemptGarbageCollection(aggressive = false) { - try { - if (global.gc) { - if (process.env.NODE_ENV === "development") { - console.log("[Memory] Running garbage collection" + (aggressive ? " (aggressive)" : "")); - } - - if (aggressive) { - global.gc(); - setTimeout(() => global.gc(), 500); - setTimeout(() => global.gc(), 1500); - } else { - global.gc(); - } - } - } catch (error) { - console.error("[Memory Optimizer] Error during garbage collection:", error); - } -} - function cleanupMemoryOptimizer() { if (memoryCheckInterval) { clearInterval(memoryCheckInterval); memoryCheckInterval = null; } - - if (gcInterval) { - clearInterval(gcInterval); - gcInterval = null; - } - - attemptGarbageCollection(true); } module.exports = { initializeMemoryOptimizer, - attemptGarbageCollection, cleanupMemoryOptimizer, MEMORY_CONFIG }; diff --git a/src/renderer/components/PerformanceMonitor.vue b/src/renderer/components/PerformanceMonitor.vue index be13455..0420ed8 100644 --- a/src/renderer/components/PerformanceMonitor.vue +++ b/src/renderer/components/PerformanceMonitor.vue @@ -235,12 +235,6 @@ > Cleanup - -