electron性能优化系列一(进程管理)

3,149 阅读3分钟

背景

做了一段时间的electron客户端开发,在性能优化方面做了很多事情。性能优化涉及到很多方面,主进程/渲染进程/数据库等。
本文主要是来做一个总结,这个系列准备分三篇文章来写一下:
1、进程管理;
2、sqlite数据库性能优化;
3、webview性能优化;
其实在electron开发过程中基本不涉及到手动的进程管理,但经常会听到测试/运营/用户反馈,内存/cpu占用很高这句话。在优化之前,需要清楚到底是什么进程什么位置出现问题,然后才能进行问题处理。
这是第一篇文章,关于 electron 进程管理。

备注:我平时开发主要是使用 macOS,所以本文主要也是以 macOS 进行说明.

process

electron的进程主要是main process和renderer process(同时默认开启 GPU process 提高渲染性能)。可以用 pstree 命令看下electron应用启动的进程:
image.png 可以看到包含了:
1、main process;
2、renderer processs(多个);
3、GPU process;
4、其他进程(如图中最后一个音频播放进程) 但如上图,4个 renderer process 是没办法区分的。

进程管理 && 监控

目前是有一些可用的工具来进行进程管理的,e.g. electron-process-manager/electron-re。
这些工具的大概流程如下:
1、通过 BrowserWindow.getAllWindows() 获取到所有的渲染进程信息(pid, url);
2、通过 pidusage 来查询资源占用情况;
3、另外创建一个 renderer 进程渲染进程资源占用的情况/通过ui交互杀死进程等;
但对我来说,主要是需要监控用户在使用期间的不同进程资源占用情况是否有异常,然后进行异常分析。完整代码如下:

const { BrowserWindow } = require('electron')
const pidusage = require('pidusage')
const logServices = require('./services/log.services')
let instance

class ProcessManager {
    constructor() {
        this.processMap = {} // 存储process映射对象
        this.timedTask() // 开始定时任务
    }
    static getInstance() {
        if(!instance) {
            instance = new ProcessManager()
        }
        return instance
    }
    insert(pid, name) {
        if(!this.processMap[pid]) {
            this.processMap[pid] = {name}
        }
    }
    delete(pid) {
        delete this.processMap[pid]
    }
    getPidList() { // 实时获取所有BrowserWindow的进程信息
        BrowserWindow.getAllWindows().forEach((process) => {
            const _webContents = process.webContents

            if(process && process.webContents) {
                this.insert(process.webContents.getOSProcessId(), _webContents.getURL())
            }
        })
    }
    getProcessUsage(pid) {
        return pidusage(pid)
    }
    async monitor() {
        this.getPidList()
        for(let pid in this.processMap) {
            try {
                const _usage = await this.getProcessUsage(pid)

                logServices.debugLog(`{name: ${this.processMap[pid].name}, pid: ${pid}, cpuUsage: ${_usage.cpu}, memoryUsage: ${_usage.memory}}`)
            }catch(err) {
                this.delete(pid)
            }
        }
    }
    timedTask() { // 10s 监控一次
        setInterval(() => {
            this.monitor()
        }, 10000)
    }
}

module.exports = ProcessManager.getInstance

另外在主进程在 whenReady 时候调用实例对象的 insert 方法将主进程也纳入监控即可:

ProcessManager().insert(process.pid, 'mainProcess')

最终我们可以获取到监控结果如下:

18:55:42.329  {name: mainProcess, pid: 33710, cpuUsage: 0.1, memoryUsage: 165498880}
18:55:42.350  {name: http://localhost:3000/#/login, pid: 33735, cpuUsage: 0, memoryUsage: 179240960}
18:55:42.355  {name: file:///Users/vb/windows/menuBar/index.html, pid: 33745, cpuUsage: 0, memoryUsage: 67768320}

注: 我这里是通过process/getAllWindows等方式获取主进程和渲染进程的pid进行监控的,其实也可以通过例如getAppMetrics/shell脚本等方法来获取更完整的pid列表。

异常分析

我目前基本是通过以上进程监控和用户操作日志来进行异常分析的,暂时可以满足我的需求。我也大概思考下后面如何进一步进行异常的定位, 关于cpu占用和内存使用需要分开来处理:
1、内存异常分析(通过 process.takeHeapSnapshot api进行快照存储对比前后两张快照异常, 并通过chrome memory工具进行快照对比分析);
2、cpu异常分析(通过electron插件 electron-profiler记录cpu增长情况,通过 chrome Javascript Profiler 工具栏进行分析);
其实这些 snapshot/profiler 其实只适合在开发环境使用,因为需要对比从正常到异常的区别。暂时先简单描述下,给有需要的人提供一个参考方案,后面如果有实践我会再补充说明。

参考文档

1、A beginner’s guide to creating desktop applications using Electron: medium.com/jspoint/a-b…
2、www.electronjs.org/zh/docs/lat…