Skip to content

Commit

Permalink
[SPARK-21254][WEBUI] History UI performance fixes
Browse files Browse the repository at this point in the history
## This is a backport of PR apache#18783 to the latest released branch 2.2.

## What changes were proposed in this pull request?

As described in JIRA ticket, History page is taking ~1min to load for cases when amount of jobs is 10k+.
Most of the time is currently being spent on DOM manipulations and all additional costs implied by this (browser repaints and reflows).
PR's goal is not to change any behavior but to optimize time of History UI rendering:

1. The most costly operation is setting `innerHTML` for `duration` column within a loop, which is [extremely unperformant](https://jsperf.com/jquery-append-vs-html-list-performance/24). [Refactoring ](criteo-forks@b7e56ee) this helped to get page load time **down to 10-15s**

2. Second big gain bringing page load time **down to 4s** was [was achieved](criteo-forks@3630ca2) by detaching table's DOM before parsing it with DataTables jQuery plugin.

3. Another chunk of improvements ([1]criteo-forks@aeeeeb5), [2](criteo-forks@e25be9a), [3](criteo-forks@9169707)) was focused on removing unnecessary DOM manipulations that in total contributed ~250ms to page load time.

## How was this patch tested?

Tested by existing Selenium tests in `org.apache.spark.deploy.history.HistoryServerSuite`.

Changes were also tested on Criteo's spark-2.1 fork with 20k+ number of rows in the table, reducing load time to 4s.

Author: Dmitry Parfenchik <d.parfenchik@criteo.com>

Closes apache#18860 from 2ooom/history-ui-perf-fix-2.2.
  • Loading branch information
2ooom authored and MatthewRBruce committed Jul 31, 2018
1 parent eea0f0f commit 1e2d62f
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,25 @@
App Name
</span>
</th>
<th class="attemptIDSpan">
{{#hasMultipleAttempts}}
<th>
<span data-toggle="tooltip" data-placement="top" title="The attempt ID of this application since one application might be launched several times">
Attempt ID
</span>
</th>
{{/hasMultipleAttempts}}
<th>
<span data-toggle="tooltip" data-placement="top" title="Started time of this application.">
Started
</span>
</th>
<th class="completedColumn">
{{#showCompletedColumn}}
<th>
<span data-toggle="tooltip" data-placement="top" title="The completed time of this application.">
Completed
</span>
</th>
{{/showCompletedColumn}}
<th>
<span data-toggle="tooltip" data-placement="top" title="The duration time of this application.">
Duration
Expand All @@ -68,13 +72,17 @@
<tbody>
{{#applications}}
<tr>
<td class="rowGroupColumn"><span title="{{id}}"><a href="{{uiroot}}/history/{{id}}/{{num}}/jobs/">{{id}}</a></span></td>
<td class="rowGroupColumn">{{name}}</td>
<td {{#hasMultipleAttempts}}style="background-color:#fff"{{/hasMultipleAttempts}}><span title="{{id}}"><a href="{{uiroot}}/history/{{id}}/{{num}}/jobs/">{{id}}</a></span></td>
<td {{#hasMultipleAttempts}}style="background-color:#fff"{{/hasMultipleAttempts}}>{{name}}</td>
{{#attempts}}
<td class="attemptIDSpan"><a href="{{uiroot}}/history/{{id}}/{{attemptId}}/jobs/">{{attemptId}}</a></td>
{{#hasMultipleAttempts}}
<td><a href="{{uiroot}}/history/{{id}}/{{attemptId}}/jobs/">{{attemptId}}</a></td>
{{/hasMultipleAttempts}}
<td>{{startTime}}</td>
<td class="completedColumn">{{endTime}}</td>
<td><span title="{{duration}}" class="durationClass">{{duration}}</span></td>
{{#showCompletedColumn}}
<td>{{endTime}}</td>
{{/showCompletedColumn}}
<td><span title="{{durationMillisec}}">{{duration}}</span></td>
<td>{{sparkUser}}</td>
<td>{{lastUpdated}}</td>
<td><a href="{{log}}" class="btn btn-info btn-mini">Download</a></td>
Expand Down
112 changes: 56 additions & 56 deletions core/src/main/resources/org/apache/spark/ui/static/historypage.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ function getParameterByName(name, searchString) {
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}

function removeColumnByName(columns, columnName) {
return columns.filter(function(col) {return col.name != columnName})
}

function getColumnIndex(columns, columnName) {
for(var i = 0; i < columns.length; i++) {
if (columns[i].name == columnName)
return i;
}
return -1;
}

jQuery.extend( jQuery.fn.dataTableExt.oSort, {
"title-numeric-pre": function ( a ) {
var x = a.match(/title="*(-?[0-9\.]+)/)[1];
Expand Down Expand Up @@ -122,79 +134,67 @@ $(document).ready(function() {
attempt["lastUpdated"] = formatDate(attempt["lastUpdated"]);
attempt["log"] = uiRoot + "/api/v1/applications/" + id + "/" +
(attempt.hasOwnProperty("attemptId") ? attempt["attemptId"] + "/" : "") + "logs";

attempt["durationMillisec"] = attempt["duration"];
attempt["duration"] = formatDuration(attempt["duration"]);
var app_clone = {"id" : id, "name" : name, "num" : num, "attempts" : [attempt]};
array.push(app_clone);
}
}
if(array.length < 20) {
$.fn.dataTable.defaults.paging = false;
}

var data = {
"uiroot": uiRoot,
"applications": array
}
"applications": array,
"hasMultipleAttempts": hasMultipleAttempts,
"showCompletedColumn": !requestedIncomplete,
}

$.get("static/historypage-template.html", function(template) {
historySummary.append(Mustache.render($(template).filter("#history-summary-template").html(),data));
var selector = "#history-summary-table";
var sibling = historySummary.prev();
historySummary.detach();
var apps = $(Mustache.render($(template).filter("#history-summary-template").html(),data));
var attemptIdColumnName = 'attemptId';
var startedColumnName = 'started';
var defaultSortColumn = completedColumnName = 'completed';
var durationColumnName = 'duration';
var conf = {
"columns": [
{name: 'first', type: "appid-numeric"},
{name: 'second'},
{name: 'third'},
{name: 'fourth'},
{name: 'fifth'},
{name: 'sixth', type: "title-numeric"},
{name: 'seventh'},
{name: 'eighth'},
{name: 'ninth'},
],
"columnDefs": [
{"searchable": false, "targets": [5]}
],
"autoWidth": false,
"order": [[ 4, "desc" ]]
};

var rowGroupConf = {
"rowsGroup": [
'first:name',
'second:name'
],
"columns": [
{name: 'appId', type: "appid-numeric"},
{name: 'appName'},
{name: attemptIdColumnName},
{name: startedColumnName},
{name: completedColumnName},
{name: durationColumnName, type: "title-numeric"},
{name: 'user'},
{name: 'lastUpdated'},
{name: 'eventLog'},
],
"autoWidth": false,
};

if (hasMultipleAttempts) {
jQuery.extend(conf, rowGroupConf);
var rowGroupCells = document.getElementsByClassName("rowGroupColumn");
for (i = 0; i < rowGroupCells.length; i++) {
rowGroupCells[i].style='background-color: #ffffff';
}
}

if (!hasMultipleAttempts) {
var attemptIDCells = document.getElementsByClassName("attemptIDSpan");
for (i = 0; i < attemptIDCells.length; i++) {
attemptIDCells[i].style.display='none';
}
conf.rowsGroup = [
'appId:name',
'appName:name'
];
} else {
conf.columns = removeColumnByName(conf.columns, attemptIdColumnName);
}

var defaultSortColumn = completedColumnName;
if (requestedIncomplete) {
var completedCells = document.getElementsByClassName("completedColumn");
for (i = 0; i < completedCells.length; i++) {
completedCells[i].style.display='none';
}
defaultSortColumn = startedColumnName;
conf.columns = removeColumnByName(conf.columns, completedColumnName);
}

var durationCells = document.getElementsByClassName("durationClass");
for (i = 0; i < durationCells.length; i++) {
var timeInMilliseconds = parseInt(durationCells[i].title);
durationCells[i].innerHTML = formatDuration(timeInMilliseconds);
}

if ($(selector.concat(" tr")).length < 20) {
$.extend(conf, {paging: false});
}

$(selector).DataTable(conf);
conf.order = [[ getColumnIndex(conf.columns, defaultSortColumn), "desc" ]];
conf.columnDefs = [
{"searchable": false, "targets": [getColumnIndex(conf.columns, durationColumnName)]}
];
historySummary.append(apps);
apps.DataTable(conf);
sibling.after(historySummary);
$('#history-summary [data-toggle="tooltip"]').tooltip();
});
});
Expand Down

0 comments on commit 1e2d62f

Please sign in to comment.