Mypal/dom/animation/test/chrome/test_animation_performance_...

958 lines
28 KiB
HTML

<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1196114 - Test metadata related to which animation properties
are running on the compositor</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
.compositable {
/* Element needs geometry to be eligible for layerization */
width: 100px;
height: 100px;
background-color: white;
}
@keyframes fade {
from { opacity: 1 }
to { opacity: 0 }
}
</style>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
target="_blank">Mozilla Bug 1196114</a>
<div id="log"></div>
<script>
'use strict';
// This is used for obtaining localized strings.
var gStringBundle;
W3CTest.runner.requestLongerTimeout(2);
SpecialPowers.pushPrefEnv({ "set": [
["general.useragent.locale", "en-US"],
// Need to set devPixelsPerPx explicitly to gain
// consistent pixel values in warning messages
// regardless of platform DPIs.
["layout.css.devPixelsPerPx", 1],
] },
start);
function compare_property_state(a, b) {
if (a.property > b.property) {
return -1;
} else if (a.property < b.property) {
return 1;
}
if (a.runningOnCompositor != b.runningOnCompositor) {
return a.runningOnCompositor ? 1 : -1;
}
return a.warning > b.warning ? -1 : 1;
}
function assert_animation_property_state_equals(actual, expected) {
assert_equals(actual.length, expected.length, 'Number of properties');
var sortedActual = actual.sort(compare_property_state);
var sortedExpected = expected.sort(compare_property_state);
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_equals(sortedActual[i].runningOnCompositor,
sortedExpected[i].runningOnCompositor,
'runningOnCompositor property should match');
if (sortedExpected[i].warning instanceof RegExp) {
assert_regexp_match(sortedActual[i].warning,
sortedExpected[i].warning,
'warning message should match');
} else if (sortedExpected[i].warning) {
assert_equals(sortedActual[i].warning,
gStringBundle.GetStringFromName(sortedExpected[i].warning),
'warning message should match');
}
}
}
// Check that the animation is running on compositor and
// warning property is not set for the CSS property regardless
// expected values.
function assert_property_state_on_compositor(actual, expected) {
assert_equals(actual.length, expected.length);
var sortedActual = actual.sort(compare_property_state);
var sortedExpected = expected.sort(compare_property_state);
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_true(sortedActual[i].runningOnCompositor,
'runningOnCompositor property should be true on ' +
sortedActual[i].property);
assert_not_exists(sortedActual[i], 'warning',
'warning property should not be set');
}
}
var gAnimationsTests = [
{
desc: 'animations on compositor',
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
}
]
},
{
desc: 'animations on main thread',
frames: {
backgroundColor: ['white', 'red']
},
expected: [
{
property: 'background-color',
runningOnCompositor: false
}
]
},
{
desc: 'animations on both threads',
frames: {
backgroundColor: ['white', 'red'],
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'background-color',
runningOnCompositor: false
},
{
property: 'transform',
runningOnCompositor: true
}
]
},
{
desc: 'two animation properties on compositor thread',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true
}
]
},
{
desc: 'opacity on compositor with animation of geometric properties',
frames: {
width: ['100px', '200px'],
opacity: [0, 1]
},
expected: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'opacity',
runningOnCompositor: true
}
]
},
];
// Test cases that check results of adding/removing a 'width' property on the
// same animation object.
var gAnimationWithGeometricKeyframeTests = [
{
desc: 'transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
},
{
desc: 'opacity and transform',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
},
];
// Performance warning tests that set and clear a style property.
var gPerformanceWarningTestsStyle = [
{
desc: 'preserve-3d transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: 'transform-style: preserve-3d',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformPreserve3D'
}
]
},
{
desc: 'transform with backface-visibility:hidden',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: 'backface-visibility: hidden;',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
}
]
},
{
desc: 'opacity and transform with preserve-3d',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
style: 'transform-style: preserve-3d',
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformPreserve3D'
}
]
},
{
desc: 'opacity and transform with backface-visibility:hidden',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
style: 'backface-visibility: hidden;',
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
}
]
},
];
// Performance warning tests that set and clear the id property
var gPerformanceWarningTestsId= [
{
desc: 'moz-element referencing a transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
id: 'transformed',
createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningHasRenderingObserver'
}
]
},
];
var gMultipleAsyncAnimationsTests = [
{
desc: 'opacity and transform with preserve-3d',
style: 'transform-style: preserve-3d',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformPreserve3D'
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
{
desc: 'opacity and transform with backface-visibility:hidden',
style: 'backface-visibility: hidden;',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
];
// Test cases that check results of adding/removing a 'width' keyframe on the
// same animation object, where multiple animation objects belong to the same
// element.
// The 'width' property is added to animations[1].
var gMultipleAsyncAnimationsWithGeometricKeyframeTests = [
{
desc: 'transform and opacity with geometric keyframes',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
},
{
frames: {
opacity: [0, 1]
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false,
},
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
}
],
},
{
desc: 'opacity and transform with geometric keyframes',
animations: [
{
frames: {
opacity: [0, 1]
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
],
withGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
},
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false,
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
}
]
},
];
// Test cases that check results of adding/removing 'width' animation on the
// same element which has async animations.
var gMultipleAsyncAnimationsWithGeometricAnimationTests = [
{
desc: 'transform',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
},
]
},
{
desc: 'opacity',
animations: [
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
}
]
},
]
},
{
desc: 'opacity and transform',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
];
var gAnimationsOnTooSmallElementTests = [
{
desc: 'opacity on too small element',
frames: {
opacity: [0, 1]
},
style: { style: 'width: 8px; height: 8px; background-color: red;' +
// We need to set transform here to try creating an
// individual frame for this opacity element.
// Without this, this small element is created on the same
// nsIFrame of mochitest iframe, i.e. the document which are
// running this test, as a result the layer corresponding
// to the frame is sent to compositor.
'transform: translateX(100px);' },
expected: [
{
property: 'opacity',
runningOnCompositor: false,
warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
}
]
},
{
desc: 'transform on too small element',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: { style: 'width: 8px; height: 8px; background-color: red;' },
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
}
]
},
];
function start() {
var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
.getService(SpecialPowers.Ci.nsIStringBundleService);
gStringBundle = bundleService
.createBundle("chrome://global/locale/layout_errors.properties");
gAnimationsTests.forEach(function(subtest) {
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
gAnimationWithGeometricKeyframeTests.forEach(function(subtest) {
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
// First, a transform animation is running on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withoutGeometric);
}).then(function() {
// Add a 'width' property.
var keyframes = animation.effect.getKeyframes();
keyframes[0].width = '100px';
keyframes[1].width = '200px';
animation.effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Now the transform animation is not running on compositor because of
// the 'width' property.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withGeometric);
}).then(function() {
// Remove the 'width' property.
var keyframes = animation.effect.getKeyframes();
delete keyframes[0].width;
delete keyframes[1].width;
animation.effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Finally, the transform animation is running on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withoutGeometric);
});
}, 'An animation has: ' + subtest.desc);
});
gPerformanceWarningTestsStyle.forEach(function(subtest) {
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.style = subtest.style;
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.style = '';
return waitForFrame();
}).then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
gPerformanceWarningTestsId.forEach(function(subtest) {
promise_test(function(t) {
if (subtest.createelement) {
addDiv(t, { style: subtest.createelement });
}
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.id = subtest.id;
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.id = '';
return waitForFrame();
}).then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
gMultipleAsyncAnimationsTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(function(anim) {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
return waitForAllAnimations(animations).then(function() {
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
div.style = subtest.style;
return waitForFrame();
}).then(function() {
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected);
});
div.style = '';
return waitForFrame();
}).then(function() {
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
});
}, 'Multiple animations: ' + subtest.desc);
});
gMultipleAsyncAnimationsWithGeometricKeyframeTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(function(anim) {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
return waitForAllAnimations(animations).then(function() {
// First, all animations are running on compositor.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withoutGeometric);
});
}).then(function() {
// Add a 'width' property to animations[1].
var keyframes = animations[1].effect.getKeyframes();
keyframes[0].width = '100px';
keyframes[1].width = '200px';
animations[1].effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Now the transform animation is not running on compositor because of
// the 'width' property.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withGeometric);
});
}).then(function() {
// Remove the 'width' property from animations[1].
var keyframes = animations[1].effect.getKeyframes();
delete keyframes[0].width;
delete keyframes[1].width;
animations[1].effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Finally, all animations are running on compositor.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withoutGeometric);
});
});
}, 'Multiple animations with geometric property: ' + subtest.desc);
});
gMultipleAsyncAnimationsWithGeometricAnimationTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(function(anim) {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
var widthAnimation;
return waitForAllAnimations(animations).then(function() {
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
}).then(function() {
// Append 'width' animation on the same element.
widthAnimation = div.animate({ width: ['100px', '200px'] },
100 * MS_PER_SEC);
return waitForFrame();
}).then(function() {
// Now transform animations are not running on compositor because of
// the 'width' animation.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected);
});
// Remove the 'width' animation.
widthAnimation.cancel();
return waitForFrame();
}).then(function() {
// Now all animations are running on compositor.
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
});
}, 'Multiple async animations and geometric animation: ' + subtest.desc);
});
gAnimationsOnTooSmallElementTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, subtest.style);
var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
{ transform: [ 'translate(0px)',
'translate(100px)'] },
100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
animation.effect.target.style = 'width: 10000px; height: 10000px';
return waitForFrame();
}).then(function() {
// viewport depends on test environment.
var expectedWarning = new RegExp(
"Animation cannot be run on the compositor because the frame size " +
"\\(10000, 10000\\) is bigger than the viewport \\(\\d+, \\d+\\) " +
"or the visual rectangle \\(10000, 10000\\) is larger than the " +
"maximum allowed value \\(\\d+\\)");
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ {
property: 'transform',
runningOnCompositor: false,
warning: expectedWarning
} ]);
animation.effect.target.style = 'width: 100px; height: 100px';
return waitForFrame();
}).then(function() {
// FIXME: Bug 1253164: the animation should get back on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: false } ]);
});
}, 'transform on too big element');
promise_test(function(t) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '100');
svg.setAttribute('height', '100');
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('width', '100');
rect.setAttribute('height', '100');
rect.setAttribute('fill', 'red');
svg.appendChild(rect);
document.body.appendChild(svg);
t.add_cleanup(function() {
svg.remove();
});
var animation = svg.animate(
{ transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
svg.setAttribute('transform', 'translate(10, 20)');
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ {
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformSVG'
} ]);
svg.removeAttribute('transform');
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
});
}, 'transform of nsIFrame with SVG transform');
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable',
style: 'animation: fade 100s' });
var cssAnimation = div.getAnimations()[0];
var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
return scriptAnimation.ready.then(function() {
assert_animation_property_state_equals(
cssAnimation.effect.getProperties(),
[ { property: 'opacity', runningOnCompositor: true } ]);
assert_animation_property_state_equals(
scriptAnimation.effect.getProperties(),
[ { property: 'opacity', runningOnCompositor: true } ]);
});
}, 'overridden animation');
}
</script>
</body>