<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[213300] trunk/Websites/perf.webkit.org</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/213300">213300</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2017-03-02 13:23:07 -0800 (Thu, 02 Mar 2017)</dd>
</dl>

<h3>Log Message</h3>
<pre>Make baseline data points selectable
https://bugs.webkit.org/show_bug.cgi?id=169069
&lt;rdar://problem/29209427&gt;

Reviewed by Antti Koivisto.

Add the capability to select data points other than &quot;current&quot; configuration type.

This patch refactors the way the &quot;chart status&quot; 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 &quot;openRepository&quot; 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 &quot;5% worse than baseline&quot; 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 &quot;from&quot; 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.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgbrowsertestsindexhtml">trunk/Websites/perf.webkit.org/browser-tests/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs">trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartstylesjs">trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentsinteractivetimeserieschartjs">trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentstimeserieschartjs">trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3indexhtml">trunk/Websites/perf.webkit.org/public/v3/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelscommitlogjs">trunk/Websites/perf.webkit.org/public/v3/models/commit-log.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmetricjs">trunk/Websites/perf.webkit.org/public/v3/models/metric.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pageschartpanestatusviewjs">trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane-status-view.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pageschartpanejs">trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesdashboardpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgbrowsertestschartrevisionrangetestsjs">trunk/Websites/perf.webkit.org/browser-tests/chart-revision-range-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgbrowsertestschartstatusevaluatortestsjs">trunk/Websites/perf.webkit.org/browser-tests/chart-status-evaluator-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartrevisionrangejs">trunk/Websites/perf.webkit.org/public/v3/components/chart-revision-range.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartstatusevaluatorjs">trunk/Websites/perf.webkit.org/public/v3/components/chart-status-evaluator.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentsdashboardchartstatusviewjs">trunk/Websites/perf.webkit.org/public/v3/components/dashboard-chart-status-view.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3lazilyevaluatedfunctionjs">trunk/Websites/perf.webkit.org/public/v3/lazily-evaluated-function.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestslazilyevaluatedfunctiontestsjs">trunk/Websites/perf.webkit.org/unit-tests/lazily-evaluated-function-tests.js</a></li>
</ul>

<h3>Removed Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartstatusviewjs">trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -1,3 +1,154 @@
</span><ins>+2017-03-02  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Make baseline data points selectable
+        https://bugs.webkit.org/show_bug.cgi?id=169069
+        &lt;rdar://problem/29209427&gt;
+
+        Reviewed by Antti Koivisto.
+
+        Add the capability to select data points other than &quot;current&quot; configuration type.
+
+        This patch refactors the way the &quot;chart status&quot; 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 &quot;openRepository&quot; 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 &quot;5% worse than baseline&quot; 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 &quot;from&quot; 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.
+
</ins><span class="cx"> 2017-03-01  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Build fix after r212853. Make creating an analysis task work again.
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgbrowsertestschartrevisionrangetestsjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/browser-tests/chart-revision-range-tests.js (0 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -0,0 +1,165 @@
</span><ins>+
+describe('ChartRevisionRange', () =&gt; {
+
+    function importRevisionList(context)
+    {
+        return ChartTest.importChartScripts(context).then(() =&gt; {
+            ChartTest.makeModelObjectsForSampleCluster(context);
+            return context.importScripts(['lazily-evaluated-function.js', 'components/chart-revision-range.js'], 'ChartRevisionRange');
+        });
+    }
+
+    describe('revisionList on a non-interactive chart', () =&gt; {
+        it('should report the list of revision for the latest point', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importRevisionList(context).then((ChartRevisionRange) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+
+        it('should not report the list of revision for the latest point when there is no selection or indicator', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importRevisionList(context).then((ChartRevisionRange) =&gt; {
+                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(() =&gt; {
+                expect(evaluator.revisionList()).to.be(null);
+            })
+        });
+
+        it('should report the list of revision for the locked indicator with differences to the previous point', () =&gt; {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importRevisionList(context).then((ChartRevisionRange) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importRevisionList(context).then((ChartRevisionRange) =&gt; {
+                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(() =&gt; {
+                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');
+            });
+        });
+    });
+
+});
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgbrowsertestschartstatusevaluatortestsjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/browser-tests/chart-status-evaluator-tests.js (0 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -0,0 +1,477 @@
</span><ins>+
+describe('ChartStatusEvaluator', () =&gt; {
+
+    function importEvaluator(context)
+    {
+        const scripts = [
+            'lazily-evaluated-function.js',
+            'components/chart-status-evaluator.js'];
+
+        return ChartTest.importChartScripts(context).then(() =&gt; {
+            return context.importScripts(scripts, 'Test', 'Metric', 'ChartStatusEvaluator');
+        }).then(() =&gt; {
+            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', () =&gt; {
+
+        it('should report the current value of the latest data point', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+
+        it('should not report the current value of the latest data point', () =&gt; {
+            const context = new BrowsingContext();
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                expect(evaluator.status()).to.be(null);
+            })
+        });
+
+        it('should report the current value and the relative delta when there is a locked indicator', () =&gt; {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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', () =&gt; {
+            const context = new BrowsingContext();
+            let chart;
+            let evaluator;
+            return importEvaluator(context).then((ChartStatusEvaluator) =&gt; {
+                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(() =&gt; {
+                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);
+            });
+        });
+
+    });
+
+});
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgbrowsertestsindexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/browser-tests/index.html (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -19,6 +19,8 @@
</span><span class="cx"> &lt;script src=&quot;editable-text-tests.js&quot;&gt;&lt;/script&gt;
</span><span class="cx"> &lt;script src=&quot;time-series-chart-tests.js&quot;&gt;&lt;/script&gt;
</span><span class="cx"> &lt;script src=&quot;interactive-time-series-chart-tests.js&quot;&gt;&lt;/script&gt;
</span><ins>+&lt;script src=&quot;chart-status-evaluator-tests.js&quot;&gt;&lt;/script&gt;
+&lt;script src=&quot;chart-revision-range-tests.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx"> &lt;script&gt;
</span><span class="cx"> 
</span><span class="cx"> afterEach(() =&gt; {
</span><span class="lines">@@ -40,6 +42,7 @@
</span><span class="cx">         this.symbols = {};
</span><span class="cx">         this.global = this.iframe.contentWindow;
</span><span class="cx">         this.document = this.iframe.contentDocument;
</span><ins>+        this._didLoadMockRemote = false;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     importScripts(pathList, ...symbolList)
</span><span class="lines">@@ -48,8 +51,12 @@
</span><span class="cx">         const global = this.iframe.contentWindow;
</span><span class="cx"> 
</span><span class="cx">         pathList = pathList.map((path) =&gt; `../public/v3/${path}`);
</span><ins>+        if (!this._didLoadMockRemote) {
+            this._didLoadMockRemote = true;
+            pathList.unshift('../unit-tests/resources/mock-remote-api.js');
+        }
</ins><span class="cx"> 
</span><del>-        return Promise.all(['../unit-tests/resources/mock-remote-api.js', ...pathList].map((path) =&gt; {
</del><ins>+        return Promise.all(pathList.map((path) =&gt; {
</ins><span class="cx">             return new Promise((resolve, reject) =&gt; {
</span><span class="cx">                 let script = doc.createElement('script');
</span><span class="cx">                 script.addEventListener('load', resolve);
</span><span class="lines">@@ -196,15 +203,21 @@
</span><span class="cx">             '../shared/statistics.js',
</span><span class="cx">             'instrumentation.js',
</span><span class="cx">             'models/data-model.js',
</span><del>-            'models/metric.js',
</del><span class="cx">             'models/time-series.js',
</span><span class="cx">             'models/measurement-set.js',
</span><span class="cx">             'models/measurement-cluster.js',
</span><span class="cx">             'models/measurement-adaptor.js',
</span><ins>+            'models/repository.js',
+            'models/platform.js',
+            'models/test.js',
+            'models/metric.js',
+            'models/root-set.js',
+            'models/commit-log.js',
</ins><span class="cx">             'components/base.js',
</span><span class="cx">             'components/time-series-chart.js',
</span><span class="cx">             'components/interactive-time-series-chart.js'],
</span><del>-            'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'Metric', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
</del><ins>+            'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart',
+            'Platform', 'Metric', 'Test', 'Repository', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
</ins><span class="cx">                 return context.symbols.TimeSeriesChart;
</span><span class="cx">             })
</span><span class="cx">     },
</span><span class="lines">@@ -211,7 +224,24 @@
</span><span class="cx"> 
</span><span class="cx">     posixTime: posixTime,
</span><span class="cx"> 
</span><del>-    sampleCluster: {
</del><ins>+    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 {
</ins><span class="cx">         &quot;clusterStart&quot;: posixTime('2016-01-01T00:00:00Z'),
</span><span class="cx">         &quot;clusterSize&quot;: 7 * dayInMilliseconds,
</span><span class="cx">         &quot;startTime&quot;: posixTime('2016-01-01T00:00:00Z'),
</span><span class="lines">@@ -228,41 +258,65 @@
</span><span class="cx">             &quot;current&quot;: [
</span><span class="cx">                 [
</span><span class="cx">                     1000, 100, 1, 100, 100 * 100, false,
</span><del>-                    [ [ 2000, 1, &quot;4000&quot;, posixTime('2016-01-05T17:35:00Z')] ],
</del><ins>+                    [ [2000, 1, &quot;4000&quot;, posixTime('2016-01-05T17:35:00Z')], [3000, 2, &quot;15B42&quot;, 0] ],
</ins><span class="cx">                     posixTime('2016-01-05T17:35:00Z'), 5000, posixTime('2016-01-05T19:23:00Z'), &quot;10&quot;, 7
</span><span class="cx">                 ],
</span><span class="cx">                 [
</span><span class="cx">                     1001, 131, 1, 131, 131 * 131, true,
</span><del>-                    [ [ 2001, 1, &quot;4001&quot;, posixTime('2016-01-05T18:43:01Z')] ],
</del><ins>+                    [ [2001, 1, &quot;4001&quot;, posixTime('2016-01-05T18:43:01Z')], [3000, 2, &quot;15B42&quot;, 0] ],
</ins><span class="cx">                     posixTime('2016-01-05T18:43:01Z'), 5001, posixTime('2016-01-05T20:58:01Z'), &quot;11&quot;, 7
</span><span class="cx">                 ],
</span><span class="cx">                 [
</span><span class="cx">                     1002, 122, 1, 122, 122 * 122, false,
</span><del>-                    [ [ 2002, 1, &quot;4002&quot;, posixTime('2016-01-05T20:01:02Z') ] ],
</del><ins>+                    [ [2002, 1, &quot;4002&quot;, posixTime('2016-01-05T20:01:02Z')], [3000, 2, &quot;15B42&quot;, 0] ],
</ins><span class="cx">                     posixTime('2016-01-05T20:01:02Z'), 5002, posixTime('2016-01-05T22:37:02Z'), &quot;12&quot;, 7
</span><span class="cx">                 ],
</span><span class="cx">                 [
</span><span class="cx">                     1003, 113, 1, 113, 113 * 113, false,
</span><del>-                    [ [ 2003, 1, &quot;4003&quot;, posixTime('2016-01-05T23:19:03Z') ] ],
</del><ins>+                    [ [2003, 1, &quot;4003&quot;, posixTime('2016-01-05T23:19:03Z')], [3000, 2, &quot;15B42&quot;, 0] ],
</ins><span class="cx">                     posixTime('2016-01-05T23:19:03Z'), 5003, posixTime('2016-01-06T23:19:03Z'), &quot;13&quot;, 7
</span><span class="cx">                 ],
</span><span class="cx">                 [
</span><span class="cx">                     1004, 124, 1, 124, 124 * 124, false,
</span><del>-                    [ [ 2004, 1, &quot;4004&quot;, posixTime('2016-01-06T01:52:04Z') ] ],
</del><ins>+                    [ [2004, 1, &quot;4004&quot;, posixTime('2016-01-06T01:52:04Z')], [3001, 2, &quot;15C50&quot;, 0] ],
</ins><span class="cx">                     posixTime('2016-01-06T01:52:04Z'), 5004, posixTime('2016-01-06T02:42:04Z'), &quot;14&quot;, 7
</span><span class="cx">                 ],
</span><span class="cx">                 [
</span><span class="cx">                     1005, 115, 1, 115, 115 * 115, true,
</span><del>-                    [ [ 2005, 1, &quot;4005&quot;, posixTime('2016-01-06T03:22:05Z') ] ],
</del><ins>+                    [ [2005, 1, &quot;4005&quot;, posixTime('2016-01-06T03:22:05Z')], [3001, 2, &quot;15C50&quot;, 0] ],
</ins><span class="cx">                     posixTime('2016-01-06T03:22:05Z'), 5005, posixTime('2016-01-06T06:01:05Z'), &quot;15&quot;, 7
</span><span class="cx">                 ],
</span><span class="cx">                 [
</span><span class="cx">                     1006, 116, 1, 116, 116 * 116, false,
</span><del>-                    [ [ 2006, 1, &quot;4006&quot;, posixTime('2016-01-06T05:59:06Z') ] ],
</del><ins>+                    [ [2006, 1, &quot;4006&quot;, posixTime('2016-01-06T05:59:06Z')], [3001, 2, &quot;15C50&quot;, 0] ],
</ins><span class="cx">                     posixTime('2016-01-06T05:59:06Z'), 5006, posixTime('2016-01-06T08:34:06Z'), &quot;16&quot;, 7
</span><span class="cx">                 ]
</span><ins>+            ],
+            &quot;baseline&quot;: [
+                [
+                    7000, baselineStart, 1, baselineStart, baselineStart * baselineStart, false,
+                    [ ],
+                    posixTime('2016-01-05T12:00:30Z'), 5030, posixTime('2016-01-05T12:00:30Z'), &quot;30&quot;, 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'), &quot;31&quot;, 7
+                ],
+            ],
+            &quot;target&quot;: [
+                [
+                    8000, targetStart, 1, targetStart, targetStart * targetStart, false,
+                    [ ],
+                    posixTime('2016-01-05T12:00:30Z'), 5030, posixTime('2016-01-05T12:00:30Z'), &quot;90&quot;, 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'), &quot;91&quot;, 7
+                ],
</ins><span class="cx">             ]
</span><del>-        },
</del><ins>+        }};
</ins><span class="cx">     },
</span><span class="cx"> 
</span><span class="cx">     createChartWithSampleCluster(context, sourceList = null, chartOptions = {}, className = 'TimeSeriesChart')
</span><span class="lines">@@ -296,11 +350,11 @@
</span><span class="cx">         return this.createChartWithSampleCluster(context, sourceList, chartOptions, 'InteractiveTimeSeriesChart');
</span><span class="cx">     },
</span><span class="cx"> 
</span><del>-    respondWithSampleCluster(request)
</del><ins>+    respondWithSampleCluster(request, options)
</ins><span class="cx">     {
</span><span class="cx">         expect(request.url).to.be('../data/measurement-set-1-1.json');
</span><span class="cx">         expect(request.method).to.be('GET');
</span><del>-        request.resolve(this.sampleCluster);
</del><ins>+        request.resolve(this.makeSampleCluster(options));
</ins><span class="cx">     },
</span><span class="cx"> };
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -12,6 +12,7 @@
</span><span class="cx">         this._metric = null;
</span><span class="cx">         this._disableSampling = false;
</span><span class="cx">         this._showOutliers = false;
</span><ins>+        this._openRepository = null;
</ins><span class="cx"> 
</span><span class="cx">         this._overviewChart = null;
</span><span class="cx">         this._mainChart = null;
</span><span class="lines">@@ -53,10 +54,13 @@
</span><span class="cx">         this._mainChart.listenToAction('annotationClick', this._openAnalysisTask.bind(this));
</span><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
</span><span class="cx"> 
</span><del>-        this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart, this._requestOpeningCommitViewer.bind(this));
</del><ins>+        this._revisionRange = new ChartRevisionRange(this._mainChart);
+
+        this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart);
+        this._mainChartStatus.listenToAction('openRepository', this.openNewRepository.bind(this));
</ins><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-details'), this._mainChartStatus);
</span><span class="cx"> 
</span><del>-        this.content().querySelector('.chart-pane').addEventListener('keyup', this._keyup.bind(this));
</del><ins>+        this.content().querySelector('.chart-pane').addEventListener('keydown', this._keyup.bind(this));
</ins><span class="cx"> 
</span><span class="cx">         this.fetchAnalysisTasks(false);
</span><span class="cx">     }
</span><span class="lines">@@ -125,11 +129,18 @@
</span><span class="cx">             this._mainChart.setSelection(selection);
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    setOpenRepository(repository)
+    {
+        this._openRepository = repository;
+        this._mainChartStatus.setCurrentRepository(repository);
+        this._updateCommitLogViewer();
+    }
+
</ins><span class="cx">     _overviewSelectionDidChange(domain, didEndDrag) { }
</span><span class="cx"> 
</span><span class="cx">     _mainSelectionDidChange(selection, didEndDrag)
</span><span class="cx">     {
</span><del>-        this._updateStatus();
</del><ins>+        this._updateCommitLogViewer();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _mainSelectionDidZoom(selection)
</span><span class="lines">@@ -141,19 +152,19 @@
</span><span class="cx"> 
</span><span class="cx">     _indicatorDidChange(indicatorID, isLocked)
</span><span class="cx">     {
</span><del>-        this._updateStatus();
</del><ins>+        this._updateCommitLogViewer();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _didFetchData()
</span><span class="cx">     {
</span><del>-        this._updateStatus();
</del><ins>+        this._updateCommitLogViewer();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _updateStatus()
</del><ins>+    _updateCommitLogViewer()
</ins><span class="cx">     {
</span><del>-        var range = this._mainChartStatus.updateRevisionList();
</del><ins>+        const range = this._revisionRange.rangeForRepository(this._openRepository);
</ins><span class="cx">         const updateRendering = () =&gt; { this.enqueueToRender(); };
</span><del>-        this._commitLogViewer.view(range.repository, range.from, range.to).then(updateRendering);
</del><ins>+        this._commitLogViewer.view(this._openRepository, range.from, range.to).then(updateRendering);
</ins><span class="cx">         updateRendering();
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -166,12 +177,10 @@
</span><span class="cx"> 
</span><span class="cx">     router() { return null; }
</span><span class="cx"> 
</span><del>-    _requestOpeningCommitViewer(repository, from, to)
</del><ins>+    openNewRepository(repository)
</ins><span class="cx">     {
</span><del>-        this._mainChartStatus.setCurrentRepository(repository);
-        const updateRendering = () =&gt; { this.enqueueToRender(); };
-        this._commitLogViewer.view(repository, from, to).then(updateRendering);
-        updateRendering();
</del><ins>+        this.content().querySelector('.chart-pane').focus();
+        this.setOpenRepository(repository);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _keyup(event)
</span><span class="lines">@@ -186,11 +195,13 @@
</span><span class="cx">                 return;
</span><span class="cx">             break;
</span><span class="cx">         case 38: // Up
</span><del>-            if (!this._mainChartStatus.moveRepositoryWithNotification(false))
</del><ins>+            if (!this._moveOpenRepository(false))
</ins><span class="cx">                 return;
</span><ins>+            break;
</ins><span class="cx">         case 40: // Down
</span><del>-            if (!this._mainChartStatus.moveRepositoryWithNotification(true))
</del><ins>+            if (!this._moveOpenRepository(true))
</ins><span class="cx">                 return;
</span><ins>+            break;
</ins><span class="cx">         default:
</span><span class="cx">             return;
</span><span class="cx">         }
</span><span class="lines">@@ -201,6 +212,28 @@
</span><span class="cx">         event.stopPropagation();
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _moveOpenRepository(forward)
+    {
+        const openRepository = this._openRepository;
+        if (!openRepository)
+            return false;
+
+        const revisionList = this._revisionRange.revisionList();
+        if (!revisionList)
+            return false;
+
+        const currentIndex = revisionList.findIndex((info) =&gt; info.repository == openRepository);
+        console.assert(currentIndex &gt;= 0);
+
+        const newIndex = currentIndex + (forward ? 1 : -1);
+        if (newIndex &lt; 0 || newIndex &gt;= revisionList.length)
+            return false;
+
+        this.openNewRepository(revisionList[newIndex].repository);
+
+        return true;
+    }
+
</ins><span class="cx">     render()
</span><span class="cx">     {
</span><span class="cx">         Instrumentation.startMeasuringTime('ChartPane', 'render');
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartrevisionrangejs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/components/chart-revision-range.js (0 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -0,0 +1,68 @@
</span><ins>+
+class ChartRevisionRange {
+
+    constructor(chart, metric)
+    {
+        this._chart = chart;
+
+        const thisClass = new.target;
+        this._computeRevisionList = new LazilyEvaluatedFunction((currentPoint, prevoiusPoint) =&gt; {
+            return thisClass._computeRevisionList(currentPoint, prevoiusPoint);
+        });
+
+        this._computeRevisionRange = new LazilyEvaluatedFunction((repository, currentPoint, previousPoint) =&gt; {
+            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;
+    }
+
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartstatusevaluatorjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/components/chart-status-evaluator.js (0 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -0,0 +1,79 @@
</span><ins>+
+class ChartStatusEvaluator {
+
+    constructor(chart, metric)
+    {
+        this._chart = chart;
+        this._computeStatus = new LazilyEvaluatedFunction((currentPoint, previousPoint, view) =&gt; {
+            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) =&gt; {
+            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 &amp;&amp; targetView != currentView;
+
+        const baselinePoint = pointIsInCurrentSeries &amp;&amp; baselineView ? baselineView.lastPointInTimeRange(0, currentPoint.time) : null;
+        const targetPoint = pointIsInCurrentSeries &amp;&amp; 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 &amp;&amp; diffFromTarget !== undefined) {
+            if (diffFromBaseline &gt; 0 == smallerIsBetter) {
+                comparison = 'worse';
+                label = labelForDiff(diffFromBaseline, baselinePoint, 'baseline', comparison);
+            } else if (diffFromTarget &lt; 0 == smallerIsBetter) {
+                comparison = 'better';
+                label = labelForDiff(diffFromTarget, targetPoint, 'target', comparison);
+            } else
+                label = labelForDiff(diffFromTarget, targetPoint, 'target', 'until');
+        } else if (diffFromBaseline !== undefined) {
+            comparison = diffFromBaseline &gt; 0 == smallerIsBetter ? 'worse' : 'better';
+            label = labelForDiff(diffFromBaseline, baselinePoint, 'baseline', comparison);
+        } else if (diffFromTarget !== undefined) {
+            comparison = diffFromTarget &lt; 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};
+    }
+
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartstatusviewjs"></a>
<div class="delfile"><h4>Deleted: trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -1,181 +0,0 @@
</span><del>-
-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
-            &amp;&amp; this._renderedComparisonClass == this._comparisonClass
-            &amp;&amp; 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 &amp;&amp; this._usedSelection == selection)
-                return false;
-
-            if (selection) {
-                const view = this._chart.selectedPoints('current');
-                if (!view)
-                    return false;
-
-                if (view &amp;&amp; view.length() &gt; 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 = this._chart.sampledTimeSeriesData('current');
-            if (!data)
-                return false;
-            if (data.length())
-                currentPoint = data.lastPoint();
-        }
-
-        if (currentPoint == this._usedCurrentPoint &amp;&amp; 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) =&gt; {
-            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 &amp;&amp; diffFromTarget !== undefined) {
-            if (diffFromBaseline &gt; 0 == smallerIsBetter) {
-                comparison = 'worse';
-                label = labelForDiff(diffFromBaseline, baselinePoint, 'baseline', comparison);
-            } else if (diffFromTarget &lt; 0 == smallerIsBetter) {
-                comparison = 'better';
-                label = labelForDiff(diffFromTarget, targetPoint, 'target', comparison);
-            } else
-                label = labelForDiff(diffFromTarget, targetPoint, 'target', 'until');
-        } else if (diffFromBaseline !== undefined) {
-            comparison = diffFromBaseline &gt; 0 == smallerIsBetter ? 'worse' : 'better';
-            label = labelForDiff(diffFromBaseline, baselinePoint, 'baseline', comparison);
-        } else if (diffFromTarget !== undefined) {
-            comparison = diffFromTarget &lt; 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 `
-            &lt;div&gt;
-                &lt;span class=&quot;chart-status-current-value&quot;&gt;&lt;/span&gt;
-                &lt;span class=&quot;chart-status-comparison&quot;&gt;&lt;/span&gt;
-            &lt;/div&gt;`;
-    }
-
-    static cssTemplate()
-    {
-        return `
-            .chart-status-current-value {
-                padding-right: 0.5rem;
-            }
-
-            .chart-status-comparison.worse {
-                color: #c33;
-            }
-
-            .chart-status-comparison.better {
-                color: #33c;
-            }`;
-    }
-}
</del><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartstylesjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -52,6 +52,7 @@
</span><span class="cx">             backgroundIntervalStyle: 'rgba(255, 153, 153, 0.1)',
</span><span class="cx">             backgroundPointStyle: '#f99',
</span><span class="cx">             backgroundLineStyle: '#fcc',
</span><ins>+            interactive: true,
</ins><span class="cx">         };
</span><span class="cx">     }
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsdashboardchartstatusviewjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/components/dashboard-chart-status-view.js (0 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -0,0 +1,45 @@
</span><ins>+
+class DashboardChartStatusView extends ComponentBase {
+
+    constructor(metric, chart)
+    {
+        super('chart-status-view');
+        this._statusEvaluator = new ChartStatusEvaluator(chart, metric);
+        this._renderLazily = new LazilyEvaluatedFunction((status) =&gt; {
+            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 `&lt;span id=&quot;current-value&quot;&gt;&lt;/span&gt; &lt;span id=&quot;comparison&quot;&gt;&lt;/span&gt;`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            :host {
+                display: block;
+            }
+
+            #comparison {
+                padding-left: 0.5rem;
+            }
+
+            #comparison.worse {
+                color: #c33;
+            }
+
+            #comparison.better {
+                color: #33c;
+            }`;
+    }
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsinteractivetimeserieschartjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -52,6 +52,27 @@
</span><span class="cx">         return selection &amp;&amp; data ? data.firstPointInTimeRange(selection[0], selection[1]) : null;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    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;
+    }
+
</ins><span class="cx">     setIndicator(id, shouldLock)
</span><span class="cx">     {
</span><span class="cx">         var selectionDidChange = !!this._sampledTimeSeriesData;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentstimeserieschartjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -131,6 +131,17 @@
</span><span class="cx">         return null;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    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};
+    }
+
</ins><span class="cx">     setAnnotations(annotations)
</span><span class="cx">     {
</span><span class="cx">         this._annotations = annotations;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -38,12 +38,12 @@
</span><span class="cx"> 
</span><span class="cx">     &lt;template id=&quot;unbundled-scripts&quot;&gt;
</span><span class="cx">         &lt;script src=&quot;../shared/statistics.js&quot;&gt;&lt;/script&gt;
</span><del>-        &lt;script src=&quot;../v2/data.js&quot;&gt;&lt;/script&gt;
</del><span class="cx"> 
</span><span class="cx">         &lt;script src=&quot;instrumentation.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;remote.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;privileged-api.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;async-task.js&quot;&gt;&lt;/script&gt;
</span><ins>+        &lt;script src=&quot;lazily-evaluated-function.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx"> 
</span><span class="cx">         &lt;script src=&quot;models/time-series.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;models/measurement-adaptor.js&quot;&gt;&lt;/script&gt;
</span><span class="lines">@@ -75,7 +75,7 @@
</span><span class="cx">         &lt;script src=&quot;components/editable-text.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/time-series-chart.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/interactive-time-series-chart.js&quot;&gt;&lt;/script&gt;
</span><del>-        &lt;script src=&quot;components/chart-status-view.js&quot;&gt;&lt;/script&gt;
</del><ins>+        &lt;script src=&quot;components/dashboard-chart-status-view.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx">         &lt;script src=&quot;components/pane-selector.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/bar-graph-group.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/results-table.js&quot;&gt;&lt;/script&gt;
</span><span class="lines">@@ -84,6 +84,8 @@
</span><span class="cx">         &lt;script src=&quot;components/test-group-form.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/customizable-test-group-form.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/chart-styles.js&quot;&gt;&lt;/script&gt;
</span><ins>+        &lt;script src=&quot;components/chart-status-evaluator.js&quot;&gt;&lt;/script&gt;
+        &lt;script src=&quot;components/chart-revision-range.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx">         &lt;script src=&quot;components/chart-pane-base.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/mutable-list-view.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/ratio-bar-graph.js&quot;&gt;&lt;/script&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3lazilyevaluatedfunctionjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/lazily-evaluated-function.js (0 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -0,0 +1,27 @@
</span><ins>+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 &amp;&amp; (!length || this._cachedArguments.every((cached, i) =&gt; 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;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelscommitlogjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/commit-log.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -57,7 +57,7 @@
</span><span class="cx">         var from = previousCommit.revision();
</span><span class="cx">         var label = null;
</span><span class="cx">         if (parseInt(to) == to) { // e.g. r12345.
</span><del>-            from = parseInt(from) + 1;
</del><ins>+            from = (parseInt(from) + 1).toString();
</ins><span class="cx">             label = `r${from}-r${this.revision()}`;
</span><span class="cx">         } else if (to.length == 40) { // e.g. git hash
</span><span class="cx">             label = `${from.substring(0, 8)}..${to.substring(0, 8)}`;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmetricjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/metric.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -8,6 +8,19 @@
</span><span class="cx">         object.test.addMetric(this);
</span><span class="cx">         this._test = object.test;
</span><span class="cx">         this._platforms = [];
</span><ins>+
+        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];
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     aggregatorName() { return this._aggregatorName; }
</span><span class="lines">@@ -57,9 +70,14 @@
</span><span class="cx">         return this.name() + suffix;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    unit() { return RunsData.unitFromMetricName(this.name()); }
-    isSmallerBetter() { return RunsData.isSmallerBetter(this.unit()); }
</del><ins>+    unit() { return this._unit; }
</ins><span class="cx"> 
</span><ins>+    isSmallerBetter()
+    {
+        const unit = this._unit;
+        return unit != 'fps' &amp;&amp; unit != '/s' &amp;&amp; unit != 'pt';
+    }
+
</ins><span class="cx">     makeFormatter(sigFig, alwaysShowSign) { return Metric.makeFormatter(this.unit(), sigFig, alwaysShowSign); }
</span><span class="cx"> 
</span><span class="cx">     static makeFormatter(unit, sigFig = 2, alwaysShowSign)
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -16,12 +16,6 @@
</span><span class="cx">             this._page._chartSelectionDidChange();
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _updateStatus()
-    {
-        super._updateStatus();
-        this._page.enqueueToRender();
-    }
-
</del><span class="cx">     selectedPoints()
</span><span class="cx">     {
</span><span class="cx">         return this._mainChart ? this._mainChart.selectedPoints('current') : null;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pageschartpanestatusviewjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane-status-view.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -1,68 +1,75 @@
</span><span class="cx"> 
</span><del>-class ChartPaneStatusView extends ChartStatusView {
-    
-    constructor(metric, chart, revisionCallback)
</del><ins>+class ChartPaneStatusView extends ComponentBase {
+    constructor(metric, chart)
</ins><span class="cx">     {
</span><del>-        super(metric, chart);
</del><ins>+        super('chart-pane-status-view');
</ins><span class="cx"> 
</span><del>-        this._buildLabel = null;
-        this._buildUrl = null;
-
-        this._revisionList = [];
</del><ins>+        this._chart = chart;
+        this._status = new ChartStatusEvaluator(chart, metric);
+        this._revisionRange = new ChartRevisionRange(chart);
</ins><span class="cx">         this._currentRepository = null;
</span><del>-        this._revisionCallback = revisionCallback;
-        this._pointsRangeForAnalysis = null;
</del><span class="cx"> 
</span><del>-        this._renderedRevisionList = null;
-        this._renderedRepository = null;
-
-        this._usedRevisionRange = [null, null, null];
</del><ins>+        this._renderStatusLazily = new LazilyEvaluatedFunction(this._renderStatus.bind(this));
+        this._renderBuildRevisionTableLazily = new LazilyEvaluatedFunction(this._renderBuildRevisionTable.bind(this));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    pointsRangeForAnalysis() { return this._pointsRangeForAnalysis; }
-
</del><span class="cx">     render()
</span><span class="cx">     {
</span><span class="cx">         super.render();
</span><span class="cx"> 
</span><del>-        if (this._renderedRevisionList == this._revisionList &amp;&amp; this._renderedRepository == this._currentRepository)
-            return;
-        this._renderedRevisionList = this._revisionList;    
-        this._renderedRepository = this._currentRepository;
</del><ins>+        this._renderStatusLazily.evaluate(this._status.status());
</ins><span class="cx"> 
</span><del>-        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 = function () {
-                if (self._currentRepository == info.repository)
-                    self._setRevisionRange(true, null, null, null);
-                else
-                    self._setRevisionRange(true, info.repository, info.from, info.to);
-            };
</del><ins>+        const indicator = this._chart.currentIndicator();
+        const build = indicator ? indicator.point.build() : null;
+        this._renderBuildRevisionTableLazily.evaluate(build, this._currentRepository, this._revisionRange.revisionList());
+    }
</ins><span class="cx"> 
</span><del>-            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)),
-            ]);
-        });
</del><ins>+    _renderStatus(status)
+    {
+        status = status || {};
+        let currentValue = status.currentValue || '';
+        if (currentValue)
+            currentValue += ` (${status.valueDelta} / ${status.relativeDelta})`;
</ins><span class="cx"> 
</span><del>-        if (this._buildInfo) {
-            var build = this._buildInfo;
-            var number = build.buildNumber();
-            var buildTime = this._formatTime(build.buildTime());
-            var url = build.url();
</del><ins>+        this.content('current-value').textContent = currentValue;
+        this.content('comparison').textContent = status.label || '';
+        this.content('comparison').className = status.comparison || '';
+    }
</ins><span class="cx"> 
</span><del>-            tableContent.unshift(element('tr', [
</del><ins>+    _renderBuildRevisionTable(build, currentRepository, revisionList)
+    {
+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+        let tableContent = [];
+
+        if (build) {
+            const url = build.url();
+            const buildNumber = build.buildNumber();
+            tableContent.push(element('tr', [
</ins><span class="cx">                 element('td', 'Build'),
</span><del>-                element('td', {colspan: 2}, [url ? link(number, build.label(), url, true) : number, ` (${buildTime})`]),
</del><ins>+                element('td', {colspan: 2}, [
+                    url ? link(buildNumber, build.label(), url, true) : buildNumber,
+                    ` (${this._formatTime(build.buildTime())})`
+                ]),
</ins><span class="cx">             ]));
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        this.renderReplace(this.content().querySelector('.chart-pane-revisions'), tableContent);
</del><ins>+        if (revisionList) {
+            for (let info of revisionList) {
+                const selected = info.repository == this._currentRepository;
+                const action = () =&gt; {
+                    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);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _formatTime(date)
</span><span class="lines">@@ -74,116 +81,30 @@
</span><span class="cx">     setCurrentRepository(repository)
</span><span class="cx">     {
</span><span class="cx">         this._currentRepository = repository;
</span><del>-        return this._updateRevisionListForNewCurrentRepository();
</del><ins>+        this.enqueueToRender();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _setRevisionRange(shouldNotify, repository, from, to)
-    {
-        if (this._usedRevisionRange[0] == repository
-            &amp;&amp; this._usedRevisionRange[1] == from &amp;&amp; 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 &gt;= 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 &amp;&amp; previousPoint &amp;&amp; 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;
-    }
-
</del><span class="cx">     static htmlTemplate()
</span><span class="cx">     {
</span><span class="cx">         return `
</span><del>-            &lt;div class=&quot;chart-pane-status&quot;&gt;
-                &lt;h3 class=&quot;chart-status-current-value&quot;&gt;&lt;/h3&gt;
-                &lt;span class=&quot;chart-status-comparison&quot;&gt;&lt;/span&gt;
</del><ins>+            &lt;div id=&quot;chart-pane-status&quot;&gt;
+                &lt;h3 id=&quot;current-value&quot;&gt;&lt;/h3&gt;
+                &lt;span id=&quot;comparison&quot;&gt;&lt;/span&gt;
</ins><span class="cx">             &lt;/div&gt;
</span><del>-            &lt;table class=&quot;chart-pane-revisions&quot;&gt;&lt;/table&gt;
</del><ins>+            &lt;table id=&quot;build-revision&quot;&gt;&lt;/table&gt;
</ins><span class="cx">         `;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static cssTemplate()
</span><span class="cx">     {
</span><del>-        return Toolbar.cssTemplate() + ChartStatusView.cssTemplate() + `
-            .chart-pane-status {
</del><ins>+        return Toolbar.cssTemplate() + `
+            #chart-pane-status {
</ins><span class="cx">                 display: block;
</span><span class="cx">                 text-align: center;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-status .chart-status-current-value,
-            .chart-pane-status .chart-status-comparison {
</del><ins>+            #current-value,
+            #comparison {
</ins><span class="cx">                 display: block;
</span><span class="cx">                 margin: 0;
</span><span class="cx">                 padding: 0;
</span><span class="lines">@@ -191,7 +112,15 @@
</span><span class="cx">                 font-size: 1rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-revisions {
</del><ins>+            #comparison.worse {
+                color: #c33;
+            }
+
+            #comparison.better {
+                color: #33c;
+            }
+
+            #build-revision {
</ins><span class="cx">                 line-height: 1rem;
</span><span class="cx">                 font-size: 0.9rem;
</span><span class="cx">                 font-weight: normal;
</span><span class="lines">@@ -203,23 +132,23 @@
</span><span class="cx">                 width: 100%;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-revisions th,
-            .chart-pane-revisions td {
</del><ins>+            #build-revision th,
+            #build-revision td {
</ins><span class="cx">                 font-weight: inherit;
</span><span class="cx">                 border-top: solid 1px #ccc;
</span><span class="cx">                 padding: 0.2rem 0.2rem;
</span><span class="cx">             }
</span><span class="cx">             
</span><del>-            .chart-pane-revisions .selected &gt; th,
-            .chart-pane-revisions .selected &gt; td {
</del><ins>+            #build-revision .selected &gt; th,
+            #build-revision .selected &gt; td {
</ins><span class="cx">                 background: rgba(204, 153, 51, 0.1);
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-revisions .commit-viewer-opener {
</del><ins>+            #build-revision .commit-viewer-opener {
</ins><span class="cx">                 width: 1rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-revisions .commit-viewer-opener a {
</del><ins>+            #build-revision .commit-viewer-opener a {
</ins><span class="cx">                 text-decoration: none;
</span><span class="cx">                 color: inherit;
</span><span class="cx">                 font-weight: inherit;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pageschartpanejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -181,21 +181,12 @@
</span><span class="cx"> 
</span><span class="cx">     router() { return this._chartsPage.router(); }
</span><span class="cx"> 
</span><del>-    _requestOpeningCommitViewer(repository, from, to)
</del><ins>+    openNewRepository(repository)
</ins><span class="cx">     {
</span><del>-        super._requestOpeningCommitViewer(repository, from, to);
</del><ins>+        this.content().querySelector('.chart-pane').focus();
</ins><span class="cx">         this._chartsPage.setOpenRepository(repository);
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    setOpenRepository(repository)
-    {
-        if (repository != this._commitLogViewer.currentRepository()) {
-            var range = this._mainChartStatus.setCurrentRepository(repository);
-            this._commitLogViewer.view(repository, range.from, range.to).then(() =&gt; { this.enqueueToRender(); });
-            this.enqueueToRender();
-        }
-    }
-
</del><span class="cx">     _indicatorDidChange(indicatorID, isLocked)
</span><span class="cx">     {
</span><span class="cx">         this._chartsPage.mainChartIndicatorDidChange(this, isLocked != this._mainChartIndicatorWasLocked);
</span><span class="lines">@@ -203,7 +194,7 @@
</span><span class="cx">         super._indicatorDidChange(indicatorID, isLocked);
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _analyzeRange(pointsRangeForAnalysis)
</del><ins>+    _analyzeRange(startPoint, endPoint)
</ins><span class="cx">     {
</span><span class="cx">         var router = this._chartsPage.router();
</span><span class="cx">         var newWindow = window.open(router.url('analysis/task/create'), '_blank');
</span><span class="lines">@@ -210,10 +201,9 @@
</span><span class="cx"> 
</span><span class="cx">         var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
</span><span class="cx">         var name = analyzePopover.querySelector('input').value;
</span><del>-        var self = this;
-        AnalysisTask.create(name, pointsRangeForAnalysis.startPointId, pointsRangeForAnalysis.endPointId).then(function (data) {
</del><ins>+        AnalysisTask.create(name, startPoint.id, endPoint.id).then((data) =&gt; {
</ins><span class="cx">             newWindow.location.href = router.url('analysis/task/' + data['taskId']);
</span><del>-            self.fetchAnalysisTasks(true);
</del><ins>+            this.fetchAnalysisTasks(true);
</ins><span class="cx">         }, function (error) {
</span><span class="cx">             newWindow.location.href = router.url('analysis/task/create', {error: error});
</span><span class="cx">         });
</span><span class="lines">@@ -279,16 +269,17 @@
</span><span class="cx">             platformPopover.style.display = 'none';
</span><span class="cx"> 
</span><span class="cx">         var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
</span><del>-        var pointsRangeForAnalysis = this._mainChartStatus.pointsRangeForAnalysis();
-        if (pointsRangeForAnalysis) {
</del><ins>+        const selectedPoints = this._mainChart.selectedPoints('current');
+        const hasSelectedPoints = selectedPoints &amp;&amp; selectedPoints.length();
+        if (hasSelectedPoints) {
</ins><span class="cx">             actions.push(this._makePopoverActionItem(analyzePopover, 'Analyze', false));
</span><del>-            analyzePopover.onsubmit = function (event) {
-                event.preventDefault();
-                self._analyzeRange(pointsRangeForAnalysis);
-            }
</del><ins>+            analyzePopover.onsubmit = this.createEventHandler(() =&gt; {
+                console.log(selectedPoints.length());
+                this._analyzeRange(selectedPoints.firstPoint(), selectedPoints.lastPoint());
+            });
</ins><span class="cx">         } else {
</span><span class="cx">             analyzePopover.style.display = 'none';
</span><del>-            analyzePopover.onsubmit = function (event) { event.preventDefault(); }
</del><ins>+            analyzePopover.onsubmit = this.createEventHandler(() =&gt; {});
</ins><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         var filteringOptions = this.content().querySelector('.chart-pane-filtering-options');
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesdashboardpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js (213299 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -137,7 +137,7 @@
</span><span class="cx">         chart.listenToAction('dataChange', () =&gt; this._fetchedData())
</span><span class="cx">         this._charts.push(chart);
</span><span class="cx"> 
</span><del>-        var statusView = new ChartStatusView(result.metric, chart);
</del><ins>+        var statusView = new DashboardChartStatusView(result.metric, chart);
</ins><span class="cx">         this._statusViews.push(statusView);
</span><span class="cx"> 
</span><span class="cx">         return {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestslazilyevaluatedfunctiontestsjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/unit-tests/lazily-evaluated-function-tests.js (0 => 213300)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -0,0 +1,187 @@
</span><ins>+
+const assert = require('assert');
+const LazilyEvaluatedFunction = require('../public/v3/lazily-evaluated-function.js').LazilyEvaluatedFunction;
+
+describe('LazilyEvaluatedFunction', () =&gt; {
+
+    describe('evaluate', () =&gt; {
+        it('should invoke the callback on the very first call with no arguments', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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 &quot;0&quot;', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(0);
+            assert.deepEqual(calls, [[0]]);
+            lazyFunction.evaluate(&quot;0&quot;);
+            assert.deepEqual(calls, [[0], [&quot;0&quot;]]);
+        });
+
+        it('should invoke the callback when calld with an object after being called with another object with the same set of properties', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(&quot;foo&quot;);
+            assert.deepEqual(calls, [[&quot;foo&quot;]]);
+            lazyFunction.evaluate(&quot;foo&quot;);
+            assert.deepEqual(calls, [[&quot;foo&quot;]]);
+        });
+
+        it('should invoke the callback when calld with a string after being called with another string', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(&quot;foo&quot;);
+            assert.deepEqual(calls, [[&quot;foo&quot;]]);
+            lazyFunction.evaluate(&quot;bar&quot;);
+            assert.deepEqual(calls, [[&quot;foo&quot;], [&quot;bar&quot;]]);
+        });
+
+        it('should return the cached result without invoking the callback when calld with a number after being called with the same number', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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 [&quot;hello&quot;, 3, &quot;world&quot;] for the second time', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(&quot;hello&quot;, 3, &quot;world&quot;);
+            assert.deepEqual(calls, [[&quot;hello&quot;, 3, &quot;world&quot;]]);
+            lazyFunction.evaluate(&quot;hello&quot;, 3, &quot;world&quot;);
+            assert.deepEqual(calls, [[&quot;hello&quot;, 3, &quot;world&quot;]]);
+        });
+
+        it('should invoke the callback when calld with [&quot;hello&quot;, 3, &quot;world&quot;] after being called with [&quot;hello&quot;, 4, &quot;world&quot;]', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(&quot;hello&quot;, 3, &quot;world&quot;);
+            assert.deepEqual(calls, [[&quot;hello&quot;, 3, &quot;world&quot;]]);
+            lazyFunction.evaluate(&quot;hello&quot;, 4, &quot;world&quot;);
+            assert.deepEqual(calls, [[&quot;hello&quot;, 3, &quot;world&quot;], [&quot;hello&quot;, 4, &quot;world&quot;]]);
+        });
+
+        it('should return the cached result without invoking the callback when called with [null, null] for the second time', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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]', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; 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]', () =&gt; {
+            const calls = [];
+            const lazyFunction = new LazilyEvaluatedFunction((...args) =&gt; calls.push(args));
+
+            assert.deepEqual(calls, []);
+            lazyFunction.evaluate(null, 4);
+            assert.deepEqual(calls, [[null, 4]]);
+            lazyFunction.evaluate(null);
+            assert.deepEqual(calls, [[null, 4], [null]]);
+        });
+
+    });
+
+});
+
</ins></span></pre>
</div>
</div>

</body>
</html>