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

