【问题标题】:Huge node leak in web app under Chrome - developer tools not reporting causeChrome下Web应用程序中的巨大节点泄漏-开发人员工具未报告原因
【发布时间】:2015-08-14 19:05:34
【问题描述】:

我有一个相当复杂的 Web 应用程序,它以极快的速度泄漏节点。使用 Windows 任务管理器,我可以看到 chrome 进程使用的内存量每秒增加约 3MB。

我最初的方法是禁用部分应用程序以隔离导致问题的区域。经过一些试验和错误发现原因是文档中的一些表更新。该应用程序每秒从服务器请求数据以更新多个数据表。每个表都以相同的方式更新。从表中删除所有行,并插入包含更新数据的新行。检查执行此操作的通用代码时,我没有发现问题。

为了解决这个问题,我显然求助于 Chrome 开发者工具。我遇到的问题是这些工具给我的信息相互矛盾。

  1. 时间轴工具显示节点数以每秒大约 28000 个的速度增加。强制垃圾回收不会将其还原到原来的水平。
  2. 时间轴工具显示 JS 堆大小随时间波动。强制垃圾回收将堆返回到其原始大小(正负数 100K)
  3. 使用“三快照”技术,Profiler - 堆快照工具显示快照 1 和快照 2 之间没有创建快照 3 中存在的 HTML 或文本节点。
  4. 比较快照显示了许多 HTMLTableRowElement、HTMLTableCellElement、HTMLInputElement 和 Text 节点的创建和删除。没有报告节点数增加。
  5. Profiler - 堆分配工具验证堆快照工具的结果。任何时候都不会报告任何节点类型的泄漏。
  6. 堆工具显示(编译代码)、(数组)和(系统)类型的小幅增加。
  7. Heap Profile 工具报告我的“原始”javascript 版本的应用程序的堆大小约为 12MB,而我的闭包编译器 - 编译版本的应用程序的堆大小约为 7MB。这些值不会随着时间的推移而增长太多。

这让我有点困惑。显然存在内存泄漏。 Windows 任务管理器和时间线工具将其报告为节点泄漏,但堆分析工具和 JS 堆时间线未显示此问题。

据我所知,HTMLTableRowElements 仅在两个地方被引用,在文档中和用于按值查找的对象中。清除表时始终清除对象。我可以通过更改我的代码来创建所有节点来“解决”这个问题,但从不将它们插入到文档中,只是在对象中引用它们。显然这不是一个修复,因为用户看不到数据。

经过 2 天的测试和调查,我现在不知道如何进行。如果你加入 IE 和 Firefox,情节就会变厚。这些浏览器似乎没有相同的内存/节点泄漏。我也相信 Chrome 以前不存在这个问题。不幸的是,似乎没有办法回到以前版本的 chrome 来查看它是否是 Chrome 中的错误。

有没有人对此有任何建议。我是否遗漏了一些东西,或者误解了开发人员工具的输出?有没有办法回到以前的 chrome 版本?这听起来像是 Chrome 中的错误吗?欢迎所有的cmets。

这是用于使用 Google 的 Closure API 插入项目的代码:

/**
 * Inserts and/or updates a row in the table.
 * 
 * @param {string|number} rowId The identifier of the row in the rows_ map
 * @param {boolean} insertTop If true the row is inserted at the top of the document
 * @param {...goog.dom.Appendable} var_args The items to add to each cell in the row
 */
sm.ui.DataTable.prototype.updateRow = function(rowId, insertTop, var_args) {
    var dom = this.getDomHelper();

    // Insert the new session data
    if(!this.rows_[rowId]) {
        // There is no row present (simple case of create one)

        // Create the table row
        var row = this.rows_[rowId] = dom.createDom('tr', {'style' : 'display: none;'});

        var colView = this.colView_;
        var colNames = this.columnNames_;

        for(var i = 0; i < colNames.length; i++) {
            var cell = dom.createDom('td');
            if(!colView[i]) {
                goog.style.showElement(cell, false);
            }
            row.appendChild(cell);
        }

        // Add to the table if element exists
        var element = this.getElement();
        var tBody = element.tBodies[0];
        if(element) {
            this.showPage_(this.currentPage_, false);
            if(insertTop) {
                // Insert a row at the top of the table
                tBody.insertBefore(row, tBody.rows[0] || null);
            } else {
                // Append to the end if insert top no set true
                tBody.appendChild(row);
            }
            this.showPage_(this.currentPage_, true);

            // Update the footer to as it may need displaying or changing
            this.updateFooter_();   
        }
    }

    // Loop over the var args and set the content of each cell
    // arguments will be string, Node, array of strings and Nodes
    for (var i = 0; i < arguments.length - 2; i++) {
        var row = this.rows_[rowId];

        dom.removeChildren(row.cells[i]);           
        dom.append(row.cells[i], arguments[i + 2]); // Removing this line "cures" the problem.
    }
};

/**
 * Removes all rows from the table.
 * @param {string=} opt_message Message to display in the instead of data (reset table again to clear)
 */
sm.ui.DataTable.prototype.reset = function (opt_message) {

    var element = this.getElement();
    var tBody = element.tBodies[0];
    while(tBody.rows.length > 0) {
        tBody.deleteRow(0);
    }

    // Reset the rows and pages
    this.rows_ = {};
    this.currentPage_ = 1;

    this.updateFooter_();

    if(opt_message) {
        // Create the row to inset in the table
        // Ensure it spans all the columns
        var dom = this.getDomHelper();
        var messageCell = dom.createDom('tr', null, 
            dom.createDom('td', {'colspan' : this.columnNames_.length}, opt_message));
        goog.dom.append(tBody,messageCell);
    }
};

【问题讨论】:

  • 你能分享一些代码吗?节点的创建和替换将是最有趣的。
  • @Hauke 我会尝试获取一些代码。我将不得不从应用程序中获取相关的 sn-ps,因为发布所有这些内容非常复杂。
  • dom.removeChildren 到底是做什么的?顾名思义,我认为它会删除单元格[i] 的子代,而不是单元格[i] 本身。对吗?
  • 是的,这是正确的,该函数中的实际代码是:var child; while ((child = node.firstChild)) { node.removeChild(child); }
  • 并且 append 将遍历字符串/节点数组并创建相应的 dom 元素?在开发工具中,您是否检查过此处提到的分离的 DOM 树条目:developer.chrome.com/devtools/docs/heap-profiling-dom-leaks?

标签: javascript google-chrome web-applications memory-leaks nodes


【解决方案1】:

这很像经典的事件清除错误。

如果将事件附加到节点,则在分离事件处理程序之前不会释放该节点。

例如,jQuery 本质上将所有事件从已删除的节点中分离出来。

正确删除一个节点(连同她的所有后代)的代码是:

function walkTheDOM(node, func)
{
  func(node);
  node = node.firstChild;
  while (node)
  {
    walkTheDOM(node, func);
    node = node.nextSibling;
  }
}

function purgeEventHandlers(node)
{
  walkTheDOM(node, function (n) {
    var f;

    for (f in n)
    {
      if (typeof n[f] === "function")
      {
        n[f] = null;
      }
    }
  });
}

// now you can remove the node from the DOM

此技术直接取自 Crockford's Good Parts。

或者,使用 jQuery,它会以同样的方式处理它。

【讨论】:

  • 这是一个合理的解释。你能告诉我为什么 Chrome 开发工具在附加事件处理程序时不会显示节点仍在堆上吗?
  • 不,我不能。也许它只显示附加到 DOM 的节点?在代码中没有引用但来自事件处理程序的引用的分离 DOMElement 是一件非常混乱的事情。 Chrome 开发工具不一定在所有方面都是完美的......
  • 好的,我会调查是否是这种情况。我可以简单地使用您的代码提取来测试清除事件是否会导致节点被释放。
  • 试过这种方法,对泄漏没有任何影响。将不得不继续搜索。
猜你喜欢
  • 2018-11-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-27
  • 2018-01-03
相关资源
最近更新 更多