Memory leak in Kibana 7.10

We use KIbana dashboard but the older version 7.10. On discover plugin when we keep manipulating filters, after certain time browser crashes with out of memory error.

I did my investigation and found out that cell.html file when commented out stops the memory leak. This cell is rendered for each column in each row. How to destroy it properly so OOM issue can be resolved.

cell.html:

<%
var attributes = '';
if (timefield) {
  attributes='class="eui-textNoWrap" width="1%"';
} else if (sourcefield) {
  attributes='class="eui-textBreakAll eui-textBreakWord"';
} else {
  attributes='class="kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord"';
}
%>
<td style="cursor: pointer;" <%=attributes %> data-test-subj="docTableField">
  <%= formatted %>
  <span class="kbnDocTableCell__filter">
    <% if (filterable) { %>
      <button
        ng-click="inlineFilter($event, '+')"
        class="kbnDocTableRowFilterButton"
        data-column="<%- column %>"
        tooltip-append-to-body="1"
        data-test-subj="docTableCellFilter"
        tooltip="{{ ::'discover.docTable.tableRow.filterForValueButtonTooltip' | i18n: {defaultMessage: 'Filter for value'} }}"
        tooltip-placement="bottom"
        aria-label="{{ ::'discover.docTable.tableRow.filterForValueButtonAriaLabel' | i18n: {defaultMessage: 'Filter for value'} }}"
      ><icon type="'plusInCircle'" size="'s'" color="'primary'"></icon></button>
      <button
        ng-click="inlineFilter($event, '-')"
        class="kbnDocTableRowFilterButton"
        data-column="<%- column %>"
        data-test-subj="docTableCellFilterNegate"
        tooltip="{{ ::'discover.docTable.tableRow.filterOutValueButtonTooltip' | i18n: {defaultMessage: 'Filter out value'} }}"
        aria-label="{{ ::'discover.docTable.tableRow.filterOutValueButtonAriaLabel' | i18n: {defaultMessage: 'Filter out value'} }}"
        tooltip-append-to-body="1"
        tooltip-placement="bottom"
      ><icon type="'minusInCircle'" size="'s'" color="'primary'"></icon></button>
      <% } %>
    <button
        id="btnPlayVoice"
        ng-click="openContent($event)"
        class="fa kbnDocTableRowFilterButton"
        tooltip="{{viewPlay.label}}"
        tooltip-append-to-body="1"
        ng-show="viewPlay"
        ng-class="viewPlay.class"
        ng-disabled=disabledViewPlay
      ></button>
      <button id="btnViewRemarks" ng-click="openRemarksContent($event)" class="kbnDocTableRowFilterButton"
        tooltip="View Remarks" tooltip-append-to-body="1">
        <icon type="'editorComment'" size="'s'" color="'primary'"></icon>
      </button>
      <button id="btnViewTranslator" ng-click="openTranslatorContent($event)" class="kbnDocTableRowFilterButton"
        tooltip="Text Translation" tooltip-append-to-body="1" ng-show="translated_text_enabled">
        <icon type="'document'" size="'s'" color="'primary'"></icon>
      </button>
      <button id="btnUnreadRecord" ng-if="row._source.acknowledged_by" ng-click="unreadRecord($event)" class="kbnDocTableRowFilterButton"
        tooltip="Mark As Unread" tooltip-append-to-body="1">
        <icon type="'returnKey'" size="'s'" color="'primary'"></icon>
      </button>
      <button id="btnViewRemarks" ng-click="openCaptureFilter($event)" class="kbnDocTableRowFilterButton"
        tooltip="Capture Filters" tooltip-append-to-body="1" ng-show="data_grid_capture_enabled">
        <icon type="'bullseye'" size="'s'" color="'primary'"></icon>
      </button>
      <button id="btnLID" ng-click="LID($event)" class="kbnDocTableRowFilterButton"
        tooltip="LID" tooltip-append-to-body="1" ng-show="lid_manual_testing_enabled">
        <!-- <icon type="'visText'" size="'s'" color="'primary'"></icon> -->
        <span class="kuiIcon fa-buysellads"></span>
      </button>
      <button id="btnSID" ng-click="SID($event)" class="fas kbnDocTableRowFilterButton"
        tooltip="SID" tooltip-append-to-body="1" ng-show="sid_manual_testing_enabled">
        <span class="kuiIcon fa-microphone"></span>
      </button>
      <div>
        <button id="processingCaption" class="fas kbnDocTableRowFilterButton" ng-show='disabledViewPlay' ng-disabled=true>
          Processing...
        </button>
      </div>  
   <% if(column =='payload.http_get'||column=='payload.http_refer'||column =='payload.attribute_value') { %>
        <button id="btnViewPIISPDI" ng-click="openPIISPDIContent($event)" class="kbnDocTableRowFilterButton"
          tooltip="View PII Information" tooltip-append-to-body="1" data-column="<%- column %>">
          <icon type="'dataVisualizer'" size="'s'" color="'primary'"></icon>
        </button>
      <% } %>
  </span>
</td>

Function in table_row.ts which is responsible for handling cell.html:

function createSummaryRow(row: any) {
        const indexPattern = $scope.indexPattern;
        $scope.flattenedRow = indexPattern.flattenHit(row);

        $scope.reconFilename = $scope.flattenedRow[
          getServices().configVars.metafields.recon_file_name
        ]
          ? $scope.flattenedRow[getServices().configVars.metafields.recon_file_name][0]
          : null;
        $scope.fileMimeType = mime.lookup($scope.reconFilename);
        $scope.disabledViewPlay = false;
        $scope.viewPlay = false;
        $scope.sid_manual_testing_enabled = false;
        $scope.lid_manual_testing_enabled = false;
        $scope.translated_text_enabled = false;
        if (
          getServices().configVars.config.menu_enabled.text_translation_enabled &&
          $scope.flattenedRow.text
        ) {
          $scope.translated_text_enabled = true;
        }
        if ($scope.reconFilename) {
          if (/audio/.test($scope.fileMimeType) || /video/.test($scope.fileMimeType)) {
            $scope.viewPlay = { class: 'fa-play', label: 'Play' };

            if (getServices().configVars.config.lid_sid.sid_manual_testing_enabled) {
              $scope.sid_manual_testing_enabled = true;
            }
            if (getServices().configVars.config.lid_sid.lid_manual_testing_enabled) {
              $scope.lid_manual_testing_enabled = true;
            }
          } else {
            $scope.viewPlay = { class: 'fa-eye', label: 'View' };
          }
        }

        // We just create a string here because its faster.
        // const newHtmls = [openRowHtml, selectRowHtml];
        // const newHtmls = [openRowHtml, selectRowHtml, $scope.isProcessedByUser === 1 ? processedByUserHtml : '<td></td>'];
        // const newHtmls = [openRowHtml];
        const newHtmls = getServices().configVars.config.menu_enabled.datagrid_flyout_enabled
          ? [selectRowHtml, $scope.isProcessedByUser === 1 ? processedByUserHtml : '<td></td>']
          : [openRowHtml, selectRowHtml, $scope.isProcessedByUser === 1 ? processedByUserHtml : '<td></td>'];

        const mapping = indexPattern.fields.getByName;
        const hideTimeColumn = getServices().uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false);
        if (indexPattern.timeFieldName && !hideTimeColumn) {
          newHtmls.push(
            cellTemplate({
              timefield: true,
              formatted: _displayField(row, indexPattern.timeFieldName),
              filterable: mapping(indexPattern.timeFieldName).filterable && $scope.filter,
              column: indexPattern.timeFieldName,
            })
          );
        }

        $scope.columns.forEach(function (column: any) {
          const isFilterable = mapping(column) && mapping(column).filterable && $scope.filter;
        
          // Written By: Satyam Shukla
          // Logic For: Showing Country Flag Beside Country Name, If Country Code Exists
          let formattedValue = _displayField(row, column, true);
          let flagClassName = '';
        
          // Check if the column is 'network.dst_geo_ip.country_name'
          if (column === 'network.dst_geo_ip.country_name' && row._source && row._source.network && row._source.network.dst_geo_ip) {
            const countryCode = row._source.network.dst_geo_ip.country_code;
        
            // Determine the CSS class name for the flag based on the country code
            if (countryCode && isValidCountryCode(countryCode.toUpperCase())) {
              flagClassName = `flag flag-${countryCode.toLowerCase()}`;
            }
          }
        
          // Check if the column is 'network.src_geo_ip.country_name'
          if (column === 'network.src_geo_ip.country_name' && row._source && row._source.network && row._source.network.src_geo_ip) {
            const countryCode = row._source.network.src_geo_ip.country_code;
        
            // Determine the CSS class name for the flag based on the country code
            if (countryCode && isValidCountryCode(countryCode.toUpperCase())) {
              flagClassName = `flag flag-${countryCode.toLowerCase()}`;
              // console.log("SRC Country Code: ", countryCode, "Flag Class: ", flagClassName)
            }
          }

          // Prepend the flag HTML to the formatted value
          if (flagClassName) {
            formattedValue = `<span class="${flagClassName}"></span> <span style="position: relative; top: 4px;">${formattedValue}</span>`;
          }
        
          newHtmls.push(
            cellTemplate({
              timefield: false,
              sourcefield: column === '_source',
              formatted: formattedValue,
              filterable: isFilterable,
              column,
            })
          );
        });

        let $cells = $el.children();
        newHtmls.forEach(function (html, i) {
          const $cell = $cells.eq(i);
          if ($cell.data('discover:html') === html) return;

          const reuse = find($cells.slice(i + 1), function (cell: any) {
            return $.data(cell, 'discover:html') === html;
          });

          const $target = reuse ? $(reuse).detach() : $(html);
          $target.data('discover:html', html);
          const $before = $cells.eq(i - 1);
          if ($before.length) {
            $before.after($target);
          } else {
            $el.append($target);
          }

          // rebuild cells since we modified the children
          $cells = $el.children();

          if (!reuse) {
            $toggleScope = $scope.$new();
            $compile($target)($toggleScope);
          }
        });

        if ($scope.open) {
          $detailsScope.row = row;
        }

        // trim off cells that were not used rest of the cells
        $cells.filter(':gt(' + (newHtmls.length - 1) + ')').remove();
        dispatchRenderComplete($el[0]);
      }

please help.

Hi @satyam,

We suggest upgrading. This code is removed and Discover does not use Angular any more.

Hi @jughosta
Let me know what is the Kibana version included "cell.html".