Skip to content

Commit

Permalink
✨ Report a new tick event metric for Layout Jank (ampproject#21060)
Browse files Browse the repository at this point in the history
* Report a new tick event metric for Layout Jank.

* Introduce tests and refactor layout jank implementation.

* Remove extra line

* Correct Performance.onVisibilityChange_ binding

* Add lj and lj-2 to Tick Events documentation

* Renamed onPageHidden_ to onVisibilityChange_

* Add Closure Compiler definition for the Layout Jank API

* Add a local definition of Safari useragent checking to avoid test failure in cross-service dependency

* Fix useragent reference bug

* Add Layout Jank extern to list of externs

* Remove duplicate useragent sniffing

* Prefer DocumentState service to custom event listening

* Prefer stubbing in tests, other small fixes
  • Loading branch information
ericandrewlewis authored and William Chou committed Mar 5, 2019
1 parent 4083661 commit ca80cbd
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 2 deletions.
34 changes: 34 additions & 0 deletions build-system/layout-jank.extern.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright 2019 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 Definitions for the Layout Jank API.
*
* Created from
* @see https://gist.github.com/skobes/2f296da1b0a88cc785a4bf10a42bca07
*
* @todo This should be removed when the definitions are released
* in closure-compiler.
*
* @externs
*/

/**
* @constructor
* @extends {PerformanceEntry}
*/
function PerformanceLayoutJank() {}
/** @type {number} */ PerformanceLayoutJank.prototype.fraction;
1 change: 1 addition & 0 deletions build-system/tasks/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ function compile(entryModuleFilenames, outputDir, outputFilename, options) {
'build-system/amp.extern.js',
'build-system/dompurify.extern.js',
'build-system/event-timing.extern.js',
'build-system/layout-jank.extern.js',
'third_party/closure-compiler/externs/web_animations.js',
'third_party/moment/moment.extern.js',
'third_party/react-externs/externs.js',
Expand Down
2 changes: 2 additions & 0 deletions extensions/amp-viewer-integration/TICKEVENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ As an example if we executed `perf.tick('label')` we assume we have a counterpar
| First contentful paint time | `fcp` | First paint with content. See https://github.com/WICG/paint-timing |
| First input delay | `fid` | Millisecond delay in handling the first user input on the page. See https://github.com/WICG/event-timing |
| First input delay, polyfill value | `fid-polyfill` | Millisecond delay in handling the first user input on the page, reported by [a polyfill](https://github.com/GoogleChromeLabs/first-input-delay) |
| Layout Jank, first exit | `lj` | The aggregate jank score when the user leaves the page (navigation, tab switching, dismissing application) for the first time. See https://gist.github.com/skobes/2f296da1b0a88cc785a4bf10a42bca07 |
| Layout Jank, second exit | `lj-2` | The aggregate jank score when the user leaves the page (navigation, tab switching, dismissing application) for the second time. |
96 changes: 95 additions & 1 deletion src/service/performance-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ function incOrDef(obj, name) {
}
}


/**
* Performance holds the mechanism to call `tick` to stamp out important
* events in the lifecycle of the AMP runtime. It can hold a small amount
Expand Down Expand Up @@ -104,6 +103,24 @@ export class Performance {
/** @private {number|null} */
this.firstViewportReady_ = null;

/**
* How many times a layout jank metric has been ticked.
*
* @private {number}
*/
this.jankScoresTicked_ = 0;

/**
* The sum of all layout jank fractions triggered on the page from the
* Layout Jank API.
*
* @private {number}
*/
this.aggregateJankScore_ = 0;

this.boundOnVisibilityChange_ = this.onVisibilityChange_.bind(this);
this.boundTickLayoutJankScore_ = this.tickLayoutJankScore_.bind(this);

// Add RTV version as experiment ID, so we can slice the data by version.
this.addEnabledExperiment('rtv-' + getMode(this.win).rtvVersion);
if (isCanary(this.win)) {
Expand Down Expand Up @@ -204,6 +221,9 @@ export class Performance {
this.tickDelta('fid', entry.processingStart - entry.startTime);
recordedFirstInputDelay = true;
}
else if (entry.entryType === 'layoutJank') {
this.aggregateJankScore_ += entry.fraction;
}
};

const entryTypesToObserve = [];
Expand All @@ -223,6 +243,31 @@ export class Performance {
entryTypesToObserve.push('firstInput');
}

if (this.win.PerformanceLayoutJank) {
// Programmatically read once as currently PerformanceObserver does not
// report past entries as of Chrome 61.
// https://bugs.chromium.org/p/chromium/issues/detail?id=725567
this.win.performance.getEntriesByType('layoutJank').forEach(processEntry);
entryTypesToObserve.push('layoutJank');

// Register a handler to record the layout jank metric when the page
// enters the hidden lifecycle state.
// @see https://developers.google.com/web/updates/2018/07/page-lifecycle-api
Services.documentStateFor(this.win)
.onVisibilityChanged(this.boundOnVisibilityChange_);

// Safari does not reliably fire the `pagehide` or `visibilitychange`
// events when closing a tab, so we have to use `beforeunload`.
// See https://bugs.webkit.org/show_bug.cgi?id=151234
const platform = Services.platformFor(this.win);
if (platform.isSafari()) {
this.win.addEventListener(
'beforeunload',
this.boundTickLayoutJankScore_
);
}
}

if (entryTypesToObserve.length === 0) {
return;
}
Expand All @@ -248,6 +293,55 @@ export class Performance {
});
}

/**
* When the visibility state of the document changes to hidden,
* send the layout jank score.
*/
onVisibilityChange_() {
if (!Services.documentStateFor(this.win).isHidden()) {
return;
}
this.tickLayoutJankScore_();
}

/**
* Tick the layout jank score metric.
*
* A value of the metric is recorded in under two names, `lj` and `lj-2`,
* for the first two times the page transitions into a hidden lifecycle state
* (when the page is navigated a way from, the tab is backgrounded for
* another tab, or the user backgrounds the browser application).
*
* Since we can't reliably detect when a page session finally ends,
* recording the value for these first two events should provide a fair
* amount of visibility into this metric.
*/
tickLayoutJankScore_() {
if (this.jankScoresTicked_ === 0) {
this.tickDelta('lj', this.aggregateJankScore_);
this.flush();
this.jankScoresTicked_ = 1;
return;
}
if (this.jankScoresTicked_ === 1) {
this.tickDelta('lj-2', this.aggregateJankScore_);
this.flush();
this.jankScoresTicked_ = 2;

// TODO(chmoux) - add the ability to remove a visibilityobservable handler
// in the DocumentState Service, and remove the handler
// this.boundOnVisibilityChange_ here.

const platform = Services.platformFor(this.win);
if (platform.isSafari()) {
this.win.removeEventListener(
'beforeunload',
this.boundTickLayoutJankScore_
);
}
}
}

/**
* Tick fp time based on Chrome's legacy paint timing API when
* appropriate.
Expand Down
Loading

0 comments on commit ca80cbd

Please sign in to comment.