Skip to content
Snippets Groups Projects
Commit e3967dc5 authored by Dmitry Parfenchik's avatar Dmitry Parfenchik Committed by Sean Owen
Browse files

[SPARK-21254][WEBUI] History UI performance fixes

## 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 ](https://github.com/criteo-forks/spark/commit/114943b21a730092aa3249b7a905b240bd46e531) 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](https://github.com/criteo-forks/spark/commit/f35fdcd5f129339fce75996e9242c88085a9b8ab) by detaching table's DOM before parsing it with DataTables jQuery plugin.

3. Another chunk of improvements ([1](https://github.com/criteo-forks/spark/commit/332b398db7eb3052484d436919185cb0b62b2385), [2](https://github.com/criteo-forks/spark/commit/0af596a547e3a1f2b594a83cbda1f6ef559de86b), [3](https://github.com/criteo-forks/spark/commit/235f164178a09e22306f05090ee1ff5f314a6710)) 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>
Author: Anna Savarin <a.savarin@criteo.com>

Closes #18783 from 2ooom/history-ui-perf-fix-upstream-master.
parent dd72b10a
No related branches found
No related tags found
No related merge requests found
......@@ -29,26 +29,30 @@
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">
{{#showCompletedColumns}}
<th>
<span data-toggle="tooltip" data-placement="top" title="The completed time of this application.">
Completed
</span>
</th>
<th class="durationColumn">
<th>
<span data-toggle="tooltip" data-placement="top" title="The duration time of this application.">
Duration
</span>
</th>
{{/showCompletedColumns}}
<th>
<span data-toggle="tooltip" data-placement="top" title="The Spark user of this application">
Spark User
......@@ -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 class="durationColumn"><span title="{{duration}}" class="durationClass">{{duration}}</span></td>
{{#showCompletedColumns}}
<td>{{endTime}}</td>
<td><span title="{{durationMillisec}}">{{duration}}</span></td>
{{/showCompletedColumns}}
<td>{{sparkUser}}</td>
<td>{{lastUpdated}}</td>
<td><a href="{{log}}" class="btn btn-info btn-mini">Download</a></td>
......
......@@ -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];
......@@ -122,84 +134,68 @@ $(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,
"showCompletedColumns": !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';
}
}
if (requestedIncomplete) {
var completedCells = document.getElementsByClassName("completedColumn");
for (i = 0; i < completedCells.length; i++) {
completedCells[i].style.display='none';
}
var durationCells = document.getElementsByClassName("durationColumn");
for (i = 0; i < durationCells.length; i++) {
durationCells[i].style.display='none';
}
conf.rowsGroup = [
'appId:name',
'appName:name'
];
} else {
var durationCells = document.getElementsByClassName("durationClass");
for (i = 0; i < durationCells.length; i++) {
var timeInMilliseconds = parseInt(durationCells[i].title);
durationCells[i].innerHTML = formatDuration(timeInMilliseconds);
}
conf.columns = removeColumnByName(conf.columns, attemptIdColumnName);
}
if ($(selector.concat(" tr")).length < 20) {
$.extend(conf, {paging: false});
var defaultSortColumn = completedColumnName;
if (requestedIncomplete) {
defaultSortColumn = startedColumnName;
conf.columns = removeColumnByName(conf.columns, completedColumnName);
conf.columns = removeColumnByName(conf.columns, durationColumnName);
}
$(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();
});
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment