Skip to content

Commit

Permalink
Improve UI performance when displaying large logs
Browse files Browse the repository at this point in the history
  • Loading branch information
spadgett committed Oct 30, 2015
1 parent 4618b0d commit 8d81d78
Show file tree
Hide file tree
Showing 15 changed files with 471 additions and 485 deletions.
16 changes: 10 additions & 6 deletions assets/app/scripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,11 @@ angular
})
.when('/project/:project/browse/builds/:buildconfig/:build', {
templateUrl: function(params) {
return params.view ?
'views/logs/'+params.view+'_log.html' :
'views/browse/build.html';
if (params.view === 'chromeless') {
return 'views/logs/chromeless-build-log.html';
}

return 'views/browse/build.html';
},
controller: 'BuildController'
})
Expand Down Expand Up @@ -157,9 +159,11 @@ angular
})
.when('/project/:project/browse/pods/:pod', {
templateUrl: function(params) {
return params.view ?
'views/logs/'+params.view+'_log.html' :
'views/browse/pod.html';
if (params.view === 'chromeless') {
return 'views/logs/chromeless-pod-log.html';
}

return 'views/browse/pod.html';
},
controller: 'PodController'
})
Expand Down
52 changes: 7 additions & 45 deletions assets/app/scripts/controllers/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,15 @@ angular.module('openshiftConsole')
var watches = [];

project.get($routeParams.project).then(function(resp) {
angular.extend($scope, {
var context = {
project: resp[0],
projectPromise: resp[1].projectPromise
});
};
angular.extend($scope, context);
// FIXME: DataService.createStream() requires a scope with a
// projectPromise rather than just a namespace, so we have to pass the
// context into the log-viewer directive.
$scope.logContext = context;
DataService.get("builds", $routeParams.build, $scope).then(
// success
function(build) {
Expand Down Expand Up @@ -109,49 +114,6 @@ angular.module('openshiftConsole')
}
}
}));


var runLogs = function() {
angular.extend($scope, {
logs: [],
logsLoading: true,
canShowDownload: false,
canInitAgain: false
});

var streamer = DataService.createStream('builds/log',$routeParams.build, $scope);
streamer.onMessage(function(msg) {
$scope.$apply(function() {
$scope.logs.push({text: msg});
$scope.canShowDownload = true;
});
});
streamer.onClose(function() {
$scope.$apply(function() {
$scope.logsLoading = false;
});
});
streamer.onError(function() {
$scope.$apply(function() {
angular.extend($scope, {
logsLoading: false,
logError: true
});
});
});

streamer.start();
$scope.$on('$destroy', function() {
streamer.stop();
});
};

angular.extend($scope, {
initLogs: _.once(runLogs),
runLogs: runLogs
});


});

$scope.startBuild = function(buildConfigName) {
Expand Down
68 changes: 9 additions & 59 deletions assets/app/scripts/controllers/pod.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ angular.module('openshiftConsole')
$scope.alerts = {};
$scope.renderOptions = $scope.renderOptions || {};
$scope.renderOptions.hideFilterWidget = true;
$scope.logOptions = {};
$scope.terminalTabWasSelected = false;
$scope.breadcrumbs = [
{
Expand All @@ -41,15 +42,21 @@ angular.module('openshiftConsole')
});

project.get($routeParams.project).then(function(resp) {
angular.extend($scope, {
var context = {
project: resp[0],
projectPromise: resp[1].projectPromise
});
};
angular.extend($scope, context);
// FIXME: DataService.createStream() requires a scope with a
// projectPromise rather than just a namespace, so we have to pass the
// context into the log-viewer directive.
$scope.logContext = context;
DataService.get("pods", $routeParams.pod, $scope).then(
// success
function(pod) {
$scope.loaded = true;
$scope.pod = pod;
$scope.logOptions.container = $routeParams.container || pod.spec.containers[0].name;
var pods = {};
pods[pod.metadata.name] = pod;
ImageStreamResolver.fetchReferencedImageStreamImages(pods, $scope.imagesByDockerReference, $scope.imageStreamImageRefByDockerReference, $scope);
Expand Down Expand Up @@ -88,63 +95,6 @@ angular.module('openshiftConsole')
$scope.builds = builds.by("metadata.name");
Logger.log("builds (subscribe)", $scope.builds);
}));

// maintaining one streamer reference & ensuring its closed before we open a new,
// since the user can (potentially) swap between multiple containers
var streamer;
var runLogs = function() {
angular.extend($scope, {
logs: [],
logsLoading: true,
canShowDownload: false,
canInitAgain: false,
options: {
container: $scope.pod.spec.containers[0].name
}
});

// TODO: clean up service / $scope stuff...
streamer = DataService.createStream('pods/log',$routeParams.pod, $scope, $scope.options);

streamer.onMessage(function(msg) {
$scope.$apply(function() {
$scope.logs.push({text: msg});
$scope.canShowDownload = true;
});
});
streamer.onClose(function() {
$scope.$apply(function() {
$scope.logsLoading = false;
});
});
streamer.onError(function() {
$scope.$apply(function() {
angular.extend($scope, {
logsLoading: false,
logError: true
});
});
});

streamer.start();
$scope.$on('$destroy', function() {
streamer.stop();
});
};

angular.extend($scope, {
initLogs: _.once(runLogs),
restartLogs: _.flow(function() {
streamer.stop();
}, runLogs)
});

$scope.selectContainer = function(container) {
$scope.options.container = container.name;
$scope.restartLogs();
};


});

$scope.containersRunning = function(containerStatuses) {
Expand Down
175 changes: 155 additions & 20 deletions assets/app/scripts/directives/logViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,170 @@ angular.module('openshiftConsole')
transclude: true,
templateUrl: 'views/directives/logs/_log-viewer.html',
scope: {
logs: '=',
loading: '=',
links: '=',
kind: '@',
name: '=',
download: '=',
start: '=',
end: '='
context: '=',
options: '=?',
status: '=?',
start: '=?',
end: '=?',
chromeless: '=?'
},
controller: [
'$scope',
function($scope) {
$scope.loading = true;

// Default to false. Let me user click the follow link to start auto-scrolling.
$scope.autoScroll = false;

// Set to true when we auto-scroll to follow log content.
var autoScrolling = false;
var onScroll = function() {
// Determine if the user scrolled or we auto-scrolled.
if (autoScrolling) {
// Reset the value.
autoScrolling = false;
} else {
// If the user scrolled the window manually, stop auto-scrolling.
$scope.$evalAsync(function() {
$scope.autoScroll = false;
});
}
};
$(window).scroll(onScroll);

var scrollBottom = function() {
// Tell the scroll listener this is an auto-scroll. The listener
// will reset it to false.
autoScrolling = true;
logLinks.scrollBottom();
};

var toggleAutoScroll = function() {
$scope.autoScroll = !$scope.autoScroll;
if ($scope.autoScroll) {
// Scroll immediately. Don't wait the next message.
scrollBottom();
}
};

var scrollTop = function() {
// Stop auto-scrolling when the user clicks the scroll top link.
$scope.autoScroll = false;
logLinks.scrollTop();
};

// maintaining one streamer reference & ensuring its closed before we open a new,
// since the user can (potentially) swap between multiple containers
var streamer;
var stopStreaming = function() {
if (streamer) {
streamer.stop();
streamer = null;
}
$('#logContent').empty();
};

var streamLogs = function() {
// Stop any active streamer.
stopStreaming();

if (!$scope.name) {
return;
}

$scope.$evalAsync(function() {
angular.extend($scope, {
loading: true,
error: false,
autoScroll: false
});
});

var options = angular.extend({
follow: true,
tailLines: 1000,
limitBytes: 10 * 1024 * 1024 // Limit log size to 10 MiB
}, $scope.options);
streamer =
DataService.createStream($scope.kind, $scope.name, $scope.context, options);

var lastLineNumber = 0;
streamer.onMessage(function(msg, raw, cumulativeBytes) {
if (options.limitBytes && cumulativeBytes >= options.limitBytes) {
$scope.$evalAsync(function() {
$scope.limitReached = true;
});
stopStreaming();
}

lastLineNumber++;

// Manipulate the DOM directly for better performance displaying large log files.
var logLine = $('<div row class="log-line"/>');
$('<div class="log-line-number"><div row flex main-axis="end">' + lastLineNumber + '</div></div>').appendTo(logLine);
$('<div flex class="log-line-text"/>').text(msg).appendTo(logLine);
logLine.appendTo('#logContent');

// Follow the bottom of the log if auto-scroll is on.
if ($scope.autoScroll) {
scrollBottom();
}

// Show the start and end links if the log is more than 25 lines.
if (!$scope.showScrollLinks && lastLineNumber > 25) {
$scope.$evalAsync(function() {
$scope.showScrollLinks = true;
});
}

// Warn the user if we might be showing a partial log.
if (!$scope.largeLog && lastLineNumber >= options.tailLines) {
$scope.$evalAsync(function() {
$scope.largeLog = true;
});
}
});

streamer.onClose(function() {
streamer = null;
$scope.$evalAsync(function() {
angular.extend($scope, {
loading: false,
autoScroll: false
});
});
});

streamer.onError(function() {
streamer = null;
$scope.$evalAsync(function() {
angular.extend($scope, {
loading: false,
error: true,
autoScroll: false
});
});
});

streamer.start();
};

$scope.$watchGroup(['name', 'options.container'], streamLogs);

$scope.$on('$destroy', function() {
stopStreaming();
$(window).off('scroll', onScroll);
});

angular.extend($scope, {
ready: true,
canDownload: logLinks.canDownload(),
makeDownload: _.flow(function(arr) {
return _.reduce(
arr,
function(memo, next, i) {
return i <= arr.length ?
memo + next.text :
memo;
}, '');
}, logLinks.makeDownload),
scrollTo: logLinks.scrollTo,
scrollTop: logLinks.scrollTop,
scrollBottom: logLinks.scrollBottom,
goFull: logLinks.fullPageLink,
scrollTop: scrollTop,
toggleAutoScroll: toggleAutoScroll,
goChromeless: logLinks.chromelessLink,
goText: logLinks.textOnlyLink
restartLogs: streamLogs
});
}
]
Expand Down
Loading

0 comments on commit 8d81d78

Please sign in to comment.