UI latency reporting Implements gr-reporting as common reporting interface. Adds UI latency reporting methods: - time and timeEnd to measure individual events. - appStarted and pageLoaded to report startup metrics. - reporter property at the moment reports metrics via DOM CustomEvents. UI Switcher extension listens to those and reports them to Google Analytics. Also, see https://gerrit-review.googlesource.com/83512 Also, go/gerrit-reporting Change-Id: I36a166f07be281761262fefa50cc539cc6ea2d19
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html new file mode 100644 index 0000000..7653655 --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -0,0 +1,21 @@ +<!-- +Copyright (C) 2016 The Android Open Source Project + +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. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<dom-module id="gr-reporting"> + <script src="gr-reporting.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js new file mode 100644 index 0000000..c7180a7 --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -0,0 +1,95 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// 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. +(function() { + 'use strict'; + + var APP_STARTED = 'App Started'; + var PAGE_LOADED = 'Page Loaded'; + var TIMING_EVENT = 'timing-report'; + var DEFAULT_CATEGORY = 'UI Latency'; + var DEFAULT_TYPE = 'timing'; + + Polymer({ + is: 'gr-reporting', + + properties: { + _baselines: { + type: Array, + value: function() { return {}; }, + } + }, + + get performanceTiming() { + return window.performance.timing; + }, + + now: function() { + return Math.round(10 * window.performance.now()) / 10; + }, + + reporter: function(type, category, eventName, eventValue) { + eventValue = eventValue; + var detail = { + type: type, + category: category, + name: eventName, + value: eventValue, + }; + document.dispatchEvent( + new CustomEvent(TIMING_EVENT, {detail: detail})); + console.log(eventName + ': ' + eventValue); + }, + + /** + * User-perceived app start time, should be reported when the app is ready. + */ + appStarted: function() { + var startTime = + new Date().getTime() - this.performanceTiming.navigationStart; + this.reporter( + DEFAULT_TYPE, DEFAULT_CATEGORY, APP_STARTED, startTime); + }, + + /** + * Page load time, should be reported at any time after navigation. + */ + pageLoaded: function() { + if (this.performanceTiming.loadEventEnd === 0) { + console.error('pageLoaded should be called after window.onload'); + this.async(this.pageLoaded, 100); + } else { + var loadTime = this.performanceTiming.loadEventEnd - + this.performanceTiming.navigationStart; + this.reporter(DEFAULT_TYPE, DEFAULT_CATEGORY, PAGE_LOADED, loadTime); + } + }, + + /** + * Reset named timer. + */ + time: function(name) { + this._baselines[name] = this.now(); + }, + + /** + * Finish named timer and report it to server. + */ + timeEnd: function(name) { + var baseTime = this._baselines[name] || 0; + var time = this.now() - baseTime; + this.reporter(DEFAULT_TYPE, DEFAULT_CATEGORY, name, time); + delete this._baselines[name]; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html new file mode 100644 index 0000000..e9226b8 --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -0,0 +1,93 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +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. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-reporting</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="gr-reporting.html"> + +<test-fixture id="basic"> + <template> + <gr-reporting></gr-reporting> + </template> +</test-fixture> + +<script> + suite('gr-reporting tests', function() { + var element; + var sandbox; + var clock; + var fakePerformance; + + var NOW_TIME = 100; + + setup(function() { + sandbox = sinon.sandbox.create(); + clock = sinon.useFakeTimers(NOW_TIME); + element = fixture('basic'); + fakePerformance = { + navigationStart: 1, + loadEventEnd: 2, + }; + sinon.stub(element, 'performanceTiming', + {get: function() {return fakePerformance;}}); + sandbox.stub(element, 'reporter'); + }); + teardown(function() { + sandbox.restore(); + clock.restore(); + }); + + test('appStarted', function() { + element.appStarted(); + assert.isTrue( + element.reporter.calledWithExactly( + 'timing', 'UI Latency', 'App Started', + NOW_TIME - fakePerformance.navigationStart + )); + }); + + test('pageLoaded', function() { + element.pageLoaded(); + assert.isTrue( + element.reporter.calledWithExactly( + 'timing', 'UI Latency', 'Page Loaded', + fakePerformance.loadEventEnd - fakePerformance.navigationStart) + ); + }); + + test('time and timeEnd', function() { + var nowStub = sinon.stub(element, 'now').returns(0); + element.time('foo'); + nowStub.returns(1); + element.time('bar'); + nowStub.returns(2); + element.timeEnd('bar'); + nowStub.returns(3.123); + element.timeEnd('foo'); + assert.isTrue(element.reporter.calledWithExactly( + 'timing', 'UI Latency', 'foo', 3.123 + )); + assert.isTrue(element.reporter.calledWithExactly( + 'timing', 'UI Latency', 'bar', 1 + )); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js index d11d438..8441372 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -18,8 +18,12 @@ // custom element having the id "app", but it is made explicit here. var app = document.querySelector('#app'); var restAPI = document.createElement('gr-rest-api-interface'); + var reporting = document.createElement('gr-reporting'); window.addEventListener('WebComponentsReady', function() { + reporting.timeEnd('WebComponentsReady'); + reporting.pageLoaded(); + // Middleware page(function(ctx, next) { document.body.scrollTop = 0;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html index ec19a2d..18c0602 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -14,10 +14,12 @@ limitations under the License. --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> <link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html"> <link rel="import" href="../gr-diff-processor/gr-diff-processor.html"> <link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html"> <link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html"> + <dom-module id="gr-diff-builder"> <template> <div class="contentWrapper"> @@ -32,6 +34,7 @@ <gr-diff-processor id="processor" groups="{{_groups}}"></gr-diff-processor> + <gr-reporting id="reporting"></gr-reporting> </template> <script src="../gr-diff/gr-diff-line.js"></script> <script src="../gr-diff/gr-diff-group.js"></script> @@ -111,17 +114,19 @@ this._clearDiffContent(); - console.time(TimingLabel.TOTAL); - console.time(TimingLabel.CONTENT); + var reporting = this.$.reporting; + + reporting.time(TimingLabel.TOTAL); + reporting.time(TimingLabel.CONTENT); return this.$.processor.process(this.diff.content).then(function() { if (this.isImageDiff) { this._builder.renderDiffImages(); } - console.timeEnd(TimingLabel.CONTENT); - console.time(TimingLabel.SYNTAX); + reporting.timeEnd(TimingLabel.CONTENT); + reporting.time(TimingLabel.SYNTAX); this.$.syntaxLayer.process().then(function() { - console.timeEnd(TimingLabel.SYNTAX); - console.timeEnd(TimingLabel.TOTAL); + reporting.timeEnd(TimingLabel.SYNTAX); + reporting.timeEnd(TimingLabel.TOTAL); }); this.fire('render'); }.bind(this));
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html index e8b1453..187a5cd 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -437,6 +437,10 @@ ] }, ]; + stub('gr-reporting', { + time: sinon.stub(), + timeEnd: sinon.stub(), + }); element = fixture('basic'); outputEl = element.queryEffectiveChildren('#diffTable'); element.addEventListener('render', function() { @@ -458,6 +462,20 @@ element.render({left: [], right: []}, prefs); }); + test('reporting', function(done) { + var timeStub = element.$.reporting.time; + var timeEndStub = element.$.reporting.timeEnd; + flush(function() { + assert.isTrue(timeStub.calledWithExactly('Diff Total Render')); + assert.isTrue(timeStub.calledWithExactly('Diff Content Render')); + assert.isTrue(timeStub.calledWithExactly('Diff Syntax Render')); + assert.isTrue(timeEndStub.calledWithExactly('Diff Total Render')); + assert.isTrue(timeEndStub.calledWithExactly('Diff Content Render')); + assert.isTrue(timeEndStub.calledWithExactly('Diff Syntax Render')); + done(); + }); + }); + test('renderSection', function() { var section = outputEl.querySelector('stub:nth-of-type(2)'); var prevInnerHTML = section.innerHTML;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js index 2a1e880..e9b39de 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -477,8 +477,8 @@ /** * Given an array and a size, return an array of arrays where no inner array * is larger than that size, preserving the original order. - * @param {!Array<T>} - * @param {number} + * @param {!Array<T>} array + * @param {number} size * @return {!Array<!Array<T>>} * @template T */ @@ -489,7 +489,7 @@ var head = array.slice(0, array.length - size); var tail = array.slice(array.length - size); - return this._breakdown(head, size).concat([tail]) + return this._breakdown(head, size).concat([tail]); }, }); })();
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html index c20795b..25dfaff 100644 --- a/polygerrit-ui/app/elements/gr-app.html +++ b/polygerrit-ui/app/elements/gr-app.html
@@ -22,6 +22,7 @@ <link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html"> <link rel="import" href="./core/gr-main-header/gr-main-header.html"> <link rel="import" href="./core/gr-router/gr-router.html"> +<link rel="import" href="./core/gr-reporting/gr-reporting.html"> <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html"> <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html"> @@ -136,6 +137,7 @@ </gr-overlay> <gr-error-manager></gr-error-manager> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + <gr-reporting id="reporting"></gr-reporting> </template> <script src="gr-app.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js index 0833a72..f24ecfd 100644 --- a/polygerrit-ui/app/elements/gr-app.js +++ b/polygerrit-ui/app/elements/gr-app.js
@@ -72,6 +72,7 @@ }, ready: function() { + this.$.reporting.appStarted(); this._viewState = { changeView: { changeNum: null,
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html new file mode 100644 index 0000000..ca26ed0 --- /dev/null +++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -0,0 +1,53 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +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. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-app</title> + +<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="gr-app.html"> + +<test-fixture id="basic"> + <template> + <gr-app></gr-app> + </template> +</test-fixture> + +<script> + suite('gr-app tests', function() { + var sandbox; + var element; + + setup(function(done) { + sandbox = sinon.sandbox.create(); + stub('gr-reporting', { + appStarted: sandbox.stub(), + }); + element = fixture('basic'); + flush(done); + }); + teardown(function() { + sandbox.restore(); + }); + + test('reporting', function() { + assert.isTrue(element.$.reporting.appStarted.calledOnce); + }); + }); +</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index d3cb316..2714f48 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html
@@ -49,8 +49,8 @@ 'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html', 'diff/gr-diff-comment/gr-diff-comment_test.html', 'diff/gr-diff-cursor/gr-diff-cursor_test.html', - 'diff/gr-diff-highlight/gr-diff-highlight_test.html', 'diff/gr-diff-highlight/gr-annotation_test.html', + 'diff/gr-diff-highlight/gr-diff-highlight_test.html', 'diff/gr-diff-preferences/gr-diff-preferences_test.html', 'diff/gr-diff-processor/gr-diff-processor_test.html', 'diff/gr-diff-selection/gr-diff-selection_test.html', @@ -61,6 +61,7 @@ 'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html', 'diff/gr-selection-action-box/gr-selection-action-box_test.html', 'diff/gr-syntax-layer/gr-syntax-layer_test.html', + 'gr-app_test.html', 'settings/gr-account-info/gr-account-info_test.html', 'settings/gr-email-editor/gr-email-editor_test.html', 'settings/gr-group-list/gr-group-list_test.html', @@ -69,10 +70,10 @@ 'settings/gr-settings-view/gr-settings-view_test.html', 'settings/gr-ssh-editor/gr-ssh-editor_test.html', 'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html', - 'shared/gr-autocomplete/gr-autocomplete_test.html', 'shared/gr-account-label/gr-account-label_test.html', 'shared/gr-account-link/gr-account-link_test.html', 'shared/gr-alert/gr-alert_test.html', + 'shared/gr-autocomplete/gr-autocomplete_test.html', 'shared/gr-avatar/gr-avatar_test.html', 'shared/gr-change-star/gr-change-star_test.html', 'shared/gr-confirm-dialog/gr-confirm-dialog_test.html',