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

调用v8.getHeapSnapshot导致Jest测试无限挂起问题排查

问题背景

当前业务代码存在内存泄漏问题,尝试通过Node.js内置v8库生成堆快照开展调试。基于TypeScript+Express编写了可调用接口,调用后返回JSON格式对象,支持写入文件后导入Chrome DevTools做内存分析。
运行Jest测试用例验证该接口功能时,测试进程会无限挂起无法结束,执行的测试命令如下:

jest --runInBand InfoController.test.ts

相关代码

InfoController.ts

import { Router } from 'express';
import { getHeapSnapshot } from 'v8';
import { constants } from 'http2';

export const InfoController = (): Router => {
  const router = Router();
  router.get('/heapdump', (_, res) => {
    console.log('requesting heap snapshot')
    const heapShot = getHeapSnapshot();
    console.log('heap');
    let data = '';
    heapShot.on('readable', () => {
      const chunk = heapShot.read();
      while (chunk !== null) {
        data += chunk;
      }
    });

    heapShot.on('end', () => res.status(constants.HTTP_STATUS_OK).json(JSON.parse(data)));
  });

  return router;
};

InfoController.test.ts

import express from 'express';
import { constants as httpConstants } from 'http2';
import Request from 'supertest';
import { InfoController } from './InfoController';

describe('🧪 InfoController', () => {
  describe('GET /heapdump', () => {
    test('should be able to retrieve a v8 heapdump of the service', async () => {
      const controller = InfoController();

      const app = express();
      app.use(express.json());
      app.use(controller);

      const result = await Request(app).get('/heapdump').expect(httpConstants.HTTP_STATUS_OK);
      console.log(result.body);
    });
  });
});

jest.config.js

module.exports = {
  preset: 'ts-jest',
  bail: true,
  verbose: true,
  testEnvironment: 'node',
  collectCoverage: false,
  testMatch: ['**/**/*.test.ts'],
  testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/build/'],
  collectCoverageFrom: ['<rootDir>/src/**', '!<rootDir>/src/index.ts'],
  coveragePathIgnorePatterns: ['<rootDir>/node_modules', '<rootDir>/__tests__'],
  globalSetup: '<rootDir>/__tests__/global/setup.ts',
};

运行现象

执行测试命令后控制台输出如下,输出后进程持续挂起,始终无法执行完成:

$ jest --runInBand src/http/controllers/InfoController.test.ts
console.log
    requesting heap snapshot

      at src/http/controllers/InfoController.ts:8:13


 RUNS  src/http/controllers/InfoController.test.ts
问题原因

代码存在两处核心问题直接导致挂起:

  1. readable事件回调逻辑错误:仅在进入回调时调用了一次heapShot.read(),while循环内部没有重新执行read操作。只要首次read返回非空的chunk,循环判断条件永远为真,直接进入无限死循环,这也是日志打印完heap后就卡住的直接原因。就算首次read返回null(流暂未就绪),后续数据到达触发readable事件时,也只会读取一次数据,剩余数据无法被完整消费,end事件永远不会触发。
  2. 堆快照流未主动销毁:v8.getHeapSnapshot()返回的可读流默认会持有Node.js事件循环的引用,就算数据完整读取完成,如果不手动调用destroy方法销毁流,会持续阻塞进程退出。
修复方案
  1. 修正流读取逻辑,按照Node.js readable事件的标准写法,将read操作放到while循环判断条件中,循环读取直到read返回null,保证所有数据被完整消费。
  2. 数据读取完成、响应返回前主动销毁堆快照流,释放事件循环引用。
  3. 补充流错误处理,避免异常场景下进程挂起。

修复后的InfoController代码如下:

import { Router } from 'express';
import { getHeapSnapshot } from 'v8';
import { constants } from 'http2';

export const InfoController = (): Router => {
  const router = Router();
  router.get('/heapdump', (_, res) => {
    console.log('requesting heap snapshot')
    const heapShot = getHeapSnapshot();
    console.log('heap');
    const chunks: Buffer[] = [];
    
    heapShot.on('readable', () => {
      let chunk: Buffer | null;
      // 循环读取直到流中无剩余数据
      while ((chunk = heapShot.read()) !== null) {
        chunks.push(chunk);
      }
    });

    heapShot.on('end', () => {
      const data = Buffer.concat(chunks).toString('utf-8');
      // 主动销毁快照流,释放事件循环引用
      heapShot.destroy();
      res.status(constants.HTTP_STATUS_OK).json(JSON.parse(data));
    });

    heapShot.on('error', (err) => {
      heapShot.destroy();
      res.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR).json({ error: err.message });
    })
  });

  return router;
};

如果修复流逻辑后仍遇到Jest进程无法退出的问题,可以在jest.config.js中添加forceExit: true配置,强制测试用例执行完成后退出进程,该配置仅作为兜底方案,优先保证流逻辑正确。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 16:36:39