Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Continuity: Make TTI calculation more reliable and performant #326

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 146 additions & 74 deletions plugins/continuity.js
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,113 @@
}
/* END_DEBUG */

//
// Constants
//
/**
* Number of "idle" intervals (of COLLECTION_INTERVAL ms) before
* Time to Interactive is called.
*
* 5 * 100 = 500ms (of no long tasks > 50ms and FPS >= 20)
*/
var TIME_TO_INTERACTIVE_IDLE_INTERVALS = 5;

/**
* For Time to Interactive, minimum FPS.
*
* ~20 FPS or max ~50ms blocked
*/
var TIME_TO_INTERACTIVE_MIN_FPS = 20;

/**
* For Time to Interactive, minimum FPS per COLLECTION_INTERVAL.
*/
var TIME_TO_INTERACTIVE_MIN_FPS_PER_INTERVAL =
TIME_TO_INTERACTIVE_MIN_FPS / (1000 / COLLECTION_INTERVAL);

/**
* For Time to Interactive, max Page Busy (if LongTasks aren't supported)
*
* ~50%
*/
var TIME_TO_INTERACTIVE_MAX_PAGE_BUSY = 50;

/**
* Determines TTI based on input timestamps, buckets and data
*
* @param {number} startTime Start time
* @param {number} visuallyReady Visually Ready time
* @param {number} startBucket Start bucket
* @param {number} endBucket End bucket
* @param {number} idleIntervals Idle intervals to start with
* @param {object} data Long Task, FPS, Busy and Interaction Data buckets
*/
function determineTti(startTime, visuallyReady, startBucket, endBucket, idleIntervals, data) {
var tti = 0, lastBucketVisited = startBucket, haveSeenBusyData = false;

for (var j = startBucket; j <= endBucket; j++) {
lastBucketVisited = j;

if (data.longtask && data.longtask[j]) {
// had a long task during this interval
idleIntervals = 0;
continue;
}

if (data.fps && (!data.fps[j] || data.fps[j] < TIME_TO_INTERACTIVE_MIN_FPS_PER_INTERVAL)) {
// No FPS or less than 20 FPS during this interval
idleIntervals = 0;
continue;
}

if (data.busy) {
// Page Busy monitor is activated

if (haveSeenBusyData && typeof data.busy[j] === "undefined") {
// We saw previous Busy data, but no Busy data filled in for this bucket yet!
// Break and try again later.
// This could happen if the PageBusyMonitor timer hasn't fired for this bucket yet.
lastBucketVisited--;
break;
}
else if (!haveSeenBusyData && typeof data.busy[j] !== "undefined") {
haveSeenBusyData = true;
}

if (data.busy[j] > TIME_TO_INTERACTIVE_MAX_PAGE_BUSY) {
// Too busy
idleIntervals = 0;
continue;
}
}

if (data.interdly && data.interdly[j]) {
// a delayed interaction happened
idleIntervals = 0;
continue;
}

// this was an idle interval
idleIntervals++;

// if we've found enough idle intervals, mark TTI as the beginning
// of this idle period
if (idleIntervals >= TIME_TO_INTERACTIVE_IDLE_INTERVALS) {
tti = startTime + ((j + 1 - TIME_TO_INTERACTIVE_IDLE_INTERVALS) * COLLECTION_INTERVAL);

// ensure we don't set TTI before TTVR
tti = Math.max(tti, visuallyReady);
break;
}
}

return {
tti: tti,
idleIntervals: idleIntervals,
lastBucketVisited: lastBucketVisited
};
}

/**
* Timeline data
*
Expand All @@ -1196,37 +1303,6 @@
* * Calculates Time to Interactive (TTI) and Visually Ready.
*/
var Timeline = function(startTime) {
//
// Constants
//
/**
* Number of "idle" intervals (of COLLECTION_INTERVAL ms) before
* Time to Interactive is called.
*
* 5 * 100 = 500ms (of no long tasks > 50ms and FPS >= 20)
*/
var TIME_TO_INTERACTIVE_IDLE_INTERVALS = 5;

/**
* For Time to Interactive, minimum FPS.
*
* ~20 FPS or max ~50ms blocked
*/
var TIME_TO_INTERACTIVE_MIN_FPS = 20;

/**
* For Time to Interactive, minimum FPS per COLLECTION_INTERVAL.
*/
var TIME_TO_INTERACTIVE_MIN_FPS_PER_INTERVAL =
TIME_TO_INTERACTIVE_MIN_FPS / (1000 / COLLECTION_INTERVAL);

/**
* For Time to Interactive, max Page Busy (if LongTasks aren't supported)
*
* ~50%
*/
var TIME_TO_INTERACTIVE_MAX_PAGE_BUSY = 50;

//
// Local Members
//
Expand All @@ -1252,6 +1328,12 @@
// whether or not to add Visually Ready to the next beacon
var addVisuallyReadyToBeacon = true;

// last bucket that was analyzed for TTI
var lastBucketVisited = false;

// number of idle intervals up to lastBucketVisited
var idleIntervals = 0;

// check for pre-Boomerang FPS log
if (BOOMR.fpsLog && BOOMR.fpsLog.length) {
// start at the first frame instead of now
Expand Down Expand Up @@ -1640,9 +1722,7 @@
* @param {number} timeOfLastBeacon Time we last sent a beacon
*/
function analyze(timeOfLastBeacon) {
var endBucket = getTimeBucket(),
j = 0,
idleIntervals = 0;
var endBucket = getTimeBucket();

// add log
if (impl.sendLog && typeof timeOfLastBeacon !== "undefined") {
Expand Down Expand Up @@ -1687,51 +1767,32 @@
}

// determine the first bucket we'd use
var startBucket = Math.floor((visuallyReady - startTime) / COLLECTION_INTERVAL);

for (j = startBucket; j <= endBucket; j++) {
if (data.longtask && data.longtask[j]) {
// had a long task during this interval
idleIntervals = 0;
continue;
}

if (data.fps && (!data.fps[j] || data.fps[j] < TIME_TO_INTERACTIVE_MIN_FPS_PER_INTERVAL)) {
// No FPS or less than 20 FPS during this interval
idleIntervals = 0;
continue;
}
var startBucket;

if (data.busy && (data.busy[j] > TIME_TO_INTERACTIVE_MAX_PAGE_BUSY)) {
// Too busy
idleIntervals = 0;
continue;
}
if (lastBucketVisited === false) {
// haven't gone over any buckets yet
startBucket = Math.max(Math.floor((visuallyReady - startTime) / COLLECTION_INTERVAL), 0);
}
else {
// already looked at some buckets, continue with the next one
startBucket = lastBucketVisited + 1;
}

if (data.interdly && data.interdly[j]) {
// a delayed interaction happened
idleIntervals = 0;
continue;
}
// calculate TTI
var results = determineTti(startTime, visuallyReady, startBucket, endBucket, idleIntervals, data);

// this was an idle interval
idleIntervals++;
if (results) {
// save results for next time
idleIntervals = results.idleIntervals;
lastBucketVisited = results.lastBucketVisited;

// if we've found enough idle intervals, mark TTI as the beginning
// of this idle period
if (idleIntervals >= TIME_TO_INTERACTIVE_IDLE_INTERVALS) {
tti = startTime + ((j - TIME_TO_INTERACTIVE_IDLE_INTERVALS) * COLLECTION_INTERVAL);
// we were able to calculate a TTI
if (results.tti > 0) {
tti = results.tti;

// ensure we don't set TTI before TTVR
tti = Math.max(tti, visuallyReady);
break;
impl.addToBeacon("c.tti", externalMetrics.timeToInteractive());
}
}

// we were able to calculate a TTI
if (tti > 0) {
impl.addToBeacon("c.tti", externalMetrics.timeToInteractive());
}
}

//
Expand Down Expand Up @@ -1807,7 +1868,17 @@
// clear the buckets
for (var type in data) {
if (data.hasOwnProperty(type)) {
data[type] = [];

if (!tti && lastBucketVisited !== false) {
// if we haven't calculated tti yet, keep enough data around so we can continue trying the calc
var oldData = data[type];
var newData = new Array(lastBucketVisited + 1);
data[type] = newData.concat(oldData.slice(lastBucketVisited + 1));
}
else {
// start fresh
data[type] = [];
}
}
}

Expand Down Expand Up @@ -4483,7 +4554,8 @@
compressBucketLog: compressBucketLog,
decompressBucketLog: decompressBucketLog,
decompressBucketLogNumber: decompressBucketLogNumber,
decompressLog: decompressLog
decompressLog: decompressLog,
determineTti: determineTti
/* END_DEBUG */
};
}());
Loading