Skip to content

Commit

Permalink
Prebid Core: TTL counts only when page is active (prebid#11803)
Browse files Browse the repository at this point in the history
* 11268 TTL counts only when page is active

* refactor

* refactor due to performance optimization

* get rid of false timer id

---------

Co-authored-by: Marcin Komorski <marcinkomorski@Marcins-MacBook-Pro.local>
  • Loading branch information
2 people authored and DecayConstant committed Jul 18, 2024
1 parent d0fa6db commit 93039a4
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 1 deletion.
41 changes: 41 additions & 0 deletions src/utils/focusTimeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
let outOfFocusStart;
let timeOutOfFocus = 0;
let suspendedTimeouts = [];

document.addEventListener('visibilitychange', () => {
if (document.hidden) {
outOfFocusStart = Date.now()
} else {
timeOutOfFocus += Date.now() - outOfFocusStart
suspendedTimeouts.forEach(({ callback, startTime, setTimerId }) => setTimerId(setFocusTimeout(callback, timeOutOfFocus - startTime)()))
outOfFocusStart = null;
}
});

/**
* Wraps native setTimeout function in order to count time only when page is focused
*
* @param {function(*): ()} [callback] - A function that will be invoked after passed time
* @param {number} [milliseconds] - Minimum duration (in milliseconds) that the callback will be executed after
* @returns {function(*): (number)} - Getter function for current timer id
*/
export default function setFocusTimeout(callback, milliseconds) {
const startTime = timeOutOfFocus;
let timerId = setTimeout(() => {
if (timeOutOfFocus === startTime && outOfFocusStart == null) {
callback();
} else if (outOfFocusStart != null) {
// case when timeout ended during page is out of focus
suspendedTimeouts.push({
callback,
startTime,
setTimerId(newId) {
timerId = newId;
}
})
} else {
timerId = setFocusTimeout(callback, timeOutOfFocus - startTime)();
}
}, milliseconds);
return () => timerId;
}
3 changes: 2 additions & 1 deletion src/utils/ttlCollection.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {GreedyPromise} from './promise.js';
import {binarySearch, logError, timestamp} from '../utils.js';
import setFocusTimeout from './focusTimeout.js';

/**
* Create a set-like collection that automatically forgets items after a certain time.
Expand Down Expand Up @@ -46,7 +47,7 @@ export function ttlCollection(
if (pendingPurge.length > 0) {
const now = timestamp();
nextPurge = Math.max(now, pendingPurge[0].expiry + slack);
task = setTimeout(() => {
task = setFocusTimeout(() => {
const now = timestamp();
let cnt = 0;
for (const entry of pendingPurge) {
Expand Down
15 changes: 15 additions & 0 deletions test/spec/auctionmanager_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {PrebidServer} from '../../modules/prebidServerBidAdapter/index.js';
import '../../modules/currency.js'
import { setConfig as setCurrencyConfig } from '../../modules/currency.js';
import { REJECTION_REASON } from '../../src/constants.js';
import { setDocumentHidden } from './unit/utils/focusTimeout_spec.js';

/**
* @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
Expand Down Expand Up @@ -888,6 +889,20 @@ describe('auctionmanager.js', function () {
})
});

it('are not dropped after `minBidCacheTTL` seconds if the page was hidden', () => {
auction.callBids();
config.setConfig({
minBidCacheTTL: 10
});
return auction.end.then(() => {
expect(auctionManager.getNoBids().length).to.eql(1);
setDocumentHidden(true);
clock.tick(10 * 10000);
setDocumentHidden(false);
expect(auctionManager.getNoBids().length).to.eql(1);
})
});

Object.entries({
'bids': {
bd: [{
Expand Down
60 changes: 60 additions & 0 deletions test/spec/unit/utils/focusTimeout_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import setFocusTimeout from '../../../../src/utils/focusTimeout';

export const setDocumentHidden = (hidden) => {
Object.defineProperty(document, 'hidden', {
configurable: true,
get: () => hidden,
});
document.dispatchEvent(new Event('visibilitychange'));
};

describe('focusTimeout', () => {
let clock;

beforeEach(() => {
clock = sinon.useFakeTimers();
});

afterEach(() => {
clock.restore();
})

it('should invoke callback when page is visible', () => {
let callback = sinon.stub();
setFocusTimeout(callback, 2000);
clock.tick(2000);
expect(callback.called).to.be.true;
});

it('should not invoke callback if page was hidden', () => {
let callback = sinon.stub();
setFocusTimeout(callback, 2000);
setDocumentHidden(true);
clock.tick(3000);
expect(callback.called).to.be.false;
});

it('should defer callback execution when page is hidden', () => {
let callback = sinon.stub();
setFocusTimeout(callback, 4000);
clock.tick(2000);
setDocumentHidden(true);
clock.tick(2000);
setDocumentHidden(false);
expect(callback.called).to.be.false;
clock.tick(2000);
expect(callback.called).to.be.true;
});

it('should return updated timerId after page was showed again', () => {
let callback = sinon.stub();
const getCurrentTimerId = setFocusTimeout(callback, 4000);
const oldTimerId = getCurrentTimerId();
clock.tick(2000);
setDocumentHidden(true);
clock.tick(2000);
setDocumentHidden(false);
const newTimerId = getCurrentTimerId();
expect(oldTimerId).to.not.equal(newTimerId);
});
});

0 comments on commit 93039a4

Please sign in to comment.