跳到主要内容

在演示应用中检测内存泄漏

本教程演示了如何使用 memlab 检测分离的 DOM 元素。

设置待测示例 Web 应用

当您点击“创建分离的 DOM”按钮时,演示应用会泄漏分离的 DOM 元素。 每次点击会创建 1024 个分离的 DOM 元素,这些元素由 window 对象引用。

memlab run result

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall memory_lab */import Link from 'next/link';import React from 'react';export default function DetachedDom() {  const addNewItem = () => {    if (!window.leakedObjects) {      window.leakedObjects = [];    }    for (let i = 0; i < 1024; i++) {      window.leakedObjects.push(document.createElement('div'));    }    console.log(      'Detached DOMs are created. Please check Memory tab in devtools',    );  };  return (    <div className="container">      <div className="row">        <Link href="/">Go back</Link>      </div>      <br />      <div className="row">        <button type="button" className="btn" onClick={addNewItem}>          Create detached DOMs        </button>      </div>    </div>  );}

源文件: packages/e2e/static/example/pages/examples/detached-dom.jsx

1. 克隆仓库

要在本地机器上运行演示 Web 应用,请克隆 memlab github 仓库

git clone git@github.com:facebook/memlab.git

2. 运行示例应用

在本地机器上克隆仓库后,从 Memlab 项目的根目录运行以下命令

npm install && npm run build
cd packages/e2e/static/example
npm install && npm run dev

这将启动一个示例 Nextjs 应用。 让我们通过浏览器访问 https://:3000 来确保它正在运行

注意

端口号 :3000 在您的情况下可能有所不同。

查找内存泄漏

1. 创建一个场景文件

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* @nolint
* @oncall memory_lab
*/

// memlab/packages/e2e/static/example/scenario/detached-dom.js
/**
* The initial `url` of the scenario we would like to run.
*/
function url() {
return 'https://:3000/examples/detached-dom';
}

/**
* Specify how memlab should perform action that you want
* to test whether the action is causing memory leak.
*
* @param page - Puppeteer's page object:
* https://pptr.cn/api/puppeteer.page/
*/
async function action(page) {
const elements = await page.$x(
"//button[contains(., 'Create detached DOMs')]",
);
const [button] = elements;
if (button) {
await button.click();
}
// clean up external references from memlab
await Promise.all(elements.map(e => e.dispose()));
}

/**
* Specify how memlab should perform action that would
* reset the action you performed above.
*
* @param page - Puppeteer's page object:
* https://pptr.cn/api/puppeteer.page/
*/
async function back(page) {
await page.click('a[href="/"]');
}

module.exports = {action, back, url};

让我们将此文件保存在 ~/memlab/scenarios/detached-dom.js

2. 运行 memlab

这可能需要几分钟

memlab run --scenario ~/memlab/scenarios/detached-dom.js

3. 调试泄漏跟踪

对于每个泄漏的对象组,memLab 打印一个代表性的泄漏跟踪。

memlab run result

让我们从上到下分解结果

第 1 部分:浏览器交互轨迹显示了 memlab 按照我们的场景文件中指定的执行的浏览器交互(导航)。

  • page-load[6.5MB](baseline)[s1] - 初始页面加载时 JavaScript 堆大小为 6.5MB。 基线堆快照将作为 s1.heapsnapshot 保存在磁盘上。
  • action-on-page[6.6MB](target)[s2] - 点击“创建分离的 DOM”按钮后,堆大小增加到 6.6MB
  • revert[7MB](final)[s3] - 在离开触发内存泄漏的页面后,网页最终达到 7MB。

第 2 部分:泄漏跟踪的总体摘要

  • 1024 leaks - 有 1024 个泄漏的对象。 示例应用的第 12 行在 for 循环中创建了 1024 个分离的 DOM 对象。
  • Retained size - 泄漏的对象集群的聚合保留大小为 143.3KB(内存泄漏根据保留者跟踪的相似性分组在一起)。

第 3 部分:每个泄漏集群的详细代表性泄漏跟踪

注意

泄漏跟踪是从 GC 根(堆图中垃圾收集器遍历堆的入口对象)到泄漏对象的对象引用链。 该跟踪显示了泄漏的对象为何以及如何在内存中保持活动状态。 断开引用链意味着泄漏的对象将不再能从 GC 根访问,因此可以进行垃圾回收。

通过从本机 Window(即 GC 根)向下逐步跟踪泄漏跟踪,您将能够找到应设置为 null 的引用(但由于错误而未设置)。

  • map - 这是 V8 HiddenClass(V8 在内部使用它来存储有关对象形状的元信息以及对其原型的引用 - 请参阅更多此处)被访问的对象 - 在大多数情况下,这是 V8 实现细节,可以忽略。

  • prototype - 这是 Window 类的实例。

  • leakedObjects - 这表明 leakedObjectsWindow 对象的一个属性,大小为 148.5KB,指向一个 Array 对象。

  • 0 - 这表明分离的 HTMLDIVElement(即当前未连接到 DOM 树的 DOM 元素)存储为 leakedObjects 数组的第一个元素(由于显示所有 1024 个泄漏跟踪令人望而却步,因此 Memlab 仅打印一个代表性的泄漏跟踪。即属性 0 而不是属性 0->1023)

    简而言之,从 window 对象到泄漏对象的泄漏跟踪路径是

[window](object) -> leakedObjects(property) -> [Array](object)
-> 0(element) -> [Detached HTMLDIVElement](native)

这与示例中的泄漏代码匹配

window.leakedObjects = [];
for (let i = 0; i < 1024; i++) {
window.leakedObjects.push(document.createElement('div'));
}