Created
November 27, 2025 08:06
-
-
Save ara4n/0a834d14f6e59f55b4df197647ea2fbc to your computer and use it in GitHub Desktop.
Poor man's profiling in Electron
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/src/electron-main.ts b/src/electron-main.ts | |
| index 8ff217f..e9a33f3 100644 | |
| --- a/src/electron-main.ts | |
| +++ b/src/electron-main.ts | |
| @@ -21,6 +21,7 @@ import { | |
| session, | |
| protocol, | |
| desktopCapturer, | |
| + WebContents, | |
| } from "electron"; | |
| // eslint-disable-next-line n/file-extension-in-import | |
| import * as Sentry from "@sentry/electron/main"; | |
| @@ -74,6 +75,212 @@ if (argv["help"]) { | |
| app.exit(); | |
| } | |
| +let heapMonitorInterval: NodeJS.Timeout | null = null; | |
| +let lastSize: number = 0; | |
| +const THRESHOLDS = [ 1000, 1300 ]; | |
| +//const THRESHOLDS = [900]; | |
| +//const THRESHOLDS = [ 50 ]; | |
| +let triggers = [...THRESHOLDS]; | |
| +const RESET_THRESHOLD = 800; | |
| +let isRunning = false; | |
| + | |
| +async function setupHeapMonitoring(window: BrowserWindow): Promise<void> { | |
| + try { | |
| + // Attach Electron's built-in debugger | |
| + window.webContents.debugger.attach('1.3'); | |
| + | |
| + const dbugger = window.webContents.debugger; | |
| + | |
| + // Enable heap profiler | |
| + await dbugger.sendCommand('HeapProfiler.enable'); | |
| + await dbugger.sendCommand('Runtime.enable'); | |
| + | |
| + console.log('Heap monitoring enabled'); | |
| + let lastRun = new Date(); | |
| + let lastHeap = 0; | |
| + let count = 0; | |
| + | |
| + heapMonitorInterval = setInterval(() => { | |
| + // we deliberately run this as a non-async function, to avoid there being an event loop tick | |
| + // resulting in overlapping intervals when the heap is thrashing. | |
| + if (isRunning) return; | |
| + isRunning = true; | |
| + | |
| + let now; | |
| + | |
| + // ...and then we have to run this async given it calls async stuff. | |
| + (async () => { | |
| + now = new Date(); | |
| + try { | |
| + const { usedSize, totalSize } = await dbugger.sendCommand('Runtime.getHeapUsage') as { | |
| + usedSize: number; | |
| + totalSize: number; | |
| + }; | |
| + | |
| + const heapUsedMB = (usedSize / 1024 / 1024).toFixed(2); | |
| + const heapTotalMB = (totalSize / 1024 / 1024).toFixed(2); | |
| + | |
| + if (now.getSeconds() != lastRun.getSeconds() || (usedSize - lastHeap) > 50 * 1024 * 1024) { | |
| + console.log(`${new Date().toISOString()}: Heap: ${heapUsedMB}MB / ${heapTotalMB}MB`); | |
| + } | |
| + lastHeap = usedSize; | |
| + | |
| + if (usedSize / 1024 / 1024 < RESET_THRESHOLD) { | |
| + // reset our triggers | |
| + triggers = [...THRESHOLDS]; | |
| + } | |
| + | |
| + if (triggers.length > 0) { | |
| + let trigger = triggers[0]; | |
| + | |
| + if (usedSize / 1024 / 1024 > trigger) { | |
| + console.warn(`${new Date().toISOString()}: ⚠️ Heap ${heapUsedMB}MB / ${heapTotalMB}MB exceeded ${trigger}MB! Trying to GC...`); | |
| + await dbugger.sendCommand('HeapProfiler.collectGarbage'); | |
| + | |
| + { | |
| + const { usedSize, totalSize } = await dbugger.sendCommand('Runtime.getHeapUsage') as { | |
| + usedSize: number; | |
| + totalSize: number; | |
| + }; | |
| + const heapUsedMB = (usedSize / 1024 / 1024).toFixed(2); | |
| + const heapTotalMB = (totalSize / 1024 / 1024).toFixed(2); | |
| + if (usedSize / 1024 / 1024 > trigger) { | |
| + console.warn(`${new Date().toISOString()}: ⚠️ Heap ${heapUsedMB}MB / ${heapTotalMB}MB still exceeded ${trigger}MB! Waiting for sampling...`); | |
| + | |
| + // // for allocation profiling (potentially after a given logline): | |
| + // | |
| + // const chunkHandler = async (_event: any, method: string, params: any) => { | |
| + // if (method == 'Runtime.consoleAPICalled' && | |
| + // params.args[0]?.value?.startsWith("FetchHttpApi: <-- GET https://matrix.org/_matrix/client/v3/rooms/!OGEhHVWSdvArJzumhm%3Amatrix.org/initialSync") | |
| + // ) { | |
| + // console.log("chunkHandler:", method, params); | |
| + // count++; | |
| + // if (count == 2) { | |
| + // console.warn(`${new Date().toISOString()}: Explosion imminent, sampling...`); | |
| + // // Start sampling heap profiler (allocation sampling) | |
| + // await dbugger.sendCommand('HeapProfiler.startSampling', { | |
| + // samplingInterval: 32768, // bytes between samples | |
| + // includeObjectsCollectedByMajorGC: false, | |
| + // includeObjectsCollectedByMinorGC: false | |
| + // }); | |
| + | |
| + // setTimeout(() => stopAllocationProfiling(window.webContents), 5000); | |
| + // } | |
| + // } | |
| + // }; | |
| + // dbugger.on('message', chunkHandler); | |
| + | |
| + //setTimeout(() => stopAllocationProfiling(window.webContents), 500); | |
| + //setTimeout(() => stopAllocationProfiling(window.webContents), 60 * 1000); | |
| + | |
| + console.warn(`${new Date().toISOString()}: ⚠️ Heap ${heapUsedMB}MB / ${heapTotalMB}MB still exceeded ${trigger}MB! Snapshotting...`); | |
| + await takeHeapSnapshot(dbugger, heapUsedMB); | |
| + triggers.shift(); | |
| + } | |
| + } | |
| + } | |
| + } | |
| + } catch (err) { | |
| + console.error('Error checking heap:', err); | |
| + } | |
| + | |
| + lastRun = now; | |
| + isRunning = false; | |
| + })(); | |
| + }, 100); | |
| + | |
| + // Handle debugger detach | |
| + dbugger.on('detach', (_event, reason) => { | |
| + console.log(`Debugger detached: ${reason}`); | |
| + if (heapMonitorInterval) { | |
| + clearInterval(heapMonitorInterval); | |
| + heapMonitorInterval = null; | |
| + } | |
| + }); | |
| + | |
| + // Clean up on window close | |
| + window.on('closed', () => { | |
| + if (heapMonitorInterval) { | |
| + clearInterval(heapMonitorInterval); | |
| + heapMonitorInterval = null; | |
| + } | |
| + if (dbugger.isAttached()) { | |
| + dbugger.detach(); | |
| + } | |
| + }); | |
| + | |
| + } catch (err) { | |
| + console.error('Failed to setup heap monitoring:', err); | |
| + } | |
| +} | |
| + | |
| +async function stopAllocationProfiling(webContents: WebContents) { | |
| + try { | |
| + console.warn(`${new Date().toISOString()}: ⚠️ Trying to stop profiling...`); | |
| + const { profile } = await webContents.debugger.sendCommand('HeapProfiler.stopSampling'); | |
| + console.warn(`${new Date().toISOString()}: ⚠️ Got profile...`); | |
| + | |
| + // Save profile | |
| + const filename = path.join(app.getPath('userData'), `allocation-${Date.now()}.heapprofile`); | |
| + | |
| + fs.writeFileSync(filename, JSON.stringify(profile, null, 2)); | |
| + console.log(`Allocation profile saved to: ${filename}`); | |
| + } catch (err) { | |
| + console.error('Failed to stop profiling:', err); | |
| + } | |
| +} | |
| + | |
| +async function takeHeapSnapshot( | |
| + dbugger: Electron.Debugger, | |
| + currentHeapMB: string | |
| +): Promise<string> { | |
| + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | |
| + const filename = `heap-snapshot-${currentHeapMB}MB-${timestamp}.heapsnapshot`; | |
| + const filepath = path.join(app.getPath('userData'), filename); | |
| + | |
| + console.log(`Saving heap snapshot to: ${filepath}`); | |
| + | |
| + const writeStream = fs.createWriteStream(filepath); | |
| + let chunkCount = 0; | |
| + | |
| + return new Promise((resolve, reject) => { | |
| + // Listen for heap snapshot chunks | |
| + const chunkHandler = (_event: any, method: string, params: any) => { | |
| + //console.log("chunkHandler:", method, params); | |
| + if (method === 'HeapProfiler.addHeapSnapshotChunk') { | |
| + writeStream.write(params.chunk); | |
| + chunkCount++; | |
| + } else if (method === 'HeapProfiler.reportHeapSnapshotProgress') { | |
| + if (params.finished) { | |
| + writeStream.end(); | |
| + console.log(`✓ Heap snapshot saved: ${filepath} (${chunkCount} chunks)`); | |
| + dbugger.removeListener('message', chunkHandler); | |
| + resolve(filepath); | |
| + } | |
| + } | |
| + }; | |
| + | |
| + dbugger.on('message', chunkHandler); | |
| + | |
| + // Start taking the snapshot | |
| + dbugger.sendCommand('HeapProfiler.takeHeapSnapshot', { | |
| + // reportProgress: true, | |
| + // captureNumericValue: true | |
| + }).catch((err) => { | |
| + console.error("failed to take heap snapshot", err); | |
| + dbugger.removeListener('message', chunkHandler); | |
| + writeStream.end(); | |
| + reject(err); | |
| + }); | |
| + | |
| + writeStream.on('error', (err) => { | |
| + console.error("failed to write heap snapshot", err); | |
| + dbugger.removeListener('message', chunkHandler); | |
| + reject(err); | |
| + }); | |
| + }); | |
| +} | |
| + | |
| const LocalConfigLocation = process.env.ELEMENT_DESKTOP_CONFIG_JSON ?? argv["config"]; | |
| const LocalConfigFilename = "config.json"; | |
| @@ -468,6 +675,12 @@ app.on("ready", async () => { | |
| }, | |
| }); | |
| + global.mainWindow.webContents.on('did-finish-load', () => { | |
| + if (global.mainWindow) { | |
| + setupHeapMonitoring(global.mainWindow); | |
| + } | |
| + }); | |
| + | |
| global.mainWindow.setContentProtection(store.get("enableContentProtection")); | |
| try { | |
| @@ -592,6 +805,9 @@ app.on("ready", async () => { | |
| }); | |
| app.on("window-all-closed", () => { | |
| + if (heapMonitorInterval) { | |
| + clearInterval(heapMonitorInterval); | |
| + } | |
| app.quit(); | |
| }); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment