memlab 测试场景(IScenario)接口完全指南:编写 E2E 内存泄漏检测脚本的 11 个回调 开发工具测试CLIMCP 服务【免费下载链接】memlabA 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-loadbaseline然后按repeat()的次数循环插入action-on-page与revert回退步骤最后执行一次action-on-pagetarget与一次revertfinal。其中repeat在 BaseSynthesizer.ts 中的默认取值为scenario.repeat ? scenario.repeat() : 0即不声明repeat就只做一轮action - back。而revert步骤只有在声明了scenario.back时才使用自定义回退回调否则回落到内置的通用回退步骤SynthesisUtils.revertStep。二、必选回调url定义位置Types.tsurl是唯一必选回调返回页面初始 URL 的字符串值const scenario { url: () https://www.npmjs.com/, }; module.exports scenario;关键语义如果场景只提供url没有actionmemlab 会尝试直接从初始页面加载中检测内存泄漏——初始加载分配的所有 JS 对象都会成为泄漏过滤的候选对象。这一点与 BaseSynthesizer.ts 中if (!scenario.action)分支的处理完全对应。三、核心交互对action 与 back3.1 action触发泄漏的交互定义位置Types.ts类型为InteractionsCallbackaction定义了初始页面加载之后、用于触发内存泄漏的浏览器交互。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 backaction 的撤销操作定义位置Types.tsback指定 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.tsrepeat返回一个数字表示 memlab 额外执行多少轮actionbackmodule.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最后一次actiontarget 快照点之后跟final回退。这与 BaseSynthesizer.ts 的循环展开逻辑一致。多轮往返交互有助于放大每次交互都残留一点内存这类渐进式泄漏的检测信号。五、生命周期钩子beforeInitialPageLoad 与 setup5.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.tssetup在初始页面加载之后仅调用一次可用来登录官方推荐优先用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.tssetup只在首个步骤执行完成后被调用。六、认证与 Cookiecookies定义位置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该回调用于判定初始加载及后续交互中页面是否加载完成返回Promiseboolean返回truememlab 认为导航完成进入下一步返回falsememlab 会持续调用该回调直到返回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、retainerReferenceFiltermemlab 的默认泄漏过滤器把**游离的 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可查询堆快照数据leakedNodeIdsSetnumber——由action分配、但back之后未释放的所有 JS 堆对象的 id 集合。8.2 leakFilter自定义泄漏判定算法定义位置Types.ts类型为LeakFilterCallbackleakFilter对每个被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; }, };nodeIHeapNode——堆快照中分配但未释放的对象snapshot最终堆快照leakedNodeIds泄漏候选节点 id 集合。仓库还提供了一个按保留大小过滤的参考实现filter-by-retained-size.js可作为自定义leakFilter的起点。8.3 retainerReferenceFilter控制保留追踪retainer trace的边定义位置Types.ts类型为ReferenceFilterCallback该回调对堆快照中的**每一条引用edge**调用决定该引用是否应纳入保留追踪retainer trace计算edgeIHeapEdge——被评估是否纳入 retainer trace 的引用边snapshot最终堆快照isReferenceUsedByDefaultboolean——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 的状态变更undorepeat() number否规划阶段额外执行多少轮action - back默认 0beforeInitialPageLoadInteractionsCallback否初始加载前仅一次设置 HTTP 头、准备数据setupInteractionsCallback否初始加载后仅一次登录推荐用 cookies或准备数据cookies() Cookies否规划阶段认证 Cookie 列表domain 可自动推断isPageLoadedCheckPageLoadCallback否每次导航后自定义页面加载完成判定默认 500ms 无网络连接beforeLeakFilterInitLeakFilterCallback否leakFilter 之前仅一次初始化数据存储等一次性预处理leakFilterLeakFilterCallback否每个候选节点自定义泄漏判定默认检测游离 DOM 与未挂载 FiberretainerReferenceFilterReferenceFilterCallback否每条引用边控制引用是否纳入 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.tsCLI 的--scenario解析ScenarioFileOption.tsAPI 的run({scenario})实现API.ts官方示例场景detached-dom.js官方示例泄漏过滤器filter-by-retained-size.js配套类型InteractionsCallback、LeakFilterCallback、ReferenceFilterCallback、Page。赞分享开发工具测试CLIMCP 服务【免费下载链接】memlabA framework for finding JavaScript memory leaks and analyzing heap snapshots项目地址https://gitcode.com/gh_mirrors/me/memlab点击查看免费下载相关推荐MemLab 内存泄漏检测实战指南从 Puppeteer 场景编写到堆快照分析的完整实践MemLab 内存泄漏检测实战指南从 Puppeteer 场景编写到堆快照分析的完整实践 MemLab 是 Meta 开源的端到端 JavaScript 内存开发工具测试CLIMCP 服务Babylon.js 内存泄漏 CI 测试实战基于 memlab 的场景化内存泄漏运行器Babylon.js 内存泄漏 CI 测试实战基于 memlab 的场景化内存泄漏运行器 本文以 Babylon.js 仓库内 packages/tools/图形学游戏开发3D渲染memlab 内存泄漏检测实战从浏览器 E2E 场景到堆快照分析与内存断言memlab 内存泄漏检测实战从浏览器 E2E 场景到堆快照分析与内存断言 memlab 是一个端到端E2E的 JavaScript 内存泄漏测试与分析框开发工具测试CLIMCP 服务上一篇ESP32音频开发中的MCLK配置终极指南深度解析与实战应用下一篇Filterizr 源码架构解析核心类设计与 6 大布局算法实现原理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考