jest 如何运行 以及 node jest 运行 内存不够问题分析

888 阅读6分钟

问题简述    

  因为之前 node jest 一直有 内存泄漏的问题(其实不是内存泄漏),所以研究了下jest 为什么会有这个问题。以及 node 现在 一直runInBand (单个运行),限制了运行的速度。从jest 以及 ts-jest 运行源码出发分析问题。因为只是 从内存方面来理解jest的,所以有很多地方并没有细看,下面在介绍jest的时候 可能不会很全面

jest 简述(和其他测试框架:mocha 比较)

 测试框架 , 类似的还有mocha 。 但是jest 明显比mocha 复杂数倍 。mocha 只是基本的测试框架提供了简单的 测试api , jest 可以看作是一个完整的 代码(不止js)运行环境,可以做到编译级别的代码控制

jest 框架 简述 (不考虑 watch 和 覆盖率 模式)

  从  cli : jest  --config jest.config.json 开始 的运行 步骤 

步骤 (function, class等)步骤内容
run()cli 运行,会在代码里调用这个 function ,主要用来build args 和 获取一些 环境args
runCLI()这里在获取 输入的args之后,jest 会init config ,从环境变量和 命令指定的dir 或者文件 获取 config , 并且会校验和填充一些args
_run10000 () : 就是这么命名的创建context ,并且会保存到缓存文件(默认:/tmp/jest_0/ 目录 , linux 系统) , 并且会用 输入的参数做 测试文件filter ,然后是 运行watch 或者 非 watch 模式
image.png
runWithoutWatch()运行非watch模式 jest
runJest ()对test 文件进行处理:排序 , 搜索 。 运行 global hook , init jest 输出 console ,然后 计划 运行 test , 最后对 运行的测试results进行缓存(这个缓存只是为了给 前面的排序使用)image.png
TestScheduler class:测试调度 test的 调度 class , 内部使用 scheduleTests 运行测试,同时负责 reporter init 和 reporter的 event 分发(emit)
scheduleTests()init 测试 数据:测试的empty results , 多线程运行(runInBand),测试运行的 event 分发事件绑定(onResult , onFailure 等) 。 使用代码transform 导入 源文件,init 运行测试的Runner (jest-runner ),对 对应的测试绑定对应的 runner 以及 runner的运行 event (test-file-start/success/failure 等) 。 然后开始运行 tests。运行之后 收集 test 运行 results 进行 合并 以及 对应的event 分发(emit)
Runner.runTests() : Runner 默认 jest-runner多线程或者 单线程 运行tests (runInBand , 这里只 说明 单线程)
_createInBandTestRun()分发 test 文件运行的各种 event (test-file-* , onResult , onFailure 等),运行 test (到这里 就是一个 test 文件 ,因为是 runInBand),for 循环运行test
runTest()运行测试,可以检测内存泄漏(leakDetector)
runTestInternal(): 测试开始运行的核心代码加载测试必须的核心文件 class : TestEnvironment , TestFramework , Runtime 并且init ,还会检测内存泄漏 和 运行代码覆盖率 。如果使用 gc,会在test 运行之后 执行 gc ,如果使用 logHeapUsage , 会用node 的api 显示 heap 的使用 , 运行之后会对 TestEnvironment clear 注意 加载文件使用 config 配置的 transform ,这也是一个class ,下面讲下 加载非test 和 test 依赖文件的方式在加载文件的时候 , jest 会拦截 require ,然后判断是否需要 做 transform 。如果需要transform,就用transform逻辑。外部使用 requireAndTranspileModule()核心文件 transfrom的逻辑1. 首先 会check 是否存在缓存文件
  1. 不存在缓存,就用 transform 对应的 文件来做 ts/js→js  的操作

  2. 然后对获取的文件保存到对应的缓存目录

  3. image.png外部使用 requireInternalModule()导入第三方文件 逻辑,  是jest  内部导入的文件1. 对第三文件进行 module 处理,从内存从读取 是否存在

  4. 不存在 继续导入 对应module ,因为文件可能有其他的依赖,所以不是简单的导入,可以理解为 在代码里做了require , 会依次执行文件依赖。通过 node api :vm 和 script 直接运行 导入的源代码 ,同时拦截 源文件的 moduleapi (commonjs   导入模块的 api ) ,对文件的依赖可以依次处理

  5. 对导入的文件 进行transform(transfromfile) , 这里使用 transform ,会对transfrom的 源代码保存到 内存中,因为都是js 所以不会用 transform的逻辑 | | testFramework():jestAdapter | 1. 导入  必须源文件 : jestAdapterInit.js , 运行 fun:initialize() , init 测试必须的 一些function 和参数 (it , describe , skip , only等)。分发 测试 event 和 添加一些 handler ,返回 init 之后的 参数

  6. 运行 global hooks : clear 运行环境

  7.  requireModule():导入 test文件 注意这里和 requireInternalModule 逻辑类似,但是这列导入的test 文件 可能是 ts 文件,所以会运行 transform 的逻辑(ts-jest)下面说明 transfrom的逻辑区别1.  运行 scriptTransformer  的 transform 

  8. transform 运行 cli 或者 jest.config.json 配置的 transform(ts-jest)

  9. ts-jest 对 ts 文件进行编译 并且 会保存编译文件到缓存目录 ,而且会保存到内存中 (object.create(null) , 无法垃圾回收)

  10. ts-jest 返回 编译信息 , jest 会 保存到缓存目录 和 内存中 ***(map , 无法垃圾回收)***transform 之后 源码会在 vm 和 script 内运行 ,里面会注入 jest 全局对象  , 类似 describe 和 it 会 执行 ,这里会分发event 和执行逻辑。然后会在对应的handler内部收集 test 或者 其他的fn 导入到 对应的 数组或 对象内部 ,这里测试代码是 还没有运行的runAndTransfromResultsToJestFormat():用上面收集的 test fun和数据 运行 测试代码 , 就是一个迭代 运行fun ,然后返回测试 运行的results ,jest的api 都已经 注入 拦截能够 使用 依次返回 函数调用迭代 ,完成 测试代码运行 |

分析node 内存问题

从上面的jest 运行能够分析出,每次运行的代码都会在内存 中,因为server代码在运行的时候 是 依赖整个项目执行的,所以每个测试 都会导入不一样的源码 。jest 会保存 一份 在内存中 ,ts-jest 会保存一份

在内存中,而且不能gc,这就是为什么node 运行之后内存会不够用(不是泄漏,process.exit 之后内存就释放了)。

如果要解决这个问题有几个方案

  1. 大内存运行

  2. node 运行测试的时候不编译源码,使用 和前端类似的模式,只获取 http 。对ts 测试预编译,不使用ts-jest ,所以的运行代码都是 js

  3. 使用多台机器运行 ,直接待用jest api执行 ,减少一台机器的内存使用 (代码已经实现 参考:dev.zstack.io:9080/qing.liang/…   jest branch),下面介绍 实现

运行了几个小时之后的 文件缓存,内存缓存 比这个多,还有源码也是保存在内存的 

image.png image.png

image.png

image.png

jest 多机器运行 

实现思路

  通过 启动一个host socket server ,其他多台包括 host 启动 socket client 连接 ,请求 test (从test列表获取) ,然后 client 运行 jest api 返回给 server ,server 对 results 合并,运行完成之后 调用jest html

  的api (customer reporter ) 生成 html

image.png

下面列出 jest 和 jest html api 

function  和 class 使用
socket.io socket class
readConfigs(): Client.ts获取jest 配置 config
buildArgv(): Client.ts从cli 构建 jest args
runCLI(): Client.ts运行 test 
jest-html-reporters class : Server.ts jest reporter class
jestReporter.onRunComplete():Server.ts生成 html