// TODO: // V - Read transition duration from style instead of hardcoded 250ms (mconley) // V - Remove caret from tart.html (unfocus url bar) // V - Add fade-only tests (added icon @DPI2/current, simple @dpi current, lastTab @current. only icon@dpi2 runs on talos by default) // V - Output JSON results (OK to log, check with MattN if that's enough) // V - UI: Add tests descriptions, button to deselect all, Notice about profiling and Accurate 1st frame, copy JSON to clipboard. // V - Add profiler markers as start/done:[test-name] // V - Add custom subtests for talos by URL (tart.html) // V - Change repeat order from column to row major // (now repeats each test N times then advances to next test - instead of repeating the entire set N times) // V - Objectify // V - make sure of window size (use pinned tart tab - enough even in scale 2 with australis) // V - Make sure test-cases work on australis (min tab width for 8th tab) // V - pref - disable paint starvation: docshell.event_starvation_delay_hint=1 -> will be set by talos or by the user // V - For manual test: display: relevent prefs instructions (rate, starve hint), instructions for many-tabs // V - allow API-less intervals recording (using rAF, since the API is b0rked with OMTC) // (but using rAF appears to record spurious very short intervals which I think are not really frames) // V - Optimize cases (minimize test count without hurting cover): // - v1: only single tab, open + close, DPI1: about:blank, with-icon-and-long-title. DPI2: only the icon case // V - Log output // X - Support scroll into view (no end event) // X - have preview images: hard to make work across versions // - Tests: // V - With favicon // V - Different DPIs // V - with fade only (no tab open/close overhead) // X - tab drag // X - tab remove from the middle // X - Without add-tab button -> can be hidden while testing manually. in talos always with the button
ChromeUtils.defineESModuleGetters(this, {
AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
});
/* globals res:true, sequenceArray:true */
function Tart() {}
Tart.prototype = { // Detector methods expect 'this' to be the detector object. // Detectors must support measureNow(e) which indicates to collect the intervals and stop the recording. // Detectors may support keepListening(e) to indicate to keep waiting before continuing to the next animation.
// Same as customizeEnterDetector, but stops recording when the CSS animation ends // The detector then waits until customizationready
customizeEnterCssDetector: {
arm(handler, win) {
win.gNavToolbox.addEventListener("customizationready", handler);
win.gNavToolbox.addEventListener("customization-transitionend", handler);
},
clickNewTab() { this._endDetection = this.tabDetector; this._win.BrowserCommands.openTab(); // Modifying the style for each tab right after opening seems like it could regress performance, // However, overlaying a global style over browser.xhtml actually ends up having greater ovrehead, // especially while closing the last of many tabs (a noticeable ~250ms delay before expanding the rest). // To overlay the style globally, add at tart/chrome.manifest: // style chrome://browser/content/browser.xhtml chrome://tart/content/tab-min-width-1px.css // where the file tab-min-width-1px.css is: // .tabbrowser-tab[fadein]:not([pinned]) { min-width: 1px !important; } // Additionally, the global style overlay apparently messes with intervals recording when layout.frame_rate=10000: // Using the startFrameTimeRecording API, the first interval appears extra long (~1000ms) even with much widget tickling, // Per-tab min-width on open it is then.
// --> many-tabs case which requires modified max-width will not go into v1. No need for now. // this._win.gBrowser.selectedTab.style.minWidth = "1px"; // Prevent overflow regrdless of DPI scale.
USE_RECORDING_API: true, // true for Start[/Stop]FrameTimeRecording, otherwise record using rAF - which will also work with OMTC // but (currently) also records iterations without paint invalidations
_animate(
preWaitMs,
triggerFunc,
onDoneCallback,
isReportResult,
name,
referenceDuration
) { var self = this; var recordingHandle; var timeoutId = 0; var detector; // will be assigned after calling trigger. var rAF = window.requestAnimationFrame || window.mozRequestAnimationFrame;
var _recording = []; var _abortRecording = false; var startRecordTimestamp; function startRecord() { if (self._config.controlProfiler) { if (isReportResult) {
Profiler.subtestStart(name);
}
} else {
Profiler.mark("Start: " + (isReportResult ? name : "[warmup]"), true);
}
startRecordTimestamp = window.performance.now(); if (self.USE_RECORDING_API) { return window.windowUtils.startFrameTimeRecording();
}
_recording = [];
_abortRecording = false;
var last = performance.now(); function rec() { // self._win.getComputedStyle(self._win.gBrowser.selectedTab).width; // force draw - not good - too much regression if (_abortRecording) { return;
}
var now = performance.now();
_recording.push(now - last);
last = now;
rAF(rec);
}
rAF(rec);
return 1; // dummy
}
var recordingAbsoluteDuration; function stopRecord() {
recordingAbsoluteDuration =
window.performance.now() - startRecordTimestamp; if (self._config.controlProfiler) { if (isReportResult) {
Profiler.subtestEnd(name);
}
} else {
Profiler.mark("End: " + (isReportResult ? name : "[warmup]"), true);
} if (self.USE_RECORDING_API) { return window.windowUtils.stopFrameTimeRecording(recordingHandle);
}
function addResult(intervals) { // For each animation sequence (intervals) we report 3 values: // 1. Average interval for the last 50% of the reference duration // (e.g. if the reference duration is 250ms then average of the last 125ms, // regardless of the actual animation duration in practice) // 2. Average interval for the entire animation. // 3. Absolute difference between reference duration to actual diration.
var sumLastHalf = 0; var countLastHalf = 0; var sumMost = 0; var sum = 0; for (var i = intervals.length - 1; i >= 0; i--) {
sum += intervals[i]; if (sumLastHalf < referenceDuration / 2) {
sumLastHalf += intervals[i];
countLastHalf++;
} if (sumMost < referenceDuration * 0.85) {
sumMost += intervals[i];
}
}
dump("overall: " + sum + "\n");
var averageLastHalf = countLastHalf ? sumLastHalf / countLastHalf : 0; var averageOverall = intervals.length ? sum / intervals.length : 0; var durationDiff = Math.abs(
recordingAbsoluteDuration - referenceDuration
);
self._results.push({ name: name + ".raw.TART", value: intervals }); // Just for display if running manually - Not collected for talos.
self._results.push({ name: name + ".half.TART", value: averageLastHalf });
self._results.push({ name: name + ".all.TART", value: averageOverall });
self._results.push({ name: name + ".error.TART", value: durationDiff });
}
function transEnd(e) { if (e) {
let isMeasureNow = detector.measureNow(e);
if (isMeasureNow) { // Get the recorded frame intervals and append result if required
let intervals = stopRecord(); if (isReportResult) {
addResult(intervals);
}
}
// If detector supports keepListening, use it, otherwise - measurement indicates the end. if (
detector.keepListening ? detector.keepListening(e) : !isMeasureNow
) { return;
}
} else { // No event == timeout
dump("TART: TIMEOUT\n");
}
function trigger(f) { if (!self.USE_RECORDING_API) { return rAF(f);
}
// When using the recording API, the first interval is to last frame, which could have been a while ago. // To make sure the first interval is counted correctly, we "tickle" a widget // and immidiatly afterwards start the test, such the last frame timestamp prior to the recording is now. // However, on some systems, modifying the widget once isn't always enough, hence the tickle loop. // var id = "back-button"; var orig = self._win.document.getElementById(id).style.opacity; var i = 0;
function tickleLoop() { if (i++ < (isReportResult && self._config.tickle ? 17 : 0)) {
self._win.document.getElementById(id).style.opacity =
(i % 10) / 10 + 0.05; // just some style modification which will force redraw return rAF(tickleLoop);
}
if (this._onTestComplete) { this._onTestComplete(JSON.parse(JSON.stringify(this._results))); // Clone results
}
},
_startTest() { // Save prefs and states which will change during the test, to get restored when done. var origPreload = Services.prefs.getBoolPref("browser.newtab.preload"); var origDpi = Services.prefs.getCharPref("layout.css.devPixelsPerPx"); var origPinned = this._tartTab.pinned;
var self = this; var animate = this._animate.bind(this); var addTab = this.clickNewTab.bind(this); var closeCurrentTab = this.clickCloseCurrentTab.bind(this); var fadein = this.fadeInCurrentTab.bind(this); var fadeout = this.fadeOutCurrentTab.bind(this);
var addSomeTab = this.addSomeChromeUriTab.bind(this); var customizeEnter = this.triggerCustomizeEnter.bind(this); var customizeEnterCss = this.triggerCustomizeEnterCss.bind(this); var customizeExit = this.triggerCustomizeExit.bind(this);
var next = this._nextCommand.bind(this); var rest = 500; // 500ms default rest before measuring an animation if (this._config.rest) {
rest = this._config.rest;
}
// NOTE: causes a (slow) reflow, don't use right before measurements. function getMaxTabTransitionTimeMs(aTab) {
let cstyle = window.getComputedStyle(aTab); try { return (
1000 *
Math.max.apply( null,
cstyle.transitionDuration.split(", ").map(s => parseFloat(s, 10))
)
);
} catch (e) { return 250;
}
}
this.unpinTart(); var tabRefDuration = getMaxTabTransitionTimeMs(this._tartTab); if (tabRefDuration < 20 || tabRefDuration > 2000) { // Hardcoded fallback in case the value doesn't make sense as tab animation duration.
tabRefDuration = 250;
}
var custRefDuration = 0;
var subtests = {
init: [ // This is called before each subtest, so it's safe to assume the following prefs: function () {
Services.prefs.setBoolPref("browser.newtab.preload", false);
self.pinTart();
self.makeNewTabURLChangePromise("about:blank").then(next);
},
],
restore: [ // Restore prefs which were modified during the test function () {
Services.prefs.setBoolPref("browser.newtab.preload", origPreload);
Services.prefs.setCharPref("layout.css.devPixelsPerPx", origDpi); if (origPinned) {
self.pinTart();
} else {
self.unpinTart();
}
self.makeNewTabURLChangePromise("about:newtab").then(next);
},
],
simple: [ function () {
Services.prefs.setCharPref("layout.css.devPixelsPerPx", "1");
next();
}, function () {
animate(0, addTab, next);
}, function () {
animate(0, closeCurrentTab, next);
},
customize: [ // Test australis customize mode animation with default DPI. function () {
Services.prefs.setCharPref("layout.css.devPixelsPerPx", "-1");
next();
}, // Adding a non-newtab since the behavior of exiting customize mode which was entered on newtab may change. See bug 957202. function () {
animate(0, addSomeTab, next);
},
// The prefixes 1- and 2- were added because talos cuts common prefixes on all "pages", which ends up as "customize-e" prefix. function () {
animate(
rest,
customizeEnter,
next, true, "1-customize-enter",
custRefDuration
);
}, function () {
animate(
rest,
customizeExit,
next, true, "2-customize-exit",
custRefDuration
);
},
// Measures the CSS-animation-only part of entering into customize mode function () {
animate(
rest,
customizeEnterCss,
next, true, "3-customize-enter-css",
custRefDuration
);
}, function () {
animate(0, customizeExit, next);
},
function () {
animate(0, closeCurrentTab, next);
},
],
};
// Construct the sequence array: config.repeat times config.subtests, // where each subtest implicitly starts with init.
sequenceArray = []; for (var i in this._config.subtests) { for (var r = 0; r < this._config.repeat; r++) {
sequenceArray = sequenceArray.concat(subtests.init);
sequenceArray = sequenceArray.concat(
subtests[this._config.subtests[i]]
);
}
}
sequenceArray = sequenceArray.concat(subtests.restore);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.