Diff
Modified: trunk/Websites/perf.webkit.org/ChangeLog (175005 => 175006)
--- trunk/Websites/perf.webkit.org/ChangeLog 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/ChangeLog 2014-10-22 00:53:39 UTC (rev 175006)
@@ -1,3 +1,113 @@
+2014-10-18 Ryosuke Niwa <[email protected]>
+
+ Perf dashboard should provide a way to associate bugs with a test run
+ https://bugs.webkit.org/show_bug.cgi?id=137857
+
+ Reviewed by Andreas Kling.
+
+ Added a "privileged" API, /privileged-api/associate-bug, to associate a bug with a test run.
+ /privileged-api/ is to be protected by an authentication mechanism such as DigestAuth over https by
+ the Apache configuration.
+
+
+ The Cross Site Request (CSRF) Forgery prevention for privileged APIs work as follows. When a user is
+ about to make a privileged API access, the front end code obtains a CSRF token generated by POST'ing
+ to privileged-api/generate-csrf-token; the page sets a randomly generated salt and an expiration time
+ via the cookie and returns a token computed from those two values as well as the remote username.
+
+ The font end code then POST's the request along with the returned token. The server side code verifies
+ that the specified token can be generated from the salt and the expiration time set in the cookie, and
+ the token hasn't expired.
+
+
+ * init-database.sql: Added bug_url to bug_trackers table, and added bugs table. Each bug tracker will
+ have zero or exactly one bug associated with a test run.
+
+ * public/admin/bug-trackers.php: Added the support for editing bug_url.
+ * public/api/runs.php:
+ (fetch_runs_for_config): Modified the query to fetch bugs associated with test_runs.
+ (parse_bugs_array): Added. Parses the aggregated bugs and creates a dictionary that maps a tracker id to
+ an associated bug if there is one.
+ (format_run): Calls parse_bugs_array.
+
+ * public/include/json-header.php: Added helper functions to deal for CSRF prevention.
+ (ensure_privileged_api_data): Added. Dies immediately if the request's method is not POST or doesn't
+ have a valid JSON payload.
+ (ensure_privileged_api_data_and_token): Ditto. Also checks that the CSRF prevention token is valid.
+ (compute_token): Computes a CSRF token using the REMOTE_USER (e.g. set via BasicAuth), the salt, and
+ the expiration time stored in the cookie.
+ (verify_token): Returns true iff the specified token matches what compute_token returns from the cookie.
+
+ * public/include/manifest.php:
+ (ManifestGenerator::bug_trackers): Include bug_url as bugUrl in the manifest. Also use tracker_id instead
+ of tracker_name as the key in the manifest. This requires changes to both v1 and v2 front end.
+
+ * public/index.html:
+ (Chart..showTooltipWithResults): Updated for the manifest format changed mentioned above.
+
+ * public/privileged-api/associate-bug.php: Added.
+ (main): Added. Associates or dissociates a bug with a test run inside a transaction. It prevent a CSRF
+ attack via ensure_privileged_api_data_and_token, which calls verify_token.
+
+ * public/privileged-api/generate-csrf-token.php: Added. Generates a CSRF token valid for one hour.
+
+ * public/v2/app.css:
+ (.disabled .icon-button:hover g): Used by the "bugs" icon when a range of points or no points are
+ selected in a chart.
+
+ * public/v2/app.js:
+ (App.PaneController.actions.toggleBugsPane): Added. Toggles the visibility of the bugs pane when exactly
+ one point is selected in the chart. Also hides the search pane when making the bugs pane visible since
+ they would overlap on each other if both of them are shown.
+ (App.PaneController.actions.associateBug): Makes a privileged API request to associate the specified bug
+ with the currently selected point (test run). Updates the bug information in "details" and colors of dots
+ in the charts to reflect new states. Because chart data objects aren't real Ember objects for performance
+ reasons, we have to use a dirty hack of modifying a dummy counter bugsChangeCount.
+ (App.PaneController.actions.toggleSearchPane): Renamed from toggleSearch. Also hides the bugs pane when
+ showing the search pane.
+ (App.PaneController.actions.rangeChanged): Takes all selected points as the second argument instead of
+ taking start and end points as the second and the third arguments so that _showDetails can enumerate all
+ bugs in the selected range.
+
+ (App.PaneController._detailsChanged): Added. Hide the bugs pane whenever a new point is selected.
+ Also update singlySelectedPoint, which is used by toggleBugsPane and associateBug.
+ (App.PaneController._currentItemChanged): Updated for the _showDetails change.
+ (App.PaneController._showDetails): Takes an array of selected points in place of old arguments.
+ Simplified the code to compute the revision information. Calls _updateBugs to format the associated bugs.
+ (App.PaneController._updateBugs): Sets details.bugTrackers to a dictionary that maps a bug tracker id to
+ a bug tracker proxy with an array of (bugNumber, bugUrl) pairs and also editedBugNumber, which is used by
+ the bugs pane to associate or dissociate a bug number, if exactly one point is selected.
+
+ (App.InteractiveChartComponent._updateDotsWithBugs): Added. Sets hasBugs class on dots as needed.
+ (App.InteractiveChartComponent._setCurrentSelection): Finds and passes all points in the selected range
+ to selectionChanged action instead of just finding the first and the last points.
+
+ * public/v2/chart-pane.css: Updated the style.
+
+ * public/v2/data.js:
+ (PrivilegedAPI): Added. A wrapper for privileged APIs' CSRF tokens.
+ (PrivilegedAPI.sendRequest): Makes a privileged API call. Fetches a new CSRF token if needed.
+ (PrivilegedAPI._generateTokenInServerIfNeeded): Makes a request to privileged-api/generate-csrf-token if
+ we haven't already obtained a CSRF token or if the token has already been expired.
+ (PrivilegedAPI._post): Makes a single POST request to /privileged-api/* with a JSON payload.
+
+ (Measurement.prototype.bugs): Added.
+ (Measurement.prototype.hasBugs): Returns true iff bugs has more than one bug number.
+ (Measurement.prototype.associateBug): Associates a bug with a test run via privileged-api/associate-bug.
+
+ * public/v2/index.html: Added the bugs pane. Also added a list of bugs associated with the current run in
+ the details.
+
+ * public/v2/manifest.js:
+ (App.BugTracker.bugUrl):
+ (App.BugTracker.newBugUrl): Added.
+ (App.BugTracker.repositories): Added. This was a missing back reference to repositories.
+ (App.MetricSerializer.normalizePayload): Now parses/loads the list of bug trackers from the manifest.
+ (App.Manifest.repositoriesWithReportedCommits): Now initialized to an empty array instead of null.
+ (App.Manifest.bugTrackers): Added.
+ (App.Manifest._fetchedManifest): Sets App.Manifest.bugTrackers. Also sorts the list of repositories by
+ their respective ids to make the ordering stable.
+
2014-10-14 Ryosuke Niwa <[email protected]>
Remove unused jobs table
Modified: trunk/Websites/perf.webkit.org/init-database.sql (175005 => 175006)
--- trunk/Websites/perf.webkit.org/init-database.sql 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/init-database.sql 2014-10-22 00:53:39 UTC (rev 175006)
@@ -29,6 +29,7 @@
CREATE TABLE bug_trackers (
tracker_id serial PRIMARY KEY,
tracker_name varchar(64) NOT NULL,
+ tracker_bug_url varchar(1024),
tracker_new_bug_url varchar(1024));
CREATE TABLE tracker_repositories (
@@ -128,3 +129,12 @@
report_content text,
report_failure varchar(64),
report_failure_details text);
+
+CREATE TABLE bugs (
+ bug_id serial PRIMARY KEY,
+ bug_run integer REFERENCES test_runs NOT NULL,
+ bug_tracker integer REFERENCES bug_trackers NOT NULL,
+ bug_number integer NOT NULL,
+ CONSTRAINT bug_tracker_and_run_must_be_unique UNIQUE(bug_tracker, bug_run));
+CREATE INDEX bugs_tracker_number_index ON bugs(bug_tracker, bug_number);
+CREATE INDEX bugs_run_index ON bugs(bug_run);
Modified: trunk/Websites/perf.webkit.org/public/admin/bug-trackers.php (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/admin/bug-trackers.php 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/admin/bug-trackers.php 2014-10-22 00:53:39 UTC (rev 175006)
@@ -11,7 +11,9 @@
} else
notice('Could not add the bug tracker.');
} else if ($action == 'update') {
- if (update_field('bug_trackers', 'tracker', 'name') || update_field('bug_trackers', 'tracker', 'new_bug_url'))
+ if (update_field('bug_trackers', 'tracker', 'name')
+ || update_field('bug_trackers', 'tracker', 'bug_url')
+ || update_field('bug_trackers', 'tracker', 'new_bug_url'))
regenerate_manifest();
else
notice('Invalid parameters.');
@@ -63,6 +65,7 @@
$page = new AdministrativePage($db, 'bug_trackers', 'tracker', array(
'name' => array('editing_mode' => 'string'),
+ 'bug_url' => array('editing_mode' => 'url', 'label' => 'Bug URL ($number)'),
'new_bug_url' => array('editing_mode' => 'text', 'label' => 'New Bug URL ($title, $description)'),
'Associated repositories' => array('custom' => function ($row) { return associated_repositories($row); }),
));
Modified: trunk/Websites/perf.webkit.org/public/api/runs.php (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/api/runs.php 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/api/runs.php 2014-10-22 00:53:39 UTC (rev 175006)
@@ -30,10 +30,14 @@
function fetch_runs_for_config($db, $config) {
$raw_runs = $db->query_and_fetch_all('
- SELECT test_runs.*, builds.*, array_agg((commit_repository, commit_revision, commit_time)) AS revisions
- FROM builds LEFT OUTER JOIN build_commits ON commit_build = build_id LEFT OUTER JOIN commits ON build_commit = commit_id, test_runs
- WHERE run_build = build_id AND run_config = $1
- GROUP BY build_id, run_id', array($config['config_id']));
+ SELECT test_runs.*, builds.*, array_agg((commit_repository, commit_revision, commit_time)) AS revisions
+ FROM builds LEFT OUTER JOIN build_commits ON commit_build = build_id
+ LEFT OUTER JOIN commits ON build_commit = commit_id,
+ (SELECT test_runs.*, array_agg((bug_tracker, bug_number)) AS bugs
+ FROM test_runs LEFT OUTER JOIN bugs ON bug_run = run_id WHERE run_config = $1 GROUP BY run_id) as test_runs
+ WHERE run_build = build_id
+ GROUP BY run_id, run_config, run_build, run_mean_cache, run_iteration_count_cache,
+ run_sum_cache, run_square_sum_cache, bugs, build_id', array($config['config_id']));
$formatted_runs = array();
if (!$raw_runs)
@@ -62,6 +66,19 @@
return $revisions;
}
+function parse_bugs_array($postgres_array) {
+ // e.g. {"(1 /* Bugzilla */, 12345)","(2 /* Radar */, 67890)"}
+ $outer_array = json_decode('[' . trim($postgres_array, '{}') . ']');
+ $bugs = array();
+ foreach ($outer_array as $item) {
+ $raw_data = explode(',', trim($item, '()'));
+ if (!$raw_data[0])
+ continue;
+ $bugs[trim($raw_data[0], '"')] = trim($raw_data[1], '"');
+ }
+ return $bugs;
+}
+
function format_run($run) {
return array(
'id' => intval($run['run_id']),
@@ -70,6 +87,7 @@
'sum' => floatval($run['run_sum_cache']),
'squareSum' => floatval($run['run_square_sum_cache']),
'revisions' => parse_revisions_array($run['revisions']),
+ 'bugs' => parse_bugs_array($run['bugs']),
'buildTime' => strtotime($run['build_time']) * 1000,
'buildNumber' => intval($run['build_number']),
'builder' => $run['build_builder']);
Modified: trunk/Websites/perf.webkit.org/public/include/json-header.php (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/include/json-header.php 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/include/json-header.php 2014-10-22 00:53:39 UTC (rev 175006)
@@ -69,4 +69,42 @@
}
}
+function ensure_privileged_api_data() {
+ global $HTTP_RAW_POST_DATA;
+
+ if ($_SERVER['REQUEST_METHOD'] != 'POST')
+ exit_with_error('InvalidRequestMethod');
+
+ if (!isset($HTTP_RAW_POST_DATA))
+ exit_with_error('InvalidRequestContent');
+
+ $data = "" true);
+
+ if ($data ="" NULL)
+ exit_with_error('InvalidRequestContent');
+
+ return $data;
+}
+
+function ensure_privileged_api_data_and_token() {
+ $data = ""
+ if (!verify_token(array_get($data, 'token')))
+ exit_with_error('InvalidToken');
+ return $data;
+}
+
+function compute_token() {
+ if (!array_key_exists('CSRFSalt', $_COOKIE) || !array_key_exists('CSRFExpiration', $_COOKIE))
+ return NULL;
+ $user = array_get($_SERVER, 'REMOTE_USER');
+ $salt = $_COOKIE['CSRFSalt'];
+ $expiration = $_COOKIE['CSRFExpiration'];
+ return hash('sha256', "$salt|$user|$expiration");
+}
+
+function verify_token($token) {
+ $expected_token = compute_token();
+ return $expected_token && $token == $expected_token && $_COOKIE['CSRFExpiration'] > time();
+}
+
?>
Modified: trunk/Websites/perf.webkit.org/public/include/manifest.php (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/include/manifest.php 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/include/manifest.php 2014-10-22 00:53:39 UTC (rev 175006)
@@ -139,7 +139,10 @@
$bug_trackers_table = $this->db->fetch_table('bug_trackers');
if ($bug_trackers_table) {
foreach ($bug_trackers_table as $row) {
- $bug_trackers[$row['tracker_name']] = array('newBugUrl' => $row['tracker_new_bug_url'],
+ $bug_trackers[$row['tracker_id']] = array(
+ 'name' => $row['tracker_name'],
+ 'bugUrl' => $row['tracker_bug_url'],
+ 'newBugUrl' => $row['tracker_new_bug_url'],
'repositories' => $tracker_id_to_repositories[$row['tracker_id']]);
}
}
Modified: trunk/Websites/perf.webkit.org/public/index.html (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/index.html 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/index.html 2014-10-22 00:53:39 UTC (rev 175006)
@@ -634,8 +634,9 @@
+ ' around ' + result.build().formattedTime();
var revisions = result.build().formattedRevisions(resultToCompare.build());
- for (var trackerName in bugTrackers) {
- var repositories = bugTrackers[trackerName].repositories;
+ for (var trackerId in bugTrackers) {
+ var tracker = bugTrackers[trackerId];
+ var repositories = tracker.repositories;
var description = 'Platform: ' + result.build().platform().name + '\n\n';
for (var i = 0; i < repositories.length; ++i) {
var repositoryName = repositories[i];
@@ -648,13 +649,13 @@
description += revision.label;
description += '\n';
}
- var url = ""
+ var url = ""
.replace(/\$title/g, encodeURIComponent(title))
.replace(/\$description/g, encodeURIComponent(description))
.replace(/\$link/g, encodeURIComponent(location.href));
if (newBugUrls)
newBugUrls += ',';
- newBugUrls += ' <a href="" + url + '" target="_blank">' + trackerName + '</a>';
+ newBugUrls += ' <a href="" + url + '" target="_blank">' + tracker.name + '</a>';
}
newBugUrls = 'File:' + newBugUrls;
}
Added: trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php (0 => 175006)
--- trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php (rev 0)
+++ trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php 2014-10-22 00:53:39 UTC (rev 175006)
@@ -0,0 +1,39 @@
+<?php
+
+require_once('../include/json-header.php');
+
+function main() {
+ $data = ""
+
+ $run_id = array_get($data, 'run');
+ $bug_tracker_id = array_get($data, 'tracker');
+ $bug_number = array_get($data, 'bugNumber');
+
+ if (!$run_id)
+ exit_with_error('InvalidRunId', array('run' => $run_id));
+ if (!$bug_tracker_id)
+ exit_with_error('InvalidBugTrackerId', array('tracker' => $bug_tracker_id));
+
+ $db = connect();
+ $db->begin_transaction();
+
+ $bug_id = NULL;
+ if (!$bug_number) {
+ $count = $db->query_and_get_affected_rows("DELETE FROM bugs WHERE bug_run = $1 AND bug_tracker = $2",
+ array($run_id, $bug_tracker_id));
+ if ($count > 1) {
+ $db->rollback_transaction();
+ exit_with_error('UnexpectedNumberOfAffectedRows', array('affectedRows' => $count));
+ }
+ } else {
+ $bug_id = $db->update_or_insert_row('bugs', 'bug', array('run' => $run_id, 'tracker' => $bug_tracker_id),
+ array('run' => $run_id, 'tracker' => $bug_tracker_id, 'number' => $bug_number));
+ }
+ $db->commit_transaction();
+
+ exit_with_success(array('bug_id' => $bug_id));
+}
+
+main();
+
+?>
Added: trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php (0 => 175006)
--- trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php (rev 0)
+++ trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php 2014-10-22 00:53:39 UTC (rev 175006)
@@ -0,0 +1,18 @@
+<?php
+
+require_once('../include/json-header.php');
+
+ensure_privileged_api_data();
+
+$user = array_get($_SERVER, 'REMOTE_USER');
+
+$expiritaion = time() + 3600; // Valid for one hour.
+$_COOKIE['CSRFSalt'] = rand();
+$_COOKIE['CSRFExpiration'] = $expiritaion;
+
+setcookie('CSRFSalt', $_COOKIE['CSRFSalt']);
+setcookie('CSRFExpiration', $expiritaion);
+
+exit_with_success(array('user' => $user, 'token' => compute_token(), 'expiration' => $expiritaion * 1000));
+
+?>
Modified: trunk/Websites/perf.webkit.org/public/v2/app.css (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/v2/app.css 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/app.css 2014-10-22 00:53:39 UTC (rev 175006)
@@ -125,6 +125,9 @@
.icon-button:hover g {
stroke: #666;
}
+.disabled .icon-button:hover g {
+ stroke: #ccc;
+}
#header {
Modified: trunk/Websites/perf.webkit.org/public/v2/app.js (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/v2/app.js 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/app.js 2014-10-22 00:53:39 UTC (rev 175006)
@@ -664,6 +664,7 @@
sharedTime: Ember.computed.alias('parentController.sharedTime'),
sharedSelection: Ember.computed.alias('parentController.sharedSelection'),
selection: null,
+ bugsChangeCount: 0, // Dirty hack. Used to call InteractiveChartComponent's _updateDotsWithBugs.
actions: {
toggleDetails: function()
{
@@ -673,14 +674,33 @@
{
this.parentController.removePane(this.get('model'));
},
- toggleSearch: function ()
+ toggleBugsPane: function ()
{
+ if (!App.Manifest.bugTrackers || !this.get('singlySelectedPoint'))
+ return;
+ if (this.toggleProperty('showingBugsPane'))
+ this.set('showingSearchPane', false);
+ },
+ associateBug: function (bugTracker, bugNumber)
+ {
+ var point = this.get('singlySelectedPoint');
+ if (!point)
+ return;
+ var self = this;
+ point.measurement.associateBug(bugTracker.get('id'), bugNumber).then(function () {
+ self._updateBugs();
+ self.set('bugsChangeCount', self.get('bugsChangeCount') + 1);
+ });
+ },
+ toggleSearchPane: function ()
+ {
if (!App.Manifest.repositoriesWithReportedCommits)
return;
var model = this.get('model');
if (!model.get('commitSearchRepository'))
model.set('commitSearchRepository', App.Manifest.repositoriesWithReportedCommits[0]);
- this.toggleProperty('showingSearchPane');
+ if (this.toggleProperty('showingSearchPane'))
+ this.set('showingBugsPane', false);
},
searchCommit: function () {
var model = this.get('model');
@@ -697,19 +717,24 @@
this.set('intrinsicDomain', intrinsicDomain);
this.get('parentController').updateSharedDomain();
},
- rangeChanged: function (extent, startPoint, endPoint)
+ rangeChanged: function (extent, points)
{
- if (!startPoint || !endPoint) {
+ if (!points) {
this._hasRange = false;
this.set('details', null);
this.set('timeRange', null);
return;
}
this._hasRange = true;
- this._showDetails(startPoint.measurement, endPoint.measurement, false);
+ this._showDetails(points);
this.set('timeRange', extent);
},
},
+ _detailsChanged: function ()
+ {
+ this.set('showingBugsPane', false);
+ this.set('singlySelectedPoint', !this._hasRange && this._selectedPoints ? this._selectedPoints[0] : null);
+ }.observes('details'),
_overviewSelectionChanged: function ()
{
var overviewSelection = this.get('overviewSelection');
@@ -745,29 +770,25 @@
if (!point || !point.measurement)
this.set('details', null);
else
- this._showDetails(point.series.previousPoint(point).measurement, point.measurement, true);
+ this._showDetails([point]);
}.observes('currentItem'),
- _showDetails: function (oldMeasurement, currentMeasurement, isShowingEndPoint)
+ _showDetails: function (points)
{
- var revisions = [];
-
+ var isShowingEndPoint = !this._hasRange;
+ var currentMeasurement = points[0].measurement;
+ var oldMeasurement = points[points.length - 1].measurement;
var formattedRevisions = currentMeasurement.formattedRevisions(oldMeasurement);
- var repositoryNames = [];
- for (var repositoryName in formattedRevisions)
- repositoryNames.push(repositoryName);
- var revisions = [];
- repositoryNames.sort().forEach(function (repositoryName) {
- var revision = formattedRevisions[repositoryName];
- var repository = App.Manifest.repository(repositoryName);
- revision['url'] = false;
- if (repository) {
- revision['url'] = revision.previousRevision
- ? repository.urlForRevisionRange(revision.previousRevision, revision.currentRevision)
- : repository.urlForRevision(revision.currentRevision);
- }
+ var revisions = App.Manifest.get('repositories')
+ .filter(function (repository) { return formattedRevisions[repository.get('id')]; })
+ .map(function (repository) {
+ var repositoryName = repository.get('id');
+ var revision = Ember.Object.create(formattedRevisions[repositoryName]);
+ revision['url'] = revision.previousRevision
+ ? repository.urlForRevisionRange(revision.previousRevision, revision.currentRevision)
+ : repository.urlForRevision(revision.currentRevision);
revision['name'] = repositoryName;
revision['repository'] = repository;
- revisions.push(Ember.Object.create(revision));
+ return revision;
});
var buildNumber = null;
@@ -778,14 +799,48 @@
if (builder)
buildURL = builder.urlFromBuildNumber(buildNumber);
}
- this.set('details', {
+
+ this._selectedPoints = points;
+ this.set('details', Ember.Object.create({
currentValue: currentMeasurement.mean().toFixed(2),
oldValue: oldMeasurement && !isShowingEndPoint ? oldMeasurement.mean().toFixed(2) : null,
buildNumber: buildNumber,
buildURL: buildURL,
buildTime: currentMeasurement.formattedBuildTime(),
revisions: revisions,
+ }));
+ this._updateBugs();
+ },
+ _updateBugs: function ()
+ {
+ if (!this._selectedPoints)
+ return;
+
+ var bugTrackers = App.Manifest.get('bugTrackers');
+ var trackerToBugNumbers = {};
+ bugTrackers.forEach(function (tracker) { trackerToBugNumbers[tracker.get('id')] = new Array(); });
+ this._selectedPoints.map(function (point) {
+ var bugs = point.measurement.bugs();
+ bugTrackers.forEach(function (tracker) {
+ var bugNumber = bugs[tracker.get('id')];
+ if (bugNumber)
+ trackerToBugNumbers[tracker.get('id')].push(bugNumber);
+ });
});
+
+ this.set('details.bugTrackers', App.Manifest.get('bugTrackers').map(function (tracker) {
+ var bugNumbers = trackerToBugNumbers[tracker.get('id')];
+ return Ember.ObjectProxy.create({
+ content: tracker,
+ bugs: bugNumbers.map(function (bugNumber) {
+ return {
+ bugNumber: bugNumber,
+ bugUrl: bugNumber && tracker.get('bugUrl') ? tracker.get('bugUrl').replace(/\$number/g, bugNumber) : null
+ };
+ }),
+ editedBugNumber: this._hasRange ? null : bugNumbers[0],
+ }); // FIXME: Create urls for new bugs.
+ }));
}
});
@@ -1063,6 +1118,7 @@
.attr("cx", function(measurement) { return xScale(measurement.time); })
.attr("cy", function(measurement) { return yScale(measurement.value); });
});
+ this._updateDotsWithBugs();
this._updateHighlightPositions();
if (this._brush) {
@@ -1091,6 +1147,13 @@
.style("z-index", "100")
.text(this._yAxisUnit);
},
+ _updateDotsWithBugs: function () {
+ if (!this.get('interactive'))
+ return;
+ this._dots.forEach(function (dot) {
+ dot.classed('hasBugs', function (point) { return !!point.measurement.hasBugs(); });
+ })
+ }.observes('bugsChangeCount'), // Never used for anything but to call this method :(
_updateHighlightPositions: function () {
var xScale = this._x;
var yScale = this._y;
@@ -1424,22 +1487,12 @@
if (this._brushExtent === newSelection)
return;
+ var points = null;
if (newSelection) {
- var startPoint;
- var endPoint;
- for (var i = 0; i < this._currentTimeSeriesData.length; i++) {
- var point = this._currentTimeSeriesData[i];
- if (!startPoint) {
- if (point.time >= newSelection[0]) {
- if (point.time > newSelection[1])
- break;
- startPoint = point;
- }
- } else if (point.time > newSelection[1])
- break;
- if (point.time >= newSelection[0] && point.time <= newSelection[1])
- endPoint = point;
- }
+ points = this._currentTimeSeriesData
+ .filter(function (point) { return point.time >= newSelection[0] && point.time <= newSelection[1]; });
+ if (!points.length)
+ points = null;
}
this._brushExtent = newSelection;
@@ -1447,7 +1500,7 @@
this._updateSelectionToolbar();
this.set('sharedSelection', newSelection);
- this.sendAction('selectionChanged', newSelection, startPoint, endPoint);
+ this.sendAction('selectionChanged', newSelection, points);
},
_updateSelectionToolbar: function ()
{
Modified: trunk/Websites/perf.webkit.org/public/v2/chart-pane.css (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/v2/chart-pane.css 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/chart-pane.css 2014-10-22 00:53:39 UTC (rev 175006)
@@ -50,6 +50,13 @@
top: 0.55rem;
}
+.chart-pane a.bugs-button {
+ display: inline-block;
+ position: absolute;
+ right: 1.85rem;
+ top: 0.55rem;
+}
+
.chart-pane a.search-button {
display: inline-block;
position: absolute;
@@ -57,9 +64,8 @@
top: 0.55rem;
}
-.search-pane {
+.search-pane, .bugs-pane {
position: absolute;
- right: 0rem;
top: 1.7rem;
border: 1px solid #bbb;
padding: 0;
@@ -68,6 +74,24 @@
background: white;
}
+.bugs-pane {
+ right: 1.3rem;
+}
+
+.bugs-pane table {
+ margin: 0.2rem;
+ font-size: 0.8rem;
+}
+
+.bugs-pane th {
+ font-weight: normal;
+}
+
+.search-pane {
+ right: 0rem;
+}
+
+.bugs-pane.hidden,
.search-pane.hidden {
display: none;
}
@@ -186,12 +210,16 @@
}
.chart-pane .details-table th {
- width: 4rem;
+ width: 7rem;
text-align: right;
font-weight: normal;
padding: 0;
}
+.chart-pane .details-table .bugs th {
+ font-weight: bold;
+}
+
.chart-pane .details-table th:after {
content: " : ";
}
@@ -230,6 +258,10 @@
stroke: none;
}
+.chart .hasBugs {
+ fill: #33f;
+}
+
.chart path.area {
stroke: none;
fill: #ccc;
Modified: trunk/Websites/perf.webkit.org/public/v2/data.js (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/v2/data.js 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/data.js 2014-10-22 00:53:39 UTC (rev 175006)
@@ -1,5 +1,55 @@
// We don't use DS.Model for these object types because we can't afford to process millions of them.
+var PrivilegedAPI = {
+ _token: null,
+ _expiration: null,
+ _maxNetworkLatency: 3 * 60 * 1000 /* 3 minutes */,
+};
+
+PrivilegedAPI.sendRequest = function (url, parameters)
+{
+ return this._generateTokenInServerIfNeeded().then(function (token) {
+ return PrivilegedAPI._post(url, $.extend({token: token}, parameters));
+ });
+}
+
+PrivilegedAPI._generateTokenInServerIfNeeded = function ()
+{
+ var self = this;
+ return new Ember.RSVP.Promise(function (resolve, reject) {
+ if (self._token && self._expiration > Date.now() + self._maxNetworkLatency)
+ resolve(self._token);
+
+ PrivilegedAPI._post('generate-csrf-token')
+ .then(function (result, reject) {
+ self._token = result['token'];
+ self._expiration = new Date(result['expiration']);
+ resolve(self._token);
+ }).catch(reject);
+ });
+}
+
+PrivilegedAPI._post = function (url, parameters)
+{
+ return new Ember.RSVP.Promise(function (resolve, reject) {
+ $.ajax({
+ url: '../privileged-api/' + url,
+ type: 'POST',
+ contentType: 'application/json',
+ data: parameters ? JSON.stringify(parameters) : '{}',
+ dataType: 'json',
+ }).done(function (data) {
+ if (data.status != 'OK')
+ reject(data.status);
+ else
+ resolve(data);
+ }).fail(function (xhr, status, error) {
+ console.log(xhr);
+ reject(xhr.status + (error ? ', ' + error : ''));
+ });
+ });
+}
+
var CommitLogs = {
_cachedCommitsByRepository: {}
};
@@ -220,6 +270,34 @@
return date.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
}
+Measurement.prototype.bugs = function ()
+{
+ return this._raw['bugs'];
+}
+
+Measurement.prototype.hasBugs = function ()
+{
+ var bugs = this.bugs();
+ return bugs && Object.keys(bugs).length;
+}
+
+Measurement.prototype.associateBug = function (trackerId, bugNumber)
+{
+ var bugs = this._raw['bugs'];
+ trackerId = parseInt(trackerId);
+ bugNumber = bugNumber ? parseInt(bugNumber) : null;
+ return PrivilegedAPI.sendRequest('associate-bug', {
+ run: this.id(),
+ tracker: trackerId,
+ bugNumber: bugNumber,
+ }).then(function () {
+ if (bugNumber)
+ bugs[trackerId] = bugNumber;
+ else
+ delete bugs[trackerId];
+ });
+}
+
function RunsData(rawData)
{
this._measurements = rawData.map(function (run) { return new Measurement(run); });
Modified: trunk/Websites/perf.webkit.org/public/v2/index.html (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/v2/index.html 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/index.html 2014-10-22 00:53:39 UTC (rev 175006)
@@ -135,9 +135,16 @@
{{metric.label}}
- {{ platform.name}}</h2>
<a href="" title="Close" class="close-button" {{action "close"}}>{{partial "close-button"}}</a>
- {{if App.Manifest.repositoriesWithReportedCommits}}
- <a href="" title="Search" class="search-button" {{action "toggleSearch"}}>{{partial "search-button"}}</a>
+ {{#if App.Manifest.bugTrackers}}
+ <a href="" title="Bugs"
+ {{bind-attr class=":bugs-button singlySelectedPoint::disabled"}}
+ {{action "toggleBugsPane"}}>
+ {{partial "bugs-button"}}
+ </a>
{{/if}}
+ {{#if App.Manifest.repositoriesWithReportedCommits}}
+ <a href="" title="Search" class="search-button" {{action "toggleSearchPane"}}>{{partial "search-button"}}</a>
+ {{/if}}
</header>
<div class="body">
@@ -155,6 +162,7 @@
sharedSelection=sharedSelection
selectionChanged="rangeChanged"
selectionIsLocked=timeRangeIsLocked
+ bugsChangeCount=bugsChangeCount
zoom="zoomed"}}
{{else}}
{{#if failure}}
@@ -191,6 +199,21 @@
{{input action="" placeholder="Name or email" value=commitSearchKeyword}}
</form>
+ <div {{bind-attr class=":bugs-pane showingBugsPane::hidden"}}>
+ <table>
+ {{#each details.bugTrackers}}
+ <tr>
+ <th>{{label}}</th>
+ <td>
+ <form {{action "associateBug" this editedBugNumber on="submit"}}>
+ {{input type=text value=editedBugNumber}}
+ </form>
+ </td>
+ </tr>
+ {{/each}}
+ </table>
+ </div>
+
</section>
{{/each}}
</script>
@@ -214,6 +237,20 @@
<script type="text/x-handlebars" data-template-name="chart-details">
<div class="details-table-container">
<table class="details-table">
+ <tbody class="bugs">
+ {{#each details.bugTrackers}}
+ {{#if bugs}}
+ <tr>
+ <th>{{label}}</th>
+ <td>
+ {{#each bugs}}
+ <a {{bind-attr href="" target="_blank">{{bugNumber}}</a>
+ {{/each}}
+ </td>
+ </tr>
+ {{/if}}
+ {{/each}}
+ </tbody>
<tr><th>Current</th><td>{{details.currentValue}} {{chartData.unit}}
{{#if details.oldValue}}
(from {{details.oldValue}})
@@ -281,6 +318,16 @@
</svg>
</script>
+ <script type="text/x-handlebars" data-template-name="bugs-button">
+ <svg class="bugs-button icon-button" viewBox="0 0 100 100">
+ <g stroke="black" stroke-width="15">
+ <circle cx="50" cy="50" r="40" fill="transparent"/>
+ <line x1="50" y1="25" x2="50" y2="55"/>
+ <circle cx="50" cy="67.5" r="2.5" fill="transparent"/>
+ </g>
+ </svg>
+ </script>
+
<script type="text/x-handlebars" data-template-name="search-button">
<svg class="search-button icon-button" viewBox="0 0 100 100">
<g stroke="black" stroke-width="15">
Modified: trunk/Websites/perf.webkit.org/public/v2/manifest.js (175005 => 175006)
--- trunk/Websites/perf.webkit.org/public/v2/manifest.js 2014-10-22 00:52:40 UTC (rev 175005)
+++ trunk/Websites/perf.webkit.org/public/v2/manifest.js 2014-10-22 00:53:39 UTC (rev 175006)
@@ -42,7 +42,9 @@
});
App.BugTracker = App.NameLabelModel.extend({
- buildUrl: DS.attr('string'),
+ bugUrl: DS.attr('string'),
+ newBugUrl: DS.attr('string'),
+ repositories: DS.hasMany('repository'),
});
App.Platform = App.NameLabelModel.extend({
@@ -93,6 +95,7 @@
}),
metrics: this._normalizeIdMap(payload['metrics']),
repositories: this._normalizeIdMap(payload['repositories']),
+ bugTrackers: this._normalizeIdMap(payload['bugTrackers']),
};
for (var testId in payload['tests']) {
@@ -144,11 +147,12 @@
App.Manifest = Ember.Controller.extend({
platforms: null,
topLevelTests: null,
+ repositories: [],
+ repositoriesWithReportedCommits: [],
+ bugTrackers: [],
_platformById: {},
_metricById: {},
_builderById: {},
- repositories: null,
- repositoriesWithReportedCommits: null,
_repositoryById: {},
_fetchPromise: null,
fetch: function ()
@@ -196,8 +200,10 @@
repositories.forEach(function (repository) {
self._repositoryById[repository.get('id')] = repository;
});
- this.set('repositories', repositories);
+ this.set('repositories', repositories.sortBy('id'));
this.set('repositoriesWithReportedCommits',
repositories.filter(function (repository) { return repository.get('hasReportedCommits'); }));
+
+ this.set('bugTrackers', store.all('bugTracker').sortBy('name'));
}
}).create();