Merge "Merge branch 'stable-2.15'"
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index ccad352..48f4b17 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -365,38 +365,25 @@
 
 [[local-action-cache]]
 
-To accelerate builds, local action cache can be activated. Note, that this
-experimental feature is not activated per default and only available since
-Bazel version 0.7.
-
-To activate the local action cache, create accessible cache directory:
+To accelerate builds, local action cache can be activated.
+To activate the local action cache add these lines to your `~/.bazelrc` file:
 
 ----
- mkdir -p ~/.gerritcodereview/bazel-cache/cas
-----
-
-and add these lines to your `~/.bazelrc` file:
-
-----
-build --experimental_local_disk_cache_path=/home/<user>/.gerritcodereview/bazel-cache/cas
-build --experimental_local_disk_cache
+build --disk_cache=~/.gerritcodereview/bazel-cache/cas
 build --experimental_strict_action_env
 build --action_env=PATH
 ----
 
-[NOTE] `experimental_local_disk_cache_path` must be absolute path. Expansion of `~` is
-unfortunately not supported yet. This is also the reason why we can't activate this
-feature by default yet (by adjusting tools/bazel.rc file).
-
 [[repository_cache]]
 
-To accelerate fetches, local repository cache can be activated. This cache is
-only used for rules_closure external repository and transitive dependendcies.
-That's because rules_closure uses standard Bazel download facility. For all
-other gerrit dependencies, the download_artifacts repository cache is used
+To accelerate fetches, local repository cache is activated per default in Bazel.
+This cache is only used for rules_closure external repository and transitive
+dependendcies. That's because rules_closure uses standard Bazel download facility.
+For all other gerrit dependencies, the download_artifacts repository cache is used
 already.
 
-To activate the local repository cache, create accessible cache directory:
+To change the default local repository cache directry, create accessible cache
+directory:
 
 ----
  mkdir -p ~/.gerritcodereview/bazel-cache/repository
@@ -405,12 +392,9 @@
 and add this line to your `~/.bazelrc` file:
 
 ----
-build --experimental_repository_cache=/home/<user>/.gerritcodereview/bazel-cache/repository
+build --repository_cache=/home/<user>/.gerritcodereview/bazel-cache/repository
 ----
 
-[NOTE] `experimental_repository_cache` must be absolute path. Expansion of `~` is
-unfortunately not supported yet. This is also the reason why we can't activate this
-feature by default yet (by adjusting tools/bazel.rc file).
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 1d3a020..eb24d8e 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -27,6 +27,7 @@
   const SIZE_BAR_MIN_WIDTH = 1.5;
 
   const RENDER_TIME = 'FileListRenderTime';
+  const RENDER_TIME_AVERAGE = 'FileListRenderTimePerFile';
 
   const FileStatus = {
     A: 'Added',
@@ -146,6 +147,13 @@
         type: Array,
         computed: '_computeFilesShown(numFilesShown, _files.*)',
       },
+
+      /**
+       * The amount of files added to the shown files list the last time it was
+       * updated. This is used for reporting the average render time.
+       */
+      _reportinShownFilesIncrement: Number,
+
       _expandedFilePaths: {
         type: Array,
         value() { return []; },
@@ -802,6 +810,9 @@
     },
 
     _computeFilesShown(numFilesShown, files) {
+      const previousNumFilesShown = this._shownFiles ?
+          this._shownFiles.length : 0;
+
       const filesShown = files.base.slice(0, numFilesShown);
       this.fire('files-shown-changed', {length: filesShown.length});
 
@@ -810,6 +821,10 @@
       // dom-repeat binding.
       this.$.reporting.time(RENDER_TIME);
 
+      // How many more files are being shown (if it's an increase).
+      this._reportinShownFilesIncrement =
+          Math.max(0, filesShown.length - previousNumFilesShown);
+
       return filesShown;
     },
 
@@ -1199,7 +1214,8 @@
     _reportRenderedRow(index) {
       if (index === this._shownFiles.length - 1) {
         this.async(() => {
-          this.$.reporting.timeEnd(RENDER_TIME);
+          this.$.reporting.timeEndWithAverage(RENDER_TIME, RENDER_TIME_AVERAGE,
+              this._reportinShownFilesIncrement);
         }, 1);
       }
       return '';
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index ae67dac..63808d1 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -295,6 +295,26 @@
       delete this._baselines[name];
     },
 
+    /**
+     * Reports just line timeEnd, but additionally reports an average given a
+     * denominator and a separate reporiting name for the average.
+     * @param {string} name Timing name.
+     * @param {string} averageName Average timing name.
+     * @param {number} denominator Number by which to divide the total to
+     *     compute the average.
+     */
+    timeEndWithAverage(name, averageName, denominator) {
+      if (!this._baselines.hasOwnProperty(name)) { return; }
+      const baseTime = this._baselines[name];
+      this.timeEnd(name);
+
+      // Guard against division by zero.
+      if (!denominator) { return; }
+      const time = Math.round(this.now() - baseTime);
+      this.reporter(TIMING.TYPE, TIMING.CATEGORY, averageName,
+          Math.round(time / denominator));
+    },
+
     reportInteraction(eventName, opt_msg) {
       this.reporter(INTERACTION_TYPE, this.category, eventName, opt_msg);
     },
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
index e2bb83d..5849042 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -166,6 +166,19 @@
       ));
     });
 
+    test('timeEndWithAverage', () => {
+      const nowStub = sandbox.stub(element, 'now').returns(0);
+      nowStub.returns(1000);
+      element.time('foo');
+      nowStub.returns(1100);
+      element.timeEndWithAverage('foo', 'bar', 10);
+      assert.isTrue(element.reporter.calledTwice);
+      assert.isTrue(element.reporter.calledWithExactly(
+          'timing-report', 'UI Latency', 'foo', 100));
+      assert.isTrue(element.reporter.calledWithExactly(
+          'timing-report', 'UI Latency', 'bar', 10));
+    });
+
     test('reportExtension', () => {
       element.reportExtension('foo');
       assert.isTrue(element.reporter.calledWithExactly(
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index de92d8a..ef8c112 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -19,7 +19,6 @@
 
   const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
   const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
-  const LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/;
 
   Polymer({
     is: 'gr-lib-loader',
@@ -110,30 +109,11 @@
      * @return {string}
      */
     _getLibRoot() {
-      // TODO(wyatta): Remove the remainder of this method logic once the
-      // STATIC_RESOURCE_PATH variable is being provided generally.
       if (window.STATIC_RESOURCE_PATH) {
         return window.STATIC_RESOURCE_PATH + '/';
       }
-
-      if (this._cachedLibRoot) { return this._cachedLibRoot; }
-
-      const appLink = document.head
-        .querySelector('link[rel=import][href$="gr-app.html"]');
-
-      if (!appLink) { throw new Error('Could not find application link'); }
-
-      this._cachedLibRoot = appLink
-          .href
-          .match(LIB_ROOT_PATTERN)[1];
-
-      if (!this._cachedLibRoot) {
-        throw new Error('Could not extract lib root');
-      }
-
-      return this._cachedLibRoot;
+      return '/';
     },
-    _cachedLibRoot: null,
 
     /**
      * Load and execute a JS file from the lib root.
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index c081b30..ed4f528 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -138,16 +138,50 @@
     JSON_PREFIX,
 
     /**
+     * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
+     * with timing and logging.
+     * @param {string} url
+     * @param {Object=} opt_fetchOptions
+     */
+    _fetch(url, opt_fetchOptions) {
+      const start = Date.now();
+      const xhr = this._auth.fetch(url, opt_fetchOptions);
+
+      // Log the call after it completes.
+      xhr.then(res => this._logCall(url, opt_fetchOptions, start, res.status));
+
+      // Return the XHR directly (without the log).
+      return xhr;
+    },
+
+    /**
+     * Log information about a REST call. Because the elapsed time is determined
+     * by this method, it should be called immediately after the request
+     * finishes.
+     * @param {string} url
+     * @param {Object|undefined} fetchOptions
+     * @param {number} startTime the time that the request was started.
+     * @param {number} status the HTTP status of the response. The status value
+     *     is used here rather than the response object so there is no way this
+     *     method can read the body stream.
+     */
+    _logCall(url, fetchOptions, startTime, status) {
+      const method = (fetchOptions && fetchOptions.method) ?
+          fetchOptions.method : 'GET';
+      const elapsed = (Date.now() - startTime) + 'ms';
+      console.log(['HTTP', status, method, elapsed, url].join(' '));
+    },
+
+    /**
      * Fetch JSON from url provided.
      * Returns a Promise that resolves to a native Response.
      * Doesn't do error checking. Supports cancel condition. Performs auth.
      * Validates auth expiry errors.
      * @param {Defs.FetchJSONRequest} req
-     * @return {Promise}
      */
     _fetchRawJSON(req) {
       const urlWithParams = this._urlWithParams(req.url, req.params);
-      return this._auth.fetch(urlWithParams, req.fetchOptions).then(res => {
+      return this._fetch(urlWithParams, req.fetchOptions).then(res => {
         if (req.cancelCondition && req.cancelCondition()) {
           res.body.cancel();
           return;
@@ -1607,7 +1641,7 @@
       if (!url.startsWith('http')) {
         url = this.getBaseUrl() + url;
       }
-      return this._auth.fetch(url, options).then(response => {
+      return this._fetch(url, options).then(response => {
         if (!response.ok) {
           if (opt_errFn) {
             return opt_errFn.call(null, response);
@@ -1867,7 +1901,7 @@
     },
 
     _fetchB64File(url) {
-      return this._auth.fetch(this.getBaseUrl() + url)
+      return this._fetch(this.getBaseUrl() + url)
           .then(response => {
             if (!response.ok) { return Promise.reject(response.statusText); }
             const type = response.headers.get('X-FYI-Content-Type');
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 7e71efa..0f1db03 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -1319,5 +1319,21 @@
         });
       });
     });
+
+    test('_fetch forwards request and logs', () => {
+      const logStub = sandbox.stub(element, '_logCall');
+      const response = {status: 404, text: sinon.stub()};
+      const url = 'my url';
+      const fetchOptions = {method: 'DELETE'};
+      sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response));
+      const startTime = 123;
+      sandbox.stub(Date, 'now').returns(startTime);
+      return element._fetch(url, fetchOptions).then(() => {
+        assert.isTrue(logStub.calledOnce);
+        assert.isTrue(logStub.calledWith(
+            url, fetchOptions, startTime, response.status));
+        assert.isFalse(response.text.called);
+      });
+    });
   });
 </script>