Commit 9cd2322d authored by Peter Kvitek's avatar Peter Kvitek Committed by Commit Bot

Implemented headless compositor protocol tests for basic rAF, GIF and CSS animations.

Change-Id: I956ea3523eb42b6d7f0cf851a126c6a11578912e
Reviewed-on: https://chromium-review.googlesource.com/1121378
Commit-Queue: Peter Kvitek <kvitekp@chromium.org>
Reviewed-by: default avatarPavel Feldman <pfeldman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#572253}
parent 0a162391
......@@ -4,5 +4,10 @@ specific_include_rules = {
"+components/viz/common/features.h",
"+components/viz/common/switches.h",
"+third_party/skia/include",
],
"headless_protocol_browsertest.cc": [
"+cc/base/switches.h",
"+components/viz/common/features.h",
"+components/viz/common/switches.h",
]
}
Tests compositor basic rAF operation.
Advanced to 10ms
Advanced to 510ms
Advanced to 1000ms
Elasped time: 1000
Requesting first animation frame
Animation frame callback #1
Advanced to 1100ms
Elasped time: 1100
Animation frame callback #2
Advanced to 1200ms
Elasped time: 1200
Animation frame callback #3
Advanced to 1300ms
Elasped time: 1300
Animation frame callback #4
Advanced to 1400ms
Elasped time: 1400
Animation frame count: 4
Animation frame callback #5
Screenshot size: 800 x 600
Screenshot rgba: 0,0,50,255
\ No newline at end of file
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function(testRunner) {
var {page, session, dp} = await testRunner.startBlank(
'Tests compositor basic rAF operation.');
await dp.Target.enable();
// Open the test page in a new tab with BeginFrameControl enabled.
await testTargetPage(await testRunner.createTargetInNewContext(
session, 800, 600, 'about:blank', true));
// This runs requestAnimationFrame five times without updating display then
// takes a screenshotin in a newly created tab with BeginFrame control.
async function testTargetPage(session) {
dp = session.protocol;
await dp.Runtime.enable();
await dp.HeadlessExperimental.enable();
dp.Runtime.onConsoleAPICalled(data => {
const text = data.params.args[0].value;
testRunner.log(text);
});
dp.Emulation.onVirtualTimeAdvanced(data => {
// Debug chrome schedules stray tasks that break this test.
// Our numbers are round, so we prevent this flake 999 times of 1000.
const time = data.params.virtualTimeElapsed;
if (time !== Math.round(time))
return;
testRunner.log(`Advanced to ${time}ms`);
});
let virtualTimeBase = 0;
let totalElapsedTime = 0;
let frameTimeTicks = 0;
let lastGrantedChunk = 0;
dp.Emulation.onVirtualTimePaused(data => {
// Remember the base time for frame time calculation.
virtualTimeBase = data.params.virtualTimeElapsed;
});
await dp.Emulation.setVirtualTimePolicy({policy: 'pause'});
lastGrantedChunk = 1000;
await dp.Emulation.setVirtualTimePolicy({
policy: 'pauseIfNetworkFetchesPending',
budget: lastGrantedChunk, waitForNavigation: true});
dp.Page.navigate(
{url: testRunner.url('/resources/compositor-basic-raf.html')});
// Renderer wants the very first frame to be fully updated.
await AdvanceTime();
await session.evaluate('startRAF()');
await dp.HeadlessExperimental.beginFrame({frameTimeTicks});
await GrantMoreTime(100);
// Send 3 updateless frames.
for (var n = 0; n < 3; ++n) {
await AdvanceTime();
await dp.HeadlessExperimental.beginFrame({frameTimeTicks,
noDisplayUpdates: true});
await GrantMoreTime(100);
}
// Grab screenshot, expected size 800x600, rgba: 0,0,50,255.
await AdvanceTime();
testRunner.log(await session.evaluate('displayRAFCount();'));
const screenshotData =
(await dp.HeadlessExperimental.beginFrame(
{frameTimeTicks, screenshot: {format: 'png'}}))
.result.screenshotData;
await logScreenShotInfo(screenshotData);
testRunner.completeTest();
async function AdvanceTime() {
await dp.Emulation.onceVirtualTimeBudgetExpired();
totalElapsedTime += lastGrantedChunk;
testRunner.log(`Elasped time: ${totalElapsedTime}`);
frameTimeTicks = virtualTimeBase + totalElapsedTime;
}
async function GrantMoreTime(budget) {
lastGrantedChunk = budget;
await dp.Emulation.setVirtualTimePolicy({
policy: 'pauseIfNetworkFetchesPending', budget});
}
function logScreenShotInfo(pngBase64) {
const image = new Image();
let callback;
let promise = new Promise(fulfill => callback = fulfill);
image.onload = function() {
testRunner.log(`Screenshot size: `
+ `${image.naturalWidth} x ${image.naturalHeight}`);
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const rgba = ctx.getImageData(0, 0, 1, 1).data;
testRunner.log(`Screenshot rgba: ${rgba}`);
callback();
}
image.src = `data:image/png;base64,${pngBase64}`;
return promise;
}
}
})
Tests compositor animated css handling.
Expired count: 1, elaspedTime: 500
Expired count: 2, elaspedTime: 1000
Event [animationstart] at 0 sec
Screenshot rgba: 255,0,0,255
Expired count: 3, elaspedTime: 1500
Event [animationiteration] at 1 sec
Screenshot rgba: 1,0,254,255
Expired count: 4, elaspedTime: 2000
Expired count: 5, elaspedTime: 2500
Event [animationiteration] at 2 sec
Expired count: 6, elaspedTime: 3000
Expired count: 7, elaspedTime: 3500
Event [animationiteration] at 3 sec
Screenshot rgba: 1,0,254,255
Expired count: 8, elaspedTime: 4000
Screenshot rgba: 255,0,0,255
Expired count: 9, elaspedTime: 4500
Event [animationend] at 4 sec
Screenshot rgba: 0,0,255,255
Expired count: 10, elaspedTime: 5000
Screenshot rgba: 0,0,255,255
\ No newline at end of file
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function(testRunner) {
var {page, session, dp} = await testRunner.startBlank(
'Tests compositor animated css handling.');
await dp.Target.enable();
// Open the test page in a new tab with BeginFrameControl enabled.
await testTargetPage(await testRunner.createTargetInNewContext(
session, 800, 600, 'about:blank', true));
// Loads a page with css animation into a a newly created tab with BeginFrame
// control and verifies that animation is advanced according to virtual time.
async function testTargetPage(session) {
dp = session.protocol;
await dp.Runtime.enable();
await dp.HeadlessExperimental.enable();
dp.Runtime.onConsoleAPICalled(data => {
const text = data.params.args[0].value;
testRunner.log(text);
});
let virtualTimeBase = 0;
let totalElapsedTime = 0;
let lastGrantedChunk = 0;
let expiredCount = 0;
dp.Emulation.onVirtualTimeBudgetExpired(async data => {
++expiredCount;
totalElapsedTime += lastGrantedChunk;
testRunner.log(`Expired count: ${expiredCount}`
+ `, elaspedTime: ${totalElapsedTime}`);
let grantVirtualTime = 500;
let frameTimeTicks = virtualTimeBase + totalElapsedTime;
if (expiredCount == 1) {
// Renderer wants the very first frame to be fully updated.
await dp.HeadlessExperimental.beginFrame({frameTimeTicks});
} else {
if (expiredCount >= 4 && expiredCount <= 6) {
// Issue updateless frames.
await dp.HeadlessExperimental.beginFrame(
{frameTimeTicks, noDisplayUpdates: true});
} else {
// Update frame and grab a screenshot, logging background color.
const {result: {screenshotData}} =
await dp.HeadlessExperimental.beginFrame(
{frameTimeTicks, screenshot: {format: 'png'}});
await logScreenShotInfo(screenshotData);
}
}
// Grant more time or quit test.
if (expiredCount < 10) {
await dp.Emulation.setVirtualTimePolicy({
policy: 'pauseIfNetworkFetchesPending',
budget: grantVirtualTime});
lastGrantedChunk = grantVirtualTime;
} else {
testRunner.completeTest();
}
});
// Pause for the first time and remember base virtual time.
const {result: {virtualTimeTicksBase}} =
await dp.Emulation.setVirtualTimePolicy({policy: 'pause'});
virtualTimeBase = virtualTimeTicksBase;
lastGrantedChunk = 500;
await dp.Emulation.setVirtualTimePolicy({
policy: 'pauseIfNetworkFetchesPending',
budget: lastGrantedChunk, waitForNavigation: true});
// Animates opacity of a blue 100px square on red blackground over 4
// seconds (1.0 -> 0 -> 1.0 four times). Logs events to console.
//
// Timeline:
// 0 ms: --- animation starts at 500ms ---
// 500 ms: 1.0 opacity -> blue background.
// 1000 ms: 0 opacity -> red background.
// 1500 ms: 1.0 opacity -> blue background.
// 2000 ms: 0 opacity -> red background.
// 2500 ms: 1.0 opacity -> blue background.
// 3000 ms: 0 opacity -> red background.
// 3500 ms: 1.0 opacity -> blue background.
// 4000 ms: 0 opacity -> red background.
// 4500 ms: 1.0 opacity -> blue background.
//
// The animation will start with the first BeginFrame after load.
dp.Page.navigate(
{url: testRunner.url('/resources/compositor-css-animation.html')});
function logScreenShotInfo(pngBase64) {
const image = new Image();
let callback;
let promise = new Promise(fulfill => callback = fulfill);
image.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const rgba = ctx.getImageData(0, 0, 1, 1).data;
testRunner.log(`Screenshot rgba: ${rgba}`);
callback();
}
image.src = `data:image/png;base64,${pngBase64}`;
return promise;
}
}
})
Tests compositor animated image handling.
Expired count: 1, elaspedTime: 500
Expired count: 2, elaspedTime: 1000
Expired count: 3, elaspedTime: 1500
Expired count: 4, elaspedTime: 2000
Expired count: 5, elaspedTime: 2500
Expired count: 6, elaspedTime: 3000
Expired count: 7, elaspedTime: 3500
Expired count: 8, elaspedTime: 4000
Screenshot rgba: 0,0,255,255
Expired count: 9, elaspedTime: 4500
Expired count: 10, elaspedTime: 5000
Expired count: 11, elaspedTime: 5500
Expired count: 12, elaspedTime: 6000
Expired count: 13, elaspedTime: 6500
Screenshot rgba: 255,255,0,255
Expired count: 14, elaspedTime: 7000
Expired count: 15, elaspedTime: 7500
Expired count: 16, elaspedTime: 8000
Expired count: 17, elaspedTime: 8500
Expired count: 18, elaspedTime: 9000
Expired count: 19, elaspedTime: 9500
Screenshot rgba: 255,255,0,255
Expired count: 20, elaspedTime: 10000
\ No newline at end of file
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function(testRunner) {
var {page, session, dp} = await testRunner.startBlank(
'Tests compositor animated image handling.');
await dp.Target.enable();
// Open the test page in a new tab with BeginFrameControl enabled.
await testTargetPage(await testRunner.createTargetInNewContext(
session, 800, 600, 'about:blank', true));
// Loads an animated GIF into a a newly created tab with BeginFrame control
// and verifies that:
// - animate_only BeginFrames don't produce CompositorFrames,
// - first screenshot starts the GIF animation,
// - animation is advanced according to virtual time.
// - the animation is not resynced after the first iteration.
async function testTargetPage(session) {
dp = session.protocol;
await dp.Runtime.enable();
await dp.HeadlessExperimental.enable();
dp.Runtime.onConsoleAPICalled(data => {
const text = data.params.args[0].value;
testRunner.log(text);
});
let virtualTimeBase = 0;
let totalElapsedTime = 0;
let lastGrantedChunk = 0;
let expiredCount = 0;
dp.Emulation.onVirtualTimeBudgetExpired(async data => {
++expiredCount;
totalElapsedTime += lastGrantedChunk;
testRunner.log(`Expired count: ${expiredCount}, `
+ `elaspedTime: ${totalElapsedTime}`);
let grantVirtualTime = 500;
let frameTimeTicks = virtualTimeBase + totalElapsedTime;
if (expiredCount === 1 + 7
|| expiredCount === 1 + 7 + 5
|| expiredCount === 1 + 7 + 5 + 6) {
// Animation starts when first screenshot is taken, so the first
// screenshot should be blue. Screenshot #2 is taken on the third second
// of the animation, so it should be yellow. Screenshot #3 is taken two
// animation cycles later, so it should be yelloe again.
const {result: {screenshotData}} =
await dp.HeadlessExperimental.beginFrame(
{frameTimeTicks, screenshot: {format: 'png'}});
await logScreenShotInfo(screenshotData);
} else {
await dp.HeadlessExperimental.beginFrame(
{frameTimeTicks, noDisplayUpdates: true});
}
// Grant more time or quit test.
if (expiredCount < 20) {
await dp.Emulation.setVirtualTimePolicy({
policy: 'pauseIfNetworkFetchesPending',
budget: grantVirtualTime});
lastGrantedChunk = grantVirtualTime;
} else {
testRunner.completeTest();
}
});
// Pause for the first time and remember base virtual time.
const {result: {virtualTimeTicksBase}} =
await dp.Emulation.setVirtualTimePolicy(
{initialVirtualTime: 100, policy: 'pause'});
virtualTimeBase = virtualTimeTicksBase;
// Renderer wants the very first frame to be fully updated.
await dp.HeadlessExperimental.beginFrame({noDisplayUpdates: false});
// Grant initial time.
lastGrantedChunk = 500;
await dp.Emulation.setVirtualTimePolicy({
policy: 'pauseIfNetworkFetchesPending',
budget: lastGrantedChunk, waitForNavigation: true});
// The loaded GIF is 100x100px and has 1 second of blue, 1 second of red and
// 1 second of yellow.
dp.Page.navigate(
{url: testRunner.url('/resources/compositor-image-animation.html')});
async function AdvanceTime() {
await dp.Emulation.onceVirtualTimeBudgetExpired();
totalElapsedTime += lastGrantedChunk;
testRunner.log(`Elasped time: ${totalElapsedTime}`);
frameTimeTicks = virtualTimeBase + totalElapsedTime;
}
async function GrantMoreTime(budget) {
lastGrantedChunk = budget;
await dp.Emulation.setVirtualTimePolicy({
policy: 'pauseIfNetworkFetchesPending', budget});
}
function logScreenShotInfo(pngBase64) {
const image = new Image();
let callback;
let promise = new Promise(fulfill => callback = fulfill);
image.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const rgba = ctx.getImageData(0, 0, 1, 1).data;
testRunner.log(`Screenshot rgba: ${rgba}`);
callback();
}
image.src = `data:image/png;base64,${pngBase64}`;
return promise;
}
setTimeout(() => {
testRunner.log('Forced test termination');
testRunner.completeTest();
}, 10 * 1000);
}
})
<html>
<script>
window.rafCount = 0;
function animationCallback() {
++window.rafCount;
console.log(`Animation frame callback #${window.rafCount}`);
// Change document body background in a predictable manner.
document.body.style.background = '#0000' + (window.rafCount * 10).toString(16);
window.requestAnimationFrame(animationCallback);
}
function startRAF() {
console.log('Requesting first animation frame');
window.requestAnimationFrame(animationCallback);
}
function displayRAFCount() {
console.log(`Animation frame count: ${window.rafCount}`);
}
</script>
<body></body>
</html>
<!doctype html>
<style>
* {
margin: 0px;
background-color: red;
}
#box {
width: 100px;
height: 100px;
background-color: blue;
animation: flash 1s steps(1, end) 4;
}
@keyframes flash {
0% { opacity: 1; }
50% { opacity: 0; }
100% { opacity: 1; }
}
</style>
<div id="box"></div>
<script>
var baseTime = Date.now();
var box = document.getElementById("box");
box.addEventListener("animationstart", onAnimationEvent, false);
box.addEventListener("animationiteration", onAnimationEvent, false);
box.addEventListener("animationend", onAnimationEvent, false);
function onAnimationEvent(event) {
console.log(`Event [${event.type}]`
+ ` at ${event.elapsedTime} sec`);
}
</script>
<!doctype html>
<style>* { margin: 0; }</style>
<img id='image' src='animated_square.gif'>
......@@ -10,8 +10,13 @@
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/path_service.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "cc/base/switches.h"
#include "components/viz/common/features.h"
#include "components/viz/common/switches.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "headless/public/devtools/domains/runtime.h"
#include "headless/public/headless_browser.h"
......@@ -174,10 +179,45 @@ class HeadlessProtocolBrowserTest
builder.SetHostResolverRules("MAP *.test 127.0.0.1");
}
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessAsyncDevTooledBrowserTest::SetUpCommandLine(command_line);
// The following switches are recommended for BeginFrameControl required by
// compositor tests, see https://goo.gl/3zHXhB for details
static const char* const compositor_switches[] = {
// We control BeginFrames ourselves and need all compositing stages to
// run.
switches::kRunAllCompositorStagesBeforeDraw,
switches::kDisableNewContentRenderingTimeout,
// Animtion-only BeginFrames are only supported when updates from the
// impl-thread are disabled, see go/headless-rendering.
cc::switches::kDisableThreadedAnimation,
cc::switches::kDisableCheckerImaging,
switches::kDisableThreadedScrolling,
// Ensure that image animations don't resync their animation timestamps
// when looping back around.
switches::kDisableImageAnimationResync,
};
for (auto* compositor_switch : compositor_switches) {
command_line->AppendSwitch(compositor_switch);
}
// In surface synchronization, child surface IDs are allocated by
// parents and new CompositorFrames only activate once all their child
// surfaces exist. In --run-all-compositor-stages-before-draw mode, this
// means that child surface initialization and resize fully propagates
// within a single BeginFrame.
scoped_feature_list_.InitAndEnableFeature(
features::kEnableSurfaceSynchronization);
}
protected:
bool test_finished_ = false;
std::string test_folder_;
std::string script_name_;
base::test::ScopedFeatureList scoped_feature_list_;
};
#define HEADLESS_PROTOCOL_TEST(TEST_NAME, SCRIPT_NAME) \
......@@ -222,6 +262,28 @@ HEADLESS_PROTOCOL_TEST(VirtualTimeVideo, "emulation/virtual-time-video.js");
HEADLESS_PROTOCOL_TEST(DISABLED_VirtualTimeHistoryNavigation,
"emulation/virtual-time-history-navigation.js");
// BeginFrameControl is not supported on MacOS yet, see: https://cs.chromium.org
// chromium/src/headless/lib/browser/protocol/target_handler.cc?
// rcl=5811aa08e60ba5ac7622f029163213cfbdb682f7&l=32
#if defined(OS_MACOSX)
#define MAYBE_CompositorBasicRaf DISABLED_CompositorBasicRaf
#define MAYBE_CompositorImageAnimation DISABLED_CompositorImageAnimation
#define MAYBE_CompositorCssAnimation DISABLED_CompositorCssAnimation
#else
#define MAYBE_CompositorBasicRaf CompositorBasicRaf
#define MAYBE_CompositorImageAnimation CompositorImageAnimation
#define MAYBE_CompositorCssAnimation CompositorCssAnimation
#endif
HEADLESS_PROTOCOL_TEST(MAYBE_CompositorBasicRaf,
"emulation/compositor-basic-raf.js");
HEADLESS_PROTOCOL_TEST(MAYBE_CompositorImageAnimation,
"emulation/compositor-image-animation-test.js");
HEADLESS_PROTOCOL_TEST(MAYBE_CompositorCssAnimation,
"emulation/compositor-css-animation-test.js");
#undef MAYBE_CompositorBasicRaf
#undef MAYBE_CompositorImageAnimation
#undef MAYBE_CompositorCssAnimation
// http://crbug.com/633321
#if defined(OS_ANDROID)
#define MAYBE_VirtualTimeTimerOrder DISABLED_VirtualTimeTimerOrder
......
......@@ -171,6 +171,18 @@ var TestRunner = class {
return this._start(description, null, url);
}
async createTargetInNewContext(session, width, height, url, enableBeginFrameControl) {
const dp = session.protocol;
const browserContextId = (await dp.Target.createBrowserContext())
.result.browserContextId;
const targetId = (await dp.Target.createTarget(
{url, browserContextId, width, height, enableBeginFrameControl}))
.result.targetId;
const sessionId = (await dp.Target.attachToTarget({targetId}))
.result.sessionId;
return session.createChild(sessionId);
}
async logStackTrace(debuggers, stackTrace, debuggerId) {
while (stackTrace) {
const {description, callFrames, parent, parentId} = stackTrace;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment