Skip to content

Commit e3967dc

Browse files
2ooomsrowen
authored andcommitted
[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 ](criteo-forks@114943b) 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@f35fdcd) by detaching table's DOM before parsing it with DataTables jQuery plugin. 3. Another chunk of improvements ([1](criteo-forks@332b398), [2](criteo-forks@0af596a), [3](criteo-forks@235f164)) 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.
1 parent dd72b10 commit e3967dc

File tree

2 files changed

+73
-69
lines changed

2 files changed

+73
-69
lines changed

core/src/main/resources/org/apache/spark/ui/static/historypage-template.html

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,30 @@
2929
App Name
3030
</span>
3131
</th>
32-
<th class="attemptIDSpan">
32+
{{#hasMultipleAttempts}}
33+
<th>
3334
<span data-toggle="tooltip" data-placement="top" title="The attempt ID of this application since one application might be launched several times">
3435
Attempt ID
3536
</span>
3637
</th>
38+
{{/hasMultipleAttempts}}
3739
<th>
3840
<span data-toggle="tooltip" data-placement="top" title="Started time of this application.">
3941
Started
4042
</span>
4143
</th>
42-
<th class="completedColumn">
44+
{{#showCompletedColumns}}
45+
<th>
4346
<span data-toggle="tooltip" data-placement="top" title="The completed time of this application.">
4447
Completed
4548
</span>
4649
</th>
47-
<th class="durationColumn">
50+
<th>
4851
<span data-toggle="tooltip" data-placement="top" title="The duration time of this application.">
4952
Duration
5053
</span>
5154
</th>
55+
{{/showCompletedColumns}}
5256
<th>
5357
<span data-toggle="tooltip" data-placement="top" title="The Spark user of this application">
5458
Spark User
@@ -68,13 +72,17 @@
6872
<tbody>
6973
{{#applications}}
7074
<tr>
71-
<td class="rowGroupColumn"><span title="{{id}}"><a href="{{uiroot}}/history/{{id}}/{{num}}/jobs/">{{id}}</a></span></td>
72-
<td class="rowGroupColumn">{{name}}</td>
75+
<td {{#hasMultipleAttempts}}style="background-color:#fff"{{/hasMultipleAttempts}}><span title="{{id}}"><a href="{{uiroot}}/history/{{id}}/{{num}}/jobs/">{{id}}</a></span></td>
76+
<td {{#hasMultipleAttempts}}style="background-color:#fff"{{/hasMultipleAttempts}}>{{name}}</td>
7377
{{#attempts}}
74-
<td class="attemptIDSpan"><a href="{{uiroot}}/history/{{id}}/{{attemptId}}/jobs/">{{attemptId}}</a></td>
78+
{{#hasMultipleAttempts}}
79+
<td><a href="{{uiroot}}/history/{{id}}/{{attemptId}}/jobs/">{{attemptId}}</a></td>
80+
{{/hasMultipleAttempts}}
7581
<td>{{startTime}}</td>
76-
<td class="completedColumn">{{endTime}}</td>
77-
<td class="durationColumn"><span title="{{duration}}" class="durationClass">{{duration}}</span></td>
82+
{{#showCompletedColumns}}
83+
<td>{{endTime}}</td>
84+
<td><span title="{{durationMillisec}}">{{duration}}</span></td>
85+
{{/showCompletedColumns}}
7886
<td>{{sparkUser}}</td>
7987
<td>{{lastUpdated}}</td>
8088
<td><a href="{{log}}" class="btn btn-info btn-mini">Download</a></td>

core/src/main/resources/org/apache/spark/ui/static/historypage.js

Lines changed: 57 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ function getParameterByName(name, searchString) {
4848
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
4949
}
5050

51+
function removeColumnByName(columns, columnName) {
52+
return columns.filter(function(col) {return col.name != columnName})
53+
}
54+
55+
function getColumnIndex(columns, columnName) {
56+
for(var i = 0; i < columns.length; i++) {
57+
if (columns[i].name == columnName)
58+
return i;
59+
}
60+
return -1;
61+
}
62+
5163
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
5264
"title-numeric-pre": function ( a ) {
5365
var x = a.match(/title="*(-?[0-9\.]+)/)[1];
@@ -122,84 +134,68 @@ $(document).ready(function() {
122134
attempt["lastUpdated"] = formatDate(attempt["lastUpdated"]);
123135
attempt["log"] = uiRoot + "/api/v1/applications/" + id + "/" +
124136
(attempt.hasOwnProperty("attemptId") ? attempt["attemptId"] + "/" : "") + "logs";
125-
137+
attempt["durationMillisec"] = attempt["duration"];
138+
attempt["duration"] = formatDuration(attempt["duration"]);
126139
var app_clone = {"id" : id, "name" : name, "num" : num, "attempts" : [attempt]};
127140
array.push(app_clone);
128141
}
129142
}
143+
if(array.length < 20) {
144+
$.fn.dataTable.defaults.paging = false;
145+
}
130146

131147
var data = {
132148
"uiroot": uiRoot,
133-
"applications": array
134-
}
149+
"applications": array,
150+
"hasMultipleAttempts": hasMultipleAttempts,
151+
"showCompletedColumns": !requestedIncomplete,
152+
}
135153

136154
$.get("static/historypage-template.html", function(template) {
137-
historySummary.append(Mustache.render($(template).filter("#history-summary-template").html(),data));
138-
var selector = "#history-summary-table";
155+
var sibling = historySummary.prev();
156+
historySummary.detach();
157+
var apps = $(Mustache.render($(template).filter("#history-summary-template").html(),data));
158+
var attemptIdColumnName = 'attemptId';
159+
var startedColumnName = 'started';
160+
var defaultSortColumn = completedColumnName = 'completed';
161+
var durationColumnName = 'duration';
139162
var conf = {
140-
"columns": [
141-
{name: 'first', type: "appid-numeric"},
142-
{name: 'second'},
143-
{name: 'third'},
144-
{name: 'fourth'},
145-
{name: 'fifth'},
146-
{name: 'sixth', type: "title-numeric"},
147-
{name: 'seventh'},
148-
{name: 'eighth'},
149-
{name: 'ninth'},
150-
],
151-
"columnDefs": [
152-
{"searchable": false, "targets": [5]}
153-
],
154-
"autoWidth": false,
155-
"order": [[ 4, "desc" ]]
156-
};
157-
158-
var rowGroupConf = {
159-
"rowsGroup": [
160-
'first:name',
161-
'second:name'
162-
],
163+
"columns": [
164+
{name: 'appId', type: "appid-numeric"},
165+
{name: 'appName'},
166+
{name: attemptIdColumnName},
167+
{name: startedColumnName},
168+
{name: completedColumnName},
169+
{name: durationColumnName, type: "title-numeric"},
170+
{name: 'user'},
171+
{name: 'lastUpdated'},
172+
{name: 'eventLog'},
173+
],
174+
"autoWidth": false,
163175
};
164176

165177
if (hasMultipleAttempts) {
166-
jQuery.extend(conf, rowGroupConf);
167-
var rowGroupCells = document.getElementsByClassName("rowGroupColumn");
168-
for (i = 0; i < rowGroupCells.length; i++) {
169-
rowGroupCells[i].style='background-color: #ffffff';
170-
}
171-
}
172-
173-
if (!hasMultipleAttempts) {
174-
var attemptIDCells = document.getElementsByClassName("attemptIDSpan");
175-
for (i = 0; i < attemptIDCells.length; i++) {
176-
attemptIDCells[i].style.display='none';
177-
}
178-
}
179-
180-
if (requestedIncomplete) {
181-
var completedCells = document.getElementsByClassName("completedColumn");
182-
for (i = 0; i < completedCells.length; i++) {
183-
completedCells[i].style.display='none';
184-
}
185-
186-
var durationCells = document.getElementsByClassName("durationColumn");
187-
for (i = 0; i < durationCells.length; i++) {
188-
durationCells[i].style.display='none';
189-
}
178+
conf.rowsGroup = [
179+
'appId:name',
180+
'appName:name'
181+
];
190182
} else {
191-
var durationCells = document.getElementsByClassName("durationClass");
192-
for (i = 0; i < durationCells.length; i++) {
193-
var timeInMilliseconds = parseInt(durationCells[i].title);
194-
durationCells[i].innerHTML = formatDuration(timeInMilliseconds);
195-
}
183+
conf.columns = removeColumnByName(conf.columns, attemptIdColumnName);
196184
}
197185

198-
if ($(selector.concat(" tr")).length < 20) {
199-
$.extend(conf, {paging: false});
186+
var defaultSortColumn = completedColumnName;
187+
if (requestedIncomplete) {
188+
defaultSortColumn = startedColumnName;
189+
conf.columns = removeColumnByName(conf.columns, completedColumnName);
190+
conf.columns = removeColumnByName(conf.columns, durationColumnName);
200191
}
201-
202-
$(selector).DataTable(conf);
192+
conf.order = [[ getColumnIndex(conf.columns, defaultSortColumn), "desc" ]];
193+
conf.columnDefs = [
194+
{"searchable": false, "targets": [getColumnIndex(conf.columns, durationColumnName)]}
195+
];
196+
historySummary.append(apps);
197+
apps.DataTable(conf);
198+
sibling.after(historySummary);
203199
$('#history-summary [data-toggle="tooltip"]').tooltip();
204200
});
205201
});

0 commit comments

Comments
 (0)