以下是memlab-analysis技能的中文翻译内容:
name: memlab-analysis description: 利用Facebook MemLab进行JavaScript内存泄漏检测的专家技能。配置MemLab场景,执行内存泄漏检测运行,分析堆快照,识别脱离DOM元素,查找事件监听器泄漏,并与CI管道集成。 allowed-tools: Bash(*) 读写编辑Glob Grep WebFetch metadata: author: babysitter-sdk version: “1.0.0” category: memory-profiling backlog-id: SK-017
memlab-analysis
你是 memlab-analysis - 一个专门用于使用Facebook的MemLab框架进行JavaScript内存泄漏检测的专家技能。这项技能提供了检测、分析和修复Web应用程序内存泄漏的专家能力。
概览
这项技能使AI驱动的JavaScript内存分析成为可能,包括:
- 配置MemLab测试场景
- 执行自动化内存泄漏检测运行
- 分析堆快照以查找内存增长
- 识别脱离DOM元素
- 查找事件监听器和闭包泄漏
- 生成可操作的MemLab报告
- 与CI/CD管道集成
前提条件
- Node.js 16+(推荐18+)
- MemLab CLI:
npm install -g memlab - Chrome/Chromium浏览器
- 可选:Puppeteer用于自定义场景
能力
1. MemLab场景开发
编写全面的MemLab测试场景:
// scenario.js - 基本内存泄漏检测场景
module.exports = {
// 场景元数据
name: 'user-dashboard-leak-test',
// 设置 - 导航到起始页面
async setup(page) {
await page.goto('https://app.example.com/');
await page.waitForSelector('.login-form');
},
// 动作 - 执行可能泄漏的操作
async action(page) {
// 登录
await page.type('#email', 'test@example.com');
await page.type('#password', 'password123');
await page.click('#login-button');
await page.waitForSelector('.dashboard');
// 导航到仪表板
await page.click('[data-testid="analytics-tab"]');
await page.waitForSelector('.analytics-charts');
// 与图表交互(潜在泄漏源)
await page.click('[data-testid="chart-filter"]');
await page.waitForSelector('.chart-updated');
},
// 后退 - 返回到干净状态
async back(page) {
// 从可能泄漏的页面导航离开
await page.click('[data-testid="home-tab"]');
await page.waitForSelector('.dashboard-home');
},
// 可选:自定义泄漏过滤器
leakFilter(node, snapshot, leakedNodeIds) {
// 忽略已知非泄漏
if (node.name === 'InternalCache') return false;
if (node.retainedSize < 1024) return false; // 忽略小泄漏
return true;
}
};
2. 高级场景模式
复杂场景配置:
// modal-leak-scenario.js - 测试模态对话框内存泄漏
module.exports = {
name: 'modal-dialog-leak',
// 初始页面状态
url: () => 'https://app.example.com/products',
async setup(page) {
await page.setViewport({ width: 1920, height: 1080 });
await page.evaluate(() => {
window.memlab = { startTime: Date.now() };
});
},
async action(page) {
// 打开模态
await page.click('[data-testid="add-product-btn"]');
await page.waitForSelector('.modal-overlay');
// 填写表单
await page.type('[name="productName"]', 'Test Product');
await page.type('[name="description"]', 'Test Description');
// 上传图片(潜在泄漏)
const input = await page.$('[type="file"]');
await input.uploadFile('./test-image.png');
await page.waitForSelector('.image-preview');
// 关闭模态(应清理)
await page.click('.modal-close');
await page.waitForSelector('.modal-overlay', { hidden: true });
},
async back(page) {
// 强制垃圾收集机会
await page.evaluate(() => {
window.dispatchEvent(new Event('beforeunload'));
});
await page.goto('https://app.example.com/');
},
// 重复动作多次以放大泄漏
repeat: () => 3,
// 自定义泄漏检测
leakFilter(node, snapshot, leakedNodeIds) {
// 专注于特定泄漏模式
const suspectTypes = [
'HTMLDivElement',
'HTMLImageElement',
'EventListener',
'Closure'
];
return suspectTypes.includes(node.type);
}
};
3. SPA路由导航测试
在路由更改期间测试内存泄漏:
// route-navigation-scenario.js
module.exports = {
name: 'spa-route-navigation',
async setup(page) {
await page.goto('https://app.example.com/');
await page.waitForNetworkIdle();
},
async action(page) {
// 通过多个路由导航
const routes = [
'/dashboard',
'/products',
'/orders',
'/settings',
'/analytics'
];
for (const route of routes) {
await page.click(`a[href="${route}"]`);
await page.waitForNetworkIdle();
await page.waitForTimeout(500);
}
},
async back(page) {
await page.click('a[href="/"]');
await page.waitForNetworkIdle();
}
};
4. 事件监听器泄漏检测
检测事件监听器累积:
// event-listener-scenario.js
module.exports = {
name: 'event-listener-leak',
async setup(page) {
await page.goto('https://app.example.com/');
// 注入事件监听器计数器
await page.evaluate(() => {
const originalAddEventListener = EventTarget.prototype.addEventListener;
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
window.__eventListenerCount = 0;
window.__eventListeners = new Map();
EventTarget.prototype.addEventListener = function(type, listener, options) {
window.__eventListenerCount++;
const key = `${this.constructor.name}:${type}`;
window.__eventListeners.set(key, (window.__eventListeners.get(key) || 0) + 1);
return originalAddEventListener.call(this, type, listener, options);
};
EventTarget.prototype.removeEventListener = function(type, listener, options) {
window.__eventListenerCount--;
const key = `${this.constructor.name}:${type}`;
window.__eventListeners.set(key, (window.__eventListeners.get(key) || 0) - 1);
return originalRemoveEventListener.call(this, type, listener, options);
};
});
},
async action(page) {
// 执行添加事件监听器的动作
await page.click('[data-testid="open-sidebar"]');
await page.waitForSelector('.sidebar');
await page.click('[data-testid="close-sidebar"]');
await page.waitForSelector('.sidebar', { hidden: true });
},
async back(page) {
// 检查监听器计数
const stats = await page.evaluate(() => ({
count: window.__eventListenerCount,
listeners: Object.fromEntries(window.__eventListeners)
}));
console.log('Event listener stats:', stats);
await page.goto('https://app.example.com/');
}
};
5. 运行MemLab分析
执行MemLab命令:
# 基本泄漏检测
memlab run --scenario scenario.js
# 增加迭代次数
memlab run --scenario scenario.js --work-dir ./memlab-results
# 运行特定阶段
memlab snapshot --scenario scenario.js
memlab find-leaks --work-dir ./memlab-results
# 分析现有的堆快照
memlab analyze ./memlab-results
# 生成详细报告
memlab report --work-dir ./memlab-results --output-dir ./reports
# 无头模式运行
memlab run --scenario scenario.js --headless
# 自定义Chromium路径
memlab run --scenario scenario.js --chromium-binary /path/to/chrome
6. 堆快照分析
分析堆快照以查找内存问题:
// heap-analysis.js - 自定义堆分析
const { takeNodeMinimalHeap, findLeaks } = require('@memlab/api');
async function analyzeHeap() {
// 拍摄堆快照
const heap = await takeNodeMinimalHeap();
// 按类型查找对象
const detachedDOMNodes = heap.nodes.filter(node =>
node.name.startsWith('Detached ') &&
node.type === 'native'
);
// 查找大保留对象
const largeObjects = heap.nodes
.filter(node => node.retainedSize > 1024 * 1024) // > 1MB
.sort((a, b) => b.retainedSize - a.retainedSize)
.slice(0, 10);
// 查找特定模式
const closureLeaks = heap.nodes.filter(node =>
node.type === 'closure' &&
node.retainedSize > 10240
);
console.log('Analysis Results:');
console.log('Detached DOM nodes:', detachedDOMNodes.length);
console.log('Large objects:', largeObjects.map(n => ({
name: n.name,
type: n.type,
size: `${(n.retainedSize / 1024 / 1024).toFixed(2)} MB`
})));
console.log('Potential closure leaks:', closureLeaks.length);
}
7. CI/CD集成
将MemLab集成到CI管道中:
# .github/workflows/memory-check.yml
name: 内存泄漏检查
on:
pull_request:
branches: [main]
jobs:
memlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 设置Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: 安装依赖
run: npm ci
- name: 构建应用程序
run: npm run build
- name: 启动应用程序
run: npm run start &
env:
PORT: 3000
- name: 等待应用程序
run: npx wait-on http://localhost:3000
- name: 安装MemLab
run: npm install -g memlab
- name: 运行内存泄漏测试
run: |
memlab run --scenario ./tests/memlab/dashboard-scenario.js \
--work-dir ./memlab-results \
--headless
- name: 检查泄漏
run: |
LEAK_COUNT=$(memlab find-leaks --work-dir ./memlab-results --json | jq '.length')
if [ "$LEAK_COUNT" -gt 0 ]; then
echo "::error::Found $LEAK_COUNT memory leaks"
memlab report --work-dir ./memlab-results
exit 1
fi
- name: 上传工件
if: failure()
uses: actions/upload-artifact@v4
with:
name: memlab-results
path: ./memlab-results
8. 常见泄漏模式检测
识别常见的JavaScript内存泄漏模式:
// leak-patterns.js - 检测常见泄漏模式
module.exports = {
// 脱离DOM元素
detectDetachedDOM(node) {
return node.name.startsWith('Detached ') &&
['HTMLDivElement', 'HTMLSpanElement', 'HTMLImageElement']
.some(type => node.name.includes(type));
},
// 事件监听器泄漏
detectEventListenerLeak(node) {
return node.type === 'object' &&
node.name === 'EventListener' &&
node.retainedSize > 1024;
},
// 闭包泄漏(持有引用)
detectClosureLeak(node) {
return node.type === 'closure' &&
node.retainedSize > 10240 &&
node.edges.some(edge => edge.name === 'context');
},
// 定时器泄漏(未清除setInterval)
detectTimerLeak(node) {
return node.name === 'Timeout' || node.name === 'Interval';
},
// 承诺链泄漏
detectPromiseLeak(node) {
return node.name === 'Promise' &&
node.edges.some(edge =>
edge.name === 'reactions' && edge.to.retainedSize > 0
);
},
// 组件状态保留
detectComponentLeak(node) {
const componentPatterns = [
'FiberNode', // React
'ComponentPublicInstance', // Vue
'ViewRef' // Angular
];
return componentPatterns.some(p => node.name.includes(p));
}
};
MCP服务器集成
这项技能可以利用以下MCP服务器:
| 服务器 | 描述 | 用例 |
|---|---|---|
| playwright-mcp | 浏览器自动化 | 自定义场景 |
| clinic.js | Node.js分析 | 替代内存分析 |
最佳实践
场景设计
- 现实用户流程 - 模拟实际用户行为
- 多次迭代 - 重复动作以放大泄漏
- 清洁回状态 - 确保适当的清理验证
- 专注测试 - 每个场景一个潜在泄漏源
泄漏检测
- 过滤噪声 - 忽略已知非泄漏
- 大小阈值 - 关注重大泄漏
- 保留者路径 - 了解为什么对象被保留
- DOM焦点 - 脱离DOM是最常见的泄漏
CI集成
- 在PR上运行 - 在合并前捕获泄漏
- 基线比较 - 与已知良好状态进行比较
- 失败阈值 - 设置可接受的泄漏限制
- 工件保留 - 保留堆转储以供分析
流程集成
这项技能与以下流程集成:
memory-leak-detection.js- 内存泄漏检测工作流memory-profiling-analysis.js- 全面内存分析
输出格式
执行操作时,提供结构化输出:
{
"operation": "detect-leaks",
"status": "completed",
"scenario": "dashboard-leak-test",
"results": {
"leaksFound": 3,
"totalLeakedSize": "2.5 MB",
"leaks": [
{
"type": "Detached HTMLDivElement",
"count": 15,
"totalSize": "1.2 MB",
"retainerPath": ["Window", "EventTarget", "handlers", "closure"],
"sourceFile": "dashboard.js:245"
},
{
"type": "EventListener",
"count": 42,
"totalSize": "850 KB",
"retainerPath": ["Window", "resize", "listener"],
"sourceFile": "resize-handler.js:12"
}
]
},
"recommendations": [
{
"leak": "Detached HTMLDivElement",
"fix": "Ensure modal DOM is removed in componentWillUnmount",
"codeLocation": "Modal.tsx:89"
}
],
"reportPath": "./memlab-results/report/index.html"
}
错误处理
常见问题
| 错误 | 原因 | 解决方案 |
|---|---|---|
Chrome not found |
缺少浏览器 | 安装Chrome或指定路径 |
Timeout exceeded |
页面加载缓慢 | 增加超时,检查网络 |
OOM in analysis |
堆过大 | 增加Node.js内存限制 |
No leaks found |
场景太短 | 增加迭代次数,延长动作 |
约束
- 需要Chrome/Chromium浏览器
- 堆分析可能内存密集
- 可能出现误报 - 推荐手动验证
- 有源映射时效果最佳