FE: Resolve "Performance issues when processing large build traces with Ansi2html"

This commit is contained in:
Filipa Lacerda 2017-04-07 11:13:23 +00:00 committed by Jacob Schatz
parent 9216f59a3f
commit 8ca5afdf27
4 changed files with 239 additions and 174 deletions

View file

@ -1,24 +1,28 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
/* eslint-disable func-names, wrap-iife, no-use-before-define,
consistent-return, prefer-rest-params */
/* global Breakpoints */
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
var AUTO_SCROLL_OFFSET = 75;
var DOWN_BUILD_TRACE = '#down-build-trace';
const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
const AUTO_SCROLL_OFFSET = 75;
const DOWN_BUILD_TRACE = '#down-build-trace';
window.Build = (function() {
window.Build = (function () {
Build.timeout = null;
Build.state = null;
function Build(options) {
options = options || $('.js-build-options').data();
this.pageUrl = options.pageUrl;
this.buildUrl = options.buildUrl;
this.buildStatus = options.buildStatus;
this.state = options.logState;
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl;
this.buildUrl = this.options.buildUrl;
this.buildStatus = this.options.buildStatus;
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
this.updateDropdown = bind(this.updateDropdown, this);
this.$body = $('body');
this.$buildTrace = $('#build-trace');
this.$autoScrollContainer = $('.autoscroll-container');
@ -29,112 +33,110 @@ window.Build = (function() {
this.$scrollTopBtn = $('#scroll-top');
this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh');
this.$buildScroll = $('#js-build-scroll');
this.$truncatedInfo = $('.js-truncated-info');
clearTimeout(Build.timeout);
// Init breakpoint checker
this.bp = Breakpoints.get();
this.initSidebar();
this.$buildScroll = $('#js-build-scroll');
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document
.off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown);
this.$document.on('scroll', this.initScrollMonitor.bind(this));
$(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
$(window)
.off('resize.build')
.on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll)
.off('click.stepTrace')
.on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
this.initScrollButtonAffix();
}
this.initScrollButtonAffix();
this.invokeBuildTrace();
}
Build.prototype.initSidebar = function() {
Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar');
this.$sidebar.niceScroll();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
};
Build.prototype.location = function() {
return window.location.href.split("#")[0];
Build.prototype.invokeBuildTrace = function () {
return this.getBuildTrace();
};
Build.prototype.invokeBuildTrace = function() {
var continueRefreshStatuses = ['running', 'pending'];
// Continue to update build trace when build is running or pending
if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
Build.timeout = setTimeout((function(_this) {
return function() {
if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
})(this), 4000);
}
};
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
Build.prototype.getBuildTrace = function () {
return $.ajax({
url: this.pageUrl + "/trace.json",
url: `${this.pageUrl}/trace.json`,
dataType: 'json',
success: function(buildData) {
$('.js-build-output').html(buildData.html);
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height());
data: {
state: this.state,
},
success: ((log) => {
const $buildContainer = $('.js-build-output');
if (log.state) {
this.state = log.state;
}
if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
if (log.append) {
$buildContainer.append(log.html);
} else {
$buildContainer.html(log.html);
if (log.truncated) {
$('.js-truncated-info-size').html(` ${log.size} `);
this.$truncatedInfo.removeClass('hidden');
this.initAffixTruncatedInfo();
} else {
this.$truncatedInfo.addClass('hidden');
}
}
this.checkAutoscroll();
if (!log.complete) {
Build.timeout = setTimeout(() => {
this.invokeBuildTrace();
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
return this.initScrollMonitor();
}
}.bind(this)
if (log.status !== this.buildStatus) {
let pageUrl = this.pageUrl;
if (this.$autoScrollStatus.data('state') === 'enabled') {
pageUrl += DOWN_BUILD_TRACE;
}
gl.utils.visitUrl(pageUrl);
}
}),
error: () => {
this.$buildRefreshAnimation.remove();
return this.initScrollMonitor();
},
});
};
Build.prototype.getBuildTrace = function() {
return $.ajax({
url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
dataType: "json",
success: (function(_this) {
return function(log) {
var pageUrl;
if (log.state) {
_this.state = log.state;
}
_this.invokeBuildTrace();
if (log.status === "running") {
if (log.append) {
$('.js-build-output').append(log.html);
} else {
$('.js-build-output').html(log.html);
}
return _this.checkAutoscroll();
} else if (log.status !== _this.buildStatus) {
pageUrl = _this.pageUrl;
if (_this.$autoScrollStatus.data('state') === 'enabled') {
pageUrl += DOWN_BUILD_TRACE;
}
return gl.utils.visitUrl(pageUrl);
}
};
})(this)
});
};
Build.prototype.checkAutoscroll = function() {
if (this.$autoScrollStatus.data("state") === "enabled") {
return $("html,body").scrollTop(this.$buildTrace.height());
Build.prototype.checkAutoscroll = function () {
if (this.$autoScrollStatus.data('state') === 'enabled') {
return $('html,body').scrollTop(this.$buildTrace.height());
}
// Handle a situation where user started new build
@ -146,7 +148,7 @@ window.Build = (function() {
}
};
Build.prototype.initScrollButtonAffix = function() {
Build.prototype.initScrollButtonAffix = function () {
// Hide everything initially
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.hide();
@ -167,15 +169,17 @@ window.Build = (function() {
// - Show Top Arrow button
// - Show Bottom Arrow button
// - Disable Autoscroll and hide indicator (when build is running)
Build.prototype.initScrollMonitor = function() {
if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
Build.prototype.initScrollMonitor = function () {
if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
!gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is somewhere in middle of Build Log
this.$scrollTopBtn.show();
if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
this.$scrollBottomBtn.show();
} else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
} else if (this.$buildRefreshAnimation.is(':visible') &&
!gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
this.$scrollBottomBtn.show();
} else {
this.$scrollBottomBtn.hide();
@ -186,10 +190,13 @@ window.Build = (function() {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
} else {
this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
this.$autoScrollContainer.css({
top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET,
}).show();
this.$autoScrollStatusText.addClass('animate');
}
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
!gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is at Top of Build Log
this.$scrollTopBtn.hide();
@ -197,17 +204,22 @@ window.Build = (function() {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
} else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
(this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
} else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
(this.$buildRefreshAnimation.is(':visible') &&
gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
// User is at Bottom of Build Log
this.$scrollTopBtn.show();
this.$scrollBottomBtn.hide();
// Show and Reposition Autoscroll Status Indicator
this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
this.$autoScrollContainer.css({
top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET,
}).show();
this.$autoScrollStatusText.addClass('animate');
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// Build Log height is small
this.$scrollTopBtn.hide();
@ -218,65 +230,81 @@ window.Build = (function() {
this.$autoScrollStatusText.removeClass('animate');
}
if (this.buildStatus === "running" || this.buildStatus === "pending") {
if (this.buildStatus === 'running' || this.buildStatus === 'pending') {
// Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
this.$autoScrollStatus.data(
'state',
gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled',
);
}
};
Build.prototype.shouldHideSidebarForViewport = function() {
var bootstrapBreakpoint;
bootstrapBreakpoint = this.bp.getBreakpointSize();
Build.prototype.shouldHideSidebarForViewport = function () {
const bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
Build.prototype.toggleSidebar = function(shouldHide) {
var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$truncatedInfo.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
};
Build.prototype.sidebarOnResize = function() {
Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport());
};
Build.prototype.sidebarOnClick = function() {
Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
};
Build.prototype.updateArtifactRemoveDate = function() {
var $date, date;
$date = $('.js-artifacts-remove');
Build.prototype.updateArtifactRemoveDate = function () {
const $date = $('.js-artifacts-remove');
if ($date.length) {
date = $date.text();
return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
const date = $date.text();
return $date.text(
gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),
);
}
};
Build.prototype.populateJobs = function(stage) {
Build.prototype.populateJobs = function (stage) {
$('.build-job').hide();
$('.build-job[data-stage="' + stage + '"]').show();
$(`.build-job[data-stage="${stage}"]`).show();
};
Build.prototype.updateStageDropdownText = function(stage) {
Build.prototype.updateStageDropdownText = function (stage) {
$('.stage-selection').text(stage);
};
Build.prototype.updateDropdown = function(e) {
Build.prototype.updateDropdown = function (e) {
e.preventDefault();
var stage = e.currentTarget.text;
const stage = e.currentTarget.text;
this.updateStageDropdownText(stage);
this.populateJobs(stage);
};
Build.prototype.stepTrace = function(e) {
var $currentTarget;
Build.prototype.stepTrace = function (e) {
e.preventDefault();
$currentTarget = $(e.currentTarget);
const $currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), {
offset: 0
offset: 0,
});
};
Build.prototype.initAffixTruncatedInfo = function () {
const offsetTop = this.$buildTrace.offset().top;
this.$truncatedInfo.affix({
offset: {
top: offsetTop,
},
});
};

View file

@ -57,6 +57,37 @@
margin-right: 5px;
}
}
.truncated-info {
text-align: center;
border-bottom: 1px solid;
background-color: $black-transparent;
height: 45px;
&.affix {
top: 0;
}
// with sidebar
&.affix.sidebar-expanded {
right: 312px;
left: 22px;
}
// without sidebar
&.affix.sidebar-collapsed {
right: 20px;
left: 20px;
}
&.affix-top {
position: absolute;
top: 0;
margin: 0 auto;
right: 5px;
left: 5px;
}
}
}
.scroll-controls {
@ -186,6 +217,7 @@
white-space: pre;
overflow-x: auto;
font-size: 12px;
position: relative;
.fa-refresh {
font-size: 24px;

View file

@ -71,6 +71,11 @@
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace
.js-truncated-info.truncated-info.hidden
%span<
Showing last
%span.js-truncated-info-size><
KiB of log
%code.bash.js-build-output
.build-loader-animation.js-build-refresh

View file

@ -64,58 +64,33 @@ describe('Build', () => {
});
});
describe('initial build trace', () => {
beforeEach(() => {
new Build();
});
it('displays the initial build trace', () => {
expect($.ajax.calls.count()).toBe(1);
const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0);
expect(url).toBe(
`${BUILD_URL}/trace.json`,
);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { html: '<span>Example</span>', status: 'running' });
expect($('#build-trace .js-build-output').text()).toMatch(/Example/);
});
it('removes the spinner', () => {
const [{ success, context }] = $.ajax.calls.argsFor(0);
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
expect($('.js-build-refresh').length).toBe(0);
});
});
describe('running build', () => {
beforeEach(function () {
$('.js-build-options').data('buildStatus', 'running');
this.build = new Build();
spyOn(this.build, 'location').and.returnValue(BUILD_URL);
});
it('updates the build trace on an interval', function () {
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
expect($.ajax.calls.count()).toBe(2);
let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
expect(url).toBe(
`${BUILD_URL}/trace.json?state=`,
);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
expect($.ajax.calls.count()).toBe(1);
success.call(context, {
// We have to do it this way to prevent Webpack to fail to compile
// when destructuring assignments and reusing
// the same variables names inside the same scope
let args = $.ajax.calls.argsFor(0)[0];
expect(args.url).toBe(`${BUILD_URL}/trace.json`);
expect(args.dataType).toBe('json');
expect(args.success).toEqual(jasmine.any(Function));
args.success.call($, {
html: '<span>Update<span>',
status: 'running',
state: 'newstate',
append: true,
complete: false,
});
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
@ -123,17 +98,20 @@ describe('Build', () => {
jasmine.clock().tick(4001);
expect($.ajax.calls.count()).toBe(3);
[{ url, dataType, success, context }] = $.ajax.calls.argsFor(2);
expect(url).toBe(`${BUILD_URL}/trace.json?state=newstate`);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
expect($.ajax.calls.count()).toBe(2);
success.call(context, {
args = $.ajax.calls.argsFor(1)[0];
expect(args.url).toBe(`${BUILD_URL}/trace.json`);
expect(args.dataType).toBe('json');
expect(args.data.state).toBe('newstate');
expect(args.success).toEqual(jasmine.any(Function));
args.success.call($, {
html: '<span>More</span>',
status: 'running',
state: 'finalstate',
append: true,
complete: true,
});
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
@ -141,19 +119,22 @@ describe('Build', () => {
});
it('replaces the entire build trace', () => {
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
let [{ success, context }] = $.ajax.calls.argsFor(1);
success.call(context, {
let args = $.ajax.calls.argsFor(0)[0];
args.success.call($, {
html: '<span>Update</span>',
status: 'running',
append: true,
append: false,
complete: false,
});
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
jasmine.clock().tick(4001);
[{ success, context }] = $.ajax.calls.argsFor(2);
success.call(context, {
args = $.ajax.calls.argsFor(1)[0];
args.success.call($, {
html: '<span>Different</span>',
status: 'running',
append: false,
@ -163,15 +144,34 @@ describe('Build', () => {
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
});
it('shows information about truncated log', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
truncated: true,
size: '50',
});
expect(
$('#build-trace .js-truncated-info').text().trim(),
).toContain('Showing last 50 KiB of log');
expect($('#build-trace .js-truncated-info-size').text()).toMatch('50');
});
it('reloads the page when the build is done', () => {
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
const [{ success, context }] = $.ajax.calls.argsFor(1);
success.call(context, {
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Final</span>',
status: 'passed',
append: true,
complete: true,
});
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);