You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Electron串口持续读取时EventEmitter内存泄漏的规避方案咨询

内存泄漏导致Electron串口应用OOM问题排查与修复

问题背景

使用Electron.js+TypeScript开发的桌面串口数据读取应用,初始运行正常,但连续读取约2小时后触发内存耗尽错误:

[electron] <--- JS stacktrace --->
[electron]
[electron] FATAL ERROR: Cannot grow ExternalPointerTable past its maximum capacity Allocation failed - process out of memory

怀疑是事件监听器与事件触发导致的内存泄漏,相关代码如下:

open() 方法(串口打开逻辑)

open(): Promise<void> {
    return new Promise<void>(async (resolve, reject) => {
      const port = new SerialPort(
        {
          path: this.sessionInfo.path,
          baudRate: this.sessionInfo.buadRate,
          dataBits: 8,
          stopBits: 2,
          parity: "none",
        },

        function (err) {
          if (err) {
            reject(err);
          }
        }
      );
      this.dataReaderParser = this.sessionInfo.readLineParser;

      port.pipe(this.bufferCleanerParser);
      this.bufferCleanerParser.on("data", (data: Buffer) => {
        console.log(`Cleared data: ${data.toString("hex")}`);
      });
      await new Promise<void>((resolve) => {
        setTimeout(() => {
          port.unpipe(this.bufferCleanerParser);
          port.pipe(this.dataReaderParser);
          resolve();
        }, 10);
      });

      const onData = (data: string) => {
        if (this.onDataCallback) {
          this.onDataCallback(data);
        }
        this.emit('data', data);
      };

      this.dataReaderParser.on('data', onData);

      this.port = port;
      resolve();
    });
  }

read() 方法(数据监听逻辑)

public read(
    callback: (data: string) => void,
    sendFlag?: boolean
  ): Promise<string> {
    if (callback) {
      return new Promise<string>((resolve, reject) => {
        if (sendFlag) {
          this.onDataCallback = (data: string) => {
            callback(data);
          };

          return;

        }

        this.on("data", (data) => {
          callback(data);
        });
        this.removeListener('data', callback)
      });
    }
    return Promise.resolve("the callback is not provided");
  }

readAnalog() 方法(UI触发的持续读取逻辑)

async readAnalog(decoder: DecoderInterface, callback: (data: PowerDeliveryAnalogModel|any) => void) {
    try {
      this.writeDigital()
      const session = this.sessionManager.getSession("serialPortSession2");

      session.read(async (data: string) => {
        try {
          this.counter++;
          if (this.counter === 50) {
            const parserAnalogData = this.parser.parseAnalogData(data);
            const res: PowerDeliveryAnalogModel =
              decoder.decodeAnalogData(parserAnalogData);
            callback(res);
            this.counter = 0;
          }
        } catch (error) {}
      });
    } catch (error) {}
  }

内存泄漏根源与修复方案

1. read()方法中无效的监听器移除逻辑

当前read()方法里,绑定匿名回调后调用removeListener无法匹配到对应监听器,导致每次调用read()都会新增一个匿名监听器,不断累积占用内存。

修复方式:

  • 使用具名函数作为监听器,确保removeListener能准确移除;
  • 维护监听器引用列表,在停止读取时批量清理;
  • 清理旧回调避免重复绑定。

修改后的read()及配套清理方法:

private dataListeners: ((data: string) => void)[] = [];

public read(
    callback: (data: string) => void,
    sendFlag?: boolean
  ): Promise<string> {
    if (callback) {
      return new Promise<string>((resolve) => {
        if (sendFlag) {
          this.onDataCallback = undefined;
          this.onDataCallback = (data: string) => {
            callback(data);
          };
          resolve("");
          return;
        }

        const dataListener = (data: string) => {
          callback(data);
        };
        this.on("data", dataListener);
        this.dataListeners.push(dataListener);
        resolve("");
      });
    }
    return Promise.resolve("the callback is not provided");
  }

public stopReading(): void {
  this.dataListeners.forEach(listener => {
    this.removeListener('data', listener);
  });
  this.dataListeners = [];
  this.onDataCallback = undefined;
}

2. readAnalog()中重复绑定回调

每次点击UI启动按钮调用readAnalog()时,会传入新的匿名回调,旧回调未被清理会持续占用内存。

修复方式:

  • 用开关变量限制重复启动;
  • 启动前调用stopReading()清理旧监听器。

修改后的readAnalog()及配套停止方法:

private isReadingAnalog = false;

async readAnalog(decoder: DecoderInterface, callback: (data: PowerDeliveryAnalogModel|any) => void) {
    if (this.isReadingAnalog) return;
    try {
      this.isReadingAnalog = true;
      this.writeDigital();
      const session = this.sessionManager.getSession("serialPortSession2");
      session.stopReading();

      session.read(async (data: string) => {
        try {
          this.counter++;
          if (this.counter === 50) {
            const parserAnalogData = this.parser.parseAnalogData(data);
            const res: PowerDeliveryAnalogModel = decoder.decodeAnalogData(parserAnalogData);
            callback(res);
            this.counter = 0;
          }
        } catch (error) {}
      });
    } catch (error) {}
  }

public stopAnalogReading(): void {
  this.isReadingAnalog = false;
  const session = this.sessionManager.getSession("serialPortSession2");
  session.stopReading();
}

3. open()方法中残留的监听器

bufferCleanerParser绑定的data监听器在unpipe后未被移除,若实例复用会导致监听器累积。

修复方式:

  • 保存监听器引用,在unpipe后主动移除。

修改后的open()相关片段:

const bufferCleanListener = (data: Buffer) => {
  console.log(`Cleared data: ${data.toString("hex")}`);
};
this.bufferCleanerParser.on("data", bufferCleanListener);

await new Promise<void>((resolve) => {
  setTimeout(() => {
    port.unpipe(this.bufferCleanerParser);
    this.bufferCleanerParser.removeListener("data", bufferCleanListener);
    port.pipe(this.dataReaderParser);
    resolve();
  }, 10);
});

4. 串口资源全生命周期管理

关闭串口时清理所有关联监听器、管道和引用,避免残留无用实例。

补充修复:

public async close(): Promise<void> {
  if (this.port) {
    this.dataReaderParser?.removeAllListeners('data');
    this.stopReading();
    await new Promise((resolve, reject) => {
      this.port.close((err) => {
        err ? reject(err) : resolve(null);
      });
    });
    this.port = null;
    this.dataReaderParser = null;
  }
}

内容的提问来源于stack exchange,提问作者Rajaa Abdallah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 14:37:06