diff options
Diffstat (limited to 'dom/animation/test/chrome/test_animation_performance_warning.html')
-rw-r--r-- | dom/animation/test/chrome/test_animation_performance_warning.html | 957 |
1 files changed, 957 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 000000000..a3bd63efc --- /dev/null +++ b/dom/animation/test/chrome/test_animation_performance_warning.html @@ -0,0 +1,957 @@ +<!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> |