| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| (function() { |
| 'use strict'; |
| |
| const DEFAULT_UPDATE_INTERVAL_MS = 1000 * 15; |
| const MAX_UPDATE_INTERVAL_MS = 1000 * 60 * 5; |
| |
| const BUILDBUCKET_URL = 'https://cr-buildbucket.appspot.com'; |
| |
| Polymer({ |
| is: 'binary-size-view', |
| |
| properties: { |
| baseUrl: { |
| type: String, |
| value: BUILDBUCKET_URL, |
| }, |
| plugin: Object, |
| projectName: String, |
| change: Object, |
| revision: Object, |
| |
| _validPatchNums: { |
| type: Array, |
| computed: '_computeValidPatchNums(change, revision._number)', |
| observer: '_refresh', |
| }, |
| _pluginConfig: Object, |
| _binarySizeInfo: { |
| type: Array, |
| value() { return []; }, |
| }, |
| _statusString: String, |
| _needsUpdate: { |
| type: Boolean, |
| value: false, |
| }, |
| _updateIntervalMs: { |
| type: Number, |
| value: DEFAULT_UPDATE_INTERVAL_MS, |
| }, |
| _updateTimeoutID: Number, |
| }, |
| |
| attached() { |
| this.listen(document, 'visibilitychange', '_handleVisibilityChange'); |
| if (!this.change) { |
| console.warn('element attached without change property set.'); |
| return; |
| } |
| this.reload(); |
| }, |
| |
| detached() { |
| this._clearUpdateTimeout(); |
| }, |
| |
| _clearUpdateTimeout() { |
| if (this._updateTimeoutID) { |
| window.clearTimeout(this._updateTimeoutID); |
| this._updateTimeoutID = null; |
| } |
| }, |
| |
| /** |
| * Reset the state of the element and fetch and display builds. |
| */ |
| async reload() { |
| const pluginName = this.plugin.getPluginName(); |
| const project = this.change.project; |
| |
| this._pluginConfig = await this.plugin.restApi().get( |
| `/projects/${encodeURIComponent(project)}/${pluginName}~config`); |
| if (!this._pluginConfig) { |
| console.log('binary-size plugin not configured for this project.'); |
| return; |
| } |
| |
| await this._refresh(); |
| }, |
| |
| _handleVisibilityChange(e) { |
| if (!document.hidden && this._needsUpdate) { |
| this._refresh(); |
| this._needsUpdate = false; |
| } |
| }, |
| |
| _updateTimerFired() { |
| if (document.hidden) { |
| this._needsUpdate = true; |
| return; |
| } |
| this._refresh(); |
| }, |
| |
| /** |
| * Fetch builds from Buildbucket, update the view with information about |
| * binary size, and update the timeout depending on success/failure. |
| */ |
| async _refresh() { |
| if (!this._pluginConfig) { |
| return; |
| } |
| if (!this.$.client || !this.$.client.searchBuilds) { |
| console.error('The "binary-size" plugin requires the "buildbucket"' + |
| ' plugin for searching builds. Please activate both.'); |
| } |
| |
| const newBinarySizeInfo = await this._tryGetNewBinarySizeInfo(); |
| if (newBinarySizeInfo != null) { |
| this._binarySizeInfo = this._sortUniqueInfoRows(newBinarySizeInfo); |
| this._statusString = this._getStatusString(this._validPatchNums); |
| this._updateIntervalMs = DEFAULT_UPDATE_INTERVAL_MS; |
| } else { |
| this._updateIntervalMs = Math.min( |
| MAX_UPDATE_INTERVAL_MS, |
| (1 + Math.random()) * this._updateIntervalMs * 2); |
| } |
| |
| this._clearUpdateTimeout(); |
| this._updateTimeoutID = window.setTimeout( |
| this._updateTimerFired.bind(this), this._updateIntervalMs); |
| }, |
| |
| /** |
| * Fetch builds from Buildbucket and return information about binary size, |
| * or null in case of failure. |
| */ |
| async _tryGetNewBinarySizeInfo() { |
| const tryBuckets = new Set(); |
| const ciBuckets = new Set(); |
| this._pluginConfig.builders.forEach((pair) => { |
| tryBuckets.add(pair.tryBucket); |
| ciBuckets.add(pair.ciBucket); |
| }); |
| |
| let tryBuilds = await this._getBuilds( |
| tryBuckets, this._tryjobTags(this._validPatchNums)); |
| if (tryBuilds == null) { |
| return null; |
| } |
| tryBuilds = this._selectRelevantBuilds(tryBuilds); |
| |
| let ciBuilds = await this._getBuilds( |
| ciBuckets, this._revisionTags(tryBuilds)); |
| if (ciBuilds == null) { |
| return null; |
| } |
| ciBuilds = this._selectRelevantBuilds(ciBuilds); |
| |
| return this._collateBinarySizeInfoFromBuildPairs( |
| this._pluginConfig.builders, tryBuilds, ciBuilds); |
| }, |
| |
| /** |
| * Return the Buildbucket tags corresponding to the provided patchset |
| * numbers. |
| */ |
| _tryjobTags(validPatchNums) { |
| return validPatchNums.map((patchNum) => { |
| const host = this._pluginConfig.gerritHost; |
| const changeNum = this.change._number; |
| return `buildset:patch/gerrit/${host}/${changeNum}/${patchNum}`; |
| }); |
| }, |
| |
| /** |
| * Return the Buildbucket tags corresponding to the base revision of each |
| * of the provided builds. |
| */ |
| _revisionTags(builds) { |
| const tags = builds.map((build) => { |
| const host = this._pluginConfig.gitHost; |
| const project = this.change.project; |
| const revision = build.properties.got_revision; |
| return `buildset:commit/gitiles/${host}/${project}/+/${revision}`; |
| }); |
| return Array.from(new Set(tags)); |
| }, |
| |
| /** |
| * Get builds that match any of the buckets and any of the tags. |
| */ |
| async _getBuilds(buckets, tags) { |
| if (buckets.length == 0 || tags.length == 0) { |
| return []; |
| } |
| try { |
| await this.$.client.getOAuthToken(); |
| |
| // Send a request for each tag. |
| const buildArrays = await Promise.all( |
| tags.map((tag) => { |
| const params = new URLSearchParams({ |
| max_builds: 500, |
| include_experimental: true, |
| fields: 'builds(bucket,id,result_details_json,tags,url)', |
| tag: tag, |
| }); |
| buckets.forEach((bucket) => { |
| params.append('bucket', bucket); |
| }); |
| return this.$.client.searchBuilds(params); |
| }) |
| ); |
| |
| // Concatenate builds in all responses. Assume that the builds in the |
| // response for each tag entry in tags are mutually exclusive, because |
| // each tag represents one patchset of the CL. |
| return Array.prototype.concat.apply([], buildArrays); |
| } catch (err) { |
| console.warn('Buildbucket search failed', err); |
| return null; |
| } |
| }, |
| |
| /** |
| * Filter out builds that don't have properties needed to display binary |
| * size information. This also mutates the builds to populate properties |
| * based on result_details_json. |
| */ |
| _selectRelevantBuilds(builds) { |
| return builds.filter((build) => { |
| try { |
| build.properties = JSON.parse(build.result_details_json).properties; |
| } catch (e) { |
| return false; |
| } |
| return (build.properties && build.properties.got_revision && |
| build.properties.binary_sizes); |
| }); |
| }, |
| |
| /** |
| * Collate the information about builds into a list of info objects. |
| * tryBuilds are tryjobs for the current CL. ciBuilds are builds that |
| * can be used as a baseline for the calculation of binary size difference, |
| * typically CI builds with base revisions that match tryBuilds. |
| */ |
| _collateBinarySizeInfoFromBuildPairs(builderPairs, tryBuilds, ciBuilds) { |
| const results = []; |
| |
| // For each definition of equivalent builder pairs, find builds that |
| // match the bucket/builder names, and also have a mutually matching |
| // got_revision. |
| builderPairs.forEach((pairDefinition) => { |
| const selectedTryBuilds = tryBuilds.filter((tryBuild) => ( |
| tryBuild.bucket === pairDefinition.tryBucket && |
| this._getBuilder(tryBuild) === pairDefinition.tryBuilder)); |
| |
| const selectedCiBuilds = ciBuilds.filter((ciBuild) => ( |
| ciBuild.bucket === pairDefinition.ciBucket && |
| this._getBuilder(ciBuild) === pairDefinition.ciBuilder)); |
| |
| selectedTryBuilds.forEach((tryBuild) => { |
| selectedCiBuilds.forEach((ciBuild) => { |
| if (tryBuild.properties.got_revision !== |
| ciBuild.properties.got_revision) { |
| return; |
| } |
| const trySizes = tryBuild.properties.binary_sizes; |
| const ciSizes = ciBuild.properties.binary_sizes; |
| |
| for (const binary in trySizes) { |
| if (!ciSizes.hasOwnProperty(binary)) { |
| continue; |
| } |
| results.push({ |
| id: tryBuild.id, |
| builder: pairDefinition.tryBuilder, |
| binary: binary, |
| trySize: trySizes[binary], |
| tryUrl: tryBuild.url, |
| ciSize: ciSizes[binary], |
| ciUrl: ciBuild.url, |
| }); |
| } |
| }); |
| }); |
| }); |
| return results; |
| }, |
| |
| /** |
| * Extract the builder name from a build. |
| */ |
| _getBuilder(build) { |
| const TAG_PREFIX = 'builder:'; |
| for (let i = 0; i < build.tags.length; i++) { |
| const tag = build.tags[i]; |
| if (tag.startsWith(TAG_PREFIX)) { |
| return tag.slice(TAG_PREFIX.length); |
| } |
| } |
| return build.properties.buildername; |
| }, |
| |
| /** |
| * Sort rows of binary size information by builder, and keep only the latest |
| * build for each. |
| */ |
| _sortUniqueInfoRows(rows) { |
| const compareBuilderName = (a, b) => { |
| return (a.builder.localeCompare(b.builder) || |
| a.binary.localeCompare(b.binary)); |
| }; |
| const compareId = (a, b) => { |
| return -this.$.client.compareIds(a.id, b.id); |
| }; |
| return this._sortUniqueItems(rows, compareBuilderName, compareId); |
| }, |
| |
| /** |
| * Sort items by primaryCmp then by secondaryCmp. Remove consecutive items |
| * which are equal by primaryCmp. |
| */ |
| _sortUniqueItems(items, primaryCmp, secondaryCmp) { |
| const sorted = items.slice().sort((a, b) => { |
| return primaryCmp(a, b) || secondaryCmp(a, b); |
| }); |
| return sorted.filter((item, i) => { |
| return i == 0 || primaryCmp(sorted[i-1], item) != 0; |
| }); |
| }, |
| |
| /** |
| * Construct a description string for the given patchset numbers. |
| */ |
| _getStatusString(validPatchNums) { |
| let patchStatus = ''; |
| if (validPatchNums) { |
| if (validPatchNums.length === 1) { |
| patchStatus = `Showing results from patch set ${validPatchNums[0]}. `; |
| } else { |
| const begin = Math.min(...validPatchNums); |
| const end = Math.max(...validPatchNums); |
| patchStatus = `Showing results from patch sets ${begin}-${end}. `; |
| } |
| } |
| return patchStatus + 'Last updated at ' + new Date().toLocaleTimeString(); |
| }, |
| |
| /** |
| * List numbers of patchsets (revisions) that are applicable. |
| * |
| * The reason why this is not just the current patchset number is because |
| * there may have been a succession of "trivial" changes before the current |
| * patchset. |
| * |
| * @param {Object} change A Gerrit ChangeInfo object. |
| * @param {number} patchNum Revision number of currently displayed patch. |
| * @return {number[]} Revision numbers for the displayed builds. |
| */ |
| _computeValidPatchNums(change, patchNum) { |
| const validKinds = ['TRIVIAL_REBASE', 'NO_CHANGE', 'NO_CODE_CHANGE']; |
| const revisions = []; |
| for (const revision in change.revisions) { |
| if (!change.revisions.hasOwnProperty(revision)) { continue; } |
| revisions.push(change.revisions[revision]); |
| } |
| revisions.sort((a, b) => { |
| // Reverse sort. |
| return b._number - a._number; |
| }); |
| const patchNums = []; |
| for (let i = 0; i < revisions.length; i++) { |
| if (i == 0 && change.status === 'MERGED') { |
| // Skip past the most recent patch on submitted CLs because the last |
| // patchset is always the autogenerated one, which may or may not |
| // count as a trivial change depending on the submit strategy. |
| continue; |
| } |
| if (revisions[i]._number > patchNum) { |
| // Patches after the one we're displaying don't count. |
| continue; |
| } |
| patchNums.push(revisions[i]._number); |
| if (validKinds.indexOf(revisions[i].kind) === -1) { |
| // If this revision was a non-trivial change, |
| // don't consider patchsets prior to it. |
| break; |
| } |
| } |
| return patchNums; |
| }, |
| |
| /** |
| * Convert a positive number of bytes to a human-readable string. |
| */ |
| _humanByteSize(bytes) { |
| const units = ['B', 'KiB', 'MiB', 'GiB']; |
| const decPoints = [0, 2, 3, 4]; |
| let unitIndex = 0; |
| while (Math.abs(bytes) >= 2048 && unitIndex < units.length - 1) { |
| bytes /= 1024; |
| unitIndex += 1; |
| } |
| return `${bytes.toFixed(decPoints[unitIndex])} ${units[unitIndex]}`; |
| }, |
| |
| /** |
| * Return a plus for positive numbers, a minus for negative numbers. |
| */ |
| _signFor(number) { |
| if (number > 0) { |
| return '+'; |
| } else if (number < 0) { |
| return '\u2212'; // MINUS SIGN |
| } else { |
| return ''; |
| } |
| }, |
| |
| /** |
| * Convert a difference between two numbers of bytes to a human-readable |
| * string, with plus or minus in front. |
| */ |
| _humanByteSizeDelta(before, after) { |
| const delta = after - before; |
| if (delta == 0) { |
| return '\u2014'; // EM DASH |
| } |
| return this._signFor(delta) + this._humanByteSize(Math.abs(delta)); |
| }, |
| |
| /** |
| * Convert a difference between two numbers to a percentage with 4 decimal |
| * places, with plus or minus in front. |
| */ |
| _percentSizeDelta(before, after) { |
| const delta = ((after - before) / before) * 100; |
| if (delta == 0) { |
| return '\u2014'; // EM DASH |
| } |
| return this._signFor(delta) + Math.abs(delta).toFixed(4) + '%'; |
| }, |
| |
| _subtract(a, b) { |
| return a - b; |
| }, |
| }); |
| })(); |