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',