Skip to content

Commit 87f7f6a

Browse files
committed
Improve UI performance when displaying large logs
1 parent 4618b0d commit 87f7f6a

File tree

15 files changed

+475
-485
lines changed

15 files changed

+475
-485
lines changed

assets/app/scripts/app.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,11 @@ angular
119119
})
120120
.when('/project/:project/browse/builds/:buildconfig/:build', {
121121
templateUrl: function(params) {
122-
return params.view ?
123-
'views/logs/'+params.view+'_log.html' :
124-
'views/browse/build.html';
122+
if (params.view === 'chromeless') {
123+
return 'views/logs/chromeless-build-log.html';
124+
}
125+
126+
return 'views/browse/build.html';
125127
},
126128
controller: 'BuildController'
127129
})
@@ -157,9 +159,11 @@ angular
157159
})
158160
.when('/project/:project/browse/pods/:pod', {
159161
templateUrl: function(params) {
160-
return params.view ?
161-
'views/logs/'+params.view+'_log.html' :
162-
'views/browse/pod.html';
162+
if (params.view === 'chromeless') {
163+
return 'views/logs/chromeless-pod-log.html';
164+
}
165+
166+
return 'views/browse/pod.html';
163167
},
164168
controller: 'PodController'
165169
})

assets/app/scripts/controllers/build.js

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,15 @@ angular.module('openshiftConsole')
4141
var watches = [];
4242

4343
project.get($routeParams.project).then(function(resp) {
44-
angular.extend($scope, {
44+
var context = {
4545
project: resp[0],
4646
projectPromise: resp[1].projectPromise
47-
});
47+
};
48+
angular.extend($scope, context);
49+
// FIXME: DataService.createStream() requires a scope with a
50+
// projectPromise rather than just a namespace, so we have to pass the
51+
// context into the log-viewer directive.
52+
$scope.logContext = context;
4853
DataService.get("builds", $routeParams.build, $scope).then(
4954
// success
5055
function(build) {
@@ -109,49 +114,6 @@ angular.module('openshiftConsole')
109114
}
110115
}
111116
}));
112-
113-
114-
var runLogs = function() {
115-
angular.extend($scope, {
116-
logs: [],
117-
logsLoading: true,
118-
canShowDownload: false,
119-
canInitAgain: false
120-
});
121-
122-
var streamer = DataService.createStream('builds/log',$routeParams.build, $scope);
123-
streamer.onMessage(function(msg) {
124-
$scope.$apply(function() {
125-
$scope.logs.push({text: msg});
126-
$scope.canShowDownload = true;
127-
});
128-
});
129-
streamer.onClose(function() {
130-
$scope.$apply(function() {
131-
$scope.logsLoading = false;
132-
});
133-
});
134-
streamer.onError(function() {
135-
$scope.$apply(function() {
136-
angular.extend($scope, {
137-
logsLoading: false,
138-
logError: true
139-
});
140-
});
141-
});
142-
143-
streamer.start();
144-
$scope.$on('$destroy', function() {
145-
streamer.stop();
146-
});
147-
};
148-
149-
angular.extend($scope, {
150-
initLogs: _.once(runLogs),
151-
runLogs: runLogs
152-
});
153-
154-
155117
});
156118

157119
$scope.startBuild = function(buildConfigName) {

assets/app/scripts/controllers/pod.js

Lines changed: 9 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ angular.module('openshiftConsole')
1616
$scope.alerts = {};
1717
$scope.renderOptions = $scope.renderOptions || {};
1818
$scope.renderOptions.hideFilterWidget = true;
19+
$scope.logOptions = {};
1920
$scope.terminalTabWasSelected = false;
2021
$scope.breadcrumbs = [
2122
{
@@ -41,15 +42,21 @@ angular.module('openshiftConsole')
4142
});
4243

4344
project.get($routeParams.project).then(function(resp) {
44-
angular.extend($scope, {
45+
var context = {
4546
project: resp[0],
4647
projectPromise: resp[1].projectPromise
47-
});
48+
};
49+
angular.extend($scope, context);
50+
// FIXME: DataService.createStream() requires a scope with a
51+
// projectPromise rather than just a namespace, so we have to pass the
52+
// context into the log-viewer directive.
53+
$scope.logContext = context;
4854
DataService.get("pods", $routeParams.pod, $scope).then(
4955
// success
5056
function(pod) {
5157
$scope.loaded = true;
5258
$scope.pod = pod;
59+
$scope.logOptions.container = $routeParams.container || pod.spec.containers[0].name;
5360
var pods = {};
5461
pods[pod.metadata.name] = pod;
5562
ImageStreamResolver.fetchReferencedImageStreamImages(pods, $scope.imagesByDockerReference, $scope.imageStreamImageRefByDockerReference, $scope);
@@ -88,63 +95,6 @@ angular.module('openshiftConsole')
8895
$scope.builds = builds.by("metadata.name");
8996
Logger.log("builds (subscribe)", $scope.builds);
9097
}));
91-
92-
// maintaining one streamer reference & ensuring its closed before we open a new,
93-
// since the user can (potentially) swap between multiple containers
94-
var streamer;
95-
var runLogs = function() {
96-
angular.extend($scope, {
97-
logs: [],
98-
logsLoading: true,
99-
canShowDownload: false,
100-
canInitAgain: false,
101-
options: {
102-
container: $scope.pod.spec.containers[0].name
103-
}
104-
});
105-
106-
// TODO: clean up service / $scope stuff...
107-
streamer = DataService.createStream('pods/log',$routeParams.pod, $scope, $scope.options);
108-
109-
streamer.onMessage(function(msg) {
110-
$scope.$apply(function() {
111-
$scope.logs.push({text: msg});
112-
$scope.canShowDownload = true;
113-
});
114-
});
115-
streamer.onClose(function() {
116-
$scope.$apply(function() {
117-
$scope.logsLoading = false;
118-
});
119-
});
120-
streamer.onError(function() {
121-
$scope.$apply(function() {
122-
angular.extend($scope, {
123-
logsLoading: false,
124-
logError: true
125-
});
126-
});
127-
});
128-
129-
streamer.start();
130-
$scope.$on('$destroy', function() {
131-
streamer.stop();
132-
});
133-
};
134-
135-
angular.extend($scope, {
136-
initLogs: _.once(runLogs),
137-
restartLogs: _.flow(function() {
138-
streamer.stop();
139-
}, runLogs)
140-
});
141-
142-
$scope.selectContainer = function(container) {
143-
$scope.options.container = container.name;
144-
$scope.restartLogs();
145-
};
146-
147-
14898
});
14999

150100
$scope.containersRunning = function(containerStatuses) {

assets/app/scripts/directives/logViewer.js

Lines changed: 158 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,173 @@ angular.module('openshiftConsole')
1010
transclude: true,
1111
templateUrl: 'views/directives/logs/_log-viewer.html',
1212
scope: {
13-
logs: '=',
14-
loading: '=',
15-
links: '=',
13+
kind: '@',
1614
name: '=',
17-
download: '=',
18-
start: '=',
19-
end: '='
15+
context: '=',
16+
options: '=?',
17+
status: '=?',
18+
start: '=?',
19+
end: '=?',
20+
chromeless: '=?'
2021
},
2122
controller: [
2223
'$scope',
2324
function($scope) {
25+
$scope.loading = true;
26+
27+
// Default to false. Let the user click the follow link to start auto-scrolling.
28+
$scope.autoScroll = false;
29+
30+
// Set to true when we auto-scroll to follow log content.
31+
var autoScrolling = false;
32+
var onScroll = function() {
33+
// Determine if the user scrolled or we auto-scrolled.
34+
if (autoScrolling) {
35+
// Reset the value.
36+
autoScrolling = false;
37+
} else {
38+
// If the user scrolled the window manually, stop auto-scrolling.
39+
$scope.$evalAsync(function() {
40+
$scope.autoScroll = false;
41+
});
42+
}
43+
};
44+
$(window).scroll(onScroll);
45+
46+
var scrollBottom = function() {
47+
// Tell the scroll listener this is an auto-scroll. The listener
48+
// will reset it to false.
49+
autoScrolling = true;
50+
logLinks.scrollBottom();
51+
};
52+
53+
var toggleAutoScroll = function() {
54+
$scope.autoScroll = !$scope.autoScroll;
55+
if ($scope.autoScroll) {
56+
// Scroll immediately. Don't wait the next message.
57+
scrollBottom();
58+
}
59+
};
60+
61+
var scrollTop = function() {
62+
// Stop auto-scrolling when the user clicks the scroll top link.
63+
$scope.autoScroll = false;
64+
logLinks.scrollTop();
65+
};
66+
67+
// maintaining one streamer reference & ensuring its closed before we open a new,
68+
// since the user can (potentially) swap between multiple containers
69+
var streamer;
70+
var stopStreaming = function(keepContent) {
71+
if (streamer) {
72+
streamer.stop();
73+
streamer = null;
74+
}
75+
if (!keepContent) {
76+
$('#logContent').empty();
77+
}
78+
};
79+
80+
var streamLogs = function() {
81+
// Stop any active streamer.
82+
stopStreaming();
83+
84+
if (!$scope.name) {
85+
return;
86+
}
87+
88+
$scope.$evalAsync(function() {
89+
angular.extend($scope, {
90+
loading: true,
91+
error: false,
92+
autoScroll: false,
93+
limitReached: false
94+
});
95+
});
96+
97+
var options = angular.extend({
98+
follow: true,
99+
tailLines: 1000,
100+
limitBytes: 10 * 1024 * 1024 // Limit log size to 10 MiB
101+
}, $scope.options);
102+
streamer =
103+
DataService.createStream($scope.kind, $scope.name, $scope.context, options);
104+
105+
var lastLineNumber = 0;
106+
streamer.onMessage(function(msg, raw, cumulativeBytes) {
107+
if (options.limitBytes && cumulativeBytes >= options.limitBytes) {
108+
$scope.$evalAsync(function() {
109+
$scope.limitReached = true;
110+
});
111+
stopStreaming(true);
112+
}
113+
114+
lastLineNumber++;
115+
116+
// Manipulate the DOM directly for better performance displaying large log files.
117+
var logLine = $('<div row class="log-line"/>');
118+
$('<div class="log-line-number"><div row flex main-axis="end">' + lastLineNumber + '</div></div>').appendTo(logLine);
119+
$('<div flex class="log-line-text"/>').text(msg).appendTo(logLine);
120+
logLine.appendTo('#logContent');
121+
122+
// Follow the bottom of the log if auto-scroll is on.
123+
if ($scope.autoScroll) {
124+
scrollBottom();
125+
}
126+
127+
// Show the start and end links if the log is more than 25 lines.
128+
if (!$scope.showScrollLinks && lastLineNumber > 25) {
129+
$scope.$evalAsync(function() {
130+
$scope.showScrollLinks = true;
131+
});
132+
}
133+
134+
// Warn the user if we might be showing a partial log.
135+
if (!$scope.largeLog && lastLineNumber >= options.tailLines) {
136+
$scope.$evalAsync(function() {
137+
$scope.largeLog = true;
138+
});
139+
}
140+
});
141+
142+
streamer.onClose(function() {
143+
streamer = null;
144+
$scope.$evalAsync(function() {
145+
angular.extend($scope, {
146+
loading: false,
147+
autoScroll: false
148+
});
149+
});
150+
});
151+
152+
streamer.onError(function() {
153+
streamer = null;
154+
$scope.$evalAsync(function() {
155+
angular.extend($scope, {
156+
loading: false,
157+
error: true,
158+
autoScroll: false
159+
});
160+
});
161+
});
162+
163+
streamer.start();
164+
};
165+
166+
$scope.$watchGroup(['name', 'options.container'], streamLogs);
167+
168+
$scope.$on('$destroy', function() {
169+
stopStreaming();
170+
$(window).off('scroll', onScroll);
171+
});
172+
24173
angular.extend($scope, {
25174
ready: true,
26-
canDownload: logLinks.canDownload(),
27-
makeDownload: _.flow(function(arr) {
28-
return _.reduce(
29-
arr,
30-
function(memo, next, i) {
31-
return i <= arr.length ?
32-
memo + next.text :
33-
memo;
34-
}, '');
35-
}, logLinks.makeDownload),
36-
scrollTo: logLinks.scrollTo,
37-
scrollTop: logLinks.scrollTop,
38175
scrollBottom: logLinks.scrollBottom,
39-
goFull: logLinks.fullPageLink,
176+
scrollTop: scrollTop,
177+
toggleAutoScroll: toggleAutoScroll,
40178
goChromeless: logLinks.chromelessLink,
41-
goText: logLinks.textOnlyLink
179+
restartLogs: streamLogs
42180
});
43181
}
44182
]

0 commit comments

Comments
 (0)