☰
memlab 测试场景(IScenario)接口完全指南:编写 E2E 内存泄漏检测脚本的 11 个回调
2026/10/13 17:43:58 网站建设 项目流程
  • 开发工具
  • 测试
  • CLI
  • MCP 服务

【免费下载链接】memlab

A framework for finding JavaScript memory leaks and analyzing heap snapshots

项目地址:https://gitcode.com/gh_mirrors/me/memlab
点击查看免费下载

导读:IScenario是 memlab 框架中定义"如何让一次 E2E 测试与浏览器交互"的核心接口。本文以 Types.ts 中的接口定义为主线,逐一拆解url、action、back、repeat、leakFilter等全部回调的语义、参数与真实代码示例,并结合 CLI 的memlab run --scenario与@memlab/api的run({scenario})两种使用方式,以及 BaseSynthesizer.ts 的源码级访问计划生成逻辑,帮助你写出可复用、可精确控制泄漏判定标准的测试场景文件。

一、什么是 IScenario:测试场景在 memlab 中的定位

IScenario(定义于 Types.ts)描述了 memlab 应该如何与一个 Web 应用交互以完成内存泄漏检测。一个测试场景既可以保存为.js文件,通过命令行传入,也可以直接作为对象字面量传给@memlab/api的runAPI:

// save as test.js and use in terminal: // $ memlab run --scenario test.js module.exports = { url: () => 'https://www.npmjs.com/', action: async () => ... , back: async () => ... , cookies: () => ... , // optional repeat: () => ... , // optional ... };
const {run} = require('@memlab/api'); (async function () { const scenario = { url: () => 'https://www.facebook.com', action: async () => ... , back: async () => ... , cookies: () => ... , // optional repeat: () => ... , // optional ... }; const leaks = await run({scenario}); })();

1.1 两条使用路径的底层一致性

从源码可以确认,两条路径最终殊途同归:

  • CLI 路径:memlab run --scenario <文件>在 ScenarioFileOption.ts 中通过utils.loadScenario(scenarioFile)读取文件(实现在 Utils.ts,会校验文件存在性并处理非法文件报错),把结果挂到config.scenario上;
  • API 路径:run({scenario})(见 API.ts)直接把场景对象传给内部流程。从 API 实现看,run等价于"预热(warmup)→ 浏览器交互并采集快照(takeSnapshots)→ 泄漏分析(findLeaks)"三步的组合。

无论哪条路径,场景对象最终都会进入 TestPlanner.ts 的prepareSynthesizer,由 BaseSynthesizer.ts 的synthesisForScenario把回调编排成一个带步骤顺序的访问计划(visit plan)。

1.2 场景被展开成怎样的执行序列

synthesisForScenario的源码逻辑清晰展示了action/back/repeat如何决定浏览器访问顺序:

  • 只有url(无action):生成单步page-load,作为target步骤执行——即仅检测初始页面加载引入的泄漏;
  • 有action时:先访问page-load(baseline),然后按repeat()的次数循环插入action-on-page与revert(回退)步骤,最后执行一次action-on-page(target)与一次revert(final)。

其中repeat在 BaseSynthesizer.ts 中的默认取值为scenario.repeat ? scenario.repeat() : 0,即不声明repeat就只做一轮action -> back。而revert步骤只有在声明了scenario.back时才使用自定义回退回调,否则回落到内置的通用回退步骤(SynthesisUtils.revertStep)。

二、必选回调:url

定义位置:Types.ts

url是唯一必选回调,返回页面初始 URL 的字符串值:

const scenario = { url: () => 'https://www.npmjs.com/', }; module.exports = scenario;

关键语义:如果场景只提供url(没有action),memlab 会尝试直接从初始页面加载中检测内存泄漏——初始加载分配的所有 JS 对象都会成为泄漏过滤的候选对象。这一点与 BaseSynthesizer.ts 中if (!scenario.action)分支的处理完全对应。

三、核心交互对:action 与 back

3.1 action:触发泄漏的交互

定义位置:Types.ts,类型为InteractionsCallback

action定义了初始页面加载之后、用于触发内存泄漏的浏览器交互。action回调触发过程中,浏览器里分配的所有 JS 对象都会被纳入泄漏过滤的候选集合。

  • 参数:page——puppeteer 的Page对象,提供与浏览器交互的 API;
  • 示例:
const scenario = { url: () => 'https://www.npmjs.com/', action: async (page) => { await page.click('a[href="/link"]'); }, back: async (page) => { await page.click('a[href="/back"]'); }, } module.exports = scenario;

务必注意:释放 puppeteer 元素句柄。官方文档与源码都强调:从page.$x或page.$取到的元素句柄(element handle)必须显式释放,否则会在浏览器上下文里造成额外内存泄漏,干扰检测结果。使用element.dispose()或Promise.all(elements.map(e => e.dispose()))完成清理:

const scenario = { url: () => 'https://www.npmjs.com/', action: async (page) => { const elements = await page.$x("//button[contains(., 'Text in Button')]"); const [button] = elements; if (button) { await button.click(); } // dispose external references to JS objects in browser context await Promise.all(elements.map(e => e.dispose())); }, back: async (page) => ... , } module.exports = scenario;

仓库自带的示例场景 detached-dom.js 完整演示了这一最佳实践:先用$x按 XPath 找到按钮,点击后立刻Promise.all(elements.map(e => e.dispose()))清理句柄。

3.2 back:action 的"撤销"操作

定义位置:Types.ts

back指定 memlab 如何回退/撤销action造成的状态变化,本质是一个 undo 操作:

const scenario = { url: () => 'https://www.npmjs.com/', action: async (page) => { await page.click('a[href="/link"]'); }, back: async (page) => { await page.click('a[href="/back"]'); }, }

memlab 需要撤销action的原因是其"差分对比"式的泄漏检测原理:对比执行交互前后的堆快照,找出"被action分配、但back之后仍未释放"的对象——这正是leakFilter与leakedNodeIds的数据来源。在 detached-dom.js 中,back通过点击a[href="/"]返回首页来复位页面状态。

四、交互次数控制:repeat

定义位置:Types.ts

repeat返回一个数字,表示 memlab 额外执行多少轮action+back:

module.exports = { url: () => ... , action: async (page) => ... , back: async (page) => ... , // browser interaction: two additional [ action -> back ] // init-load -> action -> back -> action -> back -> action -> back repeat: () => 2, };

注意:repeat的值是"额外轮数"。repeat: () => 2得到的序列是init-load -> action -> back -> action -> back -> action -> back,即总共 3 次action、前 2 次各跟一次back,最后一次action(target 快照点)之后跟final回退。这与 BaseSynthesizer.ts 的循环展开逻辑一致。多轮往返交互有助于放大"每次交互都残留一点内存"这类渐进式泄漏的检测信号。

五、生命周期钩子:beforeInitialPageLoad 与 setup

5.1 beforeInitialPageLoad:初始加载前的准备

定义位置:Types.ts

在初始页面加载之前仅调用一次,常用于设置 HTTP 头或准备数据:

const scenario = { url: () => 'https://www.npmjs.com/', beforeInitialPageLoad: async (page) => { // before the initial page load }, action: async (page) => { await page.click('a[href="/link"]'); }, back: async (page) => { await page.click('a[href="/back"]'); }, } module.exports = scenario;

源码印证:在 E2EInteractionManager.ts 中,只有当tabInfo.idx === 1(第一个步骤)时才会调用opArgs.scenario?.beforeInitialPageLoad。

5.2 setup:初始加载后的数据准备

定义位置:Types.ts

setup在初始页面加载之后仅调用一次,可用来登录(官方推荐优先用cookies)或在action之前准备数据:

const scenario = { url: () => 'https://www.npmjs.com/', setup: async (page) => { // log in or prepare data for the interaction }, action: async (page) => { await page.click('a[href="/link"]'); }, back: async (page) => { await page.click('a[href="/back"]'); }, } module.exports = scenario;

同样在 E2EInteractionManager.ts,setup只在首个步骤执行完成后被调用。

六、认证与 Cookie:cookies

定义位置:Types.ts

如果被测页面需要认证或特定 Cookie,可以以<name, value, domain>元组列表的形式传入:

const scenario = { url: () => 'https://www.facebook.com/', cookies: () => [ {name:'cm_j', value: 'none', domain: '.facebook.com'}, {name:'datr', value: 'yJvIY...', domain: '.facebook.com'}, {name:'c_user', value: '8917...', domain: '.facebook.com'}, {name:'xs', value: '95:9WQ...', domain: '.facebook.com'}, // ... ], }; module.exports = scenario;

domain 自动填充规则:请务必提供正确的domain字段;若缺失,memlab 会基于url回调推断 domain。源码实现在 TestPlanner.ts:通过new URL(scenario.url())解析主机名,去掉最左侧子域后拼上.前缀(例如https://www.facebook.com/会推导出.facebook.com)。cookies的返回类型是Cookies(即Cookie数组),单个Cookie的name、value必填,其余字段可选。

七、页面加载判定:isPageLoaded

定义位置:Types.ts,类型为CheckPageLoadCallback

该回调用于判定初始加载及后续交互中页面是否加载完成,返回Promise<boolean>:

  • 返回true:memlab 认为导航完成,进入下一步;
  • 返回false:memlab 会持续调用该回调直到返回true(可await异步逻辑解析后再返回true);
  • 未提供时的默认行为:memlab 认为"连续至少 500ms 没有网络连接"即视为导航结束。
module.exports = { url: () => ... , action: async (page) => ... , back: async (page) => ... , isPageLoaded: async (page) => { await page.waitForNavigation({ // consider navigation to be finished when there are // no more than 2 network connections for at least 500 ms. waitUntil: 'networkidle2', // Maximum navigation time in milliseconds timeout: 5000, }); return true; }, };

从 BaseSynthesizer.ts 可以看到,访问计划中的isPageLoaded字段优先取scenario.isPageLoaded,否则回落给合成器的默认检查器(getPageLoadChecker,内部通过waitFor等待delayWhenNoPageLoadCheck配置时长)。

八、泄漏判定三件套:beforeLeakFilter、leakFilter、retainerReferenceFilter

memlab 的默认泄漏过滤器把**游离的 DOM 元素(detached DOM elements)和从 React Fiber 树上脱离的未挂载 Fiber 节点(unmounted Fiber nodes)**视为内存泄漏。当默认规则不满足被测应用时,可以用下面三个回调自定义判定逻辑。它们在 Config.ts 中被统一收集为externalLeakFilter并接入分析流程(见 HeapAnalyzer.ts 中对beforeLeakFilter的调用)。

8.1 beforeLeakFilter:过滤前的初始化钩子

定义位置:Types.ts,类型为InitLeakFilterCallback

在所有leakFilter调用之前仅执行一次,适合初始化数据存储或做一次性预处理:

module.exports = { url: () => ... , action: async (page) => ... , back: async (page) => ... , beforeLeakFilter: (snapshot, leakedNodeIds) { // initialize some data stores }, };
  • snapshot:所有浏览器交互完成后采集的最终堆快照(IHeapSnapshot),可查询堆快照数据;
  • leakedNodeIds:Set<number>——由action分配、但back之后未释放的所有 JS 堆对象的 id 集合。

8.2 leakFilter:自定义泄漏判定算法

定义位置:Types.ts,类型为LeakFilterCallback

leakFilter对每个"被action分配、但back后未释放"的节点逐一调用,返回boolean决定该节点是否算泄漏。这些未释放对象可能是有意驻留内存的缓存,也可能真的是内存泄漏——回调让你针对被测 JS 程序"择优挑出"真正的泄漏:

module.exports = { url: () => ... , action: async (page) => ... , back: async (page) => ... , leakFilter(node, snapshot, leakedNodeIds) { // any unreleased node (JS heap object) with 1MB+ // retained size is considered a memory leak return node.retainedSize > 1000000; }, };
  • node:IHeapNode——堆快照中分配但未释放的对象;
  • snapshot:最终堆快照;
  • leakedNodeIds:泄漏候选节点 id 集合。

仓库还提供了一个"按保留大小过滤"的参考实现:filter-by-retained-size.js,可作为自定义leakFilter的起点。

8.3 retainerReferenceFilter:控制保留追踪(retainer trace)的边

定义位置:Types.ts,类型为ReferenceFilterCallback

该回调对堆快照中的**每一条引用(edge)**调用,决定该引用是否应纳入保留追踪(retainer trace)计算:

  • edge:IHeapEdge——被评估是否纳入 retainer trace 的引用边;
  • snapshot:最终堆快照;
  • isReferenceUsedByDefault:boolean——memlab 默认逻辑是否使用该引用(true表示默认会纳入计算)。

返回值语义需要特别注意:返回true只表示该引用成为 retainer trace 的候选,最终可能进也可能不进;返回false则必然排除。而且,如果排除的是某个对象的支配引用(dominator reference,即从 GC 根到达该堆对象必须经过的边),该对象会在堆图中被视为不可达,从而整个引用和对象都不会进入 retainer trace 检测与保留大小计算。

// save as leak-filter.js module.exports = { retainerReferenceFilter(edge, _snapshot, _leakedNodeIds) { // exclude react fiber references if (edge.name_or_index.toString().startsWith('__reactFiber$')) { return false; } return true; } };

九、IScenario 属性速查表

属性类型必选调用时机核心作用
url() => string✅规划阶段返回初始页面 URL;仅此一项时检测初始加载泄漏
actionInteractionsCallback否初始加载后(每轮)执行触发泄漏的交互,其分配对象进入候选集
backInteractionsCallback否每次 action 之后撤销 action 的状态变更(undo)
repeat() => number否规划阶段额外执行多少轮action -> back,默认 0
beforeInitialPageLoadInteractionsCallback否初始加载前(仅一次)设置 HTTP 头、准备数据
setupInteractionsCallback否初始加载后(仅一次)登录(推荐用 cookies)或准备数据
cookies() => Cookies否规划阶段认证 Cookie 列表,domain 可自动推断
isPageLoadedCheckPageLoadCallback否每次导航后自定义页面加载完成判定,默认 500ms 无网络连接
beforeLeakFilterInitLeakFilterCallback否leakFilter 之前(仅一次)初始化数据存储等一次性预处理
leakFilterLeakFilterCallback否每个候选节点自定义泄漏判定,默认检测游离 DOM 与未挂载 Fiber
retainerReferenceFilterReferenceFilterCallback否每条引用边控制引用是否纳入 retainer trace 计算

十、最小可用场景与进阶组合示例

最小场景(仅检测初始加载泄漏):

module.exports = { url: () => 'https://example.com', };

综合示例(含认证、重复交互与自定义泄漏阈值):

module.exports = { url: () => 'https://example.com/', cookies: () => [ {name: 'session', value: 'abc123', domain: '.example.com'}, ], isPageLoaded: async (page) => { await page.waitForNavigation({waitUntil: 'networkidle2', timeout: 5000}); return true; }, action: async (page) => { const elements = await page.$x("//button[contains(., 'Load more')]"); const [button] = elements; if (button) { await button.click(); } await Promise.all(elements.map(e => e.dispose())); }, back: async (page) => { await page.click('a[href="/"]'); }, repeat: () => 3, leakFilter(node) { return node.retainedSize > 1000000; // 1MB+ }, };

十一、进一步阅读

  • 接口完整定义:Types.ts;
  • 场景如何生成访问计划:BaseSynthesizer.ts;
  • CLI 的--scenario解析:ScenarioFileOption.ts;
  • API 的run({scenario})实现:API.ts;
  • 官方示例场景:detached-dom.js;
  • 官方示例泄漏过滤器:filter-by-retained-size.js;
  • 配套类型:InteractionsCallback、LeakFilterCallback、ReferenceFilterCallback、Page。
  • 开发工具
  • 测试
  • CLI
  • MCP 服务

【免费下载链接】memlab

A framework for finding JavaScript memory leaks and analyzing heap snapshots

项目地址:https://gitcode.com/gh_mirrors/me/memlab
点击查看免费下载

相关推荐

上一篇:ESP32音频开发中的MCLK配置终极指南:深度解析与实战应用
下一篇:Filterizr 源码架构解析:核心类设计与 6 大布局算法实现原理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询