<!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>[204296] 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/204296">204296</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2016-08-09 14:22:54 -0700 (Tue, 09 Aug 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>Always show segmentation on v3 charts page
https://bugs.webkit.org/show_bug.cgi?id=160576

Rubber-stamped by Chris Dumez.

Added &quot;Trend Lines&quot; popover to select and customize a moving average or a segmentation to show on charts page
and made Schwarz criterion segmentation the default trend line for all charts.

Because computing the segmentation is expensive, we use WebWorker to parallelize the computation via AsyncTask.
We also compute and cache the segmentation for each cluster separately to avoid processing the entire measurement
set as that could take 10-20s total, which was a huge problem in v2 UI. v3 UI's approach is more incremental and
even opens up an opportunity to cache the results in the server side.

Also brought back &quot;shading&quot; for the confidence interval drawing as done in v1 and v2 UI.

* public/shared/statistics.js:
(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion): Added segmentCountWeight and gridSize as arguments
to customize the algorithm.
(Statistics.splitIntoSegmentsUntilGoodEnough): Takes segmentCountWeight as BirgeAndMassartC.

* public/v3/async-task.js: Added.
(AsyncTask): Added. This class represents a task such as computing segmentation to be executed in a worker.
(AsyncTask.prototype.execute): Added. Returns a promise that gets resolved when the specified task completes.
(AsyncTaskWorker.waitForAvailableWorker): Added. Calls the given callback with the first available worker. When
all workers are processing some tasks, it waits until one becomes available by putting the callback into a queue.
_didRecieveMessage pops an item out of this queue when a worker completes a task. We don't use a promise here
because calling this function multiple times synchronously could result in all the returned promises getting
resolved with the same worker as none of the callers get to lock away the first available worker until the end
of the current micro-task.
(AsyncTaskWorker._makeWorkerEventuallyAvailable): Added. A helper function for waitForAvailableWorker. Start
a new worker if the number of workers we've started is less than the number of extra cores (e.g. 7 if there are
8 cores on the machine). Avoid starting a new worker if we've started a new worker within the last 50 ms since
starting a new worker takes some time.
(AsyncTaskWorker._findAvailableWorker): Added. Finds a worker that's available right now if there is any.
(AsyncTaskWorker): Added. An instance of AsyncTaskWorker represents a Web worker.
(AsyncTaskWorker.prototype.id): Added.
(AsyncTaskWorker.prototype.sendTask): Added. Sends a task represented by AsyncTask to the worker.
(AsyncTaskWorker.prototype._didRecieveMessage): Added. This function gets called when the current task completes
in the worker. Pop the next callback if some caller of waitForAvailableWorker is still waiting. Otherwise stop
the worker after one second of waiting to avoid worker churning.
(AsyncTaskWorker.workerDidRecieveMessage): Added. Called by onmessage on the worker. Executes the specified task
and sends back a message upon completion with the appropriate timing data.

* public/v3/components/chart-pane-base.js:
(ChartPaneBase.prototype.configure): Uses _createSourceList.
(ChartPaneBase.prototype._createSourceList): Added. Extracted from configure to customize the source list for
the main chart and the overview chart.
(ChartPaneBase.prototype._updateSourceList): Uses _createSourceList.

* public/v3/components/chart-styles.js:
(ChartStyles.createSourceList): Added a boolean showPoint as an extra argument. This specifies whether circles
are drawn for each data point.
(ChartStyles.baselineStyle): Added styles for foreground lines and background lines. They're used for trend lines
and underlying raw data respectively when trend lines are shown.
(ChartStyles.targetStyle): Ditto.
(ChartStyles.currentStyle): Ditto.

* public/v3/components/time-series-chart.js:
(TimeSeriesChart): Added _trendLines, _renderedTrendLines, and _fetchedTimeSeries as instance variables.
(TimeSeriesChart.prototype.setSourceList): Clear _fetchedTimeSeries before calling setSourceList for consistency.
(TimeSeriesChart.prototype.sourceList): Added.
(TimeSeriesChart.prototype.clearTrendLines): Added.
(TimeSeriesChart.prototype.setTrendLine): Added. Preserves the existing trend lines for other sources. This is
necessary because segmentation for &quot;current&quot; and &quot;baseline&quot; lines may become available at different times, and we
don't want to clear one or the other when setting one.
(TimeSeriesChart.prototype._layout): Added a call to _ensureTrendLines.
(TimeSeriesChart.prototype._renderChartContent): Call _renderTimeSeries for trend lines. Trend lines are always
foreground lines and &quot;regular&quot; raw data points are drawn as background if there are trend lines.
(TimeSeriesChart.prototype._renderTimeSeries): Added layerName as an argument. It could be an empty string,
&quot;foreground&quot;, or &quot;background&quot;. Draw a &quot;shade&quot; just like v1 and v2 UI instead of vertical lines for the confidence
intervals. Pick &quot;foreground&quot;, &quot;background&quot;, or &quot;regular&quot; chart style based on layerName. Also avoid drawing data
points when *PointRadius is set to zero to reduce the runtime of this function.
(TimeSeriesChart.prototype._sourceOptionWithFallback): Added.
(TimeSeriesChart.prototype._ensureSampledTimeSeries): When *PointRadius is 0, show as many points as there are x
coordinates as a fallback instead of showing every point.
(TimeSeriesChart.prototype._ensureTrendLines): Added. Returns true if the chart contents haven't been re-rendered
since the last update to trend lines. This flag is unset by setTrendLine.

* public/v3/index.html:

* public/v3/models/measurement-cluster.js:
(MeasurementCluster.prototype.addToSeries): Store the data points' index to idMap to help aid MeasurementSet's
_cachedClusterSegmentation efficiently re-create the segmentation from the cache.

* public/v3/models/measurement-set.js:
(MeasurementSet): Added _segmentationCache as an instance variable.
(MeasurementSet.prototype.fetchSegmentation): Added. Calls _cachedClusterSegmentation on each cluster, and
constructs the time series representation of the segmentation from the results.
(MeasurementSet.prototype._cachedClusterSegmentation): Computes and caches the segmentation for each cluster.
The cache of segmentation stores ID of each measurement set at which segment changes instead of its index since
the latter could change in any moment when a new test result is reported, or an existing test result is removed
from the time series; e.g. when it's marked as an outlier.
(MeasurementSet.prototype._validateSegmentationCache): Added. Checks whether the cached segmentation's name and
its parameters match that of the requested one.
(MeasurementSet.prototype._invokeSegmentationAlgorithm): Added. Invokes the segmentation algorithm either in the
main thread or in a Web worker via AsyncTask API based on the size of the time series. While parallelizing the
work is beneficial when the data set is large, the overhead can add up if we keep processing a very small data
set in a worker.

* public/v3/models/time-series.js: Made the file compatible with Node.
(TimeSeries.prototype.length): Added.
(TimeSeries.prototype.valuesBetweenRange): Added.

* public/v3/pages/chart-pane.js:
(createTrendLineExecutableFromAveragingFunction): Added.
(ChartTrendLineTypes): Added. Similar to StatisticsStrategies (statistics-strategies.js) in v2 UI.
(ChartPane): Added _trendLineType, _trendLineParameters, _trendLineVersion, and _renderedTrendLineOptions as
instance variables.
(ChartPane.prototype.serializeState): Serialize the trend line option. This format is compatible with v2 UI.
(ChartPane.prototype.updateFromSerializedState): Ditto. Parsing is compatible with v2 UI except that we now have
the default trend line set when the specified ID doesn't match an existing type ID.
(ChartPane.prototype._renderActionToolbar): Added a call to _renderTrendLinePopover. This is the popover that
specifies the type of a trend line to show as well as its parameters.
(ChartPane.prototype._renderTrendLinePopover): Added. A popover for specifying and customizing a trend line.
(ChartPane.prototype._trendLineTypeDidChange): Added. Called when a new trend line is selected.
(ChartPane.prototype._defaultParametersForTrendLine): Added.
(ChartPane.prototype._trendLineParameterDidChange): Added. Called when the trend lines' parameters are changed.
(ChartPane.prototype._didFetchData): Added. Overrides the one in ChartPaneBase to trigger a trend line update.
(ChartPane.prototype._updateTrendLine): Added. Update the trend line. Since segmentation can take an arbitrary
long time, avoid updating trend lines if this function had been called again (possibly for a different trend line
type or with different parameters) before the results become available; hence the versioning.
(ChartPane.paneHeaderTemplate): Added the trend line popover.
(ChartPane.cssTemplate): Added styles for the trend line popover. Also use a more opaque background color behind
popovers when the -webkit-backdrop-filter property is not supported.

* public/v3/pages/dashboard-page.js:
(DashboardPage.prototype._createChartForCell): Call createSourceList with showPoints set to true to preserve the
existing behavior.

* tools/js/v3-models.js: Include TimeSeries object.

* unit-tests/measurement-set-tests.js: Added two test cases for MeasurementSet's fetchSegmentation.

* unit-tests/resources/almost-equal.js: Added.
(almostEqual): Extracted out of statistics-tests.js.

* unit-tests/statistics-tests.js:</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicsharedstatisticsjs">trunk/Websites/perf.webkit.org/public/shared/statistics.js</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="#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="#trunkWebsitesperfwebkitorgpublicv3modelsmeasurementclusterjs">trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmeasurementsetjs">trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelstimeseriesjs">trunk/Websites/perf.webkit.org/public/v3/models/time-series.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>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsv3modelsjs">trunk/Websites/perf.webkit.org/tools/js/v3-models.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestsmeasurementsettestsjs">trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestsstatisticstestsjs">trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3asynctaskjs">trunk/Websites/perf.webkit.org/public/v3/async-task.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestsresourcesalmostequaljs">trunk/Websites/perf.webkit.org/unit-tests/resources/almost-equal.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 (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -1,3 +1,143 @@
</span><ins>+2016-08-08  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Always show segmentation on v3 charts page
+        https://bugs.webkit.org/show_bug.cgi?id=160576
+
+        Rubber-stamped by Chris Dumez.
+
+        Added &quot;Trend Lines&quot; popover to select and customize a moving average or a segmentation to show on charts page
+        and made Schwarz criterion segmentation the default trend line for all charts.
+
+        Because computing the segmentation is expensive, we use WebWorker to parallelize the computation via AsyncTask.
+        We also compute and cache the segmentation for each cluster separately to avoid processing the entire measurement
+        set as that could take 10-20s total, which was a huge problem in v2 UI. v3 UI's approach is more incremental and
+        even opens up an opportunity to cache the results in the server side.
+
+        Also brought back &quot;shading&quot; for the confidence interval drawing as done in v1 and v2 UI.
+
+        * public/shared/statistics.js:
+        (Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion): Added segmentCountWeight and gridSize as arguments
+        to customize the algorithm.
+        (Statistics.splitIntoSegmentsUntilGoodEnough): Takes segmentCountWeight as BirgeAndMassartC.
+
+        * public/v3/async-task.js: Added.
+        (AsyncTask): Added. This class represents a task such as computing segmentation to be executed in a worker.
+        (AsyncTask.prototype.execute): Added. Returns a promise that gets resolved when the specified task completes.
+        (AsyncTaskWorker.waitForAvailableWorker): Added. Calls the given callback with the first available worker. When
+        all workers are processing some tasks, it waits until one becomes available by putting the callback into a queue.
+        _didRecieveMessage pops an item out of this queue when a worker completes a task. We don't use a promise here
+        because calling this function multiple times synchronously could result in all the returned promises getting
+        resolved with the same worker as none of the callers get to lock away the first available worker until the end
+        of the current micro-task.
+        (AsyncTaskWorker._makeWorkerEventuallyAvailable): Added. A helper function for waitForAvailableWorker. Start
+        a new worker if the number of workers we've started is less than the number of extra cores (e.g. 7 if there are
+        8 cores on the machine). Avoid starting a new worker if we've started a new worker within the last 50 ms since
+        starting a new worker takes some time.
+        (AsyncTaskWorker._findAvailableWorker): Added. Finds a worker that's available right now if there is any.
+        (AsyncTaskWorker): Added. An instance of AsyncTaskWorker represents a Web worker.
+        (AsyncTaskWorker.prototype.id): Added.
+        (AsyncTaskWorker.prototype.sendTask): Added. Sends a task represented by AsyncTask to the worker.
+        (AsyncTaskWorker.prototype._didRecieveMessage): Added. This function gets called when the current task completes
+        in the worker. Pop the next callback if some caller of waitForAvailableWorker is still waiting. Otherwise stop
+        the worker after one second of waiting to avoid worker churning.
+        (AsyncTaskWorker.workerDidRecieveMessage): Added. Called by onmessage on the worker. Executes the specified task
+        and sends back a message upon completion with the appropriate timing data.
+
+        * public/v3/components/chart-pane-base.js:
+        (ChartPaneBase.prototype.configure): Uses _createSourceList.
+        (ChartPaneBase.prototype._createSourceList): Added. Extracted from configure to customize the source list for
+        the main chart and the overview chart.
+        (ChartPaneBase.prototype._updateSourceList): Uses _createSourceList.
+
+        * public/v3/components/chart-styles.js:
+        (ChartStyles.createSourceList): Added a boolean showPoint as an extra argument. This specifies whether circles
+        are drawn for each data point.
+        (ChartStyles.baselineStyle): Added styles for foreground lines and background lines. They're used for trend lines
+        and underlying raw data respectively when trend lines are shown.
+        (ChartStyles.targetStyle): Ditto.
+        (ChartStyles.currentStyle): Ditto.
+
+        * public/v3/components/time-series-chart.js:
+        (TimeSeriesChart): Added _trendLines, _renderedTrendLines, and _fetchedTimeSeries as instance variables.
+        (TimeSeriesChart.prototype.setSourceList): Clear _fetchedTimeSeries before calling setSourceList for consistency.
+        (TimeSeriesChart.prototype.sourceList): Added.
+        (TimeSeriesChart.prototype.clearTrendLines): Added.
+        (TimeSeriesChart.prototype.setTrendLine): Added. Preserves the existing trend lines for other sources. This is
+        necessary because segmentation for &quot;current&quot; and &quot;baseline&quot; lines may become available at different times, and we
+        don't want to clear one or the other when setting one.
+        (TimeSeriesChart.prototype._layout): Added a call to _ensureTrendLines.
+        (TimeSeriesChart.prototype._renderChartContent): Call _renderTimeSeries for trend lines. Trend lines are always
+        foreground lines and &quot;regular&quot; raw data points are drawn as background if there are trend lines.
+        (TimeSeriesChart.prototype._renderTimeSeries): Added layerName as an argument. It could be an empty string,
+        &quot;foreground&quot;, or &quot;background&quot;. Draw a &quot;shade&quot; just like v1 and v2 UI instead of vertical lines for the confidence
+        intervals. Pick &quot;foreground&quot;, &quot;background&quot;, or &quot;regular&quot; chart style based on layerName. Also avoid drawing data
+        points when *PointRadius is set to zero to reduce the runtime of this function.
+        (TimeSeriesChart.prototype._sourceOptionWithFallback): Added.
+        (TimeSeriesChart.prototype._ensureSampledTimeSeries): When *PointRadius is 0, show as many points as there are x
+        coordinates as a fallback instead of showing every point.
+        (TimeSeriesChart.prototype._ensureTrendLines): Added. Returns true if the chart contents haven't been re-rendered
+        since the last update to trend lines. This flag is unset by setTrendLine.
+
+        * public/v3/index.html:
+
+        * public/v3/models/measurement-cluster.js:
+        (MeasurementCluster.prototype.addToSeries): Store the data points' index to idMap to help aid MeasurementSet's
+        _cachedClusterSegmentation efficiently re-create the segmentation from the cache.
+
+        * public/v3/models/measurement-set.js:
+        (MeasurementSet): Added _segmentationCache as an instance variable.
+        (MeasurementSet.prototype.fetchSegmentation): Added. Calls _cachedClusterSegmentation on each cluster, and
+        constructs the time series representation of the segmentation from the results.
+        (MeasurementSet.prototype._cachedClusterSegmentation): Computes and caches the segmentation for each cluster.
+        The cache of segmentation stores ID of each measurement set at which segment changes instead of its index since
+        the latter could change in any moment when a new test result is reported, or an existing test result is removed
+        from the time series; e.g. when it's marked as an outlier.
+        (MeasurementSet.prototype._validateSegmentationCache): Added. Checks whether the cached segmentation's name and
+        its parameters match that of the requested one.
+        (MeasurementSet.prototype._invokeSegmentationAlgorithm): Added. Invokes the segmentation algorithm either in the
+        main thread or in a Web worker via AsyncTask API based on the size of the time series. While parallelizing the
+        work is beneficial when the data set is large, the overhead can add up if we keep processing a very small data
+        set in a worker.
+
+        * public/v3/models/time-series.js: Made the file compatible with Node.
+        (TimeSeries.prototype.length): Added.
+        (TimeSeries.prototype.valuesBetweenRange): Added.
+
+        * public/v3/pages/chart-pane.js:
+        (createTrendLineExecutableFromAveragingFunction): Added.
+        (ChartTrendLineTypes): Added. Similar to StatisticsStrategies (statistics-strategies.js) in v2 UI.
+        (ChartPane): Added _trendLineType, _trendLineParameters, _trendLineVersion, and _renderedTrendLineOptions as
+        instance variables.
+        (ChartPane.prototype.serializeState): Serialize the trend line option. This format is compatible with v2 UI.
+        (ChartPane.prototype.updateFromSerializedState): Ditto. Parsing is compatible with v2 UI except that we now have
+        the default trend line set when the specified ID doesn't match an existing type ID.
+        (ChartPane.prototype._renderActionToolbar): Added a call to _renderTrendLinePopover. This is the popover that
+        specifies the type of a trend line to show as well as its parameters.
+        (ChartPane.prototype._renderTrendLinePopover): Added. A popover for specifying and customizing a trend line.
+        (ChartPane.prototype._trendLineTypeDidChange): Added. Called when a new trend line is selected.
+        (ChartPane.prototype._defaultParametersForTrendLine): Added.
+        (ChartPane.prototype._trendLineParameterDidChange): Added. Called when the trend lines' parameters are changed.
+        (ChartPane.prototype._didFetchData): Added. Overrides the one in ChartPaneBase to trigger a trend line update.
+        (ChartPane.prototype._updateTrendLine): Added. Update the trend line. Since segmentation can take an arbitrary
+        long time, avoid updating trend lines if this function had been called again (possibly for a different trend line
+        type or with different parameters) before the results become available; hence the versioning.
+        (ChartPane.paneHeaderTemplate): Added the trend line popover.
+        (ChartPane.cssTemplate): Added styles for the trend line popover. Also use a more opaque background color behind
+        popovers when the -webkit-backdrop-filter property is not supported.
+
+        * public/v3/pages/dashboard-page.js:
+        (DashboardPage.prototype._createChartForCell): Call createSourceList with showPoints set to true to preserve the
+        existing behavior.
+
+        * tools/js/v3-models.js: Include TimeSeries object.
+
+        * unit-tests/measurement-set-tests.js: Added two test cases for MeasurementSet's fetchSegmentation.
+
+        * unit-tests/resources/almost-equal.js: Added.
+        (almostEqual): Extracted out of statistics-tests.js.
+
+        * unit-tests/statistics-tests.js:
+
</ins><span class="cx"> 2016-08-05  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         segmentTimeSeriesByMaximizingSchwarzCriterion returns a bogus result on empty charts
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicsharedstatisticsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/shared/statistics.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/shared/statistics.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/shared/statistics.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -173,13 +173,13 @@
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     this.debuggingSegmentation = false;
</span><del>-    this.segmentTimeSeriesByMaximizingSchwarzCriterion = function (values) {
</del><ins>+    this.segmentTimeSeriesByMaximizingSchwarzCriterion = function (values, segmentCountWeight, gridSize) {
</ins><span class="cx">         // Split the time series into grids since splitIntoSegmentsUntilGoodEnough is O(n^2).
</span><del>-        var gridLength = 500;
</del><ins>+        var gridLength = gridSize || 500;
</ins><span class="cx">         var totalSegmentation = [0];
</span><span class="cx">         for (var gridCount = 0; gridCount &lt; Math.ceil(values.length / gridLength); gridCount++) {
</span><span class="cx">             var gridValues = values.slice(gridCount * gridLength, (gridCount + 1) * gridLength);
</span><del>-            var segmentation = splitIntoSegmentsUntilGoodEnough(gridValues);
</del><ins>+            var segmentation = splitIntoSegmentsUntilGoodEnough(gridValues, segmentCountWeight);
</ins><span class="cx"> 
</span><span class="cx">             if (Statistics.debuggingSegmentation)
</span><span class="cx">                 console.log('grid=' + gridCount, segmentation);
</span><span class="lines">@@ -271,7 +271,7 @@
</span><span class="cx">     function oneSidedToTwoSidedProbability(probability) { return 2 * probability - 1; }
</span><span class="cx">     function twoSidedToOneSidedProbability(probability) { return (1 - (1 - probability) / 2); }
</span><span class="cx"> 
</span><del>-    function splitIntoSegmentsUntilGoodEnough(values) {
</del><ins>+    function splitIntoSegmentsUntilGoodEnough(values, BirgeAndMassartC) {
</ins><span class="cx">         if (values.length &lt; 2)
</span><span class="cx">             return [0, values.length];
</span><span class="cx"> 
</span><span class="lines">@@ -279,7 +279,7 @@
</span><span class="cx"> 
</span><span class="cx">         var SchwarzCriterionBeta = Math.log1p(values.length - 1) / values.length;
</span><span class="cx"> 
</span><del>-        var BirgeAndMassartC = 2.5; // Suggested by the authors.
</del><ins>+        BirgeAndMassartC = BirgeAndMassartC || 2.5; // Suggested by the authors.
</ins><span class="cx">         var BirgeAndMassartPenalization = function (segmentCount) {
</span><span class="cx">             return segmentCount * (1 + BirgeAndMassartC * Math.log1p(values.length / segmentCount - 1));
</span><span class="cx">         }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3asynctaskjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/async-task.js (0 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/async-task.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/async-task.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -0,0 +1,151 @@
</span><ins>+
+class AsyncTask {
+
+    constructor(method, args)
+    {
+        this._method = method;
+        this._args = args;
+    }
+
+    execute()
+    {
+        if (!(this._method in Statistics))
+            throw `${this._method} is not a valid method of Statistics`;
+
+        AsyncTask._asyncMessageId++;
+
+        var startTime = Date.now();
+        var method = this._method;
+        var args = this._args;
+        return new Promise(function (resolve, reject) {
+            AsyncTaskWorker.waitForAvailableWorker(function (worker) {
+                worker.sendTask({id: AsyncTask._asyncMessageId, method: method, args: args}).then(function (data) {
+                    var startLatency = data.workerStartTime - startTime;
+                    var totalTime = Date.now() - startTime;
+                    var callback = data.status == 'resolve' ? resolve : reject;
+                    callback({result: data.result, workerId: worker.id(), startLatency: startLatency, totalTime: totalTime, workerTime: data.workerTime});
+                });
+            });
+        });
+    }
+
+}
+
+AsyncTask._asyncMessageId = 0;
+
+class AsyncTaskWorker {
+
+    // Takes a callback instead of returning a promise because a worker can become unavailable before the end of the current microtask.
+    static waitForAvailableWorker(callback)
+    {
+        var worker = this._makeWorkerEventuallyAvailable();
+        if (worker)
+            callback(worker);
+        this._queue.push(callback);
+    }
+
+    static _makeWorkerEventuallyAvailable()
+    {
+        var worker = this._findAvailableWorker();
+        if (worker)
+            return worker;
+
+        var canStartMoreWorker = this._workerSet.size &lt; this._maxWorkerCount;
+        if (!canStartMoreWorker)
+            return null;
+
+        if (this._latestStartTime &gt; Date.now() - 50) {
+            setTimeout(function () {
+                var worker = AsyncTaskWorker._findAvailableWorker();
+                if (worker)
+                    AsyncTaskWorker._queue.pop()(worker);
+            }, 50);
+            return null;
+        }
+        return new AsyncTaskWorker;
+    }
+
+    static _findAvailableWorker()
+    {
+        for (var worker of this._workerSet) {
+            if (!worker._currentTaskId)
+                return worker;
+        }
+        return null;
+    }
+
+    constructor()
+    {
+        this._webWorker = new Worker('async-task.js');
+        this._webWorker.onmessage = this._didRecieveMessage.bind(this);
+        this._id = AsyncTaskWorker._workerId;
+        this._startTime = Date.now();
+        this._currentTaskId = null;
+        this._callback = null;
+
+        AsyncTaskWorker._latestStartTime = this._startTime;
+        AsyncTaskWorker._workerId++;
+        AsyncTaskWorker._workerSet.add(this);
+    }
+
+    id() { return this._id; }
+
+    sendTask(task)
+    {
+        console.assert(!this._currentTaskId);
+        console.assert(task.id);
+        var self = this;
+        this._currentTaskId = task.id;
+        return new Promise(function (resolve) {
+            self._webWorker.postMessage(task);
+            self._callback = resolve;
+        });
+    }
+
+    _didRecieveMessage(event)
+    {
+        var callback = this._callback;
+
+        console.assert(this._currentTaskId);
+        this._currentTaskId = null;
+        this._callback = null;
+
+        if (AsyncTaskWorker._queue.length)
+            AsyncTaskWorker._queue.pop()(this);
+        else {
+            var self = this;
+            setTimeout(function () {
+                if (self._currentTaskId == null)
+                    AsyncTaskWorker._workerSet.delete(self);
+            }, 1000);
+        }
+
+        callback(event.data);
+    }
+
+    static workerDidRecieveMessage(event)
+    {
+        var data = event.data;
+        var id = data.id;
+        var method = Statistics[data.method];
+        var startTime = Date.now();
+        try {
+            var returnValue = method.apply(Statistics, data.args);
+            postMessage({'id': id, 'status': 'resolve', 'result': returnValue, 'workerStartTime': startTime, 'workerTime': Date.now() - startTime});
+        } catch (error) {
+            postMessage({'id': id, 'status': 'reject', 'result': error.toString(), 'workerStartTime': startTime, 'workerTime': Date.now() - startTime});
+            throw error;
+        }
+    }
+}
+
+AsyncTaskWorker._maxWorkerCount = typeof navigator != 'undefined' &amp;&amp; 'hardwareConcurrency' in navigator ? Math.max(1, navigator.hardwareConcurrency - 1) : 1;
+AsyncTaskWorker._workerSet = new Set;
+AsyncTaskWorker._queue = [];
+AsyncTaskWorker._workerId = 1;
+AsyncTaskWorker._latestStartTime = 0;
+
+if (typeof module == 'undefined' &amp;&amp; typeof window == 'undefined' &amp;&amp; typeof importScripts != 'undefined') { // Inside a worker
+    importScripts('/shared/statistics.js');
+    onmessage = AsyncTaskWorker.workerDidRecieveMessage.bind(AsyncTaskWorker);
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -42,11 +42,9 @@
</span><span class="cx">         var formatter = result.metric.makeFormatter(4);
</span><span class="cx">         var self = this;
</span><span class="cx"> 
</span><del>-        var sourceList = ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers);
-
</del><span class="cx">         var overviewOptions = ChartStyles.overviewChartOptions(formatter);
</span><span class="cx">         overviewOptions.selection.onchange = this._overviewSelectionDidChange.bind(this);
</span><del>-        this._overviewChart = new InteractiveTimeSeriesChart(sourceList, overviewOptions);
</del><ins>+        this._overviewChart = new InteractiveTimeSeriesChart(this._createSourceList(false), overviewOptions);
</ins><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-overview'), this._overviewChart);
</span><span class="cx"> 
</span><span class="cx">         var mainOptions = ChartStyles.mainChartOptions(formatter);
</span><span class="lines">@@ -55,7 +53,7 @@
</span><span class="cx">         mainOptions.selection.onzoom = this._mainSelectionDidZoom.bind(this);
</span><span class="cx">         mainOptions.annotations.onclick = this._openAnalysisTask.bind(this);
</span><span class="cx">         mainOptions.ondata = this._didFetchData.bind(this);
</span><del>-        this._mainChart = new InteractiveTimeSeriesChart(sourceList, mainOptions);
</del><ins>+        this._mainChart = new InteractiveTimeSeriesChart(this._createSourceList(true), mainOptions);
</ins><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
</span><span class="cx"> 
</span><span class="cx">         this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart, this._requestOpeningCommitViewer.bind(this));
</span><span class="lines">@@ -80,11 +78,15 @@
</span><span class="cx">         this._updateSourceList();
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _createSourceList(isMainChart)
+    {
+        return ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers, isMainChart);
+    }
+
</ins><span class="cx">     _updateSourceList()
</span><span class="cx">     {
</span><del>-        var sourceList = ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers);
-        this._mainChart.setSourceList(sourceList);
-        this._overviewChart.setSourceList(sourceList);
</del><ins>+        this._mainChart.setSourceList(this._createSourceList(true));
+        this._overviewChart.setSourceList(this._createSourceList(false));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     fetchAnalysisTasks(noCache)
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartstylesjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -17,7 +17,7 @@
</span><span class="cx">         };
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static createSourceList(platform, metric, disableSampling, includeOutlier)
</del><ins>+    static createSourceList(platform, metric, disableSampling, includeOutlier, showPoint)
</ins><span class="cx">     {
</span><span class="cx">         console.assert(platform instanceof Platform);
</span><span class="cx">         console.assert(metric instanceof Metric);
</span><span class="lines">@@ -27,13 +27,13 @@
</span><span class="cx"> 
</span><span class="cx">         var measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
</span><span class="cx">         return [
</span><del>-            this.baselineStyle(measurementSet, disableSampling, includeOutlier),
-            this.targetStyle(measurementSet, disableSampling, includeOutlier),
-            this.currentStyle(measurementSet, disableSampling, includeOutlier),
</del><ins>+            this.baselineStyle(measurementSet, disableSampling, includeOutlier, showPoint),
+            this.targetStyle(measurementSet, disableSampling, includeOutlier, showPoint),
+            this.currentStyle(measurementSet, disableSampling, includeOutlier, showPoint),
</ins><span class="cx">         ];
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static baselineStyle(measurementSet, disableSampling, includeOutlier)
</del><ins>+    static baselineStyle(measurementSet, disableSampling, includeOutlier, showPoint)
</ins><span class="cx">     {
</span><span class="cx">         return {
</span><span class="cx">             measurementSet: measurementSet,
</span><span class="lines">@@ -42,15 +42,20 @@
</span><span class="cx">             includeOutliers: includeOutlier,
</span><span class="cx">             type: 'baseline',
</span><span class="cx">             pointStyle: '#f33',
</span><del>-            pointRadius: 2,
-            lineStyle: '#f99',
</del><ins>+            pointRadius: showPoint ? 2 : 0,
+            lineStyle: showPoint ? '#f99' : '#f66',
</ins><span class="cx">             lineWidth: 1.5,
</span><del>-            intervalStyle: '#fdd',
-            intervalWidth: 2,
</del><ins>+            intervalStyle: 'rgba(255, 153, 153, 0.25)',
+            intervalWidth: 3,
+            foregroundLineStyle: '#f33',
+            foregroundPointRadius: 0,
+            backgroundIntervalStyle: 'rgba(255, 153, 153, 0.1)',
+            backgroundPointStyle: '#f99',
+            backgroundLineStyle: '#fcc',
</ins><span class="cx">         };
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static targetStyle(measurementSet, disableSampling, includeOutlier)
</del><ins>+    static targetStyle(measurementSet, disableSampling, includeOutlier, showPoint)
</ins><span class="cx">     {
</span><span class="cx">         return {
</span><span class="cx">             measurementSet: measurementSet,
</span><span class="lines">@@ -59,15 +64,20 @@
</span><span class="cx">             includeOutliers: includeOutlier,
</span><span class="cx">             type: 'target',
</span><span class="cx">             pointStyle: '#33f',
</span><del>-            pointRadius: 2,
-            lineStyle: '#99f',
</del><ins>+            pointRadius: showPoint ? 2 : 0,
+            lineStyle: showPoint ? '#99f' : '#66f',
</ins><span class="cx">             lineWidth: 1.5,
</span><del>-            intervalStyle: '#ddf',
-            intervalWidth: 2,
</del><ins>+            intervalStyle: 'rgba(153, 153, 255, 0.25)',
+            intervalWidth: 3,
+            foregroundLineStyle: '#33f',
+            foregroundPointRadius: 0,
+            backgroundIntervalStyle: 'rgba(153, 153, 255, 0.1)',
+            backgroundPointStyle: '#99f',
+            backgroundLineStyle: '#ccf',
</ins><span class="cx">         };
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static currentStyle(measurementSet, disableSampling, includeOutlier)
</del><ins>+    static currentStyle(measurementSet, disableSampling, includeOutlier, showPoint)
</ins><span class="cx">     {
</span><span class="cx">         return {
</span><span class="cx">             measurementSet: measurementSet,
</span><span class="lines">@@ -75,11 +85,16 @@
</span><span class="cx">             includeOutliers: includeOutlier,
</span><span class="cx">             type: 'current',
</span><span class="cx">             pointStyle: '#333',
</span><del>-            pointRadius: 2,
-            lineStyle: '#999',
</del><ins>+            pointRadius: showPoint ? 2 : 0,
+            lineStyle: showPoint ? '#999' : '#666',
</ins><span class="cx">             lineWidth: 1.5,
</span><del>-            intervalStyle: '#ddd',
-            intervalWidth: 2,
</del><ins>+            intervalStyle: 'rgba(153, 153, 153, 0.25)',
+            intervalWidth: 3,
+            foregroundLineStyle: '#333',
+            foregroundPointRadius: 0,
+            backgroundIntervalStyle: 'rgba(153, 153, 153, 0.1)',
+            backgroundPointStyle: '#999',
+            backgroundLineStyle: '#ccc',
</ins><span class="cx">             interactive: true,
</span><span class="cx">         };
</span><span class="cx">     }
</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 (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -7,9 +7,11 @@
</span><span class="cx">         this.element().style.position = 'relative';
</span><span class="cx">         this._canvas = null;
</span><span class="cx">         this._sourceList = sourceList;
</span><ins>+        this._trendLines = null;
</ins><span class="cx">         this._options = options;
</span><span class="cx">         this._fetchedTimeSeries = null;
</span><span class="cx">         this._sampledTimeSeriesData = null;
</span><ins>+        this._renderedTrendLines = false;
</ins><span class="cx">         this._valueRangeCache = null;
</span><span class="cx">         this._annotations = null;
</span><span class="cx">         this._annotationRows = null;
</span><span class="lines">@@ -76,6 +78,7 @@
</span><span class="cx">         console.assert(startTime &lt; endTime, 'startTime must be before endTime');
</span><span class="cx">         this._startTime = startTime;
</span><span class="cx">         this._endTime = endTime;
</span><ins>+        this._fetchedTimeSeries = null;
</ins><span class="cx">         this.fetchMeasurementSets(false);
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -82,10 +85,30 @@
</span><span class="cx">     setSourceList(sourceList)
</span><span class="cx">     {
</span><span class="cx">         this._sourceList = sourceList;
</span><ins>+        this._fetchedTimeSeries = null;
</ins><span class="cx">         this.fetchMeasurementSets(false);
</span><del>-        this._fetchedTimeSeries = null;
</del><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    sourceList() { return this._sourceList; }
+
+    clearTrendLines()
+    {
+        this._trendLines = null;
+        this._renderedTrendLines = false;
+        this.enqueueToRender();
+    }
+
+    setTrendLine(sourceIndex, trendLine)
+    {
+        if (this._trendLines)
+            this._trendLines = this._trendLines.slice(0);
+        else
+            this._trendLines = [];
+        this._trendLines[sourceIndex] = trendLine;
+        this._renderedTrendLines = false;
+        this.enqueueToRender();
+    }
+
</ins><span class="cx">     fetchMeasurementSets(noCache)
</span><span class="cx">     {
</span><span class="cx">         var fetching = false;
</span><span class="lines">@@ -198,6 +221,7 @@
</span><span class="cx">         var doneWork = this._updateCanvasSizeIfClientSizeChanged();
</span><span class="cx">         var metrics = this._computeHorizontalRenderingMetrics();
</span><span class="cx">         doneWork |= this._ensureSampledTimeSeries(metrics);
</span><ins>+        doneWork |= this._ensureTrendLines();
</ins><span class="cx">         doneWork |= this._ensureValueRangeCache();
</span><span class="cx">         this._computeVerticalRenderingMetrics(metrics);
</span><span class="cx">         doneWork |= this._layoutAnnotationBars(metrics);
</span><span class="lines">@@ -386,9 +410,16 @@
</span><span class="cx">             var source = this._sourceList[i];
</span><span class="cx">             var series = this._sampledTimeSeriesData[i];
</span><span class="cx">             if (series)
</span><del>-                this._renderTimeSeries(context, metrics, source, series);
</del><ins>+                this._renderTimeSeries(context, metrics, source, series, this._trendLines &amp;&amp; this._trendLines[i] ? 'background' : '');
</ins><span class="cx">         }
</span><span class="cx"> 
</span><ins>+        for (var i = 0; i &lt; this._sourceList.length; i++) {
+            var source = this._sourceList[i];
+            var trendLine = this._trendLines ? this._trendLines[i] : null;
+            if (series &amp;&amp; trendLine)
+                this._renderTimeSeries(context, metrics, source, trendLine, 'foreground');
+        }
+
</ins><span class="cx">         if (!this._annotationRows)
</span><span class="cx">             return;
</span><span class="cx"> 
</span><span class="lines">@@ -402,7 +433,7 @@
</span><span class="cx">         }
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _renderTimeSeries(context, metrics, source, series)
</del><ins>+    _renderTimeSeries(context, metrics, source, series, layerName)
</ins><span class="cx">     {
</span><span class="cx">         for (var point of series) {
</span><span class="cx">             point.x = metrics.timeToX(point.time);
</span><span class="lines">@@ -412,29 +443,45 @@
</span><span class="cx">         context.strokeStyle = source.intervalStyle;
</span><span class="cx">         context.fillStyle = source.intervalStyle;
</span><span class="cx">         context.lineWidth = source.intervalWidth;
</span><ins>+
+        context.beginPath();
+        var width = 1;
</ins><span class="cx">         for (var i = 0; i &lt; series.length; i++) {
</span><span class="cx">             var point = series[i];
</span><del>-            if (!point.interval)
-                continue;
-            context.beginPath();
-            context.moveTo(point.x, metrics.valueToY(point.interval[0]))
-            context.lineTo(point.x, metrics.valueToY(point.interval[1]));
-            context.stroke();
</del><ins>+            var interval = point.interval();
+            var value = interval ? interval[0] : point.value;
+            context.lineTo(point.x - width, metrics.valueToY(value));
+            context.lineTo(point.x + width, metrics.valueToY(value));
</ins><span class="cx">         }
</span><ins>+        for (var i = series.length - 1; i &gt;= 0; i--) {
+            var point = series[i];
+            var interval = point.interval();
+            var value = interval ? interval[1] : point.value;
+            context.lineTo(point.x + width, metrics.valueToY(value));
+            context.lineTo(point.x - width, metrics.valueToY(value));
+        }
+        context.fill();
</ins><span class="cx"> 
</span><del>-        context.strokeStyle = source.lineStyle;
-        context.lineWidth = source.lineWidth;
</del><ins>+        context.strokeStyle = this._sourceOptionWithFallback(source, layerName + 'LineStyle', 'lineStyle');
+        context.lineWidth = this._sourceOptionWithFallback(source, layerName + 'LineWidth', 'lineWidth');
</ins><span class="cx">         context.beginPath();
</span><span class="cx">         for (var point of series)
</span><span class="cx">             context.lineTo(point.x, point.y);
</span><span class="cx">         context.stroke();
</span><span class="cx"> 
</span><del>-        context.fillStyle = source.pointStyle;
-        var radius = source.pointRadius;
-        for (var point of series)
-            this._fillCircle(context, point.x, point.y, radius);
</del><ins>+        context.fillStyle = this._sourceOptionWithFallback(source, layerName + 'PointStyle', 'pointStyle');
+        var radius = this._sourceOptionWithFallback(source, layerName + 'PointRadius', 'pointRadius');
+        if (radius) {
+            for (var point of series)
+                this._fillCircle(context, point.x, point.y, radius);
+        }
</ins><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _sourceOptionWithFallback(option, preferred, fallback)
+    {
+        return preferred in option ? option[preferred] : option[fallback];
+    }
+
</ins><span class="cx">     _fillCircle(context, cx, cy, radius)
</span><span class="cx">     {
</span><span class="cx">         context.beginPath();
</span><span class="lines">@@ -476,7 +523,7 @@
</span><span class="cx">                 return null;
</span><span class="cx"> 
</span><span class="cx">             // A chart with X px width shouldn't have more than 2X / &lt;radius-of-points&gt; data points.
</span><del>-            var maximumNumberOfPoints = 2 * metrics.chartWidth / source.pointRadius;
</del><ins>+            var maximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2);
</ins><span class="cx"> 
</span><span class="cx">             var pointAfterStart = timeSeries.findPointAfterTime(startTime);
</span><span class="cx">             var pointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint();
</span><span class="lines">@@ -547,6 +594,14 @@
</span><span class="cx">         return sampledData;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _ensureTrendLines()
+    {
+        if (this._renderedTrendLines)
+            return false;
+        this._renderedTrendLines = true;
+        return true;
+    }
+
</ins><span class="cx">     _ensureValueRangeCache()
</span><span class="cx">     {
</span><span class="cx">         if (this._valueRangeCache)
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/index.html        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -43,6 +43,7 @@
</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><ins>+        &lt;script src=&quot;async-task.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></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmeasurementclusterjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -21,9 +21,9 @@
</span><span class="cx">             var point = this._adaptor.applyTo(row);
</span><span class="cx">             if (point.id in idMap || (!includeOutliers &amp;&amp; point.isOutlier))
</span><span class="cx">                 continue;
</span><del>-            idMap[point.id] = true;
</del><ins>+            series.append(point);
+            idMap[point.id] = point.seriesIndex;
</ins><span class="cx">             point.cluster = this;
</span><del>-            series.append(point);
</del><span class="cx">         }
</span><span class="cx">     }
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmeasurementsetjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -18,6 +18,7 @@
</span><span class="cx">         this._allFetches = {};
</span><span class="cx">         this._callbackMap = new Map;
</span><span class="cx">         this._primaryClusterPromise = null;
</span><ins>+        this._segmentationCache = new Map;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     platformId() { return this._platformId; }
</span><span class="lines">@@ -196,6 +197,113 @@
</span><span class="cx"> 
</span><span class="cx">         return series;
</span><span class="cx">     }
</span><ins>+
+    fetchSegmentation(segmentationName, parameters, configType, includeOutliers, extendToFuture)
+    {
+        var cacheMap = this._segmentationCache.get(configType);
+        if (!cacheMap) {
+            cacheMap = new WeakMap;
+            this._segmentationCache.set(configType, cacheMap);
+        }
+
+        var timeSeries = new TimeSeries;
+        var idMap = {};
+        var promises = [];
+        for (var cluster of this._sortedClusters) {
+            var clusterStart = timeSeries.length();
+            cluster.addToSeries(timeSeries, configType, includeOutliers, idMap);
+            var clusterEnd = timeSeries.length();
+            promises.push(this._cachedClusterSegmentation(segmentationName, parameters, cacheMap,
+                cluster, timeSeries, clusterStart, clusterEnd, idMap));
+        }
+        if (!timeSeries.length())
+            return Promise.resolve(null);
+
+        var self = this;
+        return Promise.all(promises).then(function (clusterSegmentations) {
+            var segmentationSeries = [];
+            var addSegment = function (startingPoint, endingPoint) {
+                var value = Statistics.mean(timeSeries.valuesBetweenRange(startingPoint.seriesIndex, endingPoint.seriesIndex));
+                segmentationSeries.push({value: value, time: startingPoint.time, interval: function () { return null; }});
+                segmentationSeries.push({value: value, time: endingPoint.time, interval: function () { return null; }});
+            };
+
+            var startingIndex = 0;
+            for (var segmentation of clusterSegmentations) {
+                for (var endingIndex of segmentation) {
+                    addSegment(timeSeries.findPointByIndex(startingIndex), timeSeries.findPointByIndex(endingIndex));
+                    startingIndex = endingIndex;
+                }
+            }
+            if (extendToFuture)
+                timeSeries.extendToFuture();
+            addSegment(timeSeries.findPointByIndex(startingIndex), timeSeries.lastPoint());
+            return segmentationSeries;
+        });
+    }
+
+    _cachedClusterSegmentation(segmentationName, parameters, cacheMap, cluster, timeSeries, clusterStart, clusterEnd, idMap)
+    {
+        var cache = cacheMap.get(cluster);
+        if (cache &amp;&amp; this._validateSegmentationCache(cache, segmentationName, parameters)) {
+            var segmentationByIndex = new Array(cache.segmentation.length);
+            for (var i = 0; i &lt; cache.segmentation.length; i++) {
+                var id = cache.segmentation[i];
+                if (!(id in idMap))
+                    return null;
+                segmentationByIndex[i] = idMap[id];
+            }
+            return Promise.resolve(segmentationByIndex);
+        }
+
+        var clusterValues = timeSeries.valuesBetweenRange(clusterStart, clusterEnd);
+        return this._invokeSegmentationAlgorithm(segmentationName, parameters, clusterValues).then(function (segmentationInClusterIndex) {
+            // Remove cluster start/end as segmentation points. Otherwise each cluster will be placed into its own segment. 
+            var segmentation = segmentationInClusterIndex.slice(1, -1).map(function (index) { return clusterStart + index; });
+            var cache = segmentation.map(function (index) { return timeSeries.findPointByIndex(index).id; });
+            cacheMap.set(cluster, {segmentationName: segmentationName, segmentationParameters: parameters.slice(), segmentation: cache});
+            return segmentation;
+        });
+    }
+
+    _validateSegmentationCache(cache, segmentationName, parameters)
+    {
+        if (cache.segmentationName != segmentationName)
+            return false;
+        if (!!cache.segmentationParameters != !!parameters)
+            return false;
+        if (parameters) {
+            if (parameters.length != cache.segmentationParameters.length)
+                return false;
+            for (var i = 0; i &lt; parameters.length; i++) {
+                if (parameters[i] != cache.segmentationParameters[i])
+                    return false;
+            }
+        }
+        return true;
+    }
+
+    _invokeSegmentationAlgorithm(segmentationName, parameters, timeSeriesValues)
+    {
+        var args = [timeSeriesValues].concat(parameters || []);
+
+        var timeSeriesIsShortEnoughForSyncComputation = timeSeriesValues.length &lt; 100;
+        if (timeSeriesIsShortEnoughForSyncComputation) {
+            Instrumentation.startMeasuringTime('_invokeSegmentationAlgorithm', 'syncSegmentation');
+            var segmentation = Statistics[segmentationName].apply(timeSeriesValues, args);
+            Instrumentation.endMeasuringTime('_invokeSegmentationAlgorithm', 'syncSegmentation');
+            return Promise.resolve(segmentation);
+        }
+
+        var task = new AsyncTask(segmentationName, args);
+        return task.execute().then(function (response) {
+            Instrumentation.reportMeasurement('_invokeSegmentationAlgorithm', 'workerStartLatency', 'ms', response.startLatency);
+            Instrumentation.reportMeasurement('_invokeSegmentationAlgorithm', 'workerTime', 'ms', response.workerTime);
+            Instrumentation.reportMeasurement('_invokeSegmentationAlgorithm', 'totalTime', 'ms', response.totalTime);
+            return response.result;
+        });
+    }
+
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> if (typeof module != 'undefined')
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelstimeseriesjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/time-series.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/time-series.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/models/time-series.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -1,12 +1,15 @@
</span><ins>+'use strict';
</ins><span class="cx"> 
</span><span class="cx"> // v3 UI still relies on RunsData for associating metrics with units.
</span><span class="cx"> // Use declartive syntax once that dependency has been removed.
</span><del>-TimeSeries = class {
</del><ins>+var TimeSeries = class {
</ins><span class="cx">     constructor()
</span><span class="cx">     {
</span><span class="cx">         this._data = [];
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    length() { return this._data.length; }
+
</ins><span class="cx">     append(item)
</span><span class="cx">     {
</span><span class="cx">         console.assert(item.series === undefined);
</span><span class="lines">@@ -29,6 +32,17 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    valuesBetweenRange(startingIndex, endingIndex)
+    {
+        startingIndex = Math.max(startingIndex, 0);
+        endingIndex = Math.min(endingIndex, this._data.length);
+        var length = endingIndex - startingIndex;
+        var values = new Array(length);
+        for (var i = 0; i &lt; length; i++)
+            values[i] = this._data[startingIndex + i].value;
+        return values;
+    }
+
</ins><span class="cx">     firstPoint() { return this._data.length ? this._data[0] : null; }
</span><span class="cx">     lastPoint() { return this._data.length ? this._data[this._data.length - 1] : null; }
</span><span class="cx"> 
</span><span class="lines">@@ -67,3 +81,6 @@
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx"> };
</span><ins>+
+if (typeof module != 'undefined')
+    module.exports.TimeSeries = TimeSeries;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pageschartpanejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -1,4 +1,69 @@
</span><span class="cx"> 
</span><ins>+function createTrendLineExecutableFromAveragingFunction(callback) {
+    return function (source, parameters) {
+        var timeSeries = source.measurementSet.fetchedTimeSeries(source.type, source.includeOutliers, source.extendToFuture);
+        var values = timeSeries.values();
+        if (!values.length)
+            return Promise.resolve(null);
+
+        var averageValues = callback.call(null, values, parameters[0], parameters[1]);
+        if (!averageValues)
+            return Promise.resolve(null);
+
+        var interval = function () { return null; }
+        var result = new Array(averageValues.length);
+        for (var i = 0; i &lt; averageValues.length; i++)
+            result[i] = {time: timeSeries.findPointByIndex(i).time, value: averageValues[i], interval: interval};
+
+        return Promise.resolve(result);
+    }
+}
+
+var ChartTrendLineTypes = [
+    {
+        id: 0,
+        label: 'None',
+    },
+    {
+        id: 5,
+        label: 'Segmentation',
+        execute: function (source, parameters) {
+            return source.measurementSet.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', parameters,
+                source.type, source.includeOutliers, source.extendToFuture).then(function (segmentation) {
+                return segmentation;
+            });
+        },
+        parameterList: [
+            {label: &quot;Segment count weight&quot;, value: 2.5, min: 0.01, max: 10, step: 0.01},
+            {label: &quot;Grid size&quot;, value: 500, min: 100, max: 10000, step: 10}
+        ]
+    },
+    {
+        id: 1,
+        label: 'Simple Moving Average',
+        parameterList: [
+            {label: &quot;Backward window size&quot;, value: 8, min: 2, step: 1},
+            {label: &quot;Forward window size&quot;, value: 4, min: 0, step: 1}
+        ],
+        execute: createTrendLineExecutableFromAveragingFunction(Statistics.movingAverage.bind(Statistics))
+    },
+    {
+        id: 2,
+        label: 'Cumulative Moving Average',
+        execute: createTrendLineExecutableFromAveragingFunction(Statistics.cumulativeMovingAverage.bind(Statistics))
+    },
+    {
+        id: 3,
+        label: 'Exponential Moving Average',
+        parameterList: [
+            {label: &quot;Smoothing factor&quot;, value: 0.01, min: 0.001, max: 0.9, step: 0.001},
+        ],
+        execute: createTrendLineExecutableFromAveragingFunction(Statistics.exponentialMovingAverage.bind(Statistics))
+    },
+];
+ChartTrendLineTypes.DefaultType = ChartTrendLineTypes[1];
+
+
</ins><span class="cx"> class ChartPane extends ChartPaneBase {
</span><span class="cx">     constructor(chartsPage, platformId, metricId)
</span><span class="cx">     {
</span><span class="lines">@@ -7,6 +72,10 @@
</span><span class="cx">         this._mainChartIndicatorWasLocked = false;
</span><span class="cx">         this._chartsPage = chartsPage;
</span><span class="cx">         this._lockedPopover = null;
</span><ins>+        this._trendLineType = null;
+        this._trendLineParameters = [];
+        this._trendLineVersion = 0;
+        this._renderedTrandLineOptions = false;
</ins><span class="cx"> 
</span><span class="cx">         this.content().querySelector('close-button').component().setCallback(chartsPage.closePane.bind(chartsPage, this));
</span><span class="cx"> 
</span><span class="lines">@@ -34,6 +103,9 @@
</span><span class="cx">         if (graphOptions.size)
</span><span class="cx">             state[3] = graphOptions;
</span><span class="cx"> 
</span><ins>+        if (this._trendLineType)
+            state[4] = [this._trendLineType.id].concat(this._trendLineParameters);
+
</ins><span class="cx">         return state;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -52,6 +124,7 @@
</span><span class="cx">             this._mainChart.setIndicator(null, false);
</span><span class="cx"> 
</span><span class="cx">         // FIXME: This forces sourceList to be set twice. First in configure inside the constructor then here.
</span><ins>+        // FIXME: Show full y-axis when graphOptions is true to be compatible with v2 UI.
</ins><span class="cx">         var graphOptions = state[3];
</span><span class="cx">         if (graphOptions instanceof Set) {
</span><span class="cx">             this.setSamplingEnabled(!graphOptions.has('nosampling'));
</span><span class="lines">@@ -58,8 +131,21 @@
</span><span class="cx">             this.setShowOutliers(graphOptions.has('showoutliers'));
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        // FIXME: Show full y-axis when graphOptions is true to be compatible with v2 UI.
-        // FIXME: state[4] specifies moving average in v2 UI
</del><ins>+        var trendLineOptions = state[4];
+        if (!(trendLineOptions instanceof Array))
+            trendLineOptions = [];
+
+        var trendLineId = trendLineOptions[0];
+        var trendLineType = ChartTrendLineTypes.find(function (type) { return type.id == trendLineId; }) || ChartTrendLineTypes.DefaultType;
+
+        this._trendLineType = trendLineType;
+        this._trendLineParameters = (trendLineType.parameterList || []).map(function (parameter, index) {
+            var specifiedValue = parseFloat(trendLineOptions[index + 1]);
+            return !isNaN(specifiedValue) ? specifiedValue : parameter.value;
+        });
+        this._updateTrendLine();
+        this._renderedTrandLineOptions = false;
+
</ins><span class="cx">         // FIXME: state[5] specifies envelope in v2 UI
</span><span class="cx">         // FIXME: state[6] specifies change detection algorithm in v2 UI
</span><span class="cx">     }
</span><span class="lines">@@ -202,7 +288,11 @@
</span><span class="cx">         var filteringOptions = this.content().querySelector('.chart-pane-filtering-options');
</span><span class="cx">         actions.push(this._makePopoverActionItem(filteringOptions, 'Filtering', true));
</span><span class="cx"> 
</span><ins>+        var trendLineOptions = this.content().querySelector('.chart-pane-trend-line-options');
+        actions.push(this._makePopoverActionItem(trendLineOptions, 'Trend lines', true));
+
</ins><span class="cx">         this._renderFilteringPopover();
</span><ins>+        this._renderTrendLinePopover();
</ins><span class="cx"> 
</span><span class="cx">         this._lockedPopover = null;
</span><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-action-buttons'), actions);
</span><span class="lines">@@ -307,6 +397,117 @@
</span><span class="cx">         markAsOutlierButton.disabled = !firstSelectedPoint;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _renderTrendLinePopover()
+    {
+        var element = ComponentBase.createElement;
+        var link = ComponentBase.createLink;
+        var self = this;
+
+        if (this._trendLineType == null) {
+            this.renderReplace(this.content().querySelector('.trend-line-types'), [
+                element('select', {onchange: this._trendLineTypeDidChange.bind(this)},
+                    ChartTrendLineTypes.map(function (type) {
+                        return element('option', type == self._trendLineType ? {value: type.id, selected: true} : {value: type.id}, type.label);
+                    }))
+            ]);
+        } else
+            this.content().querySelector('.trend-line-types select').value = this._trendLineType.id;
+
+        if (this._renderedTrandLineOptions)
+            return;
+        this._renderedTrandLineOptions = true;
+
+        if (this._trendLineParameters.length) {
+            var configuredParameters = this._trendLineParameters;
+            this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), [
+                element('h3', 'Parameters'),
+                element('ul', this._trendLineType.parameterList.map(function (parameter, index) {
+                    var attributes = {type: 'number'};
+                    for (var name in parameter)
+                        attributes[name] = parameter[name];
+                    attributes.value = configuredParameters[index];
+                    var input = element('input', attributes);
+                    input.parameterIndex = index;
+                    input.oninput = self._trendLineParameterDidChange.bind(self);
+                    input.onchange = self._trendLineParameterDidChange.bind(self);
+                    return element('li', element('label', [parameter.label + ': ', input]));
+                }))
+            ]);
+        } else
+            this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), []);
+    }
+
+    _trendLineTypeDidChange(event)
+    {
+        var newType = ChartTrendLineTypes.find(function (type) { return type.id == event.target.value });
+        if (newType == this._trendLineType)
+            return;
+
+        this._trendLineType = newType;
+        this._trendLineParameters = this._defaultParametersForTrendLine(newType);
+        this._renderedTrandLineOptions = false;
+
+        this._updateTrendLine();
+        this._chartsPage.graphOptionsDidChange();
+        this.render();
+    }
+
+    _defaultParametersForTrendLine(type)
+    {
+        return type &amp;&amp; type.parameterList ? type.parameterList.map(function (parameter) { return parameter.value; }) : [];
+    }
+
+    _trendLineParameterDidChange(event)
+    {
+        var input = event.target;
+        var index = input.parameterIndex;
+        var newValue = parseFloat(input.value);
+        if (this._trendLineParameters[index] == newValue)
+            return;
+        this._trendLineParameters[index] = newValue;
+        var self = this;
+        setTimeout(function () { // Some trend lines, e.g. sementations, are expensive.
+            if (self._trendLineParameters[index] != newValue)
+                return;
+            self._updateTrendLine();
+            self._chartsPage.graphOptionsDidChange();
+        }, 500);
+    }
+
+    _didFetchData()
+    {
+        super._didFetchData();
+        this._updateTrendLine();
+    }
+
+    _updateTrendLine()
+    {
+        if (!this._mainChart.sourceList())
+            return;
+
+        this._trendLineVersion++;
+        var currentTrendLineType = this._trendLineType || ChartTrendLineTypes.DefaultType;
+        var currentTrendLineParameters = this._trendLineParameters || this._defaultParametersForTrendLine(currentTrendLineType);
+        var currentTrendLineVersion = this._trendLineVersion;
+        var self = this;
+        var sourceList = this._mainChart.sourceList();
+
+        if (!currentTrendLineType.execute) {
+            this._mainChart.clearTrendLines();
+            this.render();
+        } else {
+            // Wait for all trendlines to be ready. Otherwise we might see FOC when the domain is expanded.
+            Promise.all(sourceList.map(function (source, sourceIndex) {
+                return currentTrendLineType.execute.call(null, source, currentTrendLineParameters).then(function (trendlineSeries) {
+                    if (self._trendLineVersion == currentTrendLineVersion)
+                        self._mainChart.setTrendLine(sourceIndex, trendlineSeries);
+                });
+            })).then(function () {
+                self.render();
+            });
+        }
+    }
+
</ins><span class="cx">     static paneHeaderTemplate()
</span><span class="cx">     {
</span><span class="cx">         return `
</span><span class="lines">@@ -327,6 +528,10 @@
</span><span class="cx">                         &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; class=&quot;show-outliers&quot;&gt;Show outliers&lt;/label&gt;&lt;/li&gt;
</span><span class="cx">                         &lt;li&gt;&lt;button class=&quot;mark-as-outlier&quot;&gt;Mark selected points as outlier&lt;/button&gt;&lt;/li&gt;
</span><span class="cx">                     &lt;/ul&gt;
</span><ins>+                    &lt;ul class=&quot;chart-pane-trend-line-options popover&quot; style=&quot;display:none&quot;&gt;
+                        &lt;div class=&quot;trend-line-types&quot;&gt;&lt;/div&gt;
+                        &lt;div class=&quot;trend-line-parameter-list&quot;&gt;&lt;/div&gt;
+                    &lt;/ul&gt;
</ins><span class="cx">                 &lt;/nav&gt;
</span><span class="cx">             &lt;/header&gt;
</span><span class="cx">         `;
</span><span class="lines">@@ -397,14 +602,20 @@
</span><span class="cx">                 border: solid 1px #ccc;
</span><span class="cx">                 border-radius: 0.2rem;
</span><span class="cx">                 z-index: 10;
</span><del>-                background: rgba(255, 255, 255, 0.8);
-                -webkit-backdrop-filter: blur(0.5rem);
</del><span class="cx">                 padding: 0.2rem 0;
</span><span class="cx">                 margin: 0;
</span><span class="cx">                 margin-top: -0.2rem;
</span><span class="cx">                 margin-right: -0.2rem;
</span><ins>+                background: rgba(255, 255, 255, 0.95);
</ins><span class="cx">             }
</span><span class="cx"> 
</span><ins>+            @supports ( -webkit-backdrop-filter: blur(0.5rem) ) {
+                .chart-pane-actions .popover {
+                    background: rgba(255, 255, 255, 0.6);
+                    -webkit-backdrop-filter: blur(0.5rem);
+                }
+            }
+
</ins><span class="cx">             .chart-pane-actions .popover li {
</span><span class="cx">             }
</span><span class="cx"> 
</span><span class="lines">@@ -429,6 +640,32 @@
</span><span class="cx">                 font-size: 0.9rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><ins>+            .chart-pane-actions .popover.chart-pane-filtering-options {
+                padding: 0.2rem;
+            }
+
+            .chart-pane-actions .popover.chart-pane-trend-line-options h3 {
+                font-size: 0.9rem;
+                line-height: 0.9rem;
+                font-weight: inherit;
+                margin: 0;
+                padding: 0.2rem;
+                border-bottom: solid 1px #ccc;
+            }
+
+            .chart-pane-actions .popover.chart-pane-trend-line-options select,
+            .chart-pane-actions .popover.chart-pane-trend-line-options label {
+                margin: 0.2rem;
+            }
+
+            .chart-pane-actions .popover.chart-pane-trend-line-options label {
+                font-size: 0.8rem;
+            }
+
+            .chart-pane-actions .popover.chart-pane-trend-line-options input {
+                width: 2.5rem;
+            }
+
</ins><span class="cx">             .chart-pane-actions .popover input[type=text] {
</span><span class="cx">                 font-size: 1rem;
</span><span class="cx">                 width: 15rem;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesdashboardpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -134,7 +134,7 @@
</span><span class="cx"> 
</span><span class="cx">         var options = ChartStyles.dashboardOptions(result.metric.makeFormatter(3));
</span><span class="cx">         options.ondata = this._fetchedData.bind(this);
</span><del>-        var chart = new TimeSeriesChart(ChartStyles.createSourceList(result.platform, result.metric, false, false), options);
</del><ins>+        var chart = new TimeSeriesChart(ChartStyles.createSourceList(result.platform, result.metric, false, false, true), options);
</ins><span class="cx">         this._charts.push(chart);
</span><span class="cx"> 
</span><span class="cx">         var statusView = new ChartStatusView(result.metric, chart);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsv3modelsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/tools/js/v3-models.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/v3-models.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/tools/js/v3-models.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -26,6 +26,7 @@
</span><span class="cx"> importFromV3('models/root-set.js', 'RootSet');
</span><span class="cx"> importFromV3('models/test.js', 'Test');
</span><span class="cx"> importFromV3('models/test-group.js', 'TestGroup');
</span><ins>+importFromV3('models/time-series.js', 'TimeSeries');
</ins><span class="cx"> 
</span><span class="cx"> importFromV3('privileged-api.js', 'PrivilegedAPI');
</span><span class="cx"> importFromV3('instrumentation.js', 'Instrumentation');
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestsmeasurementsettestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -1,6 +1,8 @@
</span><span class="cx"> 'use strict';
</span><span class="cx"> 
</span><span class="cx"> var assert = require('assert');
</span><ins>+if (!assert.almostEqual)
+    assert.almostEqual = require('./resources/almost-equal.js');
</ins><span class="cx"> 
</span><span class="cx"> let MockRemoteAPI = require('./resources/mock-remote-api.js').MockRemoteAPI;
</span><span class="cx"> require('../tools/js/v3-models.js');
</span><span class="lines">@@ -741,4 +743,143 @@
</span><span class="cx"> 
</span><span class="cx">     });
</span><span class="cx"> 
</span><ins>+    describe('fetchSegmentation', function () {
+
+        var simpleSegmentableValues = [
+            1546.5603, 1548.1536, 1563.5452, 1539.7823, 1546.4184, 1548.9299, 1532.5444, 1546.2800, 1547.1760, 1551.3507,
+            1548.3277, 1544.7673, 1542.7157, 1538.1700, 1538.0948, 1543.0364, 1537.9737, 1542.2611, 1543.9685, 1546.4901,
+            1544.4080, 1540.8671, 1537.3353, 1549.4331, 1541.4436, 1544.1299, 1550.1770, 1553.1872, 1549.3417, 1542.3788,
+            1543.5094, 1541.7905, 1537.6625, 1547.3840, 1538.5185, 1549.6764, 1556.6138, 1552.0476, 1541.7629, 1544.7006,
+            /* segments changes here */
+            1587.1390, 1594.5451, 1586.2430, 1596.7310, 1548.1423
+        ];
+
+        function makeSampleRuns(values, startRunId, startTime, timeIncrement)
+        {
+            var runId = startRunId;
+            var buildId = 3400;
+            var buildNumber = 1;
+            var makeRun = function (value, commitTime) {
+                return [runId++, value, 1, value, value, false, [], commitTime, commitTime + 10, buildId++, buildNumber++, MockModels.builder.id()];
+            }
+
+            timeIncrement = Math.floor(timeIncrement);
+            var runs = values.map(function (value, index) { return makeRun(value, startTime + index * timeIncrement); })
+            
+            return runs;
+        }
+
+        it('should be able to segment a single cluster', function (done) {
+            var set = MeasurementSet.findSet(1, 1, 5000);
+            var promise = set.fetchBetween(4000, 5000);
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '../data/measurement-set-1-1.json');
+
+            requests[0].resolve({
+                'clusterStart': 1000,
+                'clusterSize': 1000,
+                'formatMap': ['id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier', 'revisions', 'commitTime', 'build', 'buildTime', 'buildNumber', 'builder'],
+                'configurations': {current: makeSampleRuns(simpleSegmentableValues, 6400, 4000, 1000 / 50)},
+                'startTime': 4000,
+                'endTime': 5000,
+                'lastModified': 5000,
+                'clusterCount': 4,
+                'status': 'OK'});
+
+            var timeSeries;
+            assert.equal(set.fetchedTimeSeries('current', false, false).length(), 0);
+            waitForMeasurementSet().then(function () {
+                timeSeries = set.fetchedTimeSeries('current', false, false);
+                assert.equal(timeSeries.length(), 45);
+                assert.equal(timeSeries.firstPoint().time, 4000);
+                assert.equal(timeSeries.lastPoint().time, 4880);
+                return set.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', [], 'current', false);
+            }).then(function (segmentation) {
+                assert.equal(segmentation.length, 4);
+
+                assert.equal(segmentation[0].time, 4000);
+                assert.almostEqual(segmentation[0].value, 1545.082);
+                assert.equal(segmentation[0].value, segmentation[1].value);
+                assert.equal(segmentation[1].time, timeSeries.findPointByIndex(39).time);
+
+                assert.equal(segmentation[2].time, timeSeries.findPointByIndex(39).time);
+                assert.almostEqual(segmentation[2].value, 1581.872);
+                assert.equal(segmentation[2].value, segmentation[3].value);
+                assert.equal(segmentation[3].time, 4880);
+                done();
+            }).catch(done);
+        });
+
+        it('should be able to segment two clusters', function (done) {
+            var set = MeasurementSet.findSet(1, 1, 5000);
+            var promise = set.fetchBetween(3000, 5000);
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '../data/measurement-set-1-1.json');
+
+            requests[0].resolve({
+                'clusterStart': 1000,
+                'clusterSize': 1000,
+                'formatMap': ['id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier', 'revisions', 'commitTime', 'build', 'buildTime', 'buildNumber', 'builder'],
+                'configurations': {current: makeSampleRuns(simpleSegmentableValues.slice(30), 6400, 4000, 1000 / 30)},
+                'startTime': 4000,
+                'endTime': 5000,
+                'lastModified': 5000,
+                'clusterCount': 4,
+                'status': 'OK'});
+
+            waitForMeasurementSet().then(function () {
+                assert.equal(requests.length, 2);
+                assert.equal(requests[1].url, '../data/measurement-set-1-1-4000.json');
+                return set.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', [], 'current', false);
+            }).then(function (segmentation) {
+                var timeSeries = set.fetchedTimeSeries('current', false, false);
+                assert.equal(timeSeries.length(), 15);
+                assert.equal(timeSeries.firstPoint().time, 4000);
+                assert.equal(timeSeries.lastPoint().time, 4462);
+
+                assert.equal(segmentation.length, 4);
+                assert.equal(segmentation[0].time, timeSeries.firstPoint().time);
+                assert.almostEqual(segmentation[0].value, 1545.441);
+                assert.equal(segmentation[0].value, segmentation[1].value);
+                assert.equal(segmentation[1].time, timeSeries.findPointByIndex(9).time);
+
+                assert.equal(segmentation[2].time, timeSeries.findPointByIndex(9).time);
+                assert.almostEqual(segmentation[2].value, 1581.872);
+                assert.equal(segmentation[2].value, segmentation[3].value);
+                assert.equal(segmentation[3].time, timeSeries.lastPoint().time);
+
+                requests[1].resolve({
+                    'clusterStart': 1000,
+                    'clusterSize': 1000,
+                    'formatMap': ['id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier', 'revisions', 'commitTime', 'build', 'buildTime', 'buildNumber', 'builder'],
+                    'configurations': {current: makeSampleRuns(simpleSegmentableValues.slice(0, 30), 6500, 3000, 1000 / 30)},
+                    'startTime': 3000,
+                    'endTime': 4000,
+                    'lastModified': 5000,
+                    'clusterCount': 4,
+                    'status': 'OK'});
+                return waitForMeasurementSet();
+            }).then(function () {
+                return set.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', [], 'current', false);
+            }).then(function (segmentation) {
+                var timeSeries = set.fetchedTimeSeries('current', false, false);
+                assert.equal(timeSeries.length(), 45);
+                assert.equal(timeSeries.firstPoint().time, 3000);
+                assert.equal(timeSeries.lastPoint().time, 4462);
+                assert.equal(segmentation.length, 4);
+
+                assert.equal(segmentation[0].time, timeSeries.firstPoint().time);
+                assert.almostEqual(segmentation[0].value, 1545.082);
+                assert.equal(segmentation[0].value, segmentation[1].value);
+                assert.equal(segmentation[1].time, timeSeries.findPointByIndex(39).time);
+
+                assert.equal(segmentation[2].time, timeSeries.findPointByIndex(39).time);
+                assert.almostEqual(segmentation[2].value, 1581.872);
+                assert.equal(segmentation[2].value, segmentation[3].value);
+                assert.equal(segmentation[3].time, timeSeries.lastPoint().time);
+                done();
+            }).catch(done);
+        });
+
+    });
</ins><span class="cx"> });
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestsresourcesalmostequaljs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/unit-tests/resources/almost-equal.js (0 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/resources/almost-equal.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/resources/almost-equal.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -0,0 +1,26 @@
</span><ins>+var assert = require('assert');
+
+function almostEqual(actual, expected, precision, message)
+{
+    var suffiedMessage = (message ? message + ' ' : '');
+    if (isNaN(expected)) {
+        assert(isNaN(actual), `${suffiedMessage}expected NaN but got ${actual}`);
+        return;
+    }
+
+    if (expected == 0) {
+        assert.equal(actual, expected, message);
+        return;
+    }
+
+    if (!precision)
+        precision = 6;
+    var tolerance = 1 / Math.pow(10, precision);
+    var relativeDifference = Math.abs((actual - expected) / expected);
+    var percentDifference = (relativeDifference * 100).toFixed(2);
+    assert(relativeDifference &lt; tolerance,
+        `${suffiedMessage}expected ${expected} but got ${actual} (${percentDifference}% difference)`);
+}
+
+if (typeof module != 'undefined')
+    module.exports = almostEqual;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestsstatisticstestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js (204295 => 204296)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js        2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js        2016-08-09 21:22:54 UTC (rev 204296)
</span><span class="lines">@@ -2,30 +2,10 @@
</span><span class="cx"> 
</span><span class="cx"> var assert = require('assert');
</span><span class="cx"> var Statistics = require('../public/shared/statistics.js');
</span><ins>+if (!assert.almostEqual)
+    assert.almostEqual = require('./resources/almost-equal.js');
</ins><span class="cx"> 
</span><del>-if (!assert.almostEqual) {
-    assert.almostEqual = function (actual, expected, precision, message) {
-        var suffiedMessage = (message ? message + ' ' : '');
-        if (isNaN(expected)) {
-            assert(isNaN(actual), `${suffiedMessage}expected NaN but got ${actual}`);
-            return;
-        }
</del><span class="cx"> 
</span><del>-        if (expected == 0) {
-            assert.equal(actual, expected, message);
-            return;
-        }
-
-        if (!precision)
-            precision = 6;
-        var tolerance = 1 / Math.pow(10, precision);
-        var relativeDifference = Math.abs((actual - expected) / expected);
-        var percentDifference = (relativeDifference * 100).toFixed(2);
-        assert(relativeDifference &lt; tolerance,
-            `${suffiedMessage}expected ${expected} but got ${actual} (${percentDifference}% difference)`);
-    }
-}
-
</del><span class="cx"> describe('assert.almostEqual', function () {
</span><span class="cx">     it('should not throw when values are identical', function () {
</span><span class="cx">         assert.doesNotThrow(function () { assert.almostEqual(1, 1); });
</span></span></pre>
</div>
</div>

</body>
</html>