title: Huge node leak in web app under Chrome - developer tools not reporting cause
body: <p>I have a rather complicated web-application that is leaking nodes at an incredibly fast rate. Using windows task manager, I can see the amount of memory used by the chrome process increasing by around 3MB a second. </p>
  
  <p>My initial approach was to disable parts of the application to isolate the area causing the problem. After a little trial and error discovered the cause were some table updates in the document. The app requests data from the server every second to update several tables of data. Each table is updated in the same way. All rows are removed from the table and new rows containing the updated data are inserted. I could not see a problem when inspecting the common code that does this.</p>
  
  <p>To solve this I obviously turned to the Chrome Developer Tools. The problem I have is that the tools are giving me conflicting information.</p>
  
  <ol>
  <li>The Timeline tool shows that the number of nodes is increasing by around 28000 each second. Forced garbage collection does not reduce this back to its original level.</li>
  <li>The Timeline tool shows the JS Heap size fluctuating over time. A forced garbage collection returns the heap to its original size (plus or minus a few 100K)</li>
  <li>Using the "three-snapshot" technique the Profiler - Heap Snapshot tool shows no HTML or Text nodes created between snapshot 1 and snapshot 2 that are present in snapshot 3.</li>
  <li>Comparing the snapshots shows the creation and deletion of many HTMLTableRowElement, HTMLTableCellElement, HTMLInputElement and Text nodes. No increase in node count is reported.</li>
  <li>The Profiler - Heap Allocation tool verifies the results of the Heap Snapshot tool. No leaks of any Node type are reported at any point.</li>
  <li>The Heap tools show small increases in the (compiled code), (array) and (system) types.</li>
  <li>The Heap Profile tools report heap sizes of around 12MB for my "raw" javascript version of the app, and around 7MB for my closure compiler - compiled version of the application. These values do not grow much over time.</li>
  </ol>
  
  <p>This leaves me a little confused. There is obviously a memory leak. It is reported by Windows Task Manager, and by the Timeline tool as a node leak, but the Heap profiling tools and JS Heap Timeline do not show the issue.</p>
  
  <p>As far as I can tell, the HTMLTableRowElements are referenced only in two places, in the document and in an object used for lookup by value. The object is always cleared when the table is cleared. I can "cure" the issue by changing my code to create all the nodes, but never inserting them into the document, just referencing them in the object. Obviously this is not a fix because the users cannot see the data.</p>
  
  <p>After 2 days of testing and investigation I am now at a loss on how to proceed. The plot thickens if you throw in IE and Firefox. These browsers do not appear to have the same memory/node leak. I also believe the problem did not previously exist with Chrome. Unfortunately there does not seem to be a  way to go back to previous versions of chrome to see if it is a bug in Chrome. </p>
  
  <p>Does anyone have any advice on this. Am I missing some thing, or misinterpreting the output of the developer tools? Is there a way to go back to previous chrome versions? Does this sound like a bug in Chrome? All comments welcome.</p>
  
  <p>This is the code used to insert items using Google's Closure API:</p>
  
  <pre><code>/**
   * 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 &lt; 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 &lt; 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 &gt; 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);
      }
  };
  </code></pre>