diff --git a/electron/main/index.ts b/electron/main/index.ts index 733b8b67d..057ac8860 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -51,6 +51,13 @@ findAvailablePort(browser_port).then(port => { app.commandLine.appendSwitch('remote-debugging-port', port + ''); }); +// Memory optimization settings +app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096'); +app.commandLine.appendSwitch('force-gpu-mem-available-mb', '512'); +app.commandLine.appendSwitch('max_old_space_size', '4096'); +app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction'); +app.commandLine.appendSwitch('renderer-process-limit', '8'); + // ==================== app config ==================== process.env.APP_ROOT = MAIN_DIST; process.env.VITE_PUBLIC = VITE_PUBLIC; @@ -922,8 +929,8 @@ async function createWindow() { fileReader = new FileReader(win); webViewManager = new WebViewManager(win); - // create multiple webviews - for (let i = 1; i <= 8; i++) { + // create initial webviews (reduced from 8 to 3) + for (let i = 1; i <= 3; i++) { webViewManager.createWebview(i === 1 ? undefined : i.toString()); } @@ -1197,14 +1204,24 @@ const cleanupPythonProcess = async () => { const pid = python_process.pid; log.info('Cleaning up Python process', { pid }); + // Remove all listeners to prevent memory leaks + python_process.removeAllListeners(); + await new Promise((resolve) => { - kill(pid, 'SIGINT', (err) => { + kill(pid, 'SIGTERM', (err) => { if (err) { - log.error('Failed to clean up process tree:', err); + log.error('Failed to clean up process tree with SIGTERM:', err); + // Try SIGKILL as fallback + kill(pid, 'SIGKILL', (killErr) => { + if (killErr) { + log.error('Failed to force kill process tree:', killErr); + } + resolve(); + }); } else { log.info('Successfully cleaned up Python process tree'); + resolve(); } - resolve(); }); }); } @@ -1224,17 +1241,38 @@ const cleanupPythonProcess = async () => { } } + // Clean up any temporary files in userData + try { + const tempFiles = ['backend.lock', 'uv_installing.lock']; + for (const file of tempFiles) { + const filePath = path.join(userData, file); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } + } catch (error) { + log.error('Error cleaning up temp files:', error); + } + python_process = null; } catch (error) { log.error('Error occurred while cleaning up process:', error); } }; -// brefore close +// before close const handleBeforeClose = () => { + let isQuitting = false; + + app.on('before-quit', () => { + isQuitting = true; + }); + win?.on("close", (event) => { - event.preventDefault(); - win?.webContents.send("before-close"); + if (!isQuitting) { + event.preventDefault(); + win?.webContents.send("before-close"); + } }) } @@ -1289,8 +1327,15 @@ app.whenReady().then(() => { // ==================== window close event ==================== app.on('window-all-closed', () => { log.info('window-all-closed'); - webViewManager = null; + + // Clean up WebView manager + if (webViewManager) { + webViewManager.destroy(); + webViewManager = null; + } + win = null; + if (process.platform !== 'darwin') { app.quit(); } @@ -1310,12 +1355,44 @@ app.on('activate', () => { }); // ==================== app exit event ==================== -app.on('before-quit', () => { +app.on('before-quit', async (event) => { log.info('before-quit'); log.info('quit python_process.pid: ' + python_process?.pid); - if (win) { - win.destroy(); + + // Prevent default quit to ensure cleanup completes + event.preventDefault(); + + try { + // Clean up resources + if (webViewManager) { + webViewManager.destroy(); + webViewManager = null; + } + + if (win && !win.isDestroyed()) { + win.destroy(); + win = null; + } + + // Wait for Python process cleanup + await cleanupPythonProcess(); + + // Clean up file reader if exists + if (fileReader) { + fileReader = null; + } + + // Clear any remaining timeouts/intervals + if (global.gc) { + global.gc(); + } + + log.info('All cleanup completed, exiting...'); + } catch (error) { + log.error('Error during cleanup:', error); + } finally { + // Force quit after cleanup + app.exit(0); } - cleanupPythonProcess(); }); diff --git a/electron/main/webview.ts b/electron/main/webview.ts index 123d464d2..8036dda6d 100644 --- a/electron/main/webview.ts +++ b/electron/main/webview.ts @@ -20,6 +20,9 @@ export class WebViewManager { private webViews = new Map() private win: BrowserWindow | null = null private size: Size = { x: 0, y: 0, width: 0, height: 0 } + private maxInactiveWebviews = 5 + private lastCleanupTime = Date.now() + constructor(window: BrowserWindow) { this.win = window } @@ -63,6 +66,12 @@ export class WebViewManager { webPreferences: { nodeIntegration: false, contextIsolation: true, + backgroundThrottling: true, + offscreen: false, + sandbox: true, + disableBlinkFeatures: 'Accelerated2dCanvas', + enableBlinkFeatures: 'IdleDetection', + autoplayPolicy: 'document-user-activation-required', }, }) view.webContents.on('did-finish-load', () => { @@ -119,13 +128,22 @@ export class WebViewManager { webViewInfo.view.setBounds({ x: -1919, y: -1079, width: 1920, height: 1080 }) const activeSize = this.getActiveWebview().length const allSize = Array.from(this.webViews.values()).length - if (allSize - activeSize <= 3) { + const inactiveSize = allSize - activeSize + + // Clean up inactive webviews if too many + if (inactiveSize > this.maxInactiveWebviews && Date.now() - this.lastCleanupTime > 30000) { + this.cleanupInactiveWebviews() + this.lastCleanupTime = Date.now() + } + + // Create new webviews if needed + if (inactiveSize <= 2) { const existingKeys = Array.from(this.webViews.keys()).map(Number).filter(n => !isNaN(n)) const maxId = existingKeys.length > 0 ? Math.max(...existingKeys) : 0 const startId = maxId + 1 - // Create webviews sequentially to avoid race conditions - for (let i = 0; i < 3; i++) { + // Create only 2 new webviews to reduce memory usage + for (let i = 0; i < 2; i++) { const nextId = (startId + i).toString() this.createWebview(nextId, 'about:blank?use=0') } @@ -190,6 +208,10 @@ export class WebViewManager { let newId = Number(id) webViewInfo.view.setBounds({ x: -9999 + newId * 100, y: -9999 + newId * 100, width: 100, height: 100 }) webViewInfo.isShow = false + + if (webViewInfo.view.webContents && !webViewInfo.view.webContents.isDestroyed()) { + webViewInfo.view.webContents.setBackgroundThrottling(true) + } return { success: true } } @@ -198,19 +220,36 @@ export class WebViewManager { let newId = Number(webview.id) webview.view.setBounds({ x: -9999 + newId * 100, y: -9999 + newId * 100, width: 100, height: 100 }) webview.isShow = false + + if (webview.view.webContents && !webview.view.webContents.isDestroyed()) { + webview.view.webContents.setBackgroundThrottling(true) + } }) } - public showWebview(id: string) { - const webViewInfo = this.webViews.get(id) + public async showWebview(id: string) { + let webViewInfo = this.webViews.get(id) + + // If webview doesn't exist, create it if (!webViewInfo) { - return { success: false, error: `Webview with id ${id} not found` } + console.log(`Webview ${id} not found, creating new one`) + const createResult = await this.createWebview(id, 'about:blank?use=0') + if (!createResult.success) { + return { success: false, error: `Failed to create webview ${id}` } + } + webViewInfo = this.webViews.get(id)! } + const currentUrl = webViewInfo.view.webContents.getURL(); this.win?.webContents.send("url-updated", currentUrl); webViewInfo.isShow = true this.changeViewSize(id, this.size) console.log("showWebview", id, this.size) + + if (webViewInfo.view.webContents && !webViewInfo.view.webContents.isDestroyed()) { + webViewInfo.view.webContents.setBackgroundThrottling(false) + } + if (this.win && !this.win.isDestroyed()) { this.win.webContents.send('webview-show', id) } @@ -228,6 +267,14 @@ export class WebViewManager { return { success: false, error: `Webview with id ${id} not found` } } + if (!webViewInfo.view.webContents.isDestroyed()) { + webViewInfo.view.webContents.removeAllListeners() + webViewInfo.view.webContents.session.clearCache() + webViewInfo.view.webContents.session.clearStorageData({ + storages: ['cookies', 'localstorage', 'websql', 'indexdb', 'serviceworkers', 'cachestorage'] + }) + } + // remove webview from parent container if (this.win?.contentView) { this.win.contentView.removeChildView(webViewInfo.view) @@ -254,5 +301,18 @@ export class WebViewManager { }) this.webViews.clear() } + + private cleanupInactiveWebviews() { + const inactiveWebviews = Array.from(this.webViews.entries()) + .filter(([id, info]) => !info.isActive && !info.isShow && info.currentUrl === 'about:blank?use=0') + .sort((a, b) => parseInt(a[0]) - parseInt(b[0])) + + const toRemove = inactiveWebviews.slice(this.maxInactiveWebviews) + + toRemove.forEach(([id, _]) => { + console.log(`Cleaning up inactive webview: ${id}`) + this.destroyWebview(id) + }) + } }