-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'); | ||
}; |
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) { | ||
/** @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') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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_); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dividing over a division is always difficult to understand. How about There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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(); | ||
|
@@ -170,6 +174,7 @@ export class Resources { | |
}); | ||
this.viewport_.onScroll(() => { | ||
this.lastScrollTime_ = timer.now(); | ||
this.framerate_.collect(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. guessing There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True. Looks like this should be fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dvoytenko yes, just mentioning it, since we dont do anything right now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is cool to call flush every time we tick these? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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(); | ||
|
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'); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it always installed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Good idea?
There was a problem hiding this comment.
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.