Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env mozilla/frame-script */
// Note: This file is used at both tscrollx and tp5o_scroll. With the former as
// unprivileged code.
// - Please make sure that any changes apply cleanly to all use cases.
function testScroll(target, stepSize, opt_reportFunc, opt_numSteps) {
var win;
if (target == "content") {
target = content.wrappedJSObject;
win = content;
} else {
win = window;
}
var result = {
names: [],
values: [],
};
// We report multiple results, so we base the name on the path.
// Everything after '/tp5n/' if exists (for tp5o_scroll), or the file name at
// the path if non-empty (e.g. with tscrollx), or the last dir otherwise (e.g.
// 'mydir' for 'http://my.domain/dir1/mydir/').
var href = win.location.href;
var testBaseName =
href.split("/tp5n/")[1] ||
href.split("/").pop() ||
href.split("/").splice(-2, 1)[0] ||
"REALLY_WEIRD_URI";
// Verbatim copy from talos-powers/content/TalosPowersContent.js
// If the origin changes, this copy should be updated.
TalosPowersParent = {
replyId: 1,
// dispatch an event to the framescript and register the result/callback event
exec(commandName, arg, callback, opt_custom_window) {
let win = opt_custom_window || window;
let replyEvent = "TalosPowers:ParentExec:ReplyEvent:" + this.replyId++;
if (callback) {
win.addEventListener(
replyEvent,
function (e) {
callback(e.detail);
},
{ once: true }
);
}
win.dispatchEvent(
new win.CustomEvent("TalosPowers:ParentExec:QueryEvent", {
bubbles: true,
detail: {
command: {
name: commandName,
data: arg,
},
listeningTo: replyEvent,
},
})
);
},
};
// End of code from talos-powers
var report;
/**
* Sets up the value of 'report' as a function for reporting the test result[s].
* Chooses between the "usual" tpRecordTime which the pageloader addon injects
* to pages, or a custom function in case we're a framescript which pageloader
* added to the tested page, or a debug tpRecordTime from talos-debug.js if
* running in a plain browser.
*
* @returns Promise
*/
function P_setupReportFn() {
return new Promise(function (resolve) {
report = opt_reportFunc || win.tpRecordTime;
if (report == "PageLoader:RecordTime") {
report = function (duration, start, name) {
var msg = { time: duration, startTime: start, testName: name };
sendAsyncMessage("PageLoader:RecordTime", msg);
};
resolve();
return;
}
// Not part of the test and does nothing if we're within talos.
// Provides an alternative tpRecordTime (with some stats display) if running in a browser.
if (!report && document.head) {
var imported = document.createElement("script");
imported.addEventListener("load", function () {
report = tpRecordTime;
resolve();
});
imported.src =
"../../scripts/talos-debug.js?dummy=" + win.performance.now(); // For some browsers to re-read
document.head.appendChild(imported);
return;
}
resolve();
});
}
function FP_wait(ms) {
return function () {
return new Promise(function (resolve) {
win.setTimeout(resolve, ms);
});
};
}
function rAF(fn) {
return win.requestAnimationFrame(fn);
}
function P_rAF() {
return new Promise(function (resolve) {
rAF(resolve);
});
}
function P_MozAfterPaint() {
return new Promise(function (resolve) {
win.addEventListener("MozAfterPaint", () => resolve(), { once: true });
});
}
var isWindow = target.self === target;
var getPos = isWindow
? function () {
return target.pageYOffset;
}
: function () {
return target.scrollTop;
};
var gotoTop = isWindow
? function () {
target.scroll(0, 0);
ensureScroll();
}
: function () {
target.scrollTop = 0;
ensureScroll();
};
var doScrollTick = isWindow
? function () {
target.scrollBy(0, stepSize);
ensureScroll();
}
: function () {
target.scrollTop += stepSize;
ensureScroll();
};
var setSmooth = isWindow
? function () {
target.document.scrollingElement.style.scrollBehavior = "smooth";
}
: function () {
target.style.scrollBehavior = "smooth";
};
var gotoBottom = isWindow
? function () {
target.scrollTo(0, target.scrollMaxY);
}
: function () {
target.scrollTop = target.scrollHeight;
};
function ensureScroll() {
// Ensure scroll by reading computed values. screenY is for X11.
if (!this.dummyEnsureScroll) {
this.dummyEnsureScroll = 1;
}
this.dummyEnsureScroll += win.screenY + getPos();
}
// For reference, rAF should fire on vsync, but Gecko currently doesn't use vsync.
// Instead, it uses 1000/layout.frame_rate
// (with 60 as default value when layout.frame_rate == -1).
function P_syncScrollTest() {
return new Promise(function (resolve) {
// We should be at the top of the page now.
var start = win.performance.now();
var lastScrollPos = getPos();
var lastScrollTime = start;
var durations = [];
function tick() {
var now = win.performance.now();
var duration = now - lastScrollTime;
lastScrollTime = now;
durations.push(duration);
doScrollTick();
/* stop scrolling if we can't scroll more, or if we've reached requested number of steps */
if (
getPos() == lastScrollPos ||
(opt_numSteps && durations.length >= opt_numSteps + 2)
) {
let profilerPaused = Promise.resolve();
if (typeof TalosContentProfiler !== "undefined") {
profilerPaused = TalosContentProfiler.subtestEnd(
testBaseName,
true
);
}
profilerPaused.then(() => {
// Note: The first (1-5) intervals WILL be longer than the rest.
// First interval might include initial rendering and be extra slow.
// Also requestAnimationFrame needs to sync (optimally in 1 frame) after long frames.
// Suggested: Ignore the first 5 intervals.
durations.pop(); // Last step was 0.
durations.pop(); // and the prev one was shorter and with end-of-page logic, ignore both.
if (win.talosDebug) {
win.talosDebug.displayData = true;
} // In a browser: also display all data points.
// For analysis (otherwise, it's too many data points for talos):
var sum = 0;
for (var i = 0; i < durations.length; i++) {
sum += Number(durations[i]);
}
// Report average interval or (failsafe) 0 if no intervls were recorded
result.values.push(durations.length ? sum / durations.length : 0);
result.names.push(testBaseName);
resolve();
});
return;
}
lastScrollPos = getPos();
P_MozAfterPaint().then(tick);
}
if (typeof TalosContentProfiler !== "undefined") {
TalosContentProfiler.subtestStart(testBaseName, true).then(() => {
rAF(tick);
});
}
});
}
function P_testAPZScroll() {
var APZ_MEASURE_MS = 1000;
function startFrameTimeRecording(cb) {
TalosPowersParent.exec("startFrameTimeRecording", null, cb, win);
}
function stopFrameTimeRecording(handle, cb) {
TalosPowersParent.exec("stopFrameTimeRecording", handle, cb, win);
}
return new Promise(function (resolve) {
setSmooth();
var handle = -1;
startFrameTimeRecording(function (rv) {
handle = rv;
});
// Get the measurements after APZ_MEASURE_MS of scrolling
win.setTimeout(function () {
stopFrameTimeRecording(handle, function (intervals) {
function average(arr) {
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
return arr.length ? sum / arr.length : 0;
}
// remove two frames on each side of the recording to get a cleaner result
result.values.push(average(intervals.slice(2, intervals.length - 2)));
result.names.push("CSSOM." + testBaseName);
resolve();
});
}, APZ_MEASURE_MS);
gotoBottom(); // trigger the APZ scroll
});
}
P_setupReportFn()
.then(FP_wait(260))
.then(gotoTop)
.then(P_rAF)
.then(P_syncScrollTest)
.then(gotoTop)
.then(FP_wait(260))
.then(P_testAPZScroll)
.then(function () {
report(result.values.join(","), 0, result.names.join(","));
});
}
// This code below here is unique to tscroll.js inside of pageloader
try {
function handleMessageFromChrome(message) {
var payload = message.data.details;
testScroll(
payload.target,
payload.stepSize,
"PageLoader:RecordTime",
payload.opt_numSteps
);
}
addMessageListener("PageLoader:ScrollTest", handleMessageFromChrome);
} catch (e) {}