Title: [213300] trunk/Websites/perf.webkit.org
Revision
213300
Author
rn...@webkit.org
Date
2017-03-02 13:23:07 -0800 (Thu, 02 Mar 2017)

Log Message

Make baseline data points selectable
https://bugs.webkit.org/show_bug.cgi?id=169069
<rdar://problem/29209427>

Reviewed by Antti Koivisto.

Add the capability to select data points other than "current" configuration type.

This patch refactors the way the "chart status" is computed. Before this patch, ChartStatusView was
responsible for determining two data points for which to compute the status, and computing the status
between two data points. ChartPaneStatusView which inherits from ChartStatusView and used in the charts
page relied upon ChartStatusView to compute these values, and computed the list of revision ranges for
each relevant repository between the data points. ChartPane then had callbacks on ChartPaneStatusView
to know whenever these values changed. Because of this entangled mess, ChartStatusView had to be aware
of InteractiveTimeSeriesChart even though only ChartPaneStatusView could be used with it.

This patch dramatically simplifies the situation by adding referencePoints() on TimeSeriesChart and
InteractiveTimeSeriesChart which returns the current point, the previous point if there is any, and
their time series view. It also extracts ChartStatusEvaluator which computes the current status values
and ChartRevisionRange which computes a list of revision differences both based on the referencePoints.
As a result, ChartPaneStatusView no longer inherits from ChartStatusView, and ChartStatusView has been
renamed to DashboardChartStatusView to reflect its purpose. Furthermore, ChartPane which used to rely on
ChartPaneStatusView's revisionCallback to update the commit log viewer simply uses another instance of
ChartRevisionRange, eliminating the need for the callback.

To implement these classes easily, this patch also introduces a new class, LazilyEvaluatedFunction to
memoize the return value of a function when called with the same arguments. Delaying the computation of
a value and avoiding the work when the values are the same is a very common pattern in the perf dashboard
so I expect this class would be used in a lot more places in the future.

* browser-tests/chart-revision-range-tests.js: Added. Tests for ChartRevisionRange.
* browser-tests/chart-status-evaluator-tests.js: Added. Tests for ChartStatusEvaluator.

* browser-tests/index.html:
(BrowsingContext):
(BrowsingContext.importScripts): Fixed the bug that calling importScripts twice results in MockRemoteAPI
being loaded twice.
(ChartTest.importChartScripts): Import more model objects.
(ChartTest.sampleCluster): Made this a getter.
(ChartTest.makeModelObjectsForSampleCluster):
(ChartTest.makeSampleCluster): Added. Cutomizes the valus of baseline / target based on options.
(ChartTest.respondWithSampleCluster): Now takes an options argument for makeSampleCluster.

* public/v3/components/chart-pane-base.js:
(ChartPaneBase): Added _openRepository to keep track of the currently open repository instead of relying
on _mainChartStatus or _commitLogViewer to keep track of it.
(ChartPaneBase.prototype.configure):  The callback for when the user clicked on a repository name in
ChartPaneStatusView has been replaced by "openRepository" action.
(ChartPaneBase.prototype.setOpenRepository): Moved from ChartPane.
(ChartPaneBase.prototype._mainSelectionDidChange):
(ChartPaneBase.prototype._indicatorDidChange):
(ChartPaneBase.prototype._didFetchData):
(ChartPaneBase.prototype._updateCommitLogViewer): Renamed from _updateStatus.
(ChartPaneBase.prototype.openNewRepository): Renamed from _requestOpeningCommitViewer. Fixed a bug that
clicking on the repository name inside ChartPaneStatusView would not focus the pane, which resulted in
arrow keys to be ignored instead of moving the main chart's indicator or the currently open repository.
(ChartPaneBase.prototype._keyup):
(ChartPaneBase.prototype._moveOpenRepository): Moved from ChartPaneStatusView's
moveRepositoryWithNotification. Used when changing the open repository by up/down arrow keys.

* public/v3/components/chart-revision-range.js: Added. Extracted from ChartPaneStatusView.
(ChartRevisionRange): Added.
(ChartRevisionRange.prototype.revisionList): Added.
(ChartRevisionRange.prototype.rangeForRepository): Added.
(ChartRevisionRange._revisionForPoint): Added. Extracted from ChartPaneStatusView's
_updateRevisionListForNewCurrentRepository.
(ChartRevisionRange._computeRevisionList): Ditto from computeChartStatusLabels.

* public/v3/components/chart-status-evaluator.js: Added.
(ChartStatusEvaluator): Added.
(ChartStatusEvaluator.prototype.status): Added.
(ChartStatusEvaluator.computeChartStatus): Added. Extracted from ChartStatusView's updateStatusIfNeeded.

* public/v3/components/chart-status-view.js: Removed.
(ChartStatusView): Deleted. Split into ChartStatusEvaluator and DashboardChartStatusView.

* public/v3/components/chart-styles.js:
(ChartStyles.baselineStyle): Make baseline data points interactive. This single line change is what
enables the user to interact with the data points. The rest of changes in this patch mostly deals with
the status text such as "5% worse than baseline" and the list of revisions shown in the commit log viewer
which would have shown the wrong range without these changes.

* public/v3/components/dashboard-chart-status-view.js: Added. Extracted from ChartStatusView.
(DashboardChartStatusView): Added.
(DashboardChartStatusView.prototype.render): Added.
(DashboardChartStatusView.htmlTemplate): Added.
(DashboardChartStatusView.cssTemplate): Added.

* public/v3/components/interactive-time-series-chart.js:
(InteractiveTimeSeriesChart.prototype.referencePoints): Added. Return the first point and the last point
as the reference points when there is a selection. Only report the previous point if they are distinct as
showing a range of revisions from a data point to itself makes no sense. When there is a indicator simply
return it and its previous point as reference points. Otherwise return null unlike TimeSeriesChart's
referencePoints which always returns the latest point as the reference point.

* public/v3/components/time-series-chart.js:
(TimeSeriesChart.prototype.referencePoints): Added. Return the latest point as the reference point. It
never returns the previous point even if there were more data points as there is no way for the user to
specify which data points to compare.

* public/v3/index.html: Include newly added files.

* public/v3/lazily-evaluated-function.js: Added.
(LazilyEvaluatedFunction): Added.
(LazilyEvaluatedFunction.prototype.evaluate): Added.

* public/v3/models/commit-log.js:
(CommitLog.prototype.diff): Fixed a bug that computing the diff of two Subversion-like revisions results
in "from" field to be unexpectedly an integer instead of a string.

* public/v3/models/metric.js:
(Metric): Moved the code to compute the unit from the metric name from v2's RunsData class. This makes
writing tests easier since it eliminates the need to load v2's data.js.
(Metric.prototype.unit):
(Metric.prototype.isSmallerBetter): Ditto for determining whether the unit is smaller-is-better.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskChartPane.prototype._updateStatus): Deleted the unused code.

* public/v3/pages/chart-pane-status-view.js:
(ChartPaneStatusView): No longer inherits from ChartStatusView. Uses ChartStatusEvaluator and
ChartRevisionRange to to compute the chart status and the list of revision changes.
(ChartPaneStatusView.prototype.pointsRangeForAnalysis): Deleted.
(ChartPaneStatusView.prototype.render): Split it into _renderStatus and _renderBuildRevisionTable using
LazilyEvaluatedFunction.
(ChartPaneStatusView.prototype._renderStatus): Added.
(ChartPaneStatusView.prototype._renderBuildRevisionTable): Added.
(ChartPaneStatusView.prototype.setCurrentRepository): _updateRevisionListForNewCurrentRepository has been
moved into ChartRevisionRange. Just enqueue itself to re-render.
(ChartPaneStatusView.prototype._setRevisionRange): Deleted.
(ChartPaneStatusView.prototype.moveRepositoryWithNotification): Deleted.
(ChartPaneStatusView.prototype.updateRevisionList): Deleted.
(ChartPaneStatusView.prototype._updateRevisionListForNewCurrentRepository): Deleted.
(ChartPaneStatusView.prototype.computeChartStatusLabels): Deleted.
(ChartPaneStatusView.htmlTemplate):
(ChartPaneStatusView.cssTemplate):

* public/v3/pages/chart-pane.js:
(ChartPane.prototype.openNewRepository): Overrides the one in ChartPaneBase, which has been renamed from
_requestOpeningCommitViewer.
(ChartPane.prototype._analyzeRange):
(ChartPane.prototype._renderActionToolbar): Use the main chart's selection directly to determine whether
an analysis task can be created for the currenty selected range.

* public/v3/pages/dashboard-page.js:
(DashboardPage.prototype._createChartForCell):

* unit-tests/lazily-evaluated-function-tests.js: Added. Tests for LazilyEvaluatedFunction.

Modified Paths

Added Paths

Removed Paths

Diff

Modified: trunk/Websites/perf.webkit.org/ChangeLog (213299 => 213300)


--- trunk/Websites/perf.webkit.org/ChangeLog	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/ChangeLog	2017-03-02 21:23:07 UTC (rev 213300)
@@ -1,3 +1,154 @@
+2017-03-02  Ryosuke Niwa  <rn...@webkit.org>
+
+        Make baseline data points selectable
+        https://bugs.webkit.org/show_bug.cgi?id=169069
+        <rdar://problem/29209427>
+
+        Reviewed by Antti Koivisto.
+
+        Add the capability to select data points other than "current" configuration type.
+
+        This patch refactors the way the "chart status" is computed. Before this patch, ChartStatusView was
+        responsible for determining two data points for which to compute the status, and computing the status
+        between two data points. ChartPaneStatusView which inherits from ChartStatusView and used in the charts
+        page relied upon ChartStatusView to compute these values, and computed the list of revision ranges for
+        each relevant repository between the data points. ChartPane then had callbacks on ChartPaneStatusView
+        to know whenever these values changed. Because of this entangled mess, ChartStatusView had to be aware
+        of InteractiveTimeSeriesChart even though only ChartPaneStatusView could be used with it.
+
+        This patch dramatically simplifies the situation by adding referencePoints() on TimeSeriesChart and
+        InteractiveTimeSeriesChart which returns the current point, the previous point if there is any, and
+        their time series view. It also extracts ChartStatusEvaluator which computes the current status values
+        and ChartRevisionRange which computes a list of revision differences both based on the referencePoints.
+        As a result, ChartPaneStatusView no longer inherits from ChartStatusView, and ChartStatusView has been
+        renamed to DashboardChartStatusView to reflect its purpose. Furthermore, ChartPane which used to rely on
+        ChartPaneStatusView's revisionCallback to update the commit log viewer simply uses another instance of
+        ChartRevisionRange, eliminating the need for the callback.
+
+        To implement these classes easily, this patch also introduces a new class, LazilyEvaluatedFunction to
+        memoize the return value of a function when called with the same arguments. Delaying the computation of
+        a value and avoiding the work when the values are the same is a very common pattern in the perf dashboard
+        so I expect this class would be used in a lot more places in the future.
+
+        * browser-tests/chart-revision-range-tests.js: Added. Tests for ChartRevisionRange.
+        * browser-tests/chart-status-evaluator-tests.js: Added. Tests for ChartStatusEvaluator.
+
+        * browser-tests/index.html:
+        (BrowsingContext):
+        (BrowsingContext.importScripts): Fixed the bug that calling importScripts twice results in MockRemoteAPI
+        being loaded twice.
+        (ChartTest.importChartScripts): Import more model objects.
+        (ChartTest.sampleCluster): Made this a getter.
+        (ChartTest.makeModelObjectsForSampleCluster):
+        (ChartTest.makeSampleCluster): Added. Cutomizes the valus of baseline / target based on options.
+        (ChartTest.respondWithSampleCluster): Now takes an options argument for makeSampleCluster.
+
+        * public/v3/components/chart-pane-base.js:
+        (ChartPaneBase): Added _openRepository to keep track of the currently open repository instead of relying
+        on _mainChartStatus or _commitLogViewer to keep track of it.
+        (ChartPaneBase.prototype.configure):  The callback for when the user clicked on a repository name in
+        ChartPaneStatusView has been replaced by "openRepository" action.
+        (ChartPaneBase.prototype.setOpenRepository): Moved from ChartPane.
+        (ChartPaneBase.prototype._mainSelectionDidChange):
+        (ChartPaneBase.prototype._indicatorDidChange):
+        (ChartPaneBase.prototype._didFetchData):
+        (ChartPaneBase.prototype._updateCommitLogViewer): Renamed from _updateStatus.
+        (ChartPaneBase.prototype.openNewRepository): Renamed from _requestOpeningCommitViewer. Fixed a bug that
+        clicking on the repository name inside ChartPaneStatusView would not focus the pane, which resulted in
+        arrow keys to be ignored instead of moving the main chart's indicator or the currently open repository.
+        (ChartPaneBase.prototype._keyup):
+        (ChartPaneBase.prototype._moveOpenRepository): Moved from ChartPaneStatusView's
+        moveRepositoryWithNotification. Used when changing the open repository by up/down arrow keys.
+
+        * public/v3/components/chart-revision-range.js: Added. Extracted from ChartPaneStatusView.
+        (ChartRevisionRange): Added.
+        (ChartRevisionRange.prototype.revisionList): Added.
+        (ChartRevisionRange.prototype.rangeForRepository): Added.
+        (ChartRevisionRange._revisionForPoint): Added. Extracted from ChartPaneStatusView's
+        _updateRevisionListForNewCurrentRepository.
+        (ChartRevisionRange._computeRevisionList): Ditto from computeChartStatusLabels.
+
+        * public/v3/components/chart-status-evaluator.js: Added.
+        (ChartStatusEvaluator): Added.
+        (ChartStatusEvaluator.prototype.status): Added.
+        (ChartStatusEvaluator.computeChartStatus): Added. Extracted from ChartStatusView's updateStatusIfNeeded.
+
+        * public/v3/components/chart-status-view.js: Removed.
+        (ChartStatusView): Deleted. Split into ChartStatusEvaluator and DashboardChartStatusView.
+
+        * public/v3/components/chart-styles.js:
+        (ChartStyles.baselineStyle): Make baseline data points interactive. This single line change is what
+        enables the user to interact with the data points. The rest of changes in this patch mostly deals with
+        the status text such as "5% worse than baseline" and the list of revisions shown in the commit log viewer
+        which would have shown the wrong range without these changes.
+
+        * public/v3/components/dashboard-chart-status-view.js: Added. Extracted from ChartStatusView.
+        (DashboardChartStatusView): Added.
+        (DashboardChartStatusView.prototype.render): Added.
+        (DashboardChartStatusView.htmlTemplate): Added.
+        (DashboardChartStatusView.cssTemplate): Added.
+
+        * public/v3/components/interactive-time-series-chart.js:
+        (InteractiveTimeSeriesChart.prototype.referencePoints): Added. Return the first point and the last point
+        as the reference points when there is a selection. Only report the previous point if they are distinct as
+        showing a range of revisions from a data point to itself makes no sense. When there is a indicator simply
+        return it and its previous point as reference points. Otherwise return null unlike TimeSeriesChart's
+        referencePoints which always returns the latest point as the reference point.
+
+        * public/v3/components/time-series-chart.js:
+        (TimeSeriesChart.prototype.referencePoints): Added. Return the latest point as the reference point. It
+        never returns the previous point even if there were more data points as there is no way for the user to
+        specify which data points to compare.
+
+        * public/v3/index.html: Include newly added files.
+
+        * public/v3/lazily-evaluated-function.js: Added.
+        (LazilyEvaluatedFunction): Added.
+        (LazilyEvaluatedFunction.prototype.evaluate): Added.
+
+        * public/v3/models/commit-log.js:
+        (CommitLog.prototype.diff): Fixed a bug that computing the diff of two Subversion-like revisions results
+        in "from" field to be unexpectedly an integer instead of a string.
+
+        * public/v3/models/metric.js:
+        (Metric): Moved the code to compute the unit from the metric name from v2's RunsData class. This makes
+        writing tests easier since it eliminates the need to load v2's data.js.
+        (Metric.prototype.unit):
+        (Metric.prototype.isSmallerBetter): Ditto for determining whether the unit is smaller-is-better.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskChartPane.prototype._updateStatus): Deleted the unused code.
+
+        * public/v3/pages/chart-pane-status-view.js:
+        (ChartPaneStatusView): No longer inherits from ChartStatusView. Uses ChartStatusEvaluator and
+        ChartRevisionRange to to compute the chart status and the list of revision changes.
+        (ChartPaneStatusView.prototype.pointsRangeForAnalysis): Deleted.
+        (ChartPaneStatusView.prototype.render): Split it into _renderStatus and _renderBuildRevisionTable using
+        LazilyEvaluatedFunction.
+        (ChartPaneStatusView.prototype._renderStatus): Added.
+        (ChartPaneStatusView.prototype._renderBuildRevisionTable): Added.
+        (ChartPaneStatusView.prototype.setCurrentRepository): _updateRevisionListForNewCurrentRepository has been
+        moved into ChartRevisionRange. Just enqueue itself to re-render.
+        (ChartPaneStatusView.prototype._setRevisionRange): Deleted.
+        (ChartPaneStatusView.prototype.moveRepositoryWithNotification): Deleted.
+        (ChartPaneStatusView.prototype.updateRevisionList): Deleted.
+        (ChartPaneStatusView.prototype._updateRevisionListForNewCurrentRepository): Deleted.
+        (ChartPaneStatusView.prototype.computeChartStatusLabels): Deleted.
+        (ChartPaneStatusView.htmlTemplate):
+        (ChartPaneStatusView.cssTemplate):
+
+        * public/v3/pages/chart-pane.js:
+        (ChartPane.prototype.openNewRepository): Overrides the one in ChartPaneBase, which has been renamed from
+        _requestOpeningCommitViewer.
+        (ChartPane.prototype._analyzeRange):
+        (ChartPane.prototype._renderActionToolbar): Use the main chart's selection directly to determine whether
+        an analysis task can be created for the currenty selected range.
+
+        * public/v3/pages/dashboard-page.js:
+        (DashboardPage.prototype._createChartForCell):
+
+        * unit-tests/lazily-evaluated-function-tests.js: Added. Tests for LazilyEvaluatedFunction.
+
 2017-03-01  Ryosuke Niwa  <rn...@webkit.org>
 
         Build fix after r212853. Make creating an analysis task work again.

Added: trunk/Websites/perf.webkit.org/browser-tests/chart-revision-range-tests.js (0 => 213300)


--- trunk/Websites/perf.webkit.org/browser-tests/chart-revision-range-tests.js	                        (rev 0)
+++ trunk/Websites/perf.webkit.org/browser-tests/chart-revision-range-tests.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -0,0 +1,165 @@
+
+describe('ChartRevisionRange', () => {
+
+    function importRevisionList(context)
+    {
+        return ChartTest.importChartScripts(context).then(() => {
+            ChartTest.makeModelObjectsForSampleCluster(context);
+            return context.importScripts(['lazily-evaluated-function.js', 'components/chart-revision-range.js'], 'ChartRevisionRange');
+        });
+    }
+
+    describe('revisionList on a non-interactive chart', () => {
+        it('should report the list of revision for the latest point', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importRevisionList(context).then((ChartRevisionRange) => {
+                const chart = ChartTest.createChartWithSampleCluster(context);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                evaluator = new ChartRevisionRange(chart);
+                expect(evaluator.revisionList()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const revisionList = evaluator.revisionList();
+                expect(revisionList).to.not.be(null);
+                expect(revisionList.length).to.be(2);
+
+                expect(revisionList[0].repository.label()).to.be('SomeApp');
+                expect(revisionList[0].label).to.be('r4006');
+                expect(revisionList[0].from).to.be(null);
+                expect(revisionList[0].to).to.be('4006');
+
+                expect(revisionList[1].repository.label()).to.be('macOS');
+                expect(revisionList[1].label).to.be('15C50');
+                expect(revisionList[1].from).to.be(null);
+                expect(revisionList[1].to).to.be('15C50');
+            })
+        });
+    });
+
+
+    describe('revisionList on an interactive chart', () => {
+
+        it('should not report the list of revision for the latest point when there is no selection or indicator', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importRevisionList(context).then((ChartRevisionRange) => {
+                const chart = ChartTest.createInteractiveChartWithSampleCluster(context);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                evaluator = new ChartRevisionRange(chart);
+                expect(evaluator.revisionList()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                expect(evaluator.revisionList()).to.be(null);
+            })
+        });
+
+        it('should report the list of revision for the locked indicator with differences to the previous point', () => {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importRevisionList(context).then((ChartRevisionRange) => {
+                chart = ChartTest.createInteractiveChartWithSampleCluster(context);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                evaluator = new ChartRevisionRange(chart);
+                expect(evaluator.revisionList()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                expect(evaluator.revisionList()).to.be(null);
+
+                const currentView = chart.sampledTimeSeriesData('current');
+                chart.setIndicator(currentView.lastPoint().id, true);
+
+                let revisionList = evaluator.revisionList();
+                expect(revisionList).to.not.be(null);
+                expect(revisionList.length).to.be(2);
+
+                expect(revisionList[0].repository.label()).to.be('SomeApp');
+                expect(revisionList[0].label).to.be('r4005-r4006');
+                expect(revisionList[0].from).to.be('4005');
+                expect(revisionList[0].to).to.be('4006');
+
+                expect(revisionList[1].repository.label()).to.be('macOS');
+                expect(revisionList[1].label).to.be('15C50');
+                expect(revisionList[1].from).to.be(null);
+                expect(revisionList[1].to).to.be('15C50');
+
+                chart.setIndicator(1004, true); // Across macOS change.
+
+                revisionList = evaluator.revisionList();
+                expect(revisionList.length).to.be(2);
+
+                expect(revisionList[0].repository.label()).to.be('SomeApp');
+                expect(revisionList[0].label).to.be('r4004-r4004');
+                expect(revisionList[0].from).to.be('4004');
+                expect(revisionList[0].to).to.be('4004');
+
+                expect(revisionList[1].repository.label()).to.be('macOS');
+                expect(revisionList[1].label).to.be('15B42 - 15C50');
+                expect(revisionList[1].from).to.be('15B42');
+                expect(revisionList[1].to).to.be('15C50');
+            });
+        });
+
+        it('should report the list of revision for the selected range', () => {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importRevisionList(context).then((ChartRevisionRange) => {
+                chart = ChartTest.createInteractiveChartWithSampleCluster(context);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                evaluator = new ChartRevisionRange(chart);
+                expect(evaluator.revisionList()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                expect(evaluator.revisionList()).to.be(null);
+
+                const currentView = chart.sampledTimeSeriesData('current');
+                chart.setSelection([currentView.firstPoint().time + 1, currentView.lastPoint().time - 1]);
+
+                let revisionList = evaluator.revisionList();
+                expect(revisionList).to.not.be(null);
+                expect(revisionList.length).to.be(2);
+
+                expect(revisionList[0].repository.label()).to.be('SomeApp');
+                expect(revisionList[0].label).to.be('r4003-r4004'); // 4002 and 4005 are outliers and skipped.
+                expect(revisionList[0].from).to.be('4003');
+                expect(revisionList[0].to).to.be('4004');
+
+                expect(revisionList[1].repository.label()).to.be('macOS');
+                expect(revisionList[1].label).to.be('15B42 - 15C50');
+                expect(revisionList[1].from).to.be('15B42');
+                expect(revisionList[1].to).to.be('15C50');
+            });
+        });
+    });
+
+});

Added: trunk/Websites/perf.webkit.org/browser-tests/chart-status-evaluator-tests.js (0 => 213300)


--- trunk/Websites/perf.webkit.org/browser-tests/chart-status-evaluator-tests.js	                        (rev 0)
+++ trunk/Websites/perf.webkit.org/browser-tests/chart-status-evaluator-tests.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -0,0 +1,477 @@
+
+describe('ChartStatusEvaluator', () => {
+
+    function importEvaluator(context)
+    {
+        const scripts = [
+            'lazily-evaluated-function.js',
+            'components/chart-status-evaluator.js'];
+
+        return ChartTest.importChartScripts(context).then(() => {
+            return context.importScripts(scripts, 'Test', 'Metric', 'ChartStatusEvaluator');
+        }).then(() => {
+            return context.symbols.ChartStatusEvaluator;
+        });
+    }
+
+    function makeMetric(context, name) {
+        const Test = context.symbols.Test;
+        const Metric = context.symbols.Metric;
+
+        const test = new Test(10, {name: 'SomeTest'});
+        const metric = new Metric(1, {name: name, test: test});
+
+        return metric;
+    }
+
+    describe('status on a non-interactive chart', () => {
+
+        it('should report the current value of the latest data point', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be(null);
+                expect(status.label).to.be(null);
+                expect(status.currentValue).to.be('116 ms');
+                expect(status.relativeDelta).to.be(null);
+            })
+        });
+
+        it('should compare the latest current data point to the baseline when for a smaller-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'baseline'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be('better');
+                expect(status.label).to.be('11.5% better than baseline (131 ms)');
+                expect(status.currentValue).to.be('116 ms');
+                expect(status.relativeDelta).to.be(null);
+            })
+        });
+
+        it('should compare the latest current data point to the baseline when for a bigger-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'baseline'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Score');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be('worse');
+                expect(status.label).to.be('11.5% worse than baseline (131 pt)');
+                expect(status.currentValue).to.be('116 pt');
+                expect(status.relativeDelta).to.be(null);
+            })
+        });
+
+        it('should compare the latest current data point to the target when for a smaller-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'target'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be('worse');
+                expect(status.label).to.be('27.5% until target (91.0 ms)');
+                expect(status.currentValue).to.be('116 ms');
+                expect(status.relativeDelta).to.be(null);
+            })
+        });
+
+        it('should compare the latest current data point to the target when for a bigger-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'target'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Score');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be('better');
+                expect(status.label).to.be('27.5% until target (91.0 pt)');
+                expect(status.currentValue).to.be('116 pt');
+                expect(status.relativeDelta).to.be(null);
+            })
+        });
+
+        it('should compare the latest current data point to the target when it is smaller than the baseline for a smaller-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'target'}, {type: 'baseline'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be(null);
+                expect(status.label).to.be('27.5% until target (91.0 ms)');
+                expect(status.currentValue).to.be('116 ms');
+                expect(status.relativeDelta).to.be(null);
+            });
+        });
+
+        it('should compare the latest current data point to the baseline when it is smaller than the baseline for a bigger-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'target'}, {type: 'baseline'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Score');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be('worse');
+                expect(status.label).to.be('11.5% worse than baseline (131 pt)');
+                expect(status.currentValue).to.be('116 pt');
+                expect(status.relativeDelta).to.be(null);
+            });
+        });
+
+        it('should compare the latest current data point to the baseline when it is bigger than the baseline and the target for a smaller-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'target'}, {type: 'baseline'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0], {baselineIsSmaller: true});
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be('worse');
+                expect(status.label).to.be('274.2% worse than baseline (31.0 ms)');
+                expect(status.currentValue).to.be('116 ms');
+                expect(status.relativeDelta).to.be(null);
+            });
+        });
+
+        it('should compare the latest current data point to the target when it is bigger than the baseline but smaller than the target for a bigger-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'target'}, {type: 'baseline'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0], {baselineIsSmaller: true, targetIsBigger: true});
+
+                const metric = makeMetric(context, 'Score');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be(null);
+                expect(status.label).to.be('4.1% until target (121 pt)');
+                expect(status.currentValue).to.be('116 pt');
+                expect(status.relativeDelta).to.be(null);
+            });
+        });
+
+        it('should compare the latest current data point to the target when it is smaller than the target for a smaller-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'target'}, {type: 'baseline'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0], {targetIsBigger: true});
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be('better');
+                expect(status.label).to.be('4.1% better than target (121 ms)');
+                expect(status.currentValue).to.be('116 ms');
+                expect(status.relativeDelta).to.be(null);
+            });
+        });
+
+        it('should compare the latest current data point to the baseline when it is smaller than the target but bigger than the baseline for a bigger-is-better unit', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createChartWithSampleCluster(context, [{type: 'current'}, {type: 'target'}, {type: 'baseline'}]);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0], {baselineIsSmaller: true, targetIsBigger: true});
+
+                const metric = makeMetric(context, 'Score');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                const status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be(null);
+                expect(status.label).to.be('4.1% until target (121 pt)');
+                expect(status.currentValue).to.be('116 pt');
+                expect(status.relativeDelta).to.be(null);
+            });
+        });
+    });
+
+    describe('status on an interactive chart', () => {
+
+        it('should not report the current value of the latest data point', () => {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                const chart = ChartTest.createInteractiveChartWithSampleCluster(context);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                expect(evaluator.status()).to.be(null);
+            })
+        });
+
+        it('should report the current value and the relative delta when there is a locked indicator', () => {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                chart = ChartTest.createInteractiveChartWithSampleCluster(context);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                expect(evaluator.status()).to.be(null);
+
+                const currentView = chart.sampledTimeSeriesData('current');
+                chart.setIndicator(currentView.lastPoint().id, true);
+
+                let status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be(null);
+                expect(status.label).to.be(null);
+                expect(status.currentValue).to.be('116 ms');
+                expect(status.relativeDelta).to.be('-6%');
+
+                chart.setIndicator(currentView.previousPoint(currentView.lastPoint()).id, true);
+                status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be(null);
+                expect(status.label).to.be(null);
+                expect(status.currentValue).to.be('124 ms');
+                expect(status.relativeDelta).to.be('10%');
+
+                chart.setIndicator(currentView.firstPoint().id, true);
+                status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be(null);
+                expect(status.label).to.be(null);
+                expect(status.currentValue).to.be('100 ms');
+                expect(status.relativeDelta).to.be(null);
+
+                return waitForComponentsToRender(context);
+            });
+        });
+
+        it('should report the current value and the relative delta when there is a selection with at least two points', () => {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                chart = ChartTest.createInteractiveChartWithSampleCluster(context);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                expect(evaluator.status()).to.be(null);
+
+                const currentView = chart.sampledTimeSeriesData('current');
+                const firstPoint = currentView.firstPoint();
+                const lastPoint = currentView.lastPoint();
+                chart.setSelection([firstPoint.time + 1, lastPoint.time - 1]);
+
+                let status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be(null);
+                expect(status.label).to.be(null);
+                expect(status.currentValue).to.be('124 ms');
+                expect(status.relativeDelta).to.be('2%');
+
+                return waitForComponentsToRender(context);
+            });
+        });
+
+        it('should report the current value but not the relative delta when there is a selection with exaclyt one point', () => {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) => {
+                chart = ChartTest.createInteractiveChartWithSampleCluster(context);
+                chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                ChartTest.respondWithSampleCluster(requests[0]);
+
+                const metric = makeMetric(context, 'Time');
+                evaluator = new ChartStatusEvaluator(chart, metric);
+                expect(evaluator.status()).to.be(null);
+
+                return waitForComponentsToRender(context);
+            }).then(() => {
+                expect(evaluator.status()).to.be(null);
+
+                const currentView = chart.sampledTimeSeriesData('current');
+                const firstPoint = currentView.firstPoint();
+                chart.setSelection([firstPoint.time + 1, currentView.nextPoint(firstPoint).time + 1]);
+
+                let status = evaluator.status();
+                expect(status).to.not.be(null);
+                expect(status.comparison).to.be(null);
+                expect(status.label).to.be(null);
+                expect(status.currentValue).to.be('122 ms');
+                expect(status.relativeDelta).to.be(null);
+
+                return waitForComponentsToRender(context);
+            });
+        });
+
+    });
+
+});

Modified: trunk/Websites/perf.webkit.org/browser-tests/index.html (213299 => 213300)


--- trunk/Websites/perf.webkit.org/browser-tests/index.html	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/browser-tests/index.html	2017-03-02 21:23:07 UTC (rev 213300)
@@ -19,6 +19,8 @@
 <script src=""
 <script src=""
 <script src=""
+<script src=""
+<script src=""
 <script>
 
 afterEach(() => {
@@ -40,6 +42,7 @@
         this.symbols = {};
         this.global = this.iframe.contentWindow;
         this.document = this.iframe.contentDocument;
+        this._didLoadMockRemote = false;
     }
 
     importScripts(pathList, ...symbolList)
@@ -48,8 +51,12 @@
         const global = this.iframe.contentWindow;
 
         pathList = pathList.map((path) => `../public/v3/${path}`);
+        if (!this._didLoadMockRemote) {
+            this._didLoadMockRemote = true;
+            pathList.unshift('../unit-tests/resources/mock-remote-api.js');
+        }
 
-        return Promise.all(['../unit-tests/resources/mock-remote-api.js', ...pathList].map((path) => {
+        return Promise.all(pathList.map((path) => {
             return new Promise((resolve, reject) => {
                 let script = doc.createElement('script');
                 script.addEventListener('load', resolve);
@@ -196,15 +203,21 @@
             '../shared/statistics.js',
             'instrumentation.js',
             'models/data-model.js',
-            'models/metric.js',
             'models/time-series.js',
             'models/measurement-set.js',
             'models/measurement-cluster.js',
             'models/measurement-adaptor.js',
+            'models/repository.js',
+            'models/platform.js',
+            'models/test.js',
+            'models/metric.js',
+            'models/root-set.js',
+            'models/commit-log.js',
             'components/base.js',
             'components/time-series-chart.js',
             'components/interactive-time-series-chart.js'],
-            'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'Metric', 'MeasurementSet', 'MockRemoteAPI').then(() => {
+            'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart',
+            'Platform', 'Metric', 'Test', 'Repository', 'MeasurementSet', 'MockRemoteAPI').then(() => {
                 return context.symbols.TimeSeriesChart;
             })
     },
@@ -211,7 +224,24 @@
 
     posixTime: posixTime,
 
-    sampleCluster: {
+    get sampleCluster() { return this.makeSampleCluster(); },
+
+    makeModelObjectsForSampleCluster(context)
+    {
+        const test = context.symbols.Test.ensureSingleton(2, {name: 'Test'});
+        const metric = context.symbols.Metric.ensureSingleton(1, {name: 'Time', test})
+        const platform = context.symbols.Platform.ensureSingleton(1,
+            {name: 'SomePlatform', metrics: [metric], lastModifiedByMetric: [posixTime('2016-01-18T00:00:00Z')]});
+        metric.addPlatform(platform);
+        context.symbols.Repository.ensureSingleton(1, {name: 'SomeApp'});
+        context.symbols.Repository.ensureSingleton(2, {name: 'macOS'});
+    },
+
+    makeSampleCluster(options = {})
+    {
+        const baselineStart = options.baselineIsSmaller ? 30 : 130;
+        const targetStart = options.targetIsBigger ? 120 : 90;
+        return {
         "clusterStart": posixTime('2016-01-01T00:00:00Z'),
         "clusterSize": 7 * dayInMilliseconds,
         "startTime": posixTime('2016-01-01T00:00:00Z'),
@@ -228,41 +258,65 @@
             "current": [
                 [
                     1000, 100, 1, 100, 100 * 100, false,
-                    [ [ 2000, 1, "4000", posixTime('2016-01-05T17:35:00Z')] ],
+                    [ [2000, 1, "4000", posixTime('2016-01-05T17:35:00Z')], [3000, 2, "15B42", 0] ],
                     posixTime('2016-01-05T17:35:00Z'), 5000, posixTime('2016-01-05T19:23:00Z'), "10", 7
                 ],
                 [
                     1001, 131, 1, 131, 131 * 131, true,
-                    [ [ 2001, 1, "4001", posixTime('2016-01-05T18:43:01Z')] ],
+                    [ [2001, 1, "4001", posixTime('2016-01-05T18:43:01Z')], [3000, 2, "15B42", 0] ],
                     posixTime('2016-01-05T18:43:01Z'), 5001, posixTime('2016-01-05T20:58:01Z'), "11", 7
                 ],
                 [
                     1002, 122, 1, 122, 122 * 122, false,
-                    [ [ 2002, 1, "4002", posixTime('2016-01-05T20:01:02Z') ] ],
+                    [ [2002, 1, "4002", posixTime('2016-01-05T20:01:02Z')], [3000, 2, "15B42", 0] ],
                     posixTime('2016-01-05T20:01:02Z'), 5002, posixTime('2016-01-05T22:37:02Z'), "12", 7
                 ],
                 [
                     1003, 113, 1, 113, 113 * 113, false,
-                    [ [ 2003, 1, "4003", posixTime('2016-01-05T23:19:03Z') ] ],
+                    [ [2003, 1, "4003", posixTime('2016-01-05T23:19:03Z')], [3000, 2, "15B42", 0] ],
                     posixTime('2016-01-05T23:19:03Z'), 5003, posixTime('2016-01-06T23:19:03Z'), "13", 7
                 ],
                 [
                     1004, 124, 1, 124, 124 * 124, false,
-                    [ [ 2004, 1, "4004", posixTime('2016-01-06T01:52:04Z') ] ],
+                    [ [2004, 1, "4004", posixTime('2016-01-06T01:52:04Z')], [3001, 2, "15C50", 0] ],
                     posixTime('2016-01-06T01:52:04Z'), 5004, posixTime('2016-01-06T02:42:04Z'), "14", 7
                 ],
                 [
                     1005, 115, 1, 115, 115 * 115, true,
-                    [ [ 2005, 1, "4005", posixTime('2016-01-06T03:22:05Z') ] ],
+                    [ [2005, 1, "4005", posixTime('2016-01-06T03:22:05Z')], [3001, 2, "15C50", 0] ],
                     posixTime('2016-01-06T03:22:05Z'), 5005, posixTime('2016-01-06T06:01:05Z'), "15", 7
                 ],
                 [
                     1006, 116, 1, 116, 116 * 116, false,
-                    [ [ 2006, 1, "4006", posixTime('2016-01-06T05:59:06Z') ] ],
+                    [ [2006, 1, "4006", posixTime('2016-01-06T05:59:06Z')], [3001, 2, "15C50", 0] ],
                     posixTime('2016-01-06T05:59:06Z'), 5006, posixTime('2016-01-06T08:34:06Z'), "16", 7
                 ]
+            ],
+            "baseline": [
+                [
+                    7000, baselineStart, 1, baselineStart, baselineStart * baselineStart, false,
+                    [ ],
+                    posixTime('2016-01-05T12:00:30Z'), 5030, posixTime('2016-01-05T12:00:30Z'), "30", 7
+                ],
+                [
+                    7001, baselineStart + 1, 1, baselineStart + 1, Math.pow(baselineStart + 1, 2), false,
+                    [ ],
+                    posixTime('2016-01-06T00:00:31Z'), 5031, posixTime('2016-01-06T00:00:31Z'), "31", 7
+                ],
+            ],
+            "target": [
+                [
+                    8000, targetStart, 1, targetStart, targetStart * targetStart, false,
+                    [ ],
+                    posixTime('2016-01-05T12:00:30Z'), 5030, posixTime('2016-01-05T12:00:30Z'), "90", 7
+                ],
+                [
+                    8001, targetStart + 1, 1, targetStart + 1, Math.pow(targetStart + 1, 2), false,
+                    [ ],
+                    posixTime('2016-01-06T00:00:31Z'), 5031, posixTime('2016-01-06T00:00:31Z'), "91", 7
+                ],
             ]
-        },
+        }};
     },
 
     createChartWithSampleCluster(context, sourceList = null, chartOptions = {}, className = 'TimeSeriesChart')
@@ -296,11 +350,11 @@
         return this.createChartWithSampleCluster(context, sourceList, chartOptions, 'InteractiveTimeSeriesChart');
     },
 
-    respondWithSampleCluster(request)
+    respondWithSampleCluster(request, options)
     {
         expect(request.url).to.be('../data/measurement-set-1-1.json');
         expect(request.method).to.be('GET');
-        request.resolve(this.sampleCluster);
+        request.resolve(this.makeSampleCluster(options));
     },
 };
 

Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -12,6 +12,7 @@
         this._metric = null;
         this._disableSampling = false;
         this._showOutliers = false;
+        this._openRepository = null;
 
         this._overviewChart = null;
         this._mainChart = null;
@@ -53,10 +54,13 @@
         this._mainChart.listenToAction('annotationClick', this._openAnalysisTask.bind(this));
         this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
 
-        this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart, this._requestOpeningCommitViewer.bind(this));
+        this._revisionRange = new ChartRevisionRange(this._mainChart);
+
+        this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart);
+        this._mainChartStatus.listenToAction('openRepository', this.openNewRepository.bind(this));
         this.renderReplace(this.content().querySelector('.chart-pane-details'), this._mainChartStatus);
 
-        this.content().querySelector('.chart-pane').addEventListener('keyup', this._keyup.bind(this));
+        this.content().querySelector('.chart-pane').addEventListener('keydown', this._keyup.bind(this));
 
         this.fetchAnalysisTasks(false);
     }
@@ -125,11 +129,18 @@
             this._mainChart.setSelection(selection);
     }
 
+    setOpenRepository(repository)
+    {
+        this._openRepository = repository;
+        this._mainChartStatus.setCurrentRepository(repository);
+        this._updateCommitLogViewer();
+    }
+
     _overviewSelectionDidChange(domain, didEndDrag) { }
 
     _mainSelectionDidChange(selection, didEndDrag)
     {
-        this._updateStatus();
+        this._updateCommitLogViewer();
     }
 
     _mainSelectionDidZoom(selection)
@@ -141,19 +152,19 @@
 
     _indicatorDidChange(indicatorID, isLocked)
     {
-        this._updateStatus();
+        this._updateCommitLogViewer();
     }
 
     _didFetchData()
     {
-        this._updateStatus();
+        this._updateCommitLogViewer();
     }
 
-    _updateStatus()
+    _updateCommitLogViewer()
     {
-        var range = this._mainChartStatus.updateRevisionList();
+        const range = this._revisionRange.rangeForRepository(this._openRepository);
         const updateRendering = () => { this.enqueueToRender(); };
-        this._commitLogViewer.view(range.repository, range.from, range.to).then(updateRendering);
+        this._commitLogViewer.view(this._openRepository, range.from, range.to).then(updateRendering);
         updateRendering();
     }
 
@@ -166,12 +177,10 @@
 
     router() { return null; }
 
-    _requestOpeningCommitViewer(repository, from, to)
+    openNewRepository(repository)
     {
-        this._mainChartStatus.setCurrentRepository(repository);
-        const updateRendering = () => { this.enqueueToRender(); };
-        this._commitLogViewer.view(repository, from, to).then(updateRendering);
-        updateRendering();
+        this.content().querySelector('.chart-pane').focus();
+        this.setOpenRepository(repository);
     }
 
     _keyup(event)
@@ -186,11 +195,13 @@
                 return;
             break;
         case 38: // Up
-            if (!this._mainChartStatus.moveRepositoryWithNotification(false))
+            if (!this._moveOpenRepository(false))
                 return;
+            break;
         case 40: // Down
-            if (!this._mainChartStatus.moveRepositoryWithNotification(true))
+            if (!this._moveOpenRepository(true))
                 return;
+            break;
         default:
             return;
         }
@@ -201,6 +212,28 @@
         event.stopPropagation();
     }
 
+    _moveOpenRepository(forward)
+    {
+        const openRepository = this._openRepository;
+        if (!openRepository)
+            return false;
+
+        const revisionList = this._revisionRange.revisionList();
+        if (!revisionList)
+            return false;
+
+        const currentIndex = revisionList.findIndex((info) => info.repository == openRepository);
+        console.assert(currentIndex >= 0);
+
+        const newIndex = currentIndex + (forward ? 1 : -1);
+        if (newIndex < 0 || newIndex >= revisionList.length)
+            return false;
+
+        this.openNewRepository(revisionList[newIndex].repository);
+
+        return true;
+    }
+
     render()
     {
         Instrumentation.startMeasuringTime('ChartPane', 'render');

Added: trunk/Websites/perf.webkit.org/public/v3/components/chart-revision-range.js (0 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/components/chart-revision-range.js	                        (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-revision-range.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -0,0 +1,68 @@
+
+class ChartRevisionRange {
+
+    constructor(chart, metric)
+    {
+        this._chart = chart;
+
+        const thisClass = new.target;
+        this._computeRevisionList = new LazilyEvaluatedFunction((currentPoint, prevoiusPoint) => {
+            return thisClass._computeRevisionList(currentPoint, prevoiusPoint);
+        });
+
+        this._computeRevisionRange = new LazilyEvaluatedFunction((repository, currentPoint, previousPoint) => {
+            return {
+                repository,
+                from: thisClass._revisionForPoint(repository, previousPoint),
+                to: thisClass._revisionForPoint(repository, currentPoint)};
+        });
+    }
+
+    revisionList()
+    {
+        const referencePoints = this._chart.referencePoints('current');
+        const currentPoint = referencePoints ? referencePoints.currentPoint : null;
+        const previousPoint = referencePoints ? referencePoints.previousPoint : null;
+        return this._computeRevisionList.evaluate(currentPoint, previousPoint);
+    }
+
+    rangeForRepository(repository)
+    {
+        const referencePoints = this._chart.referencePoints('current');
+        const currentPoint = referencePoints ? referencePoints.currentPoint : null;
+        const previousPoint = referencePoints ? referencePoints.previousPoint : null;
+        return this._computeRevisionRange.evaluate(repository, currentPoint, previousPoint);
+    }
+
+    static _revisionForPoint(repository, point)
+    {
+        if (!point || !repository)
+            return null;
+        const rootSet = point.rootSet();
+        if (!rootSet)
+            return null;
+        const commit = rootSet.commitForRepository(repository);
+        if (!commit)
+            return null;
+        return commit.revision();
+    }
+
+    static _computeRevisionList(currentPoint, previousPoint)
+    {
+        if (!currentPoint)
+            return null;
+
+        const currentRootSet = currentPoint.rootSet();
+        const previousRootSet = previousPoint ? previousPoint.rootSet() : null;
+
+        const repositoriesInCurrentRootSet = Repository.sortByNamePreferringOnesWithURL(currentRootSet.repositories());
+        const revisionList = [];
+        for (let repository of repositoriesInCurrentRootSet) {
+            let currentCommit = currentRootSet.commitForRepository(repository);
+            let previousCommit = previousRootSet ? previousRootSet.commitForRepository(repository) : null;
+            revisionList.push(currentCommit.diff(previousCommit));
+        }
+        return revisionList;
+    }
+
+}

Added: trunk/Websites/perf.webkit.org/public/v3/components/chart-status-evaluator.js (0 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/components/chart-status-evaluator.js	                        (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-status-evaluator.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -0,0 +1,79 @@
+
+class ChartStatusEvaluator {
+
+    constructor(chart, metric)
+    {
+        this._chart = chart;
+        this._computeStatus = new LazilyEvaluatedFunction((currentPoint, previousPoint, view) => {
+            if (!currentPoint)
+                return null;
+
+            const baselineView = this._chart.sampledTimeSeriesData('baseline');
+            const targetView = this._chart.sampledTimeSeriesData('target');
+            return ChartStatusEvaluator.computeChartStatus(metric, currentPoint, previousPoint, view, baselineView, targetView);
+        });
+    }
+
+    status()
+    {
+        const referencePoints = this._chart.referencePoints('current');
+        const currentPoint = referencePoints ? referencePoints.currentPoint : null;
+        const previousPoint = referencePoints ? referencePoints.previousPoint : null;
+        const view = referencePoints ? referencePoints.view : null;
+        return this._computeStatus.evaluate(currentPoint, previousPoint, view);
+    }
+
+    static computeChartStatus(metric, currentPoint, previousPoint, currentView, baselineView, targetView)
+    {
+        const formatter = metric.makeFormatter(3);
+        const deltaFormatter = metric.makeFormatter(2, true);
+        const smallerIsBetter = metric.isSmallerBetter();
+
+        const labelForDiff = (diff, referencePoint, name, comparison) => {
+            const relativeDiff = Math.abs(diff * 100).toFixed(1);
+            const referenceValue = referencePoint ? ` (${formatter(referencePoint.value)})` : '';
+            if (comparison != 'until')
+                comparison += ' than';
+            return `${relativeDiff}% ${comparison} ${name}${referenceValue}`;
+        }
+
+        const pointIsInCurrentSeries = baselineView != currentView && targetView != currentView;
+
+        const baselinePoint = pointIsInCurrentSeries && baselineView ? baselineView.lastPointInTimeRange(0, currentPoint.time) : null;
+        const targetPoint = pointIsInCurrentSeries && targetView ? targetView.lastPointInTimeRange(0, currentPoint.time) : null;
+
+        const diffFromBaseline = baselinePoint ? (currentPoint.value - baselinePoint.value) / baselinePoint.value : undefined;
+        const diffFromTarget = targetPoint ? (currentPoint.value - targetPoint.value) / targetPoint.value : undefined;
+
+        let label = null;
+        let comparison = null;
+
+        if (diffFromBaseline !== undefined && diffFromTarget !== undefined) {
+            if (diffFromBaseline > 0 == smallerIsBetter) {
+                comparison = 'worse';
+                label = labelForDiff(diffFromBaseline, baselinePoint, 'baseline', comparison);
+            } else if (diffFromTarget < 0 == smallerIsBetter) {
+                comparison = 'better';
+                label = labelForDiff(diffFromTarget, targetPoint, 'target', comparison);
+            } else
+                label = labelForDiff(diffFromTarget, targetPoint, 'target', 'until');
+        } else if (diffFromBaseline !== undefined) {
+            comparison = diffFromBaseline > 0 == smallerIsBetter ? 'worse' : 'better';
+            label = labelForDiff(diffFromBaseline, baselinePoint, 'baseline', comparison);
+        } else if (diffFromTarget !== undefined) {
+            comparison = diffFromTarget < 0 == smallerIsBetter ? 'better' : 'worse';
+            label = labelForDiff(diffFromTarget, targetPoint, 'target', 'until');
+        }
+
+        let valueDelta = null;
+        let relativeDelta = null;
+        if (previousPoint) {
+            valueDelta = deltaFormatter(currentPoint.value - previousPoint.value);
+            relativeDelta = (currentPoint.value - previousPoint.value) / previousPoint.value;
+            relativeDelta = (relativeDelta * 100).toFixed(0) + '%';
+        }
+
+        return {comparison, label, currentValue: formatter(currentPoint.value), valueDelta, relativeDelta};
+    }
+
+}

Deleted: trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -1,181 +0,0 @@
-
-class ChartStatusView extends ComponentBase {
-
-    constructor(metric, chart)
-    {
-        super('chart-status');
-        this._metric = metric;
-        this._chart = chart;
-
-        this._usedSelection = null;
-        this._usedCurrentPoint = null;
-        this._usedPreviousPoint = null;
-
-        this._currentValue = null;
-        this._comparisonClass = null;
-        this._comparisonLabel = null;
-
-        this._renderedCurrentValue = null;
-        this._renderedComparisonClass = null;
-        this._renderedComparisonLabel = null;
-    }
-
-    render()
-    {
-        this.updateStatusIfNeeded();
-
-        if (this._renderedCurrentValue == this._currentValue
-            && this._renderedComparisonClass == this._comparisonClass
-            && this._renderedComparisonLabel == this._comparisonLabel)
-            return;
-
-        this._renderedCurrentValue = this._currentValue;
-        this._renderedComparisonClass = this._comparisonClass;
-        this._renderedComparisonLabel = this._comparisonLabel;
-
-        this.content().querySelector('.chart-status-current-value').textContent = this._currentValue || '';
-        var comparison = this.content().querySelector('.chart-status-comparison');
-        comparison.className = 'chart-status-comparison ' + (this._comparisonClass || '');
-        comparison.textContent = this._comparisonLabel;
-    }
-
-    updateStatusIfNeeded()
-    {
-        var currentPoint;
-        var previousPoint;
-
-        if (this._chart instanceof InteractiveTimeSeriesChart) {
-            var selection = this._chart.currentSelection();
-            if (selection && this._usedSelection == selection)
-                return false;
-
-            if (selection) {
-                const view = this._chart.selectedPoints('current');
-                if (!view)
-                    return false;
-
-                if (view && view.length() > 1) {
-                    this._usedSelection = selection;
-                    currentPoint = view.lastPoint();
-                    previousPoint = view.firstPoint();
-                }
-            } else  {
-                const indicator = this._chart.currentIndicator();
-                if (indicator) {
-                    currentPoint = indicator.point;
-                    previousPoint = indicator.view.previousPoint(currentPoint);
-                }
-            }
-        } else {
-            var data = ""
-            if (!data)
-                return false;
-            if (data.length())
-                currentPoint = data.lastPoint();
-        }
-
-        if (currentPoint == this._usedCurrentPoint && previousPoint == this._usedPreviousPoint)
-            return false;
-
-        this._usedCurrentPoint = currentPoint;
-        this._usedPreviousPoint = previousPoint;
-
-        this.computeChartStatusLabels(currentPoint, previousPoint);
-
-        return true;
-    }
-
-    computeChartStatusLabels(currentPoint, previousPoint)
-    {
-        var status = currentPoint ? this._computeChartStatus(this._metric, this._chart, currentPoint, previousPoint) : null;
-        if (status) {
-            this._currentValue = status.currentValue;
-            if (previousPoint)
-                this._currentValue += ` (${status.valueDelta} / ${status.relativeDelta})`;
-            this._comparisonClass = status.className;
-            this._comparisonLabel = status.label;
-        } else {
-            this._currentValue = null;
-            this._comparisonClass = null;
-            this._comparisonLabel = null;
-        }
-    }
-
-    _computeChartStatus(metric, chart, currentPoint, previousPoint)
-    {
-        console.assert(currentPoint);
-        const baselineView = chart.sampledTimeSeriesData('baseline');
-        const targetView = chart.sampledTimeSeriesData('target');
-
-        const formatter = metric.makeFormatter(3);
-        const deltaFormatter = metric.makeFormatter(2, true);
-        const smallerIsBetter = metric.isSmallerBetter();
-
-        const labelForDiff = (diff, referencePoint, name, comparison) => {
-            const relativeDiff = Math.abs(diff * 100).toFixed(1);
-            const referenceValue = referencePoint ? ` (${formatter(referencePoint.value)})` : '';
-            return `${relativeDiff}% ${comparison} than ${name}${referenceValue}`;
-        };
-
-        const baselinePoint = baselineView ? baselineView.lastPointInTimeRange(0, currentPoint.time) : null;
-        const targetPoint = targetView ? targetView.lastPointInTimeRange(0, currentPoint.time) : null;
-
-        const diffFromBaseline = baselinePoint ? (currentPoint.value - baselinePoint.value) / baselinePoint.value : undefined;
-        const diffFromTarget = targetPoint ? (currentPoint.value - targetPoint.value) / targetPoint.value : undefined;
-
-        let label = null;
-        let comparison = null;
-
-        if (diffFromBaseline !== undefined && diffFromTarget !== undefined) {
-            if (diffFromBaseline > 0 == smallerIsBetter) {
-                comparison = 'worse';
-                label = labelForDiff(diffFromBaseline, baselinePoint, 'baseline', comparison);
-            } else if (diffFromTarget < 0 == smallerIsBetter) {
-                comparison = 'better';
-                label = labelForDiff(diffFromTarget, targetPoint, 'target', comparison);
-            } else
-                label = labelForDiff(diffFromTarget, targetPoint, 'target', 'until');
-        } else if (diffFromBaseline !== undefined) {
-            comparison = diffFromBaseline > 0 == smallerIsBetter ? 'worse' : 'better';
-            label = labelForDiff(diffFromBaseline, baselinePoint, 'baseline', comparison);
-        } else if (diffFromTarget !== undefined) {
-            comparison = diffFromTarget < 0 == smallerIsBetter ? 'better' : 'worse';
-            label = labelForDiff(diffFromTarget, targetPoint, 'target', comparison);
-        }
-
-        let valueDelta = null;
-        let relativeDelta = null;
-        if (previousPoint) {
-            valueDelta = deltaFormatter(currentPoint.value - previousPoint.value);
-            relativeDelta = (currentPoint.value - previousPoint.value) / previousPoint.value;
-            relativeDelta = (relativeDelta * 100).toFixed(0) + '%';
-        }
-
-        return {className: comparison, label, currentValue: formatter(currentPoint.value), valueDelta, relativeDelta};
-    }
-
-    static htmlTemplate()
-    {
-        return `
-            <div>
-                <span class="chart-status-current-value"></span>
-                <span class="chart-status-comparison"></span>
-            </div>`;
-    }
-
-    static cssTemplate()
-    {
-        return `
-            .chart-status-current-value {
-                padding-right: 0.5rem;
-            }
-
-            .chart-status-comparison.worse {
-                color: #c33;
-            }
-
-            .chart-status-comparison.better {
-                color: #33c;
-            }`;
-    }
-}
\ No newline at end of file

Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -52,6 +52,7 @@
             backgroundIntervalStyle: 'rgba(255, 153, 153, 0.1)',
             backgroundPointStyle: '#f99',
             backgroundLineStyle: '#fcc',
+            interactive: true,
         };
     }
 

Added: trunk/Websites/perf.webkit.org/public/v3/components/dashboard-chart-status-view.js (0 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/components/dashboard-chart-status-view.js	                        (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/components/dashboard-chart-status-view.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -0,0 +1,45 @@
+
+class DashboardChartStatusView extends ComponentBase {
+
+    constructor(metric, chart)
+    {
+        super('chart-status-view');
+        this._statusEvaluator = new ChartStatusEvaluator(chart, metric);
+        this._renderLazily = new LazilyEvaluatedFunction((status) => {
+            status = status || {};
+            this.content('current-value').textContent = status.currentValue || '';
+            this.content('comparison').textContent = status.label || '';
+            this.content('comparison').className = status.comparison || '';
+        });
+    }
+
+    render()
+    {
+        this._renderLazily.evaluate(this._statusEvaluator.status());
+    }
+
+    static htmlTemplate()
+    {
+        return `<span id="current-value"></span> <span id="comparison"></span>`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            :host {
+                display: block;
+            }
+
+            #comparison {
+                padding-left: 0.5rem;
+            }
+
+            #comparison.worse {
+                color: #c33;
+            }
+
+            #comparison.better {
+                color: #33c;
+            }`;
+    }
+}

Modified: trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -52,6 +52,27 @@
         return selection && data ? data.firstPointInTimeRange(selection[0], selection[1]) : null;
     }
 
+    referencePoints(type)
+    {
+        const selection = this.currentSelection();
+        if (selection) {
+            const view = this.selectedPoints('current');
+            if (!view)
+                return null;
+            const firstPoint = view.lastPoint();
+            const lastPoint = view.firstPoint();
+            if (!firstPoint)
+                return null;
+            return {view, currentPoint: firstPoint, previousPoint: firstPoint != lastPoint ? lastPoint : null};
+        } else  {
+            const indicator = this.currentIndicator();
+            if (!indicator)
+                return null;
+            return {view: indicator.view, currentPoint: indicator.point, previousPoint: indicator.view.previousPoint(indicator.point)};
+        }
+        return null;
+    }
+
     setIndicator(id, shouldLock)
     {
         var selectionDidChange = !!this._sampledTimeSeriesData;

Modified: trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -131,6 +131,17 @@
         return null;
     }
 
+    referencePoints(type)
+    {
+        const view = this.sampledTimeSeriesData(type);
+        if (!view || !this._startTime || !this._endTime)
+            return null;
+        const point = view.lastPointInTimeRange(this._startTime, this._endTime);
+        if (!point)
+            return null;
+        return {view, currentPoint: point, previousPoint: null};
+    }
+
     setAnnotations(annotations)
     {
         this._annotations = annotations;

Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/index.html	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html	2017-03-02 21:23:07 UTC (rev 213300)
@@ -38,12 +38,12 @@
 
     <template id="unbundled-scripts">
         <script src=""
-        <script src=""
 
         <script src=""
         <script src=""
         <script src=""
         <script src=""
+        <script src=""
 
         <script src=""
         <script src=""
@@ -75,7 +75,7 @@
         <script src=""
         <script src=""
         <script src=""
-        <script src=""
+        <script src=""
         <script src=""
         <script src=""
         <script src=""
@@ -84,6 +84,8 @@
         <script src=""
         <script src=""
         <script src=""
+        <script src=""
+        <script src=""
         <script src=""
         <script src=""
         <script src=""

Added: trunk/Websites/perf.webkit.org/public/v3/lazily-evaluated-function.js (0 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/lazily-evaluated-function.js	                        (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/lazily-evaluated-function.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -0,0 +1,27 @@
+class LazilyEvaluatedFunction {
+    constructor(callback, ...observedPropertiesList)
+    {
+        console.assert(typeof(callback) == 'function');
+        this._callback = callback;
+        this._observedPropertiesList = observedPropertiesList;
+        this._cachedArguments = null;
+        this._cachedResult = undefined;
+    }
+
+    evaluate(...args)
+    {
+        if (this._cachedArguments) {
+            const length = this._cachedArguments.length;
+            if (args.length == length && (!length || this._cachedArguments.every((cached, i) => cached === args[i])))
+                return this._cachedResult;
+        }
+
+        this._cachedArguments = args;
+        this._cachedResult = this._callback.apply(null, args);
+
+        return this._cachedResult;
+    }
+}
+
+if (typeof module != 'undefined')
+    module.exports.LazilyEvaluatedFunction = LazilyEvaluatedFunction;

Modified: trunk/Websites/perf.webkit.org/public/v3/models/commit-log.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/models/commit-log.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/models/commit-log.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -57,7 +57,7 @@
         var from = previousCommit.revision();
         var label = null;
         if (parseInt(to) == to) { // e.g. r12345.
-            from = parseInt(from) + 1;
+            from = (parseInt(from) + 1).toString();
             label = `r${from}-r${this.revision()}`;
         } else if (to.length == 40) { // e.g. git hash
             label = `${from.substring(0, 8)}..${to.substring(0, 8)}`;

Modified: trunk/Websites/perf.webkit.org/public/v3/models/metric.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/models/metric.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/models/metric.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -8,6 +8,19 @@
         object.test.addMetric(this);
         this._test = object.test;
         this._platforms = [];
+
+        const suffix = this.name().match('([A-z][a-z]+|FrameRate)$')[0];
+        this._unit = {
+            'FrameRate': 'fps',
+            'Runs': '/s',
+            'Time': 'ms',
+            'Duration': 'ms',
+            'Malloc': 'B',
+            'Heap': 'B',
+            'Allocations': 'B',
+            'Size': 'B',
+            'Score': 'pt',
+        }[suffix];
     }
 
     aggregatorName() { return this._aggregatorName; }
@@ -57,9 +70,14 @@
         return this.name() + suffix;
     }
 
-    unit() { return RunsData.unitFromMetricName(this.name()); }
-    isSmallerBetter() { return RunsData.isSmallerBetter(this.unit()); }
+    unit() { return this._unit; }
 
+    isSmallerBetter()
+    {
+        const unit = this._unit;
+        return unit != 'fps' && unit != '/s' && unit != 'pt';
+    }
+
     makeFormatter(sigFig, alwaysShowSign) { return Metric.makeFormatter(this.unit(), sigFig, alwaysShowSign); }
 
     static makeFormatter(unit, sigFig = 2, alwaysShowSign)

Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -16,12 +16,6 @@
             this._page._chartSelectionDidChange();
     }
 
-    _updateStatus()
-    {
-        super._updateStatus();
-        this._page.enqueueToRender();
-    }
-
     selectedPoints()
     {
         return this._mainChart ? this._mainChart.selectedPoints('current') : null;

Modified: trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane-status-view.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane-status-view.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane-status-view.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -1,68 +1,75 @@
 
-class ChartPaneStatusView extends ChartStatusView {
-    
-    constructor(metric, chart, revisionCallback)
+class ChartPaneStatusView extends ComponentBase {
+    constructor(metric, chart)
     {
-        super(metric, chart);
+        super('chart-pane-status-view');
 
-        this._buildLabel = null;
-        this._buildUrl = null;
-
-        this._revisionList = [];
+        this._chart = chart;
+        this._status = new ChartStatusEvaluator(chart, metric);
+        this._revisionRange = new ChartRevisionRange(chart);
         this._currentRepository = null;
-        this._revisionCallback = revisionCallback;
-        this._pointsRangeForAnalysis = null;
 
-        this._renderedRevisionList = null;
-        this._renderedRepository = null;
-
-        this._usedRevisionRange = [null, null, null];
+        this._renderStatusLazily = new LazilyEvaluatedFunction(this._renderStatus.bind(this));
+        this._renderBuildRevisionTableLazily = new LazilyEvaluatedFunction(this._renderBuildRevisionTable.bind(this));
     }
 
-    pointsRangeForAnalysis() { return this._pointsRangeForAnalysis; }
-
     render()
     {
         super.render();
 
-        if (this._renderedRevisionList == this._revisionList && this._renderedRepository == this._currentRepository)
-            return;
-        this._renderedRevisionList = this._revisionList;    
-        this._renderedRepository = this._currentRepository;
+        this._renderStatusLazily.evaluate(this._status.status());
 
-        var element = ComponentBase.createElement;
-        var link = ComponentBase.createLink;
-        var self = this;
-        var buildInfo = this._buildInfo;
-        var tableContent = this._revisionList.map(function (info, rowIndex) {
-            var selected = info.repository == self._currentRepository;
-            var action = "" () {
-                if (self._currentRepository == info.repository)
-                    self._setRevisionRange(true, null, null, null);
-                else
-                    self._setRevisionRange(true, info.repository, info.from, info.to);
-            };
+        const indicator = this._chart.currentIndicator();
+        const build = indicator ? indicator.point.build() : null;
+        this._renderBuildRevisionTableLazily.evaluate(build, this._currentRepository, this._revisionRange.revisionList());
+    }
 
-            return element('tr', {class: selected ? 'selected' : ''}, [
-                element('td', info.repository.name()),
-                element('td', info.url ? link(info.label, info.label, info.url, true) : info.label),
-                element('td', {class: 'commit-viewer-opener'}, link('\u00BB', action)),
-            ]);
-        });
+    _renderStatus(status)
+    {
+        status = status || {};
+        let currentValue = status.currentValue || '';
+        if (currentValue)
+            currentValue += ` (${status.valueDelta} / ${status.relativeDelta})`;
 
-        if (this._buildInfo) {
-            var build = this._buildInfo;
-            var number = build.buildNumber();
-            var buildTime = this._formatTime(build.buildTime());
-            var url = ""
+        this.content('current-value').textContent = currentValue;
+        this.content('comparison').textContent = status.label || '';
+        this.content('comparison').className = status.comparison || '';
+    }
 
-            tableContent.unshift(element('tr', [
+    _renderBuildRevisionTable(build, currentRepository, revisionList)
+    {
+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+        let tableContent = [];
+
+        if (build) {
+            const url = ""
+            const buildNumber = build.buildNumber();
+            tableContent.push(element('tr', [
                 element('td', 'Build'),
-                element('td', {colspan: 2}, [url ? link(number, build.label(), url, true) : number, ` (${buildTime})`]),
+                element('td', {colspan: 2}, [
+                    url ? link(buildNumber, build.label(), url, true) : buildNumber,
+                    ` (${this._formatTime(build.buildTime())})`
+                ]),
             ]));
         }
 
-        this.renderReplace(this.content().querySelector('.chart-pane-revisions'), tableContent);
+        if (revisionList) {
+            for (let info of revisionList) {
+                const selected = info.repository == this._currentRepository;
+                const action = "" => {
+                    this.dispatchAction('openRepository', this._currentRepository == info.repository ? null : info.repository);
+                };
+
+                tableContent.push(element('tr', {class: selected ? 'selected' : ''}, [
+                    element('td', info.repository.name()),
+                    element('td', info.url ? link(info.label, info.label, info.url, true) : info.label),
+                    element('td', {class: 'commit-viewer-opener'}, link('\u00BB', action)),
+                ]));
+            }
+        }
+
+        this.renderReplace(this.content('build-revision'), tableContent);
     }
 
     _formatTime(date)
@@ -74,116 +81,30 @@
     setCurrentRepository(repository)
     {
         this._currentRepository = repository;
-        return this._updateRevisionListForNewCurrentRepository();
+        this.enqueueToRender();
     }
 
-    _setRevisionRange(shouldNotify, repository, from, to)
-    {
-        if (this._usedRevisionRange[0] == repository
-            && this._usedRevisionRange[1] == from && this._usedRevisionRange[2] == to)
-            return;
-        this._usedRevisionRange = [repository, from, to];
-        if (shouldNotify)
-            this._revisionCallback(repository, from, to);
-    }
-
-    moveRepositoryWithNotification(forward)
-    {
-        var currentRepository = this._currentRepository;
-        if (!currentRepository)
-            return false;
-        var index = this._revisionList.findIndex(function (info) { return info.repository == currentRepository; });
-        console.assert(index >= 0);
-
-        var newIndex = index + (forward ? 1 : -1);
-        newIndex = Math.min(this._revisionList.length - 1, Math.max(0, newIndex));
-        if (newIndex == index)
-            return false;
-
-        var item = this._revisionList[newIndex];
-        this.setCurrentRepository(item ? item.repository : null);
-
-        return true;
-    }
-
-    updateRevisionList()
-    {
-        if (!this._currentRepository)
-            return {repository: null, from: null, to: null};
-        return this._updateRevisionListForNewCurrentRepository();
-    }
-
-    _updateRevisionListForNewCurrentRepository()
-    {
-        this.updateStatusIfNeeded();
-
-        for (var info of this._revisionList) {
-            if (info.repository == this._currentRepository) {
-                this._setRevisionRange(false, info.repository, info.from, info.to);
-                return {repository: info.repository, from: info.from, to: info.to};
-            }
-        }
-        this._setRevisionRange(false, null, null, null);
-        return {repository: this._currentRepository, from: null, to: null};
-    }
-
-    computeChartStatusLabels(currentPoint, previousPoint)
-    {
-        super.computeChartStatusLabels(currentPoint, previousPoint);
-
-        this._buildInfo = null;
-        this._revisionList = [];
-        this._pointsRangeForAnalysis = null;
-
-        if (!currentPoint)
-            return;
-
-        if (!this._chart.currentSelection())
-            this._buildInfo = currentPoint.build();
-
-        if (currentPoint && previousPoint && this._chart.currentSelection()) {
-            this._pointsRangeForAnalysis = {
-                startPointId: previousPoint.id,
-                endPointId: currentPoint.id,
-            };
-        }
-
-        // FIXME: Rewrite the interface to obtain the list of revision changes.
-        var currentRootSet = currentPoint.rootSet();
-        var previousRootSet = previousPoint ? previousPoint.rootSet() : null;
-
-        var repositoriesInCurrentRootSet = Repository.sortByNamePreferringOnesWithURL(currentRootSet.repositories());
-        var revisionList = [];
-        for (var repository of repositoriesInCurrentRootSet) {
-            var currentCommit = currentRootSet.commitForRepository(repository);
-            var previousCommit = previousRootSet ? previousRootSet.commitForRepository(repository) : null;
-            revisionList.push(currentCommit.diff(previousCommit));
-        }
-
-        this._revisionList = revisionList;
-    }
-
     static htmlTemplate()
     {
         return `
-            <div class="chart-pane-status">
-                <h3 class="chart-status-current-value"></h3>
-                <span class="chart-status-comparison"></span>
+            <div id="chart-pane-status">
+                <h3 id="current-value"></h3>
+                <span id="comparison"></span>
             </div>
-            <table class="chart-pane-revisions"></table>
+            <table id="build-revision"></table>
         `;
     }
 
     static cssTemplate()
     {
-        return Toolbar.cssTemplate() + ChartStatusView.cssTemplate() + `
-            .chart-pane-status {
+        return Toolbar.cssTemplate() + `
+            #chart-pane-status {
                 display: block;
                 text-align: center;
             }
 
-            .chart-pane-status .chart-status-current-value,
-            .chart-pane-status .chart-status-comparison {
+            #current-value,
+            #comparison {
                 display: block;
                 margin: 0;
                 padding: 0;
@@ -191,7 +112,15 @@
                 font-size: 1rem;
             }
 
-            .chart-pane-revisions {
+            #comparison.worse {
+                color: #c33;
+            }
+
+            #comparison.better {
+                color: #33c;
+            }
+
+            #build-revision {
                 line-height: 1rem;
                 font-size: 0.9rem;
                 font-weight: normal;
@@ -203,23 +132,23 @@
                 width: 100%;
             }
 
-            .chart-pane-revisions th,
-            .chart-pane-revisions td {
+            #build-revision th,
+            #build-revision td {
                 font-weight: inherit;
                 border-top: solid 1px #ccc;
                 padding: 0.2rem 0.2rem;
             }
             
-            .chart-pane-revisions .selected > th,
-            .chart-pane-revisions .selected > td {
+            #build-revision .selected > th,
+            #build-revision .selected > td {
                 background: rgba(204, 153, 51, 0.1);
             }
 
-            .chart-pane-revisions .commit-viewer-opener {
+            #build-revision .commit-viewer-opener {
                 width: 1rem;
             }
 
-            .chart-pane-revisions .commit-viewer-opener a {
+            #build-revision .commit-viewer-opener a {
                 text-decoration: none;
                 color: inherit;
                 font-weight: inherit;

Modified: trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -181,21 +181,12 @@
 
     router() { return this._chartsPage.router(); }
 
-    _requestOpeningCommitViewer(repository, from, to)
+    openNewRepository(repository)
     {
-        super._requestOpeningCommitViewer(repository, from, to);
+        this.content().querySelector('.chart-pane').focus();
         this._chartsPage.setOpenRepository(repository);
     }
 
-    setOpenRepository(repository)
-    {
-        if (repository != this._commitLogViewer.currentRepository()) {
-            var range = this._mainChartStatus.setCurrentRepository(repository);
-            this._commitLogViewer.view(repository, range.from, range.to).then(() => { this.enqueueToRender(); });
-            this.enqueueToRender();
-        }
-    }
-
     _indicatorDidChange(indicatorID, isLocked)
     {
         this._chartsPage.mainChartIndicatorDidChange(this, isLocked != this._mainChartIndicatorWasLocked);
@@ -203,7 +194,7 @@
         super._indicatorDidChange(indicatorID, isLocked);
     }
 
-    _analyzeRange(pointsRangeForAnalysis)
+    _analyzeRange(startPoint, endPoint)
     {
         var router = this._chartsPage.router();
         var newWindow = window.open(router.url('analysis/task/create'), '_blank');
@@ -210,10 +201,9 @@
 
         var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
         var name = analyzePopover.querySelector('input').value;
-        var self = this;
-        AnalysisTask.create(name, pointsRangeForAnalysis.startPointId, pointsRangeForAnalysis.endPointId).then(function (data) {
+        AnalysisTask.create(name, startPoint.id, endPoint.id).then((data) => {
             newWindow.location.href = "" + data['taskId']);
-            self.fetchAnalysisTasks(true);
+            this.fetchAnalysisTasks(true);
         }, function (error) {
             newWindow.location.href = "" {error: error});
         });
@@ -279,16 +269,17 @@
             platformPopover.style.display = 'none';
 
         var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
-        var pointsRangeForAnalysis = this._mainChartStatus.pointsRangeForAnalysis();
-        if (pointsRangeForAnalysis) {
+        const selectedPoints = this._mainChart.selectedPoints('current');
+        const hasSelectedPoints = selectedPoints && selectedPoints.length();
+        if (hasSelectedPoints) {
             actions.push(this._makePopoverActionItem(analyzePopover, 'Analyze', false));
-            analyzePopover._onsubmit_ = function (event) {
-                event.preventDefault();
-                self._analyzeRange(pointsRangeForAnalysis);
-            }
+            analyzePopover._onsubmit_ = this.createEventHandler(() => {
+                console.log(selectedPoints.length());
+                this._analyzeRange(selectedPoints.firstPoint(), selectedPoints.lastPoint());
+            });
         } else {
             analyzePopover.style.display = 'none';
-            analyzePopover._onsubmit_ = function (event) { event.preventDefault(); }
+            analyzePopover._onsubmit_ = this.createEventHandler(() => {});
         }
 
         var filteringOptions = this.content().querySelector('.chart-pane-filtering-options');

Modified: trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js (213299 => 213300)


--- trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js	2017-03-02 21:22:01 UTC (rev 213299)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -137,7 +137,7 @@
         chart.listenToAction('dataChange', () => this._fetchedData())
         this._charts.push(chart);
 
-        var statusView = new ChartStatusView(result.metric, chart);
+        var statusView = new DashboardChartStatusView(result.metric, chart);
         this._statusViews.push(statusView);
 
         return {

Added: trunk/Websites/perf.webkit.org/unit-tests/lazily-evaluated-function-tests.js (0 => 213300)


--- trunk/Websites/perf.webkit.org/unit-tests/lazily-evaluated-function-tests.js	                        (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/lazily-evaluated-function-tests.js	2017-03-02 21:23:07 UTC (rev 213300)
@@ -0,0 +1,187 @@
+
+const assert = require('assert');
+const LazilyEvaluatedFunction = require('../public/v3/lazily-evaluated-function.js').LazilyEvaluatedFunction;
+
+describe('LazilyEvaluatedFunction', () => {
+
+    describe('evaluate', () => {
+        it('should invoke the callback on the very first call with no arguments', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate();
+            assert.deepEqual(calls, [[]]);
+        });
+
+        it('should retrun the cached results without invoking the callback on the second call with no arguments', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate();
+            assert.deepEqual(calls, [[]]);
+            lazyFunction.evaluate();
+            assert.deepEqual(calls, [[]]);
+        });
+
+        it('should invoke the callback when calld with an argument after being called with no argument', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate();
+            assert.deepEqual(calls, [[]]);
+            lazyFunction.evaluate(1);
+            assert.deepEqual(calls, [[], [1]]);
+        });
+
+        it('should invoke the callback when calld with no arguments after being called with an argument', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate('foo');
+            assert.deepEqual(calls, [['foo']]);
+            lazyFunction.evaluate();
+            assert.deepEqual(calls, [['foo'], []]);
+        });
+
+        it('should invoke the callback when calld with null after being called with undefined', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(undefined);
+            assert.deepEqual(calls, [[undefined]]);
+            lazyFunction.evaluate(null);
+            assert.deepEqual(calls, [[undefined], [null]]);
+        });
+
+        it('should invoke the callback when calld with 0 after being called with "0"', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(0);
+            assert.deepEqual(calls, [[0]]);
+            lazyFunction.evaluate("0");
+            assert.deepEqual(calls, [[0], ["0"]]);
+        });
+
+        it('should invoke the callback when calld with an object after being called with another object with the same set of properties', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            const x = {};
+            const y = {};
+            lazyFunction.evaluate(x);
+            assert.deepEqual(calls, [[x]]);
+            lazyFunction.evaluate(y);
+            assert.deepEqual(calls, [[x], [y]]);
+        });
+
+        it('should return the cached result without invoking the callback when calld with a string after being called with the same string', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate("foo");
+            assert.deepEqual(calls, [["foo"]]);
+            lazyFunction.evaluate("foo");
+            assert.deepEqual(calls, [["foo"]]);
+        });
+
+        it('should invoke the callback when calld with a string after being called with another string', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate("foo");
+            assert.deepEqual(calls, [["foo"]]);
+            lazyFunction.evaluate("bar");
+            assert.deepEqual(calls, [["foo"], ["bar"]]);
+        });
+
+        it('should return the cached result without invoking the callback when calld with a number after being called with the same number', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(8);
+            assert.deepEqual(calls, [[8]]);
+            lazyFunction.evaluate(8);
+            assert.deepEqual(calls, [[8]]);
+        });
+
+        it('should invoke the callback when calld with a number after being called with another number', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(4);
+            assert.deepEqual(calls, [[4]]);
+            lazyFunction.evaluate(2);
+            assert.deepEqual(calls, [[4], [2]]);
+        });
+
+        it('should return the cached result without invoking the callback when calld with ["hello", 3, "world"] for the second time', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate("hello", 3, "world");
+            assert.deepEqual(calls, [["hello", 3, "world"]]);
+            lazyFunction.evaluate("hello", 3, "world");
+            assert.deepEqual(calls, [["hello", 3, "world"]]);
+        });
+
+        it('should invoke the callback when calld with ["hello", 3, "world"] after being called with ["hello", 4, "world"]', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate("hello", 3, "world");
+            assert.deepEqual(calls, [["hello", 3, "world"]]);
+            lazyFunction.evaluate("hello", 4, "world");
+            assert.deepEqual(calls, [["hello", 3, "world"], ["hello", 4, "world"]]);
+        });
+
+        it('should return the cached result without invoking the callback when called with [null, null] for the second time', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(null, null);
+            assert.deepEqual(calls, [[null, null]]);
+            lazyFunction.evaluate(null, null);
+            assert.deepEqual(calls, [[null, null]]);
+        });
+
+        it('should invoke the callback when calld with [null] after being called with [null, null]', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(null, null);
+            assert.deepEqual(calls, [[null, null]]);
+            lazyFunction.evaluate(null);
+            assert.deepEqual(calls, [[null, null], [null]]);
+        });
+
+        it('should invoke the callback when calld with [null, 4] after being called with [null]', () => {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) => calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(null, 4);
+            assert.deepEqual(calls, [[null, 4]]);
+            lazyFunction.evaluate(null);
+            assert.deepEqual(calls, [[null, 4], [null]]);
+        });
+
+    });
+
+});
+
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to