diff options
Diffstat (limited to '')
-rw-r--r-- | dom/animation/test/chrome/test_animation_performance_warning.html | 1692 |
1 files changed, 1692 insertions, 0 deletions
diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html new file mode 100644 index 0000000000..ca0572cfaa --- /dev/null +++ b/dom/animation/test/chrome/test_animation_performance_warning.html @@ -0,0 +1,1692 @@ +<!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 } +} +@keyframes translate { + from { transform: none } + to { transform: translate(100px) } +} +</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); + +const Services = SpecialPowers.Services; +Services.locale.requestedLocales = ["en-US"]; + +SpecialPowers.pushPrefEnv({ "set": [ + // Need to set devPixelsPerPx explicitly to gain + // consistent pixel values in warning messages + // regardless of platform DPIs. + ["layout.css.devPixelsPerPx", 1], + ["layout.animation.prerender.partial", false], + ] }, + 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_all_properties_running_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'); + } +} + +function testBasicOperation() { + [ + { + desc: 'animations on compositor', + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true + } + ] + }, + { + desc: 'animations on main thread', + frames: { + zIndex: ['0', '999'] + }, + expected: [ + { + property: 'z-index', + runningOnCompositor: false + } + ] + }, + { + desc: 'animations on both threads', + frames: { + zIndex: ['0', '999'], + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'z-index', + 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: 'two transform-like animation properties on compositor thread', + frames: { + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: true + }, + { + property: 'translate', + 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 + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + var animation = addDivAndAnimate(t, { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); + }); +} + +// Test adding/removing a 'width' property on the same animation object. +function testKeyframesWithGeometricProperties() { + [ + { + 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: 'translate', + frames: { + translate: ['0px', '100px'] + }, + expected: { + withoutGeometric: [ + { + property: 'translate', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false + }, + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + } + }, + { + desc: 'opacity and transform-like properties', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + expected: { + withoutGeometric: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: true + }, + { + property: 'translate', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false + }, + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + }, + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + } + }, + ].forEach(subtest => { + promise_test(async t => { + var animation = addDivAndAnimate(t, { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + + // First, a transform animation is running on compositor. + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected.withoutGeometric); + + // Add a 'width' property. + var keyframes = animation.effect.getKeyframes(); + + keyframes[0].width = '100px'; + keyframes[1].width = '200px'; + + animation.effect.setKeyframes(keyframes); + await waitForFrame(); + + // 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); + + // Remove the 'width' property. + var keyframes = animation.effect.getKeyframes(); + + delete keyframes[0].width; + delete keyframes[1].width; + + animation.effect.setKeyframes(keyframes); + await waitForFrame(); + + // Finally, the transform animation is running on compositor. + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected.withoutGeometric); + }, 'An animation has: ' + subtest.desc); + }); +} + +// Test that the expected set of geometric properties all block transform +// animations. +function testSetOfGeometricProperties() { + const geometricProperties = [ + 'width', 'height', + 'top', 'right', 'bottom', 'left', + 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', + 'padding-top', 'padding-right', 'padding-bottom', 'padding-left' + ]; + + geometricProperties.forEach(property => { + promise_test(async t => { + const keyframes = { + [propertyToIDL(property)]: [ '100px', '200px' ], + transform: [ 'translate(0px)', 'translate(100px)' ] + }; + var animation = addDivAndAnimate(t, { class: 'compositable' }, + keyframes, 100 * MS_PER_SEC); + + await waitForPaints(); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ + { + property, + runningOnCompositor: false + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations' + } + ]); + }, `${property} is treated as a geometric property`); + }); +} + +// Performance warning tests that set and clear a style property. +function testStyleChanges() { + [ + { + desc: 'preserve-3d transform', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + style: 'transform-style: preserve-3d', + expected: [ + { + property: 'transform', + runningOnCompositor: true, + } + ] + }, + { + desc: 'preserve-3d translate', + frames: { + translate: ['0px', '100px'] + }, + style: 'transform-style: preserve-3d', + expected: [ + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + { + desc: 'transform with backface-visibility:hidden', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + style: 'backface-visibility: hidden;', + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden' + } + ] + }, + { + desc: 'translate with backface-visibility:hidden', + frames: { + translate: ['0px', '100px'] + }, + style: 'backface-visibility: hidden;', + expected: [ + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden' + } + ] + }, + { + desc: 'opacity and transform-like properties with preserve-3d', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + style: 'transform-style: preserve-3d', + expected: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: true, + }, + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + { + desc: 'opacity and transform-like properties with ' + + 'backface-visibility:hidden', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + style: 'backface-visibility: hidden;', + expected: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden' + }, + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden' + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + var animation = addDivAndAnimate(t, { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + assert_all_properties_running_on_compositor( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.style = subtest.style; + await waitForFrame(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.style = ''; + await waitForFrame(); + + assert_all_properties_running_on_compositor( + animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); + }); +} + +// Performance warning tests that set and clear the id property +function testIdChanges() { + [ + { + 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' + } + ] + }, + { + desc: 'moz-element referencing a translate', + frames: { + translate: ['0px', '100px'] + }, + id: 'transformed', + createelement: 'width:100px; height:100px; background: -moz-element(#transformed)', + expected: [ + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasRenderingObserver' + } + ] + }, + { + desc: 'moz-element referencing a translate and transform', + frames: { + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + id: 'transformed', + createelement: 'width:100px; height:100px; background: -moz-element(#transformed)', + expected: [ + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasRenderingObserver' + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasRenderingObserver' + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + if (subtest.createelement) { + addDiv(t, { style: subtest.createelement }); + } + + var animation = addDivAndAnimate(t, { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + + assert_all_properties_running_on_compositor( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.id = subtest.id; + await waitForFrame(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.id = ''; + await waitForFrame(); + + assert_all_properties_running_on_compositor( + animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); + }); +} + +function testMultipleAnimations() { + [ + { + desc: 'opacity and transform-like properties with preserve-3d', + style: 'transform-style: preserve-3d', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: true, + } + ] + }, + { + frames: { + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + ], + }, + { + desc: 'opacity and transform-like properties with ' + + 'backface-visibility:hidden', + style: 'backface-visibility: hidden;', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden' + } + ] + }, + { + frames: { + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden' + } + ] + }, + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + ], + }, + ].forEach(subtest => { + promise_test(async t => { + var div = addDiv(t, { class: 'compositable' }); + var animations = subtest.animations.map(anim => { + var animation = div.animate(anim.frames, 100 * MS_PER_SEC); + + // Bind expected values to animation object. + animation.expected = anim.expected; + return animation; + }); + await waitForPaints(); + + animations.forEach(anim => { + assert_all_properties_running_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + div.style = subtest.style; + await waitForFrame(); + + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected); + }); + div.style = ''; + await waitForFrame(); + + animations.forEach(anim => { + assert_all_properties_running_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + }, 'Multiple animations: ' + subtest.desc); + }); +} + +// Test 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]. +function testMultipleAnimationsWithGeometricKeyframes() { + [ + { + 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' + } + ] + } + } + ] + }, + { + desc: 'opacity and translate with geometric keyframes', + animations: [ + { + frames: { + opacity: [0, 1] + }, + expected: { + withoutGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ], + withGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + }, + { + frames: { + translate: ['0px', '100px'] + }, + expected: { + withoutGeometric: [ + { + property: 'translate', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false, + }, + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + } + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + var div = addDiv(t, { class: 'compositable' }); + var animations = subtest.animations.map(anim => { + var animation = div.animate(anim.frames, 100 * MS_PER_SEC); + + // Bind expected values to animation object. + animation.expected = anim.expected; + return animation; + }); + await waitForPaints(); + // First, all animations are running on compositor. + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected.withoutGeometric); + }); + + // 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); + await waitForFrame(); + + // Now the transform animation is not running on compositor because of + // the 'width' property. + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected.withGeometric); + }); + + // 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); + await waitForFrame(); + + // Finally, all animations are running on compositor. + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected.withoutGeometric); + }); + }, 'Multiple animations with geometric property: ' + subtest.desc); + }); +} + +// Tests adding/removing 'width' animation on the same element which has async +// animations. +function testMultipleAnimationsWithGeometricAnimations() { + [ + { + desc: 'transform', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + }, + ] + }, + { + desc: 'translate', + animations: [ + { + frames: { + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + }, + ] + }, + { + desc: 'opacity', + animations: [ + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true + } + ] + }, + ] + }, + { + desc: 'opacity, transform, and translate', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + }, + { + frames: { + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + }, + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + ], + }, + ].forEach(subtest => { + promise_test(async t => { + var div = addDiv(t, { class: 'compositable' }); + var animations = subtest.animations.map(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; + + await waitForPaints(); + animations.forEach(anim => { + assert_all_properties_running_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + + // Append 'width' animation on the same element. + widthAnimation = div.animate({ width: ['100px', '200px'] }, + 100 * MS_PER_SEC); + await waitForFrame(); + + // Now transform animations are not running on compositor because of + // the 'width' animation. + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected); + }); + // Remove the 'width' animation. + widthAnimation.cancel(); + await waitForFrame(); + + // Now all animations are running on compositor. + animations.forEach(anim => { + assert_all_properties_running_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + }, 'Multiple async animations and geometric animation: ' + subtest.desc); + }); +} + +function testSmallElements() { + [ + { + desc: 'opacity on 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: true + } + ] + }, + { + desc: 'transform on small element', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + style: { style: 'width: 8px; height: 8px; background-color: red;' }, + expected: [ + { + property: 'transform', + runningOnCompositor: true + } + ] + }, + { + desc: 'translate on small element', + frames: { + translate: ['0px', '100px'] + }, + style: { style: 'width: 8px; height: 8px; background-color: red;' }, + expected: [ + { + property: 'translate', + runningOnCompositor: true + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + var div = addDiv(t, subtest.style); + var animation = div.animate(subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); + }); +} + +function testSynchronizedAnimations() { + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations' + } ]); + }, 'Animations created within the same tick are synchronized' + + ' (compositor animation created first)'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + const elemC = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ translate: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + const animC = elemC.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ + { property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations' + } ]); + assert_animation_property_state_equals( + animB.effect.getProperties(), + [ + { property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations' + } ]); + }, 'Animations created within the same tick are synchronized' + + ' (compositor animation created first/second)'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + const elemC = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animC = elemC.animate({ translate: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_property_state_equals( + animB.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations' + } ]); + assert_animation_property_state_equals( + animC.effect.getProperties(), + [ + { property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations' + } ]); + }, 'Animations created within the same tick are synchronized' + + ' (compositor animation created second/third)'); + + promise_test(async t => { + const attrs = { class: 'compositable', + style: 'transition: all 100s' }; + const elemA = addDiv(t, attrs); + const elemB = addDiv(t, attrs); + elemA.style.transform = 'translate(0px)'; + elemB.style.marginLeft = '0px'; + getComputedStyle(elemA).transform; + getComputedStyle(elemB).marginLeft; + + // Generally the sequence of steps is as follows: + // + // Tick -> requestAnimationFrame -> Style -> Paint -> Events (-> Tick...) + // + // In this test we want to set up two transitions during the "Events" + // stage but only flush style for one such that the second one is actually + // generated during the "Style" stage of the *next* tick. + // + // Web content often generates transitions in this way (that is, it doesn't + // pay regard to when style is flushed and nor should it). However, we + // still want transitions generated in this way to be synchronized. + let timeForFirstFrame; + await waitForIdle(); + + timeForFirstFrame = document.timeline.currentTime; + elemA.style.transform = 'translate(100px)'; + // Flush style to trigger first transition + getComputedStyle(elemA).transform; + elemB.style.marginLeft = '100px'; + // DON'T flush style here (this includes calling getAnimations!) + await waitForFrame(); + + assert_not_equals(timeForFirstFrame, document.timeline.currentTime, + 'Should be on the other side of a tick'); + // Wait another tick so we can let the transition be started + // by regular style resolution. + await waitForFrame(); + + const transitionA = elemA.getAnimations()[0]; + assert_animation_property_state_equals( + transitionA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations' + } ]); + }, 'Transitions created before and after a tick are synchronized'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ], + opacity: [ 0, 1 ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations' + }, + { property: 'opacity', + runningOnCompositor: true + } ]); + }, 'Opacity animations on the same element continue running on the' + + ' compositor when transform animations are synchronized with geometric' + + ' animations'); + + promise_test(async t => { + const transitionElem = addDiv(t, { + style: 'margin-left: 0px; transition: margin-left 100s', + }); + getComputedStyle(transitionElem).marginLeft; + + await waitForFrame(); + + transitionElem.style.marginLeft = '100px'; + const cssTransition = transitionElem.getAnimations()[0]; + + const animationElem = addDiv(t, { + class: 'compositable', + style: 'animation: translate 100s', + }); + const cssAnimation = animationElem.getAnimations()[0]; + + await Promise.all([cssTransition.ready, cssAnimation.ready]); + + assert_animation_property_state_equals(cssAnimation.effect.getProperties(), + [{ property: 'transform', + runningOnCompositor: true }]); + }, 'CSS Animations are NOT synchronized with CSS Transitions'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + await waitForPaints(); + + let animB = elemB.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + await animB.ready; + + assert_animation_property_state_equals( + animB.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true } ]); + }, 'Transform animations are NOT synchronized with geometric animations' + + ' started in the previous frame'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + await waitForPaints(); + + let animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + await animB.ready; + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true } ]); + }, 'Transform animations are NOT synchronized with geometric animations' + + ' started in the next frame'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + animB.pause(); + + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', runningOnCompositor: true } ]); + }, 'Paused animations are not synchronized'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + // Seek one of the animations so that their start times will differ + animA.currentTime = 5000; + + await waitForPaints(); + + assert_not_equals(animA.startTime, animB.startTime, + 'Animations should have different start times'); + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations' + } ]); + }, 'Animations are synchronized based on when they are started' + + ' and NOT their start time'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: false } ]); + // Restart animation + animA.pause(); + animA.play(); + await animA.ready; + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true } ]); + }, 'An initially synchronized animation may be unsynchronized if restarted'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + // Clear target effect + animB.effect.target = null; + + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true } ]); + }, 'A geometric animation with no target element is not synchronized'); +} + +function testTooLargeFrame() { + [ + { + property: 'transform', + frames: { transform: ['translate(0px)', 'translate(100px)'] }, + }, + { + property: 'translate', + frames: { translate: ['0px', '100px'] }, + }, + ].forEach(subtest => { + promise_test(async t => { + var animation = addDivAndAnimate(t, + { class: 'compositable' }, + subtest.frames, + 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: true } ]); + animation.effect.target.style = 'width: 10000px; height: 10000px'; + await waitForFrame(); + + // viewport depends on test environment. + var expectedWarning = new RegExp( + "Animation cannot be run on the compositor because the area of the frame " + + "\\(\\d+\\) is too large relative to the viewport " + + "\\(larger than \\d+\\)"); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { + property: subtest.property, + runningOnCompositor: false, + warning: expectedWarning + } ]); + animation.effect.target.style = 'width: 100px; height: 100px'; + await waitForFrame(); + + // With WebRender we appear to stick to the previous layerization decision + // after changing the bounds back to a smaller object. + const isWebRender = + SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender'); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: !isWebRender } ]); + }, subtest.property + ' on too big element - area'); + + promise_test(async t => { + var animation = addDivAndAnimate(t, + { class: 'compositable' }, + subtest.frames, + 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: true } ]); + animation.effect.target.style = 'width: 20000px; height: 1px'; + await waitForFrame(); + + // viewport depends on test environment. + var expectedWarning = new RegExp( + "Animation cannot be run on the compositor because the frame size " + + "\\(20000, 1\\) is too large relative to the viewport " + + "\\(larger than \\(\\d+, \\d+\\)\\) or larger than the " + + "maximum allowed value \\(\\d+, \\d+\\)"); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { + property: subtest.property, + runningOnCompositor: false, + warning: expectedWarning + } ]); + animation.effect.target.style = 'width: 100px; height: 100px'; + await waitForFrame(); + + const isWebRender = + SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender'); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: !isWebRender } ]); + }, subtest.property + ' on too big element - dimensions'); + }); +} + +function testTransformSVG() { + [ + { + property: 'transform', + frames: { transform: ['translate(0px)', 'translate(100px)'] }, + }, + { + property: 'translate', + frames: { translate: ['0px', '100px'] }, + }, + { + property: 'rotate', + frames: { rotate: ['0deg', '45deg'] }, + }, + { + property: 'scale', + frames: { scale: ['1', '2'] }, + }, + ].forEach(subtest => { + promise_test(async 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(() => { + svg.remove(); + }); + + var animation = svg.animate(subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: true } ]); + svg.setAttribute('transform', 'translate(10, 20)'); + await waitForFrame(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { + property: subtest.property, + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformSVG' + } ]); + svg.removeAttribute('transform'); + await waitForFrame(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: true } ]); + }, subtest.property + ' of nsIFrame with SVG transform'); + }); +} + +function testImportantRuleOverride() { + promise_test(async t => { + const elem = addDiv(t, { class: 'compositable' }); + const anim = elem.animate({ translate: [ '0px', '100px' ], + rotate: ['0deg', '90deg'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(anim); + await waitForPaints(); + + assert_animation_property_state_equals( + anim.effect.getProperties(), + [ { property: 'translate', runningOnCompositor: true }, + { property: 'rotate', runningOnCompositor: true } ] + ); + + elem.style.setProperty('rotate', '45deg', 'important'); + getComputedStyle(elem).rotate; + + await waitForFrame(); + + assert_animation_property_state_equals( + anim.effect.getProperties(), + [ + { + property: 'translate', + runningOnCompositor: false, + warning: + 'CompositorAnimationWarningTransformIsBlockedByImportantRules' + }, + { + property: 'rotate', + runningOnCompositor: false, + warning: + 'CompositorAnimationWarningTransformIsBlockedByImportantRules' + }, + ] + ); + }, 'The animations of transform-like properties are not running on the ' + + 'compositor because any of the properties has important rules'); +} + +function testCurrentColor() { + if (SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender')) { + return; // skip this test until bug 1510030 landed. + } + promise_test(async t => { + const animation = addDivAndAnimate(t, { class: 'compositable' }, + { backgroundColor: [ 'currentColor', + 'red' ] }, + 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: 'background-color', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasCurrentColor' + } ]); + }, 'Background color animations with `current-color` don\'t run on the ' + + 'compositor'); +} + +function start() { + var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1'] + .getService(SpecialPowers.Ci.nsIStringBundleService); + gStringBundle = bundleService + .createBundle("chrome://global/locale/layout_errors.properties"); + + testBasicOperation(); + testKeyframesWithGeometricProperties(); + testSetOfGeometricProperties(); + testStyleChanges(); + testIdChanges(); + testMultipleAnimations(); + testMultipleAnimationsWithGeometricKeyframes(); + testMultipleAnimationsWithGeometricAnimations(); + testSmallElements(); + testSynchronizedAnimations(); + testTooLargeFrame(); + testTransformSVG(); + testImportantRuleOverride(); + testCurrentColor(); + + promise_test(async 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); + + await waitForPaints(); + 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'); + + done(); +} + +</script> + +</body> |