Changeset 180333 in webkit


Ignore:
Timestamp:
Feb 18, 2015 6:41:45 PM (9 years ago)
Author:
rniwa@webkit.org
Message:

Analysis task pages are unusable
https://bugs.webkit.org/show_bug.cgi?id=141786

Reviewed by Andreas Kling.

This patch makes following improvements to analysis task pages:

  1. Making the main chart interactive. This change required the use of App.Pane as well as moving the code to

compute the data for the details pane from PaneController.

  1. Moving the form to add a new test group to the top of test groups instead of the bottom of them.
  2. Grouping the build requests in each test group by root sets instead of the order by which they were ran.

This change required the creation of App.TestGroupPane as well as its methods.

  1. Show a box plot for each root set configuration as well as each build request. This change required

App.BoxPlotComponent.

  1. Show revisions of each repository (e.g. WebKit) for each root set and build request.
  • public/api/build-requests.php:

(main): Update per the rename of BuildRequestsFetcher::root_sets to BuildRequestsFetcher::root_sets_by_id.

  • public/api/test-groups.php:

(main): Include root sets and roots in the response.
(format_test_group):

  • public/include/build-requests-fetcher.php:

(BuildRequestsFetcher::root_sets_by_id): Renamed from root_sets.
(BuildRequestsFetcher::root_sets): Added.
(BuildRequestsFetcher::roots): Added.
(BuildRequestsFetcher::fetch_roots_for_set): Takes a boolean argument $resolve_ids. This flag is only set to
true in /api/build-requests/ (as done prior to this patch) to use repository names as identifiers since
tools/sync-with-buildbot.py can't convert repository names to their ids.

  • public/v2/analysis.js:

(App.Root): Added.
(App.RootSet): Added.
(App.RootSet.revisionForRepository): Added.
(App.TestGroup.rootSets): Deleted the code to compute root set ids from build requests now that the JSON
response at /api/test-groups will include them.
(App.BuildRequest): Ditto. Also deleted 'configLetter' property, which has been moved to a proxy created by
_createConfigurationSummary.
(App.BuildRequest.statusLabel): Use 'Completed' as the human readable label for 'completed' status.
(App.BuildRequest.aggregateStatuses): Added. Generates a human readable status for a set of build requests.

  • public/v2/app.css: Updated style rules for analysis task pages.
  • public/v2/app.js:

(App.Pane): This class is now used in analysis task pages to make the main chart interactive.
(App.Pane._updateDetails): Moved from App.PaneController.

(App.PaneController._updateCanAnalyze): Updated the code per the move of selectedPoints.

(App.AnalysisTaskController): Added 'details'.
(App.AnalysisTaskController._taskUpdated):
(App.AnalysisTaskController.paneDomain):Renamed from _fetchedRuns.
(App.AnalysisTaskController.updateTestGroupPanes): Added. Creates App.TestGroupPane for each test group.
(App.AnalysisTaskController.actions.toggleShowRequestList): Added.

(App.TestGroupPane): Added.
(App.TestGroupPane._populate): Added. Group build requests by root sets and create a summary for each group.
(App.TestGroupPane._computeRepositoryList): Added. Returns a sorted list of repositories which is the union
of all repositories appearing in root sets and builds associated with A/B testing results.
(App.TestGroupPane._groupRequestsByConfigurations): Added. Groups build requests by root sets.
(App.TestGroupPane._createConfigurationSummary): Added. Creates a summary for a group of build requests that
use the same root set. We start by wrapping "raw" build requests in a proxy with formatted values,
build numbers, etc... obtained from the fetched chart data. The list of revisions shown in the group summary
is a union of revisions in the root set and the first build request in the group. We null-out revision info
for a build request if it is identical to the one in the summary. The range of values is expanded as needed
by the values in the group as well as 95% percentile confidence interval.

(App.BoxPlotComponent): Added. Controls a box plot shown for each test group summary and build request.
(App.BoxPlotComponent.didInsertElement): Added. Inserts a SVG element as well as two indicator rects to show
the mean and the confidence interval.
(App.BoxPlotComponent._updateBars): Added. Updates the dimensions of the indicator rects.
(App.BoxPlotComponent.valueChanged): Added. Computes the relative dimensions of the indicator rects and
calls _updateBars to update the rects.

  • public/v2/chart-pane.css: Added some style rules to be used in the details pane in analysis task pages.
  • public/v2/data.js:

(Measurement.prototype.formattedRevisions):
(Measurement.formatRevisionRange): Renamed from Measurement.prototype._formatRevisionRange so that it can be
called in _createConfigurationSummary.

  • public/v2/index.html: Updated the templates for analysis task pages. Moved the form to create a new test

group above all test groups, and replaced the list of data points by "details" pane used in the charts page.
Also made the fetching of chartData no longer block showing of test groups.

  • public/v2/interactive-chart.js:

(App.InteractiveChartComponent._updateDomain): Added an early exit to fix a newly revealed race condition.
(App.InteractiveChartComponent._domainChanged): Ditto.
(App.InteractiveChartComponent._updateSelectionToolbar): Made it respect 'zoomable' boolean property.

  • public/v2/js/statistics.js:

(Statistics.min): Added.
(Statistics.max): Added.

  • public/v2/manifest.js:

(App.Manifest.fetchRunsWithPlatformAndMetric): Added formatWithDeltaAndUnit to be used in _createConfigurationSummary.

Location:
trunk/Websites/perf.webkit.org
Files:
13 edited

Legend:

Unmodified
Added
Removed
  • trunk/Websites/perf.webkit.org/ChangeLog

    r180120 r180333  
     12015-02-18  Ryosuke Niwa  <rniwa@webkit.org>
     2
     3        Analysis task pages are unusable
     4        https://bugs.webkit.org/show_bug.cgi?id=141786
     5
     6        Reviewed by Andreas Kling.
     7
     8        This patch makes following improvements to analysis task pages:
     9        1. Making the main chart interactive. This change required the use of App.Pane as well as moving the code to
     10        compute the data for the details pane from PaneController.
     11        2. Moving the form to add a new test group to the top of test groups instead of the bottom of them.
     12        3. Grouping the build requests in each test group by root sets instead of the order by which they were ran.
     13        This change required the creation of App.TestGroupPane as well as its methods.
     14        4. Show a box plot for each root set configuration as well as each build request. This change required
     15        App.BoxPlotComponent.
     16        5. Show revisions of each repository (e.g. WebKit) for each root set and build request.
     17
     18        * public/api/build-requests.php:
     19        (main): Update per the rename of BuildRequestsFetcher::root_sets to BuildRequestsFetcher::root_sets_by_id.
     20
     21        * public/api/test-groups.php:
     22        (main): Include root sets and roots in the response.
     23        (format_test_group):
     24
     25        * public/include/build-requests-fetcher.php:
     26        (BuildRequestsFetcher::root_sets_by_id): Renamed from root_sets.
     27        (BuildRequestsFetcher::root_sets): Added.
     28        (BuildRequestsFetcher::roots): Added.
     29        (BuildRequestsFetcher::fetch_roots_for_set): Takes a boolean argument $resolve_ids. This flag is only set to
     30        true in /api/build-requests/ (as done prior to this patch) to use repository names as identifiers since
     31        tools/sync-with-buildbot.py can't convert repository names to their ids.
     32
     33        * public/v2/analysis.js:
     34        (App.Root): Added.
     35        (App.RootSet): Added.
     36        (App.RootSet.revisionForRepository): Added.
     37        (App.TestGroup.rootSets): Deleted the code to compute root set ids from build requests now that the JSON
     38        response at /api/test-groups will include them.
     39        (App.BuildRequest): Ditto. Also deleted 'configLetter' property, which has been moved to a proxy created by
     40        _createConfigurationSummary.
     41        (App.BuildRequest.statusLabel): Use 'Completed' as the human readable label for 'completed' status.
     42        (App.BuildRequest.aggregateStatuses): Added. Generates a human readable status for a set of build requests.
     43
     44        * public/v2/app.css: Updated style rules for analysis task pages.
     45
     46        * public/v2/app.js:
     47        (App.Pane): This class is now used in analysis task pages to make the main chart interactive.
     48        (App.Pane._updateDetails): Moved from App.PaneController.
     49
     50        (App.PaneController._updateCanAnalyze): Updated the code per the move of selectedPoints.
     51
     52        (App.AnalysisTaskController): Added 'details'.
     53        (App.AnalysisTaskController._taskUpdated):
     54        (App.AnalysisTaskController.paneDomain):Renamed from _fetchedRuns.
     55        (App.AnalysisTaskController.updateTestGroupPanes): Added. Creates App.TestGroupPane for each test group.
     56        (App.AnalysisTaskController.actions.toggleShowRequestList): Added.
     57
     58        (App.TestGroupPane): Added.
     59        (App.TestGroupPane._populate): Added. Group build requests by root sets and create a summary for each group.
     60        (App.TestGroupPane._computeRepositoryList): Added. Returns a sorted list of repositories which is the union
     61        of all repositories appearing in root sets and builds associated with A/B testing results.
     62        (App.TestGroupPane._groupRequestsByConfigurations): Added. Groups build requests by root sets.
     63        (App.TestGroupPane._createConfigurationSummary): Added. Creates a summary for a group of build requests that
     64        use the same root set. We start by wrapping "raw" build requests in a proxy with formatted values,
     65        build numbers, etc... obtained from the fetched chart data. The list of revisions shown in the group summary
     66        is a union of revisions in the root set and the first build request in the group. We null-out revision info
     67        for a build request if it is identical to the one in the summary. The range of values is expanded as needed
     68        by the values in the group as well as 95% percentile confidence interval.
     69
     70        (App.BoxPlotComponent): Added. Controls a box plot shown for each test group summary and build request.
     71        (App.BoxPlotComponent.didInsertElement): Added. Inserts a SVG element as well as two indicator rects to show
     72        the mean and the confidence interval.
     73        (App.BoxPlotComponent._updateBars): Added. Updates the dimensions of the indicator rects.
     74        (App.BoxPlotComponent.valueChanged): Added. Computes the relative dimensions of the indicator rects and
     75        calls _updateBars to update the rects.
     76
     77        * public/v2/chart-pane.css: Added some style rules to be used in the details pane in analysis task pages.
     78
     79        * public/v2/data.js:
     80        (Measurement.prototype.formattedRevisions):
     81        (Measurement.formatRevisionRange): Renamed from Measurement.prototype._formatRevisionRange so that it can be
     82        called in _createConfigurationSummary.
     83
     84        * public/v2/index.html: Updated the templates for analysis task pages. Moved the form to create a new test
     85        group above all test groups, and replaced the list of data points by "details" pane used in the charts page.
     86        Also made the fetching of chartData no longer block showing of test groups.
     87
     88        * public/v2/interactive-chart.js:
     89        (App.InteractiveChartComponent._updateDomain): Added an early exit to fix a newly revealed race condition.
     90        (App.InteractiveChartComponent._domainChanged): Ditto.
     91        (App.InteractiveChartComponent._updateSelectionToolbar): Made it respect 'zoomable' boolean property.
     92
     93        * public/v2/js/statistics.js:
     94        (Statistics.min): Added.
     95        (Statistics.max): Added.
     96
     97        * public/v2/manifest.js:
     98        (App.Manifest.fetchRunsWithPlatformAndMetric): Added formatWithDeltaAndUnit to be used in _createConfigurationSummary.
     99
    11002015-02-14  Ryosuke Niwa  <rniwa@webkit.org>
    2101
  • trunk/Websites/perf.webkit.org/public/api/build-requests.php

    r180091 r180333  
    4646    exit_with_success(array(
    4747        'buildRequests' => $requests_fetcher->results_with_resolved_ids(),
    48         'rootSets' => $requests_fetcher->root_sets(),
     48        'rootSets' => $requests_fetcher->root_sets_by_id(),
    4949        'updates' => $updates,
    5050    ));
  • trunk/Websites/perf.webkit.org/public/api/test-groups.php

    r178234 r180333  
    4040
    4141    $build_requests = $build_requests_fetcher->results();
    42     foreach ($build_requests as $request)
    43         array_push($group_by_id[$request['testGroup']]['buildRequests'], $request['id']);
     42    foreach ($build_requests as $request) {
     43        $request_group = &$group_by_id[$request['testGroup']];
     44        array_push($request_group['buildRequests'], $request['id']);
     45        array_push($request_group['rootSets'], $request['rootSet']);
     46    }
    4447
    45     exit_with_success(array('testGroups' => $test_groups, 'buildRequests' => $build_requests));
     48    exit_with_success(array('testGroups' => $test_groups,
     49        'buildRequests' => $build_requests,
     50        'rootSets' => $build_requests_fetcher->root_sets(),
     51        'roots' => $build_requests_fetcher->roots()));
    4652}
    4753
     
    5460        'createdAt' => strtotime($group_row['testgroup_created_at']) * 1000,
    5561        'buildRequests' => array(),
     62        'rootSets' => array(),
    5663    );
    5764}
  • trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php

    r178234 r180333  
    77        $this->db = $db;
    88        $this->rows = null;
     9        $this->root_sets = array();
     10        $this->roots = array();
    911        $this->root_sets_by_id = array();
    1012    }
     
    5153
    5254            if (!array_key_exists($root_set_id, $this->root_sets_by_id))
    53                 $this->root_sets_by_id[$root_set_id] = $this->fetch_roots_for_set($root_set_id);
     55                $this->root_sets_by_id[$root_set_id] = $this->fetch_roots_for_set($root_set_id, $resolve_ids);
    5456
    5557            array_push($requests, array(
     
    7072    }
    7173
    72     function root_sets() {
     74    function root_sets_by_id() {
    7375        return $this->root_sets_by_id;
    7476    }
    7577
    76     private function fetch_roots_for_set($root_set_id) {
     78    function root_sets() {
     79        return $this->root_sets;
     80    }
     81
     82    function roots() {
     83        return $this->roots;
     84    }
     85
     86    private function fetch_roots_for_set($root_set_id, $resolve_ids) {
    7787        $root_rows = $this->db->query_and_fetch_all('SELECT *
    7888            FROM roots, commits LEFT OUTER JOIN repositories ON commit_repository = repository_id
     
    8090
    8191        $roots = array();
    82         foreach ($root_rows as $row)
    83             $roots[$row['repository_name']] = $row['commit_revision'];
     92        $root_ids = array();
     93        foreach ($root_rows as $row) {
     94            $repository = $row['repository_id'];
     95            $revision = $row['commit_revision'];
     96            $root_id = $root_set_id . '-' . $repository;
     97            array_push($root_ids, $root_id);
     98            array_push($this->roots, array('id' => $root_id, 'repository' => $repository, 'revision' => $revision));
     99            $roots[$resolve_ids ? $row['repository_name'] : $row['repository_id']] = $revision;
     100        }
     101        array_push($this->root_sets, array('id' => $root_set_id, 'roots' => $root_ids));
    84102
    85103        return $roots;
  • trunk/Websites/perf.webkit.org/public/v2/analysis.js

    r180000 r180333  
    6666});
    6767
     68App.Root = App.Model.extend({
     69    repository: DS.belongsTo('repository'),
     70    revision: DS.attr('string'),
     71});
     72
     73App.RootSet = App.Model.extend({
     74    roots: DS.hasMany('roots'),
     75    revisionForRepository: function (repository)
     76    {
     77        var root = this.get('roots').findBy('repository', repository);
     78        if (!root)
     79            return null;
     80        return root.get('revision');
     81    }
     82});
     83
    6884App.TestGroup = App.NameLabelModel.extend({
    6985    task: DS.belongsTo('analysisTask'),
     
    7187    createdAt: DS.attr('date'),
    7288    buildRequests: DS.hasMany('buildRequests'),
    73     rootSets: function ()
    74     {
    75         var rootSetIds = [];
    76         this.get('buildRequests').forEach(function (request) {
    77             var rootSet = request.get('rootSet');
    78             if (!rootSetIds.contains(rootSet))
    79                 rootSetIds.push(rootSet);
    80         });
    81         return rootSetIds;
    82     }.property('buildRequests'),
     89    rootSets: DS.hasMany('rootSets'),
    8390    _fetchChartData: function ()
    8491    {
     
    145152        return this.get('order') + 1;
    146153    }.property('order'),
    147     rootSet: DS.attr('number'),
    148     configLetter: function ()
    149     {
    150         var rootSets = this.get('testGroup').get('rootSets');
    151         var index = rootSets.indexOf(this.get('rootSet'));
    152         return String.fromCharCode('A'.charCodeAt(0) + index);
    153     }.property('testGroup', 'testGroup.rootSets'),
     154    rootSet: DS.belongsTo('rootSet'),
    154155    status: DS.attr('string'),
    155156    statusLabel: function ()
     
    165166            return 'Failed';
    166167        case 'completed':
    167             return 'Finished';
     168            return 'Completed';
    168169        }
    169170    }.property('status'),
    170171    url: DS.attr('string'),
    171172    build: DS.attr('number'),
    172     _fetchMean: function ()
    173     {
    174         var testGroup = this.get('testGroup');
    175         if (!testGroup)
    176             return;
    177         var chartData = testGroup.get('chartData');
    178         if (!chartData)
    179             return;
     173});
    180174
    181         var point = chartData.current.findPointByBuild(this.get('build'));
    182         if (!point)
    183             return;
    184         this.set('mean', chartData.formatter(point.value) + (chartData.unit ? ' ' + chartData.unit : ''));
    185         this.set('buildNumber', point.measurement.buildNumber());
    186     }.observes('build', 'testGroup', 'testGroup.chartData').on('init'),
    187 });
     175App.BuildRequest.aggregateStatuses = function (requests)
     176{
     177    var completeCount = 0;
     178    var failureCount = 0;
     179    requests.forEach(function (request) {
     180        switch (request.get('status')) {
     181        case 'failed':
     182            failureCount++;
     183            break;
     184        case 'completed':
     185            completeCount++;
     186            break;
     187        }
     188    });
     189    if (completeCount == requests.length)
     190        return 'Done';
     191    if (failureCount == requests.length)
     192        return 'All failed';
     193    var status = completeCount + ' out of ' + requests.length + ' completed';
     194    if (failureCount)
     195        status += ', ' + failureCount + ' failed';
     196    return status;
     197}
  • trunk/Websites/perf.webkit.org/public/v2/app.css

    r179878 r180333  
    452452}
    453453
     454.analysis-group .results .summary td {
     455    vertical-align: top;
     456}
     457
     458.analysis-group .results thead td {
     459    text-align: center;
     460}
     461
     462.analysis-group .results .config-letter,
     463.analysis-group .results .summary {
     464    cursor: pointer;
     465}
     466
     467.analysis-group .results .request .config-letter {
     468    border-color: transparent;
     469}
     470
     471.analysis-group .results .hideRequests .request {
     472    display: none;
     473}
     474
     475.box-plot {
     476    display: inline-block;
     477    width: 100px;
     478    height: 0.6rem;
     479    border: solid 1px #ddd;
     480    padding: 1px;
     481    vertical-align: middle;
     482}
     483
     484.box-plot .percentage {
     485    fill: #ccc;
     486}
     487
     488.box-plot .delta {
     489    fill: #333;
     490    opacity: 0.5;
     491}
     492
     493.box-plot svg {
     494    display: block;
     495}
     496
    454497#analysis-task-title {
    455498    font-weight: normal;
     
    471514    border-radius: 0.5rem;
    472515    box-shadow: rgba(0, 0, 0, 0.03) 1px 1px 0px 0px;
    473 
    474     padding: 0.5rem 1rem;
    475516    margin-bottom: 1.5rem;
    476517}
    477518
    478 .analysis-group caption {
     519.analysis-group > * {
     520    margin: 0.5rem;
     521}
     522
     523.analysis-group > h1 {
    479524    font-size: 1.1rem;
     525    font-weight: normal;
    480526    text-align: left;
    481527    margin-bottom: 0.5rem;
     528    border-bottom: 1px solid #bbb;
     529    margin: 0;
     530    padding: 0.2rem 0.5rem;
     531}
     532
     533.analysis-group h1 > input {
     534    font-size: 1rem;
     535    min-width: 20rem;
     536    margin: 0.2rem 0;
    482537}
    483538
  • trunk/Websites/perf.webkit.org/public/v2/app.js

    r180000 r180333  
    298298    metric: null,
    299299    selectedItem: null,
     300    selectedPoints: null,
     301    hoveredOrSelectedItem: null,
    300302    showFullYAxis: false,
    301303    searchCommit: function (repository, keyword) {
     
    539541            this.set(configName, config);
    540542    },
     543    _updateDetails: function ()
     544    {
     545        var selectedPoints = this.get('selectedPoints');
     546        var currentPoint = this.get('hoveredOrSelectedItem');
     547        if (!selectedPoints && !currentPoint) {
     548            this.set('details', null);
     549            return;
     550        }
     551
     552        var currentMeasurement;
     553        var previousPoint;
     554        if (!selectedPoints)
     555            previousPoint = currentPoint.series.previousPoint(currentPoint);
     556        else {
     557            currentPoint = selectedPoints[selectedPoints.length - 1];
     558            previousPoint = selectedPoints[0];
     559        }
     560        var currentMeasurement = currentPoint.measurement;
     561        var oldMeasurement = previousPoint ? previousPoint.measurement : null;
     562
     563        var formattedRevisions = currentMeasurement.formattedRevisions(oldMeasurement);
     564        var revisions = App.Manifest.get('repositories')
     565            .filter(function (repository) { return formattedRevisions[repository.get('id')]; })
     566            .map(function (repository) {
     567            var revision = Ember.Object.create(formattedRevisions[repository.get('id')]);
     568            revision['url'] = revision.previousRevision
     569                ? repository.urlForRevisionRange(revision.previousRevision, revision.currentRevision)
     570                : repository.urlForRevision(revision.currentRevision);
     571            revision['name'] = repository.get('name');
     572            revision['repository'] = repository;
     573            return revision;
     574        });
     575
     576        var buildNumber = null;
     577        var buildURL = null;
     578        if (!selectedPoints) {
     579            buildNumber = currentMeasurement.buildNumber();
     580            var builder = App.Manifest.builder(currentMeasurement.builderId());
     581            if (builder)
     582                buildURL = builder.urlFromBuildNumber(buildNumber);
     583        }
     584
     585        this.set('details', Ember.Object.create({
     586            status: this.computeStatus(currentPoint, previousPoint),
     587            buildNumber: buildNumber,
     588            buildURL: buildURL,
     589            buildTime: currentMeasurement.formattedBuildTime(),
     590            revisions: revisions,
     591        }));
     592    }.observes('hoveredOrSelectedItem', 'selectedPoints'),
    541593});
    542594
     
    901953        this.set('overviewSelection', newSelection);
    902954    }.observes('parentController.sharedZoom').on('init'),
    903     _updateDetails: function ()
    904     {
    905         var selectedPoints = this.get('selectedPoints');
    906         var currentPoint = this.get('currentItem');
    907         if (!selectedPoints && !currentPoint) {
    908             this.set('details', null);
    909             return;
    910         }
    911 
    912         var currentMeasurement;
    913         var previousPoint;
    914         if (!selectedPoints)
    915             previousPoint = currentPoint.series.previousPoint(currentPoint);
    916         else {
    917             currentPoint = selectedPoints[selectedPoints.length - 1];
    918             previousPoint = selectedPoints[0];
    919         }
    920         var currentMeasurement = currentPoint.measurement;
    921         var oldMeasurement = previousPoint ? previousPoint.measurement : null;
    922 
    923         var formattedRevisions = currentMeasurement.formattedRevisions(oldMeasurement);
    924         var revisions = App.Manifest.get('repositories')
    925             .filter(function (repository) { return formattedRevisions[repository.get('id')]; })
    926             .map(function (repository) {
    927             var revision = Ember.Object.create(formattedRevisions[repository.get('id')]);
    928             revision['url'] = revision.previousRevision
    929                 ? repository.urlForRevisionRange(revision.previousRevision, revision.currentRevision)
    930                 : repository.urlForRevision(revision.currentRevision);
    931             revision['name'] = repository.get('name');
    932             revision['repository'] = repository;
    933             return revision;
    934         });
    935 
    936         var buildNumber = null;
    937         var buildURL = null;
    938         if (!selectedPoints) {
    939             buildNumber = currentMeasurement.buildNumber();
    940             var builder = App.Manifest.builder(currentMeasurement.builderId());
    941             if (builder)
    942                 buildURL = builder.urlFromBuildNumber(buildNumber);
    943         }
    944 
    945         this.set('details', Ember.Object.create({
    946             status: this.get('model').computeStatus(currentPoint, previousPoint),
    947             buildNumber: buildNumber,
    948             buildURL: buildURL,
    949             buildTime: currentMeasurement.formattedBuildTime(),
    950             revisions: revisions,
    951         }));
    952         this._updateCanAnalyze();
    953     }.observes('currentItem', 'selectedPoints'),
    954955    _updateCanAnalyze: function ()
    955956    {
    956         var points = this.get('selectedPoints');
     957        var points = this.get('model').get('selectedPoints');
    957958        this.set('cannotAnalyze', !this.get('newAnalysisTaskName') || !points || points.length < 2);
    958     }.observes('newAnalysisTaskName'),
    959 });
    960 
     959    }.observes('newAnalysisTaskName', 'model.selectedPoints'),
     960});
    961961
    962962App.AnalysisRoute = Ember.Route.extend({
     
    979979    platform: Ember.computed.alias('model.platform'),
    980980    metric: Ember.computed.alias('model.metric'),
    981     testGroups: Ember.computed.alias('model.testGroups'),
     981    details: Ember.computed.alias('pane.details'),
    982982    testSets: [],
    983983    roots: [],
     
    990990            return;
    991991
    992         var platformId = model.get('platform').get('id');
    993         var metricId = model.get('metric').get('id');
    994992        App.Manifest.fetch(this.store).then(this._fetchedManifest.bind(this));
    995         App.Manifest.fetchRunsWithPlatformAndMetric(this.store, platformId, metricId).then(this._fetchedRuns.bind(this));
     993        this.set('pane', App.Pane.create({
     994            store: this.store,
     995            platformId: model.get('platform').get('id'),
     996            metricId: model.get('metric').get('id'),
     997        }));
    996998    }.observes('model').on('init'),
    997999    _fetchedManifest: function ()
     
    10111013        }));
    10121014    },
    1013     _fetchedRuns: function (result)
    1014     {
    1015         var chartData = result.data;
     1015    paneDomain: function ()
     1016    {
     1017        var pane = this.get('pane');
     1018        if (!pane)
     1019            return;
     1020
     1021        var chartData = pane.get('chartData');
     1022        if (!chartData)
     1023            return null;
     1024
    10161025        var currentTimeSeries = chartData.current;
    10171026        if (!currentTimeSeries)
    1018             return; // FIXME: Report an error.
     1027            return null; // FIXME: Report an error.
    10191028
    10201029        var start = currentTimeSeries.findPointByMeasurementId(this.get('model').get('startRun'));
    10211030        var end = currentTimeSeries.findPointByMeasurementId(this.get('model').get('endRun'));
    10221031        if (!start || !end)
    1023             return; // FIXME: Report an error.
     1032            return null; // FIXME: Report an error.
    10241033
    10251034        var highlightedItems = {};
     
    10321041                measurement: point.measurement,
    10331042                label: 'Point ' + (index + 1),
    1034                 value: chartData.formatter(point.value) + (chartData.unit ? ' ' + chartData.unit : ''),
     1043                value: chartData.formatWithUnit(point.value),
    10351044            };
    10361045        });
    10371046
    10381047        var margin = (end.time - start.time) * 0.1;
    1039         this.set('chartData', chartData);
    1040         this.set('chartDomain', [start.time - margin, +end.time + margin]);
    10411048        this.set('highlightedItems', highlightedItems);
    10421049        this.set('analysisPoints', formatedPoints);
    1043     },
     1050
     1051        return [start.time - margin, +end.time + margin];
     1052    }.property('pane.chartData', 'model', 'model'),
    10441053    testSets: function ()
    10451054    {
     
    11161125        });
    11171126    }.observes('analysisPoints'),
     1127    updateTestGroupPanes: function ()
     1128    {
     1129        var model = this.get('model');
     1130        if (!model)
     1131            return;
     1132        var self = this;
     1133        model.get('testGroups').then(function (groups) {
     1134            self.set('testGroupPanes', groups.map(function (group) { return App.TestGroupPane.create({content: group}); }));
     1135        });
     1136    }.observes('model'),
    11181137    actions: {
    11191138        associateBug: function (bugTracker, bugNumber)
     
    11371156            });
    11381157        },
    1139     },
    1140 });
     1158        toggleShowRequestList: function (configuration)
     1159        {
     1160            configuration.toggleProperty('showRequestList');
     1161        }
     1162    },
     1163});
     1164
     1165App.TestGroupPane = Ember.ObjectProxy.extend({
     1166    _populate: function ()
     1167    {
     1168        var buildRequests = this.get('buildRequests');
     1169        var chartData = this.get('chartData');
     1170        if (!buildRequests || !chartData)
     1171            return [];
     1172
     1173        var repositories = this._computeRepositoryList();
     1174        this.set('repositories', repositories);
     1175
     1176        var requestsByRooSet = this._groupRequestsByConfigurations(buildRequests);
     1177
     1178        var configurations = [];
     1179        var index = 0;
     1180        var range = {min: Infinity, max: -Infinity};
     1181        for (var rootSetId in requestsByRooSet) {
     1182            var configLetter = String.fromCharCode('A'.charCodeAt(0) + index++);
     1183            configurations.push(this._createConfigurationSummary(requestsByRooSet[rootSetId], configLetter, range));
     1184        }
     1185
     1186        var margin = 0.1 * (range.max - range.min);
     1187        range.max += margin;
     1188        range.min -= margin;
     1189
     1190        this.set('configurations', configurations);
     1191    }.observes('chartData', 'buildRequests'),
     1192    _computeRepositoryList: function ()
     1193    {
     1194        var specifiedRepositories = new Ember.Set();
     1195        (this.get('rootSets') || []).forEach(function (rootSet) {
     1196            (rootSet.get('roots') || []).forEach(function (root) {
     1197                specifiedRepositories.add(root.get('repository'));
     1198            });
     1199        });
     1200        var reportedRepositories = new Ember.Set();
     1201        var chartData = this.get('chartData');
     1202        (this.get('buildRequests') || []).forEach(function (request) {
     1203            var point = chartData.current.findPointByBuild(request.get('build'));
     1204            if (!point)
     1205                return;
     1206
     1207            var revisionByRepositoryId = point.measurement.formattedRevisions();
     1208            for (var repositoryId in revisionByRepositoryId) {
     1209                var repository = App.Manifest.repository(repositoryId);
     1210                if (!specifiedRepositories.contains(repository))
     1211                    reportedRepositories.add(repository);
     1212            }
     1213        });
     1214        return specifiedRepositories.sortBy('name').concat(reportedRepositories.sortBy('name'));
     1215    },
     1216    _groupRequestsByConfigurations: function (requests, repositoryList)
     1217    {
     1218        var rootSetIdToRequests = {};
     1219        var testGroup = this;
     1220        requests.forEach(function (request) {
     1221            var rootSetId = request.get('rootSet').get('id');
     1222            if (!rootSetIdToRequests[rootSetId])
     1223                rootSetIdToRequests[rootSetId] = [];
     1224            rootSetIdToRequests[rootSetId].push(request);
     1225        });
     1226        return rootSetIdToRequests;
     1227    },
     1228    _createConfigurationSummary: function (buildRequests, configLetter, range)
     1229    {
     1230        var repositories = this.get('repositories');
     1231        var chartData = this.get('chartData');
     1232        var requests = buildRequests.map(function (originalRequest) {
     1233            var point = chartData.current.findPointByBuild(originalRequest.get('build'));
     1234            var revisionByRepositoryId = point ? point.measurement.formattedRevisions() : {};
     1235            return Ember.ObjectProxy.create({
     1236                content: originalRequest,
     1237                revisions: repositories.map(function (repository, index) {
     1238                    return (revisionByRepositoryId[repository.get('id')] || {label:null}).label;
     1239                }),
     1240                value: point ? point.value : null,
     1241                valueRange: range,
     1242                formattedValue: point ? chartData.formatWithUnit(point.value) : null,
     1243                buildNumber: point ? point.measurement.buildNumber() : null,
     1244            });
     1245        });
     1246
     1247        var rootSet = requests ? requests[0].get('rootSet') : null;
     1248        var summaryRevisions = repositories.map(function (repository, index) {
     1249            var revision = rootSet ? rootSet.revisionForRepository(repository) : null;
     1250            if (!revision)
     1251                return requests[0].get('revisions')[index];
     1252            return Measurement.formatRevisionRange(revision).label;
     1253        });
     1254
     1255        requests.forEach(function (request) {
     1256            var revisions = request.get('revisions');
     1257            repositories.forEach(function (repository, index) {
     1258                if (revisions[index] == summaryRevisions[index])
     1259                    revisions[index] = null;
     1260            });
     1261        });
     1262
     1263        var valuesInConfig = requests.mapBy('value').filter(function (value) { return typeof(value) === 'number' && !isNaN(value); });
     1264        var sum = Statistics.sum(valuesInConfig);
     1265        var ciDelta = Statistics.confidenceIntervalDelta(0.95, valuesInConfig.length, sum, Statistics.squareSum(valuesInConfig));
     1266        var mean = sum / valuesInConfig.length;
     1267
     1268        range.min = Math.min(range.min, Statistics.min(valuesInConfig));
     1269        range.max = Math.max(range.max, Statistics.max(valuesInConfig));
     1270        if (ciDelta && !isNaN(ciDelta)) {
     1271            range.min = Math.min(range.min, mean - ciDelta);
     1272            range.max = Math.max(range.max, mean + ciDelta);
     1273        }
     1274
     1275        var summary = Ember.Object.create({
     1276            isAverage: true,
     1277            configLetter: configLetter,
     1278            revisions: summaryRevisions,
     1279            formattedValue: isNaN(mean) ? null : chartData.formatWithDeltaAndUnit(mean, ciDelta),
     1280            value: mean,
     1281            confidenceIntervalDelta: ciDelta,
     1282            valueRange: range,
     1283            statusLabel: App.BuildRequest.aggregateStatuses(requests),
     1284        });
     1285
     1286        return Ember.Object.create({summary: summary, items: requests});
     1287    },
     1288});
     1289
     1290App.BoxPlotComponent = Ember.Component.extend({
     1291    classNames: ['box-plot'],
     1292    range: null,
     1293    value: null,
     1294    delta: null,
     1295    didInsertElement: function ()
     1296    {
     1297        var element = this.get('element');
     1298        var svg = d3.select(element).append('svg')
     1299            .attr('viewBox', '0 0 100 20')
     1300            .attr('preserveAspectRatio', 'none')
     1301            .style({width: '100%', height: '100%'});
     1302
     1303        this._percentageRect = svg
     1304            .append('rect')
     1305            .attr('x', 0)
     1306            .attr('y', 0)
     1307            .attr('width', 0)
     1308            .attr('height', 20)
     1309            .attr('class', 'percentage');
     1310
     1311        this._deltaRect = svg
     1312            .append('rect')
     1313            .attr('x', 0)
     1314            .attr('y', 5)
     1315            .attr('width', 0)
     1316            .attr('height', 10)
     1317            .attr('class', 'delta')
     1318            .attr('opacity', 0.5)
     1319        this._updateBars();
     1320    },
     1321    _updateBars: function ()
     1322    {
     1323        if (!this._percentageRect || typeof(this._percentage) !== 'number' || isNaN(this._percentage))
     1324            return;
     1325
     1326        this._percentageRect.attr('width', this._percentage);
     1327        if (typeof(this._delta) === 'number' && !isNaN(this._delta)) {
     1328            this._deltaRect.attr('x', this._percentage - this._delta);
     1329            this._deltaRect.attr('width', this._delta * 2);
     1330        }
     1331    },
     1332    valueChanged: function ()
     1333    {
     1334        var range = this.get('range');
     1335        var value = this.get('value');
     1336        if (!range || !value)
     1337            return;
     1338        var scalingFactor = 100 / (range.max - range.min);
     1339        var percentage = (value - range.min) * scalingFactor;
     1340        this._percentage = percentage;
     1341        this._delta = this.get('delta') * scalingFactor;
     1342        this._updateBars();
     1343    }.observes('value', 'range').on('init'),
     1344});
  • trunk/Websites/perf.webkit.org/public/v2/chart-pane.css

    r179913 r180333  
    218218}
    219219
     220.analysis-chart-pane {
     221    height: 15rem;
     222}
     223
    220224.analysis-chart-pane .details {
    221225    overflow: scroll;
     
    237241    height: 13rem;
    238242    overflow: scroll;
     243}
     244.analysis-chart-pane .details-table-container {
     245    position: static;
     246    height: 15rem;
    239247}
    240248
  • trunk/Websites/perf.webkit.org/public/v2/data.js

    r180000 r180333  
    170170        var currentRevision = revisions[repositoryId][0];
    171171        var previousRevision = previousRevisions ? previousRevisions[repositoryId][0] : null;
    172         var formatttedRevision = this._formatRevisionRange(previousRevision, currentRevision);
     172        var formatttedRevision = Measurement.formatRevisionRange(currentRevision, previousRevision);
    173173        formattedRevisions[repositoryId] = formatttedRevision;
    174174    }
     
    177177}
    178178
    179 Measurement.prototype._formatRevisionRange = function (previousRevision, currentRevision)
     179Measurement.formatRevisionRange = function (currentRevision, previousRevision)
    180180{
    181181    var revisionChanged = false;
  • trunk/Websites/perf.webkit.org/public/v2/index.html

    r180000 r180333  
    170170                            interactive=true
    171171                            chartPointRadius=2
    172                             currentItem=currentItem
     172                            currentItem=hoveredOrSelectedItem
    173173                            currentTime=sharedTime
    174174                            selectedItem=selectedItem
     
    177177                            selection=timeRange
    178178                            selectedPoints=selectedPoints
    179                             markedPoints=markedPoints
    180179                            showFullYAxis=showFullYAxis
     180                            zoomable=true
    181181                            zoom="zoomed"}}
    182182                    {{else}}
     
    198198                        {{/if}}
    199199                        </div>
    200                         {{#if details}}
    201                             {{partial "chart-details"}}
    202                         {{/if}}
     200                        {{partial "chart-details"}}
    203201                    </div>
    204202                </div>
     
    249247
    250248    <script type="text/x-handlebars" data-template-name="chart-details">
     249    {{#if details}}
    251250    <div class="details-table-container">
    252251        <table class="details-table">
     
    314313        </div>
    315314    </div>
     315    {{/if}}
    316316    </script>
    317317
     
    530530        {{/if}}
    531531
    532         {{#if chartData}}
    533             <section class="analysis-chart-pane chart-pane">
     532        {{#if pane}}
     533            <section class="analysis-chart-pane chart-pane" tabindex="0">
    534534                <div class="svg-container">
    535535                    {{interactive-chart
    536                         chartData=chartData
    537                         enableSelection=false
     536                        chartData=pane.chartData
     537                        ranges=pane.analyticRanges
     538                        domain=paneDomain
     539                        interactive=true
    538540                        chartPointRadius=2
    539                         domain=chartDomain
    540                         highlightedItems=highlightedItems}}
     541                        currentItem=pane.hoveredOrSelectedItem
     542                        selectedPoints=pane.selectedPoints
     543                        selection=timeRange
     544                        highlightedItems=highlightedItems
     545                        rangeRoute="analysisTask"}}
    541546                </div>
    542547                <div class="details">
    543                     <table class="analysis-bugs">
    544                         <tbody>
    545                             {{#each bugTrackers}}
    546                                 <tr>
    547                                     <th>{{label}}</th>
    548                                     <td>
    549                                         <form {{action "associateBug" this editedBugNumber on="submit"}}>
    550                                             {{input type=text value=editedBugNumber}}
    551                                         </form>
    552                                     </td>
    553                                 </tr>
    554                             {{/each}}
    555                         </tbody>
    556                     </table>
    557                     <table>
    558                         <tbody>
    559                             {{#each analysisPoints}}
    560                                 <tr><td>{{label}}</td><td>{{value}}</td></tr>
    561                             {{/each}}
    562                         </tbody>
    563                     </table>
     548                    {{partial "chart-details"}}
    564549                </div>
    565550            </section>
    566             {{#each testGroups}}
    567                 <section class="analysis-group">
    568                     <table>
    569                         <caption>{{name}}</caption>
    570                         <thead>
    571                             <tr>
    572                                 <td>Order</td>
    573                                 <td>Configuration</td>
    574                                 <td>Status</td>
    575                                 <td>Build</td>
    576                                 <td>{{../metric.fullName}}</td>
     551        {{/if}}
     552
     553        {{partial "testGroupForm"}}
     554
     555        {{#each testGroupPanes}}
     556            {{partial "testGroup"}}
     557        {{/each}}
     558    </script>
     559
     560    <script type="text/x-handlebars" data-template-name="testGroup">
     561        <section class="analysis-group">
     562            <h1>{{name}}</h1>
     563            <table class="results">
     564                <thead>
     565                    <tr>
     566                        <td colspan="2">Configuration</td>
     567                        {{#each repositories}}
     568                            <td>{{name}}</td>
     569                        {{/each}}
     570                        <td>Results</td>
     571                        <td>Status</td>
     572                    </tr>
     573                </thead>
     574                {{#each configurations}}
     575                    <tbody {{bind-attr class="showRequestList::hideRequests"}}>
     576                        <tr class="summary" {{action toggleShowRequestList this}}>
     577                            <td class="config-letter" colspan="2">{{summary.configLetter}}</td>
     578                            {{#with summary}}
     579                                {{partial "testGroupRow"}}
     580                            {{/with}}
     581                        </tr>
     582                        {{#each items}}
     583                            <tr class="request">
     584                                {{#with ../this}}
     585                                    <td class="config-letter" {{action toggleShowRequestList this}}></td>
     586                                {{/with}}
     587                                <td>Run {{orderLabel}}</td>
     588                                {{partial "testGroupRow"}}
    577589                            </tr>
    578                         </thead>
    579                         <tbody>
    580                             {{#each buildRequests}}
    581                                 <tr>
    582                                     <td>{{orderLabel}}</td>
    583                                     <td>{{configLetter}}</td>
    584                                     <td><a {{bind-attr href=url}}>{{statusLabel}}</a></td>
    585                                     <td>{{buildNumber}}</td>
    586                                     <td>{{mean}}</td>
    587                                 </tr>
    588                             {{/each}}
    589                         </tbody>
    590                     </table>
    591                 </section>
    592             {{/each}}
    593 
    594             {{#if roots}}
    595             <form method="POST" {{action "createTestGroup" newTestGroupName repetitionCount on="submit"}} class="analysis-group">
     590                        {{/each}}
     591                    </tbody>
     592                {{/each}}
     593            </table>
     594        </section>
     595    </script>
     596
     597    <script type="text/x-handlebars" data-template-name="testGroupRow">
     598        {{#each revisions}}
     599            <td>{{this}}</td>
     600        {{/each}}
     601        <td>
     602            {{#if value}}
     603                {{box-plot range=valueRange value=value delta=confidenceIntervalDelta}}
     604            {{/if}}
     605            {{formattedValue}}
     606        </td>
     607        <td>
     608            {{#if buildNumber}}
     609                 {{statusLabel}} / <a {{bind-attr href=url}}>Build {{buildNumber}}</a>
     610            {{else}}
     611                <a {{bind-attr href=url}}>{{statusLabel}}</a>
     612            {{/if}}
     613        </td>
     614    </script>
     615
     616    <script type="text/x-handlebars" data-template-name="testGroupForm">
     617    {{#if roots}}
     618        <form method="POST" {{action "createTestGroup" newTestGroupName repetitionCount on="submit"}}>
     619            <section class="analysis-group">
     620                <h1>{{input name="name" value=newTestGroupName placeholder="Test group name" required=true type="text"}}</h1>
    596621                <table>
    597                     <caption>{{input name="name" value=newTestGroupName placeholder="Test group name" required=true type="text"}}</caption>
    598622                    <thead>
    599623                        <tr>
     
    634658
    635659                <button type="submit">Start A/B testing</button>
    636             </form>
    637             {{/if}}
    638         {{/if}}
     660            </section>
     661        </form>
     662    {{/if}}
    639663    </script>
    640664
  • trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js

    r179989 r180333  
    198198    {
    199199        var xDomain = this.get('domain');
     200        if (!xDomain || !this._currentTimeSeriesData)
     201            return null;
    200202        var intrinsicXDomain = this._computeXAxisDomain(this._currentTimeSeriesData);
    201203        if (!xDomain)
     
    374376        var selection = this._currentSelection() || this.get('sharedSelection');
    375377        var newXDomain = this._updateDomain();
     378        if (!newXDomain)
     379            return;
    376380
    377381        if (selection && newXDomain && selection[0] <= newXDomain[0] && newXDomain[1] <= selection[1])
     
    754758    _updateSelectionToolbar: function ()
    755759    {
    756         if (!this.get('interactive'))
     760        if (!this.get('zoomable'))
    757761            return;
    758762
  • trunk/Websites/perf.webkit.org/public/v2/js/statistics.js

    r179913 r180333  
    11var Statistics = new (function () {
     2
     3    this.min = function (values) {
     4        return Math.min.apply(Math, values);
     5    }
     6
     7    this.max = function (values) {
     8        return Math.max.apply(Math, values);
     9    }
    210
    311    this.sum = function (values) {
  • trunk/Websites/perf.webkit.org/public/v2/manifest.js

    r180120 r180333  
    303303
    304304            var useSI = unit == 'bytes';
     305            var unitSuffix = unit ? ' ' + unit : '';
     306            var deltaFormatterWithoutSign = useSI ? d3.format('.2s') : d3.format('.2g');
    305307            return {
    306308                platform: platform,
     
    311313                    target: runs.target ? runs.target.timeSeriesByCommitTime() : null,
    312314                    unit: unit,
     315                    formatWithUnit: function (value) { return this.formatter(value) + unitSuffix; },
     316                    formatWithDeltaAndUnit: function (value, delta)
     317                    {
     318                        return this.formatter(value) + (delta && !isNaN(delta) ? ' \u00b1 ' + deltaFormatterWithoutSign(delta) : '') + unitSuffix;
     319                    },
    313320                    formatter: useSI ? d3.format('.4s') : d3.format('.4g'),
    314321                    deltaFormatter: useSI ? d3.format('+.2s') : d3.format('+.2g'),
Note: See TracChangeset for help on using the changeset viewer.