This commit is contained in:
puzhen 2025-10-09 17:36:23 +01:00
parent c2dc0d2f8a
commit 9ce50205ef
2 changed files with 156 additions and 19 deletions

View file

@ -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<void>((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();
});

View file

@ -20,6 +20,9 @@ export class WebViewManager {
private webViews = new Map<string, WebViewInfo>()
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)
})
}
}