Automatic update from web-platform-tests Reland "Skip static animations on compositor and main" This is a reland of commit 3690cd7872423e81e74dc66f9886e355a7c1b659 The two tests that regressed have been dealt with. One already had equivalent test functionality in WPT and could simply be deleted with no loss of test coverage. The other was ported from a non-WPT testRunner based pixel test to a WPT ref-test. Original change's description: > Skip static animations on compositor and main > > An animation with empty keyframes or with keyframes whose property > values are identical between frames previously ran as a normal > animation. As there is no visual change other than at a phase boundary, > these animations do not need to run on the compositor, and only need > to be scheduled on the main thread at phase boundaries. > > This is the first step in optimizing no-op animations. A follow up > patch will short-circuit sampling when the property value is constant. > Another patch will refine the rules for background color compositing > to only consider dynamic properties. Finally, metrics will be added > to track shorthand expansion of CSS properties, which is likely the > most common source of static properties in CSS animations. > > Bug: 40728212 > Change-Id: I1baff2d0d26400f91f12d4cac2141e9bb101b3d0 > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5328026 > Reviewed-by: Ian Clelland <iclelland@chromium.org> > Reviewed-by: Robert Flack <flackr@chromium.org> > Commit-Queue: Kevin Ellis <kevers@chromium.org> > Cr-Commit-Position: refs/heads/main@{#1268998} Bug: 40728212 Change-Id: Ibc3ab166567b93175c7a2d1da21092e87e9d30ff Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5359414 Commit-Queue: Kevin Ellis <kevers@chromium.org> Reviewed-by: Robert Flack <flackr@chromium.org> Reviewed-by: Ian Clelland <iclelland@chromium.org> Cr-Commit-Position: refs/heads/main@{#1272150} -- wpt-commits: 0d7498fd1f531b56b7d3f4476f7529d78779faa1 wpt-pr: 45021
343 lines
11 KiB
JavaScript
343 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
const MS_PER_SEC = 1000;
|
|
|
|
// The recommended minimum precision to use for time values[1].
|
|
//
|
|
// [1] https://drafts.csswg.org/web-animations/#precision-of-time-values
|
|
const TIME_PRECISION = 0.0005; // ms
|
|
|
|
// Allow implementations to substitute an alternative method for comparing
|
|
// times based on their precision requirements.
|
|
if (!window.assert_times_equal) {
|
|
window.assert_times_equal = (actual, expected, description) => {
|
|
assert_approx_equals(actual, expected, TIME_PRECISION * 2, description);
|
|
};
|
|
}
|
|
|
|
// Allow implementations to substitute an alternative method for comparing
|
|
// times based on their precision requirements.
|
|
if (!window.assert_time_greater_than_equal) {
|
|
window.assert_time_greater_than_equal = (actual, expected, description) => {
|
|
assert_greater_than_equal(actual, expected - 2 * TIME_PRECISION,
|
|
description);
|
|
};
|
|
}
|
|
|
|
// Allow implementations to substitute an alternative method for comparing
|
|
// a time value based on its precision requirements with a fixed value.
|
|
if (!window.assert_time_equals_literal) {
|
|
window.assert_time_equals_literal = (actual, expected, description) => {
|
|
if (Math.abs(expected) === Infinity) {
|
|
assert_equals(actual, expected, description);
|
|
} else {
|
|
assert_approx_equals(actual, expected, TIME_PRECISION, description);
|
|
}
|
|
}
|
|
}
|
|
|
|
// creates div element, appends it to the document body and
|
|
// removes the created element during test cleanup
|
|
function createDiv(test, doc) {
|
|
return createElement(test, 'div', doc);
|
|
}
|
|
|
|
// creates element of given tagName, appends it to the document body and
|
|
// removes the created element during test cleanup
|
|
// if tagName is null or undefined, returns div element
|
|
function createElement(test, tagName, doc) {
|
|
if (!doc) {
|
|
doc = document;
|
|
}
|
|
const element = doc.createElement(tagName || 'div');
|
|
doc.body.appendChild(element);
|
|
test.add_cleanup(() => {
|
|
element.remove();
|
|
});
|
|
return element;
|
|
}
|
|
|
|
// Creates a style element with the specified rules, appends it to the document
|
|
// head and removes the created element during test cleanup.
|
|
// |rules| is an object. For example:
|
|
// { '@keyframes anim': '' ,
|
|
// '.className': 'animation: anim 100s;' };
|
|
// or
|
|
// { '.className1::before': 'content: ""; width: 0px; transition: all 10s;',
|
|
// '.className2::before': 'width: 100px;' };
|
|
// The object property name could be a keyframes name, or a selector.
|
|
// The object property value is declarations which are property:value pairs
|
|
// split by a space.
|
|
function createStyle(test, rules, doc) {
|
|
if (!doc) {
|
|
doc = document;
|
|
}
|
|
const extraStyle = doc.createElement('style');
|
|
doc.head.appendChild(extraStyle);
|
|
if (rules) {
|
|
const sheet = extraStyle.sheet;
|
|
for (const selector in rules) {
|
|
sheet.insertRule(`${selector}{${rules[selector]}}`,
|
|
sheet.cssRules.length);
|
|
}
|
|
}
|
|
test.add_cleanup(() => {
|
|
extraStyle.remove();
|
|
});
|
|
}
|
|
|
|
// Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1).
|
|
function cubicBezier(x1, y1, x2, y2) {
|
|
const xForT = t => {
|
|
const omt = 1-t;
|
|
return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t;
|
|
};
|
|
|
|
const yForT = t => {
|
|
const omt = 1-t;
|
|
return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t;
|
|
};
|
|
|
|
const tForX = x => {
|
|
// Binary subdivision.
|
|
let mint = 0, maxt = 1;
|
|
for (let i = 0; i < 30; ++i) {
|
|
const guesst = (mint + maxt) / 2;
|
|
const guessx = xForT(guesst);
|
|
if (x < guessx) {
|
|
maxt = guesst;
|
|
} else {
|
|
mint = guesst;
|
|
}
|
|
}
|
|
return (mint + maxt) / 2;
|
|
};
|
|
|
|
return x => {
|
|
if (x == 0) {
|
|
return 0;
|
|
}
|
|
if (x == 1) {
|
|
return 1;
|
|
}
|
|
return yForT(tForX(x));
|
|
};
|
|
}
|
|
|
|
function stepEnd(nsteps) {
|
|
return x => Math.floor(x * nsteps) / nsteps;
|
|
}
|
|
|
|
function stepStart(nsteps) {
|
|
return x => {
|
|
const result = Math.floor(x * nsteps + 1.0) / nsteps;
|
|
return (result > 1.0) ? 1.0 : result;
|
|
};
|
|
}
|
|
|
|
function waitForAnimationFrames(frameCount) {
|
|
return new Promise(resolve => {
|
|
function handleFrame() {
|
|
if (--frameCount <= 0) {
|
|
resolve();
|
|
} else {
|
|
window.requestAnimationFrame(handleFrame); // wait another frame
|
|
}
|
|
}
|
|
window.requestAnimationFrame(handleFrame);
|
|
});
|
|
}
|
|
|
|
// Continually calls requestAnimationFrame until |minDelay| has elapsed
|
|
// as recorded using document.timeline.currentTime (i.e. frame time not
|
|
// wall-clock time).
|
|
function waitForAnimationFramesWithDelay(minDelay) {
|
|
const startTime = document.timeline.currentTime;
|
|
return new Promise(resolve => {
|
|
(function handleFrame() {
|
|
if (document.timeline.currentTime - startTime >= minDelay) {
|
|
resolve();
|
|
} else {
|
|
window.requestAnimationFrame(handleFrame);
|
|
}
|
|
}());
|
|
});
|
|
}
|
|
|
|
function runAndWaitForFrameUpdate(callback) {
|
|
return new Promise(resolve => {
|
|
window.requestAnimationFrame(() => {
|
|
callback();
|
|
window.requestAnimationFrame(resolve);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Waits for a requestAnimationFrame callback in the next refresh driver tick.
|
|
function waitForNextFrame() {
|
|
const timeAtStart = document.timeline.currentTime;
|
|
return new Promise(resolve => {
|
|
(function handleFrame() {
|
|
if (timeAtStart === document.timeline.currentTime) {
|
|
window.requestAnimationFrame(handleFrame);
|
|
} else {
|
|
resolve();
|
|
}
|
|
}());
|
|
});
|
|
}
|
|
|
|
async function insertFrameAndAwaitLoad(test, iframe, doc) {
|
|
const eventWatcher = new EventWatcher(test, iframe, ['load']);
|
|
const event_promise = eventWatcher.wait_for('load');
|
|
|
|
doc.body.appendChild(iframe);
|
|
test.add_cleanup(() => { doc.body.removeChild(iframe); });
|
|
|
|
await event_promise;
|
|
}
|
|
|
|
// Returns 'matrix()' or 'matrix3d()' function string generated from an array.
|
|
function createMatrixFromArray(array) {
|
|
return (array.length == 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`;
|
|
}
|
|
|
|
// Returns 'matrix3d()' function string equivalent to
|
|
// 'rotate3d(x, y, z, radian)'.
|
|
function rotate3dToMatrix3d(x, y, z, radian) {
|
|
return createMatrixFromArray(rotate3dToMatrix(x, y, z, radian));
|
|
}
|
|
|
|
// Returns an array of the 4x4 matrix equivalent to 'rotate3d(x, y, z, radian)'.
|
|
// https://drafts.csswg.org/css-transforms-2/#Rotate3dDefined
|
|
function rotate3dToMatrix(x, y, z, radian) {
|
|
const sc = Math.sin(radian / 2) * Math.cos(radian / 2);
|
|
const sq = Math.sin(radian / 2) * Math.sin(radian / 2);
|
|
|
|
// Normalize the vector.
|
|
const length = Math.sqrt(x*x + y*y + z*z);
|
|
x /= length;
|
|
y /= length;
|
|
z /= length;
|
|
|
|
return [
|
|
1 - 2 * (y*y + z*z) * sq,
|
|
2 * (x * y * sq + z * sc),
|
|
2 * (x * z * sq - y * sc),
|
|
0,
|
|
2 * (x * y * sq - z * sc),
|
|
1 - 2 * (x*x + z*z) * sq,
|
|
2 * (y * z * sq + x * sc),
|
|
0,
|
|
2 * (x * z * sq + y * sc),
|
|
2 * (y * z * sq - x * sc),
|
|
1 - 2 * (x*x + y*y) * sq,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
1
|
|
];
|
|
}
|
|
|
|
// Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)' with tolerances.
|
|
function assert_matrix_equals(actual, expected, description) {
|
|
const matrixRegExp = /^matrix(?:3d)*\((.+)\)/;
|
|
assert_regexp_match(actual, matrixRegExp,
|
|
'Actual value is not a matrix')
|
|
assert_regexp_match(expected, matrixRegExp,
|
|
'Expected value is not a matrix');
|
|
|
|
const actualMatrixArray =
|
|
actual.match(matrixRegExp)[1].split(',').map(Number);
|
|
const expectedMatrixArray =
|
|
expected.match(matrixRegExp)[1].split(',').map(Number);
|
|
|
|
assert_equals(actualMatrixArray.length, expectedMatrixArray.length,
|
|
`dimension of the matrix: ${description}`);
|
|
for (let i = 0; i < actualMatrixArray.length; i++) {
|
|
assert_approx_equals(actualMatrixArray[i], expectedMatrixArray[i], 0.0001,
|
|
`expected ${expected} but got ${actual}: ${description}`);
|
|
}
|
|
}
|
|
|
|
// Compare rotate3d vector like '0 1 0 45deg' with tolerances.
|
|
function assert_rotate3d_equals(actual, expected, description) {
|
|
const rotationRegExp =/^((([+-]?\d+(\.+\d+)?\s){3})?\d+(\.+\d+)?)deg/;
|
|
|
|
assert_regexp_match(actual, rotationRegExp,
|
|
'Actual value is not a rotate3d vector')
|
|
assert_regexp_match(expected, rotationRegExp,
|
|
'Expected value is not a rotate3d vector');
|
|
|
|
const actualRotationVector =
|
|
actual.match(rotationRegExp)[1].split(' ').map(Number);
|
|
const expectedRotationVector =
|
|
expected.match(rotationRegExp)[1].split(' ').map(Number);
|
|
|
|
assert_equals(actualRotationVector.length, expectedRotationVector.length,
|
|
`dimension of the matrix: ${description}`);
|
|
for (let i = 0; i < actualRotationVector.length; i++) {
|
|
assert_approx_equals(
|
|
actualRotationVector[i],
|
|
expectedRotationVector[i],
|
|
0.0001,
|
|
`expected ${expected} but got ${actual}: ${description}`);
|
|
}
|
|
}
|
|
|
|
function assert_phase_at_time(animation, phase, currentTime) {
|
|
animation.currentTime = currentTime;
|
|
assert_phase(animation, phase);
|
|
}
|
|
|
|
function assert_phase(animation, phase) {
|
|
const fillMode = animation.effect.getTiming().fill;
|
|
const currentTime = animation.currentTime;
|
|
|
|
if (phase === 'active') {
|
|
// If the fill mode is 'none', then progress will only be non-null if we
|
|
// are in the active phase.
|
|
animation.effect.updateTiming({ fill: 'none' });
|
|
assert_not_equals(animation.effect.getComputedTiming().progress, null,
|
|
'Animation effect is in active phase when current time ' +
|
|
`is ${currentTime}.`);
|
|
} else {
|
|
// The easiest way to distinguish between the 'before' phase and the 'after'
|
|
// phase is to toggle the fill mode. For example, if the progress is null
|
|
// when the fill mode is 'none' but non-null when the fill mode is
|
|
// 'backwards' then we are in the before phase.
|
|
animation.effect.updateTiming({ fill: 'none' });
|
|
assert_equals(animation.effect.getComputedTiming().progress, null,
|
|
`Animation effect is in ${phase} phase when current time ` +
|
|
`is ${currentTime} (progress is null with 'none' fill mode)`);
|
|
|
|
animation.effect.updateTiming({
|
|
fill: phase === 'before' ? 'backwards' : 'forwards',
|
|
});
|
|
assert_not_equals(animation.effect.getComputedTiming().progress, null,
|
|
`Animation effect is in ${phase} phase when current ` +
|
|
`time is ${currentTime} (progress is non-null with ` +
|
|
`appropriate fill mode)`);
|
|
}
|
|
|
|
// Reset fill mode to avoid side-effects.
|
|
animation.effect.updateTiming({ fill: fillMode });
|
|
}
|
|
|
|
|
|
// Use with reftest-wait to wait until compositor commits are no longer deferred
|
|
// before taking the screenshot.
|
|
// crbug.com/1378671
|
|
async function waitForCompositorReady() {
|
|
const animation =
|
|
document.body.animate({ opacity: [ 0, 1 ] }, {duration: 1 });
|
|
return animation.finished;
|
|
}
|
|
|
|
async function takeScreenshotOnAnimationsReady() {
|
|
await Promise.all(document.getAnimations().map(a => a.ready));
|
|
requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
|
|
}
|
|
|