Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement performance ticks for framerate. #1420

Merged
merged 1 commit into from
Jan 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/amp-core-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import {installActionService} from './service/action-impl';
import {installFramerateService} from './service/framerate-impl';
import {installHistoryService} from './service/history-impl';
import {installResourcesService} from './service/resources-impl';
import {installStandardActions} from './service/standard-actions-impl';
Expand All @@ -36,4 +37,5 @@ export function installCoreServices(window) {
installActionService(window);
installResourcesService(window);
installStandardActions(window);
installFramerateService(window);
}
29 changes: 29 additions & 0 deletions src/framerate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @fileoverview Factory for ./service/cid-impl.js
*/

import {getService} from './service';

/**
* @param {!Window} window
* @return {!Framerate}
*/
export function framerateFor(window) {
return getService(window, 'framerate');
};
152 changes: 152 additions & 0 deletions src/service/framerate-impl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {getService} from '../service';
import {timer} from '../timer';
import {viewerFor} from '../viewer';
import {performanceFor} from '../performance';

const collectTime = 5000;

/**
* Collects framerate data and reports it via the performance service.
*/
export class Framerate {

/**
* @param {!Window} win
*/
constructor(win) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it always installed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Good idea?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine for now. We can always skip it for sampling later.

/** @const {!Window} */
this.win = win;

/**
* Return value of requestAnimationFrame for use with
* cancelRequestAnimationFrame.
* @private {number}
*/
this.requestedFrame_ = null;

/** @private {number} */
this.lastFrameTime_ = 0;

/** @private {number} */
this.collectUntilTime_ = 0;

/** @private {number} */
this.collectStartTime_ = 0;

/** @private {number} */
this.frameCount_ = 0;

/**
* Whether we loaded an ad on this page.
* @private {boolean}
*/
this.loadedAd_ = false;

const viewer = viewerFor(this.win);

/**
* We do not make measurements when the window is hidden, because
* animation frames not not fire in that case.
* @private {boolean}
*/
this.isActive_ = viewer.isVisible();

viewer.onVisibilityChanged(() => {
this.isActive_ = viewer.isVisible();
this.reset_();
if (this.isActive_) {
this.collect();
}
});

this.collect();
}

/**
* Call this when something interesting is about to happen on screen.
* This class will then measure the framerate for the next few seconds.
* @param {!Element=} opt_element Element for which the current
* collection is requested.
*/
collect(opt_element) {
if (!this.isActive_ || !this.win.requestAnimationFrame) {
return;
}
const now = timer.now();
if (this.lastFrameTime_ == 0) {
this.collectStartTime_ = now;
}
if (opt_element && opt_element.tagName == 'AMP-AD') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, initially you'd have one long stretch while first set of elements are loaded, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. We definitely care about framerate at that time.

this.loadedAd_ = true;
}
this.collectUntilTime_ = now + collectTime;
this.requestFrame_(now);
}

reset_() {
this.frameCount_ = 0;
this.lastFrameTime_ = 0;
if (this.win.cancelAnimationFrame) {
this.win.cancelAnimationFrame(this.requestedFrame_);
}
this.requestedFrame_ = null;
}

requestFrame_(now) {
if (this.requestedFrame_ != null) {
return;
}
if (now > this.collectUntilTime_) {
// Done.
const duration = now - this.collectStartTime_;
const framerate = 1000 / (duration / this.frameCount_);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dividing over a division is always difficult to understand. How about this.frameCount_ / duration * 1000?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, my brain likes this :)

const performance = performanceFor(this.win);
performance.tick('fps', undefined, framerate);
if (this.loadedAd_) {
performance.tick('fal', undefined, framerate);
}
performance.flush();
this.reset_();
return;
}
this.requestedFrame_ = this.win.requestAnimationFrame(() => {
this.requestedFrame_ = null;
const lastFrameTime = this.lastFrameTime_;
const now = this.lastFrameTime_ = timer.now();
if (lastFrameTime != 0 &&
// Chrome bug?
lastFrameTime != now) {
this.frameCount_++;
}
this.requestFrame_(now);
});
}

};


/**
* @param {!Window} win
* @return {!ActionService}
*/
export function installFramerateService(win) {
return getService(win, 'framerate', () => {
return new Framerate(win);
});
};
6 changes: 6 additions & 0 deletions src/service/resources-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {inputFor} from '../input';
import {log} from '../log';
import {reportError} from '../error';
import {timer} from '../timer';
import {installFramerateService} from './framerate-impl';
import {installViewerService} from './viewer-impl';
import {installViewportService} from './viewport-impl';
import {installVsyncService} from './vsync-impl';
Expand Down Expand Up @@ -161,6 +162,9 @@ export class Resources {
/** @private {boolean} */
this.vsyncScheduled_ = false;

/** @private @const {!Framerate} */
this.framerate_ = installFramerateService(this.win);

// When viewport is resized, we have to re-measure all elements.
this.viewport_.onChanged(event => {
this.lastScrollTime_ = timer.now();
Expand All @@ -170,6 +174,7 @@ export class Resources {
});
this.viewport_.onScroll(() => {
this.lastScrollTime_ = timer.now();
this.framerate_.collect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guessing onscroll is already throttled? do we need to throttle on the viewer/perf end?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The operation here is extremely cheap. Doesn't do anything except once per frame.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Looks like this should be fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, we do need to call flush at some point for these. one i'm guessing should be on doc unload and one when the queue reaches max (100 if i remember correctly)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flush should be called by the Performance itself, right? But seems like certainly not in this class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dvoytenko yes, just mentioning it, since we dont do anything right now

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is cool to call flush every time we tick these?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean flush at the end of the collection phase? If so, I think yes - it's just once per 5 seconds or so.

});

// When document becomes visible, e.g. from "prerender" mode, do a
Expand Down Expand Up @@ -1620,6 +1625,7 @@ export class Resource {
this.layoutCount_++;
this.state_ = ResourceState_.LAYOUT_SCHEDULED;

this.resources_.framerate_.collect(this.element);
let promise;
try {
promise = this.element.layoutCallback();
Expand Down
167 changes: 167 additions & 0 deletions test/functional/test-framerate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {installFramerateService} from '../../src/service/framerate-impl';
import * as sinon from 'sinon';

describe('the framerate service', () => {

let fr;
let win;
let lastRafCallback;
let call = 0;
let visible;
let performance;
let viewer;
let sandbox;
let clock;

beforeEach(() => {
sandbox = sinon.sandbox.create();
clock = sandbox.useFakeTimers();
lastRafCallback = null;
visible = true;
performance = {
tick: sinon.spy(),
flush: sinon.spy()
};
viewer = {
isVisible: () => {
return visible;
},
onVisibilityChanged: sinon.spy()
};
win = {
services: {
performance: {obj: performance},
viewer: {obj: viewer}
},
requestAnimationFrame: cb => {
lastRafCallback = cb;
return call++;
},
cancelAnimationFrame: index => {
if (index === null) {
return;
}
expect(index).to.equal(call - 1);
}
};
});

afterEach(() => {
sandbox.restore();
});

it('should tick and flush', () => {
fr = installFramerateService(win);
expect(lastRafCallback).to.not.be.null;
expect(call).to.equal(1);
fr.collect();
fr.collect();
fr.collect();
expect(call).to.equal(1);
clock.tick(16);
lastRafCallback();
clock.tick(16);
lastRafCallback();
expect(fr.frameCount_).to.equal(1);
clock.tick(16);
lastRafCallback();
expect(fr.frameCount_).to.equal(2);
for (let i = 0; i < 100; i++) {
clock.tick(16);
lastRafCallback();
}
clock.tick(5000);
lastRafCallback();
expect(performance.tick.callCount).to.equal(1);
expect(performance.flush.callCount).to.equal(1);
expect(performance.tick.args[0][0]).to.equal('fps');
expect(performance.tick.args[0][2]).to.within(15, 16);
expect(fr.frameCount_).to.equal(0);

// Second round
fr.collect();
clock.tick(16);
lastRafCallback();
clock.tick(16);
lastRafCallback();
expect(fr.frameCount_).to.equal(1);
clock.tick(16);
lastRafCallback();
expect(fr.frameCount_).to.equal(2);
for (let i = 0; i < 50; i++) {
clock.tick(16);
lastRafCallback();
}
clock.tick(5000);
lastRafCallback();
expect(performance.tick.callCount).to.equal(2);
expect(performance.flush.callCount).to.equal(2);
expect(performance.tick.args[1][0]).to.equal('fps');
expect(performance.tick.args[1][2]).to.within(9, 10);
});

it('does nothing with an invisible window', () => {
visible = false;
fr = installFramerateService(win);
expect(viewer.onVisibilityChanged.callCount).to.equal(1);
expect(fr.isActive_).to.be.false;
fr.collect();
expect(fr.requestedFrame_).to.be.null;
visible = true;
viewer.onVisibilityChanged.args[0][0]();
expect(fr.isActive_).to.be.true;
fr.collect();
expect(fr.requestedFrame_).to.not.be.null;
visible = false;
viewer.onVisibilityChanged.args[0][0](false);
expect(fr.isActive_).to.be.false;
expect(fr.requestedFrame_).to.be.null;
fr.collect();
expect(fr.requestedFrame_).to.be.null;
});

it('sends extra tick for ads', () => {
fr = installFramerateService(win);
fr.collect(document.createElement('amp-ad'));
for (let i = 0; i < 100; i++) {
clock.tick(16);
lastRafCallback();
}
clock.tick(5000);
lastRafCallback();
expect(performance.tick.callCount).to.equal(2);
expect(performance.flush.callCount).to.equal(1);
expect(performance.tick.args[0][0]).to.equal('fps');
expect(performance.tick.args[0][2]).to.within(15, 16);
expect(performance.tick.args[1][0]).to.equal('fal');
expect(performance.tick.args[1][2]).to.within(15, 16);
// Second round
fr.collect();
for (let i = 0; i < 50; i++) {
clock.tick(16);
lastRafCallback();
}
clock.tick(5000);
lastRafCallback();
expect(performance.tick.callCount).to.equal(4);
expect(performance.flush.callCount).to.equal(2);
expect(performance.tick.args[2][0]).to.equal('fps');
expect(performance.tick.args[3][0]).to.equal('fal');
});
});
Loading