Commit 5103cfae authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'ph/210377/increaseMrPollTimes' into 'master'

Increase times for the merge request widget polling

Closes #210377

See merge request gitlab-org/gitlab!29318
parents fe0b31c1 cc315c59
...@@ -33,7 +33,7 @@ export default class SmartInterval { ...@@ -33,7 +33,7 @@ export default class SmartInterval {
this.state = { this.state = {
intervalId: null, intervalId: null,
currentInterval: this.cfg.startingInterval, currentInterval: this.cfg.startingInterval,
pageVisibility: 'visible', pagevisibile: true,
}; };
this.initInterval(); this.initInterval();
...@@ -91,8 +91,10 @@ export default class SmartInterval { ...@@ -91,8 +91,10 @@ export default class SmartInterval {
} }
destroy() { destroy() {
document.removeEventListener('visibilitychange', this.onVisibilityChange);
window.removeEventListener('blur', this.onWindowVisibilityChange);
window.removeEventListener('focus', this.onWindowVisibilityChange);
this.cancel(); this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document) $(document)
.off('visibilitychange') .off('visibilitychange')
.off('beforeunload'); .off('beforeunload');
...@@ -124,9 +126,21 @@ export default class SmartInterval { ...@@ -124,9 +126,21 @@ export default class SmartInterval {
}); });
} }
onWindowVisibilityChange(e) {
this.state.pagevisibile = e.type === 'focus';
this.handleVisibilityChange();
}
onVisibilityChange(e) {
this.state.pagevisibile = e.target.visibilityState === 'visible';
this.handleVisibilityChange();
}
initVisibilityChangeHandling() { initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling) // cancel interval when tab or window is no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); document.addEventListener('visibilitychange', this.onVisibilityChange.bind(this));
window.addEventListener('blur', this.onWindowVisibilityChange.bind(this));
window.addEventListener('focus', this.onWindowVisibilityChange.bind(this));
} }
initPageUnloadHandling() { initPageUnloadHandling() {
...@@ -135,8 +149,7 @@ export default class SmartInterval { ...@@ -135,8 +149,7 @@ export default class SmartInterval {
$(document).on('beforeunload', () => this.cancel()); $(document).on('beforeunload', () => this.cancel());
} }
handleVisibilityChange(e) { handleVisibilityChange() {
this.state.pageVisibility = e.target.visibilityState;
const intervalAction = this.isPageVisible() const intervalAction = this.isPageVisible()
? this.onVisibilityVisible ? this.onVisibilityVisible
: this.onVisibilityHidden; : this.onVisibilityHidden;
...@@ -166,7 +179,7 @@ export default class SmartInterval { ...@@ -166,7 +179,7 @@ export default class SmartInterval {
} }
isPageVisible() { isPageVisible() {
return this.state.pageVisibility === 'visible'; return this.state.pagevisibile;
} }
stopTimer() { stopTimer() {
......
...@@ -214,8 +214,6 @@ export default { ...@@ -214,8 +214,6 @@ export default {
return new MRWidgetService(this.getServiceEndpoints(store)); return new MRWidgetService(this.getServiceEndpoints(store));
}, },
checkStatus(cb, isRebased) { checkStatus(cb, isRebased) {
if (document.visibilityState !== 'visible') return Promise.resolve();
return this.service return this.service
.checkStatus() .checkStatus()
.then(({ data }) => { .then(({ data }) => {
...@@ -238,10 +236,10 @@ export default { ...@@ -238,10 +236,10 @@ export default {
initPolling() { initPolling() {
this.pollingInterval = new SmartInterval({ this.pollingInterval = new SmartInterval({
callback: this.checkStatus, callback: this.checkStatus,
startingInterval: 10000, startingInterval: 10 * 1000,
maxInterval: 30000, maxInterval: 240 * 1000,
hiddenInterval: 120000, hiddenInterval: window.gon?.features?.widgetVisibilityPolling && 360 * 1000,
incrementByFactorOf: 5000, incrementByFactorOf: 2,
}); });
}, },
initDeploymentsPolling() { initDeploymentsPolling() {
...@@ -253,10 +251,9 @@ export default { ...@@ -253,10 +251,9 @@ export default {
deploymentsPoll(callback) { deploymentsPoll(callback) {
return new SmartInterval({ return new SmartInterval({
callback, callback,
startingInterval: 30000, startingInterval: 30 * 1000,
maxInterval: 120000, maxInterval: 240 * 1000,
hiddenInterval: 240000, incrementByFactorOf: 4,
incrementByFactorOf: 15000,
immediateExecution: true, immediateExecution: true,
}); });
}, },
......
...@@ -24,6 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -24,6 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:single_mr_diff_view, @project, default_enabled: true) push_frontend_feature_flag(:single_mr_diff_view, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
push_frontend_feature_flag(:code_navigation, @project) push_frontend_feature_flag(:code_navigation, @project)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
end end
before_action do before_action do
......
---
title: Increase the timing of polling for the merge request widget
merge_request:
author:
type: changed
import $ from 'jquery';
import { assignIn } from 'lodash';
import waitForPromises from 'helpers/wait_for_promises';
import SmartInterval from '~/smart_interval';
jest.useFakeTimers();
let interval;
describe('SmartInterval', () => {
const DEFAULT_MAX_INTERVAL = 100;
const DEFAULT_STARTING_INTERVAL = 5;
const DEFAULT_INCREMENT_FACTOR = 2;
function createDefaultSmartInterval(config) {
const defaultParams = {
callback: () => Promise.resolve(),
startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
lazyStart: false,
immediateExecution: false,
hiddenInterval: null,
};
if (config) {
assignIn(defaultParams, config);
}
return new SmartInterval(defaultParams);
}
afterEach(() => {
interval.destroy();
});
describe('Increment Interval', () => {
it('should increment the interval delay', () => {
interval = createDefaultSmartInterval();
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
const intervalConfig = interval.cfg;
const iterationCount = 4;
const maxIntervalAfterIterations =
intervalConfig.startingInterval * intervalConfig.incrementByFactorOf ** iterationCount;
const currentInterval = interval.getCurrentInterval();
// Provide some flexibility for performance of testing environment
expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
expect(currentInterval).toBeLessThanOrEqual(maxIntervalAfterIterations);
});
});
it('should not increment past maxInterval', () => {
interval = createDefaultSmartInterval({ maxInterval: DEFAULT_STARTING_INTERVAL });
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
const currentInterval = interval.getCurrentInterval();
expect(currentInterval).toBe(interval.cfg.maxInterval);
});
});
it('does not increment while waiting for callback', () => {
interval = createDefaultSmartInterval({
callback: () => new Promise($.noop),
});
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
const oneInterval = interval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
expect(interval.getCurrentInterval()).toEqual(oneInterval);
});
});
});
describe('Public methods', () => {
beforeEach(() => {
interval = createDefaultSmartInterval();
});
it('should cancel an interval', () => {
jest.runOnlyPendingTimers();
interval.cancel();
return waitForPromises().then(() => {
const { intervalId } = interval.state;
const currentInterval = interval.getCurrentInterval();
const intervalLowerLimit = interval.cfg.startingInterval;
expect(intervalId).toBeUndefined();
expect(currentInterval).toBe(intervalLowerLimit);
});
});
it('should resume an interval', () => {
jest.runOnlyPendingTimers();
interval.cancel();
interval.resume();
return waitForPromises().then(() => {
const { intervalId } = interval.state;
expect(intervalId).toBeTruthy();
});
});
});
describe('DOM Events', () => {
beforeEach(() => {
// This ensures DOM and DOM events are initialized for these specs.
setFixtures('<div></div>');
interval = createDefaultSmartInterval();
});
it('should pause when page is not visible', () => {
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined();
});
});
it('should change to the hidden interval when page is not visible', () => {
interval.destroy();
const HIDDEN_INTERVAL = 1500;
interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
expect(interval.state.intervalId).toBeTruthy();
expect(
interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL,
).toBeTruthy();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeTruthy();
expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
});
});
it('should resume when page is becomes visible at the previous interval', () => {
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'visible' } });
expect(interval.state.intervalId).toBeTruthy();
});
});
it('should cancel on page unload', () => {
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
$(document).triggerHandler('beforeunload');
expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
});
});
it('should execute callback before first interval', () => {
interval = createDefaultSmartInterval({ immediateExecution: true });
expect(interval.cfg.immediateExecution).toBeFalsy();
});
});
});
...@@ -273,25 +273,6 @@ describe('mrWidgetOptions', () => { ...@@ -273,25 +273,6 @@ describe('mrWidgetOptions', () => {
}; };
}); });
it('should not tell service to check status if document is not visible', () => {
Object.defineProperty(document, 'visibilityState', {
value: 'hidden',
configurable: true,
});
vm.checkStatus(cb);
return vm.$nextTick().then(() => {
expect(vm.service.checkStatus).not.toHaveBeenCalled();
expect(vm.mr.setData).not.toHaveBeenCalled();
expect(vm.handleNotification).not.toHaveBeenCalled();
expect(isCbExecuted).toBeFalsy();
Object.defineProperty(document, 'visibilityState', {
value: 'visible',
configurable: true,
});
});
});
it('should tell service to check status if document is visible', () => { it('should tell service to check status if document is visible', () => {
vm.checkStatus(cb); vm.checkStatus(cb);
......
import $ from 'jquery';
import { assignIn } from 'lodash';
import waitForPromises from 'spec/helpers/wait_for_promises';
import SmartInterval from '~/smart_interval';
describe('SmartInterval', function() {
const DEFAULT_MAX_INTERVAL = 100;
const DEFAULT_STARTING_INTERVAL = 5;
const DEFAULT_SHORT_TIMEOUT = 75;
const DEFAULT_INCREMENT_FACTOR = 2;
function createDefaultSmartInterval(config) {
const defaultParams = {
callback: () => Promise.resolve(),
startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
lazyStart: false,
immediateExecution: false,
hiddenInterval: null,
};
if (config) {
assignIn(defaultParams, config);
}
return new SmartInterval(defaultParams);
}
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
describe('Increment Interval', function() {
it('should increment the interval delay', done => {
const smartInterval = createDefaultSmartInterval();
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
waitForPromises()
.then(() => {
const intervalConfig = smartInterval.cfg;
const iterationCount = 4;
const maxIntervalAfterIterations =
intervalConfig.startingInterval * intervalConfig.incrementByFactorOf ** iterationCount;
const currentInterval = smartInterval.getCurrentInterval();
// Provide some flexibility for performance of testing environment
expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
expect(currentInterval).toBeLessThanOrEqual(maxIntervalAfterIterations);
})
.then(done)
.catch(done.fail);
});
it('should not increment past maxInterval', done => {
const smartInterval = createDefaultSmartInterval({ maxInterval: DEFAULT_STARTING_INTERVAL });
jasmine.clock().tick(DEFAULT_STARTING_INTERVAL);
jasmine.clock().tick(DEFAULT_STARTING_INTERVAL * DEFAULT_INCREMENT_FACTOR);
waitForPromises()
.then(() => {
const currentInterval = smartInterval.getCurrentInterval();
expect(currentInterval).toBe(smartInterval.cfg.maxInterval);
})
.then(done)
.catch(done.fail);
});
it('does not increment while waiting for callback', done => {
const smartInterval = createDefaultSmartInterval({
callback: () => new Promise($.noop),
});
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
waitForPromises()
.then(() => {
const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
expect(smartInterval.getCurrentInterval()).toEqual(oneInterval);
})
.then(done)
.catch(done.fail);
});
});
describe('Public methods', function() {
beforeEach(function() {
this.smartInterval = createDefaultSmartInterval();
});
it('should cancel an interval', function(done) {
const interval = this.smartInterval;
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
interval.cancel();
waitForPromises()
.then(() => {
const { intervalId } = interval.state;
const currentInterval = interval.getCurrentInterval();
const intervalLowerLimit = interval.cfg.startingInterval;
expect(intervalId).toBeUndefined();
expect(currentInterval).toBe(intervalLowerLimit);
})
.then(done)
.catch(done.fail);
});
it('should resume an interval', function(done) {
const interval = this.smartInterval;
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
interval.cancel();
interval.resume();
waitForPromises()
.then(() => {
const { intervalId } = interval.state;
expect(intervalId).toBeTruthy();
})
.then(done)
.catch(done.fail);
});
});
describe('DOM Events', function() {
beforeEach(function() {
// This ensures DOM and DOM events are initialized for these specs.
setFixtures('<div></div>');
this.smartInterval = createDefaultSmartInterval();
});
it('should pause when page is not visible', function(done) {
const interval = this.smartInterval;
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
waitForPromises()
.then(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined();
})
.then(done)
.catch(done.fail);
});
it('should change to the hidden interval when page is not visible', done => {
const HIDDEN_INTERVAL = 1500;
const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
waitForPromises()
.then(() => {
expect(interval.state.intervalId).toBeTruthy();
expect(
interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL,
).toBeTruthy();
// simulates triggering of visibilitychange event
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeTruthy();
expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
})
.then(done)
.catch(done.fail);
});
it('should resume when page is becomes visible at the previous interval', function(done) {
const interval = this.smartInterval;
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
waitForPromises()
.then(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined();
// simulates triggering of visibilitychange event
interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
expect(interval.state.intervalId).toBeTruthy();
})
.then(done)
.catch(done.fail);
});
it('should cancel on page unload', function(done) {
const interval = this.smartInterval;
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
waitForPromises()
.then(() => {
$(document).triggerHandler('beforeunload');
expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
})
.then(done)
.catch(done.fail);
});
it('should execute callback before first interval', function() {
const interval = createDefaultSmartInterval({ immediateExecution: true });
expect(interval.cfg.immediateExecution).toBeFalsy();
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment