REGRESSION(r212853): Comparisons to baseline no longer shows up
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / pages / chart-pane.js
1
2 function createTrendLineExecutableFromAveragingFunction(callback) {
3     return function (source, parameters) {
4         var timeSeries = source.measurementSet.fetchedTimeSeries(source.type, source.includeOutliers, source.extendToFuture);
5         var values = timeSeries.values();
6         if (!values.length)
7             return Promise.resolve(null);
8
9         var averageValues = callback.call(null, values, ...parameters);
10         if (!averageValues)
11             return Promise.resolve(null);
12
13         var interval = function () { return null; }
14         var result = new Array(averageValues.length);
15         for (var i = 0; i < averageValues.length; i++)
16             result[i] = {time: timeSeries.findPointByIndex(i).time, value: averageValues[i], interval: interval};
17
18         return Promise.resolve(result);
19     }
20 }
21
22 var ChartTrendLineTypes = [
23     {
24         id: 0,
25         label: 'None',
26     },
27     {
28         id: 5,
29         label: 'Segmentation',
30         execute: function (source, parameters) {
31             return source.measurementSet.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', parameters,
32                 source.type, source.includeOutliers, source.extendToFuture).then(function (segmentation) {
33                 return segmentation;
34             });
35         },
36         parameterList: [
37             {label: "Segment count weight", value: 2.5, min: 0.01, max: 10, step: 0.01},
38             {label: "Grid size", value: 500, min: 100, max: 10000, step: 10}
39         ]
40     },
41     {
42         id: 1,
43         label: 'Simple Moving Average',
44         parameterList: [
45             {label: "Backward window size", value: 8, min: 2, step: 1},
46             {label: "Forward window size", value: 4, min: 0, step: 1}
47         ],
48         execute: createTrendLineExecutableFromAveragingFunction(Statistics.movingAverage.bind(Statistics))
49     },
50     {
51         id: 2,
52         label: 'Cumulative Moving Average',
53         execute: createTrendLineExecutableFromAveragingFunction(Statistics.cumulativeMovingAverage.bind(Statistics))
54     },
55     {
56         id: 3,
57         label: 'Exponential Moving Average',
58         parameterList: [
59             {label: "Smoothing factor", value: 0.01, min: 0.001, max: 0.9, step: 0.001},
60         ],
61         execute: createTrendLineExecutableFromAveragingFunction(Statistics.exponentialMovingAverage.bind(Statistics))
62     },
63 ];
64 ChartTrendLineTypes.DefaultType = ChartTrendLineTypes[1];
65
66
67 class ChartPane extends ChartPaneBase {
68     constructor(chartsPage, platformId, metricId)
69     {
70         super('chart-pane');
71
72         this._mainChartIndicatorWasLocked = false;
73         this._chartsPage = chartsPage;
74         this._lockedPopover = null;
75         this._trendLineType = null;
76         this._trendLineParameters = [];
77         this._trendLineVersion = 0;
78         this._renderedTrendLineOptions = false;
79
80         this.configure(platformId, metricId);
81     }
82
83     didConstructShadowTree()
84     {
85         this.part('close').listenToAction('activate', () => {
86             this._chartsPage.closePane(this);
87         })
88     }
89
90     serializeState()
91     {
92         var state = [this._platformId, this._metricId];
93         if (this._mainChart) {
94             var selection = this._mainChart.currentSelection();
95             const indicator = this._mainChart.currentIndicator();
96             if (selection)
97                 state[2] = selection;
98             else if (indicator && indicator.isLocked)
99                 state[2] = indicator.point.id;
100         }
101
102         var graphOptions = new Set;
103         if (!this.isSamplingEnabled())
104             graphOptions.add('noSampling');
105         if (this.isShowingOutliers())
106             graphOptions.add('showOutliers');
107
108         if (graphOptions.size)
109             state[3] = graphOptions;
110
111         if (this._trendLineType)
112             state[4] = [this._trendLineType.id].concat(this._trendLineParameters);
113
114         return state;
115     }
116
117     updateFromSerializedState(state, isOpen)
118     {
119         if (!this._mainChart)
120             return;
121
122         var selectionOrIndicatedPoint = state[2];
123         if (selectionOrIndicatedPoint instanceof Array)
124             this._mainChart.setSelection([parseFloat(selectionOrIndicatedPoint[0]), parseFloat(selectionOrIndicatedPoint[1])]);
125         else if (typeof(selectionOrIndicatedPoint) == 'number') {
126             this._mainChart.setIndicator(selectionOrIndicatedPoint, true);
127             this._mainChartIndicatorWasLocked = true;
128         } else
129             this._mainChart.setIndicator(null, false);
130
131         // FIXME: This forces sourceList to be set twice. First in configure inside the constructor then here.
132         // FIXME: Show full y-axis when graphOptions is true to be compatible with v2 UI.
133         var graphOptions = state[3];
134         if (graphOptions instanceof Set) {
135             this.setSamplingEnabled(!graphOptions.has('nosampling'));
136             this.setShowOutliers(graphOptions.has('showoutliers'));
137         }
138
139         var trendLineOptions = state[4];
140         if (!(trendLineOptions instanceof Array))
141             trendLineOptions = [];
142
143         var trendLineId = trendLineOptions[0];
144         var trendLineType = ChartTrendLineTypes.find(function (type) { return type.id == trendLineId; }) || ChartTrendLineTypes.DefaultType;
145
146         this._trendLineType = trendLineType;
147         this._trendLineParameters = (trendLineType.parameterList || []).map(function (parameter, index) {
148             var specifiedValue = parseFloat(trendLineOptions[index + 1]);
149             return !isNaN(specifiedValue) ? specifiedValue : parameter.value;
150         });
151         this._updateTrendLine();
152         this._renderedTrendLineOptions = false;
153
154         // FIXME: state[5] specifies envelope in v2 UI
155         // FIXME: state[6] specifies change detection algorithm in v2 UI
156     }
157
158     setOverviewSelection(selection)
159     {
160         if (this._overviewChart)
161             this._overviewChart.setSelection(selection);
162     }
163
164     _overviewSelectionDidChange(domain, didEndDrag)
165     {
166         super._overviewSelectionDidChange(domain, didEndDrag);
167         this._chartsPage.setMainDomainFromOverviewSelection(domain, this, didEndDrag);
168     }
169
170     _mainSelectionDidChange(selection, didEndDrag)
171     {
172         super._mainSelectionDidChange(selection, didEndDrag);
173         this._chartsPage.mainChartSelectionDidChange(this, didEndDrag);
174     }
175
176     _mainSelectionDidZoom(selection)
177     {
178         super._mainSelectionDidZoom(selection);
179         this._chartsPage.setMainDomainFromZoom(selection, this);
180     }
181
182     router() { return this._chartsPage.router(); }
183
184     _requestOpeningCommitViewer(repository, from, to)
185     {
186         super._requestOpeningCommitViewer(repository, from, to);
187         this._chartsPage.setOpenRepository(repository);
188     }
189
190     setOpenRepository(repository)
191     {
192         if (repository != this._commitLogViewer.currentRepository()) {
193             var range = this._mainChartStatus.setCurrentRepository(repository);
194             this._commitLogViewer.view(repository, range.from, range.to).then(() => { this.enqueueToRender(); });
195             this.enqueueToRender();
196         }
197     }
198
199     _indicatorDidChange(indicatorID, isLocked)
200     {
201         this._chartsPage.mainChartIndicatorDidChange(this, isLocked != this._mainChartIndicatorWasLocked);
202         this._mainChartIndicatorWasLocked = isLocked;
203         super._indicatorDidChange(indicatorID, isLocked);
204     }
205
206     _analyzeRange(pointsRangeForAnalysis)
207     {
208         var router = this._chartsPage.router();
209         var newWindow = window.open(router.url('analysis/task/create'), '_blank');
210
211         var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
212         var name = analyzePopover.querySelector('input').value;
213         var self = this;
214         AnalysisTask.create(name, pointsRangeForAnalysis.startPointId, pointsRangeForAnalysis.endPointId).then(function (data) {
215             newWindow.location.href = router.url('analysis/task/' + data['taskId']);
216             self.fetchAnalysisTasks(true);
217         }, function (error) {
218             newWindow.location.href = router.url('analysis/task/create', {error: error});
219         });
220     }
221
222     _markAsOutlier(markAsOutlier, points)
223     {
224         var self = this;
225         return Promise.all(points.map(function (point) {
226             return PrivilegedAPI.sendRequest('update-run-status', {'run': point.id, 'markedOutlier': markAsOutlier});
227         })).then(function () {
228             self._mainChart.fetchMeasurementSets(true /* noCache */);
229         }, function (error) {
230             alert('Failed to update the outlier status: ' + error);
231         }).catch();
232     }
233
234     render()
235     {
236         if (this._platform && this._metric) {
237             var metric = this._metric;
238             var platform = this._platform;
239
240             this.renderReplace(this.content().querySelector('.chart-pane-title'),
241                 metric.fullName() + ' on ' + platform.name());
242         }
243
244         if (this._mainChartStatus)
245             this._renderActionToolbar();
246
247         super.render();
248     }
249
250     _renderActionToolbar()
251     {
252         var actions = [];
253         var platform = this._platform;
254         var metric = this._metric;
255
256         var element = ComponentBase.createElement;
257         var link = ComponentBase.createLink;
258         var self = this;
259
260         this.part('close').enqueueToRender();
261
262         if (this._chartsPage.canBreakdown(platform, metric)) {
263             actions.push(element('li', link('Breakdown', function () {
264                 self._chartsPage.insertBreakdownPanesAfter(platform, metric, self);
265             })));
266         }
267
268         var platformPopover = this.content().querySelector('.chart-pane-alternative-platforms');
269         var alternativePlatforms = this._chartsPage.alternatePlatforms(platform, metric);
270         if (alternativePlatforms.length) {
271             this.renderReplace(platformPopover, Platform.sortByName(alternativePlatforms).map(function (platform) {
272                 return element('li', link(platform.label(), function () {
273                     self._chartsPage.insertPaneAfter(platform, metric, self);
274                 }));
275             }));
276
277             actions.push(this._makePopoverActionItem(platformPopover, 'Other Platforms', true));
278         } else
279             platformPopover.style.display = 'none';
280
281         var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
282         var pointsRangeForAnalysis = this._mainChartStatus.pointsRangeForAnalysis();
283         if (pointsRangeForAnalysis) {
284             actions.push(this._makePopoverActionItem(analyzePopover, 'Analyze', false));
285             analyzePopover.onsubmit = function (event) {
286                 event.preventDefault();
287                 self._analyzeRange(pointsRangeForAnalysis);
288             }
289         } else {
290             analyzePopover.style.display = 'none';
291             analyzePopover.onsubmit = function (event) { event.preventDefault(); }
292         }
293
294         var filteringOptions = this.content().querySelector('.chart-pane-filtering-options');
295         actions.push(this._makePopoverActionItem(filteringOptions, 'Filtering', true));
296
297         var trendLineOptions = this.content().querySelector('.chart-pane-trend-line-options');
298         actions.push(this._makePopoverActionItem(trendLineOptions, 'Trend lines', true));
299
300         this._renderFilteringPopover();
301         this._renderTrendLinePopover();
302
303         this._lockedPopover = null;
304         this.renderReplace(this.content().querySelector('.chart-pane-action-buttons'), actions);
305     }
306
307     _makePopoverActionItem(popover, label, shouldRespondToHover)
308     {
309         var self = this;
310         popover.anchor = ComponentBase.createLink(label, function () {
311             var makeVisible = self._lockedPopover != popover;
312             self._setPopoverVisibility(popover, makeVisible);
313             if (makeVisible)
314                 self._lockedPopover = popover;
315         });
316         if (shouldRespondToHover)
317             this._makePopoverOpenOnHover(popover);
318
319         return ComponentBase.createElement('li', {class: this._lockedPopover == popover ? 'selected' : ''}, popover.anchor);
320     }
321
322     _makePopoverOpenOnHover(popover)
323     {
324         var mouseIsInAnchor = false;
325         var mouseIsInPopover = false;
326
327         var self = this;
328         var closeIfNeeded = function () {
329             setTimeout(function () {
330                 if (self._lockedPopover != popover && !mouseIsInAnchor && !mouseIsInPopover)
331                     self._setPopoverVisibility(popover, false);
332             }, 0);
333         }
334
335         popover.anchor.onmouseenter = function () {
336             if (self._lockedPopover)
337                 return;
338             mouseIsInAnchor = true;
339             self._setPopoverVisibility(popover, true);
340         }
341         popover.anchor.onmouseleave = function () {
342             mouseIsInAnchor = false;
343             closeIfNeeded();         
344         }
345
346         popover.onmouseenter = function () {
347             mouseIsInPopover = true;
348         }
349         popover.onmouseleave = function () {
350             mouseIsInPopover = false;
351             closeIfNeeded();
352         }
353     }
354
355     _setPopoverVisibility(popover, visible)
356     {
357         var anchor = popover.anchor;
358         if (visible) {
359             var width = anchor.offsetParent.offsetWidth;
360             popover.style.top = anchor.offsetTop + anchor.offsetHeight + 'px';
361             popover.style.right = (width - anchor.offsetLeft - anchor.offsetWidth) + 'px';
362         }
363         popover.style.display = visible ? null : 'none';
364         anchor.parentNode.className = visible ? 'selected' : '';
365
366         if (this._lockedPopover && this._lockedPopover != popover && visible)
367             this._setPopoverVisibility(this._lockedPopover, false);
368
369         if (this._lockedPopover == popover && !visible)
370             this._lockedPopover = null;
371     }
372
373     _renderFilteringPopover()
374     {
375         var enableSampling = this.content().querySelector('.enable-sampling');
376         enableSampling.checked = this.isSamplingEnabled();
377         enableSampling.onchange = function () {
378             self.setSamplingEnabled(enableSampling.checked);
379             self._chartsPage.graphOptionsDidChange();
380         }
381
382         var showOutliers = this.content().querySelector('.show-outliers');
383         showOutliers.checked = this.isShowingOutliers();
384         showOutliers.onchange = function () {
385             self.setShowOutliers(showOutliers.checked);
386             self._chartsPage.graphOptionsDidChange();
387         }
388
389         var markAsOutlierButton = this.content().querySelector('.mark-as-outlier');
390         const indicator = this._mainChart.currentIndicator();
391         let firstSelectedPoint = indicator && indicator.isLocked ? indicator.point : null;
392         if (!firstSelectedPoint)
393             firstSelectedPoint = this._mainChart.firstSelectedPoint('current');
394         var alreayMarkedAsOutlier = firstSelectedPoint && firstSelectedPoint.markedOutlier;
395
396         var self = this;
397         markAsOutlierButton.textContent = (alreayMarkedAsOutlier ? 'Unmark' : 'Mark') + ' selected points as outlier';
398         markAsOutlierButton.onclick = function () {
399             var selectedPoints = [firstSelectedPoint];
400             if (self._mainChart.currentSelection('current'))
401                 selectedPoints = self._mainChart.selectedPoints('current');
402             self._markAsOutlier(!alreayMarkedAsOutlier, selectedPoints);
403         }
404         markAsOutlierButton.disabled = !firstSelectedPoint;
405     }
406
407     _renderTrendLinePopover()
408     {
409         var element = ComponentBase.createElement;
410         var link = ComponentBase.createLink;
411         var self = this;
412
413         const trendLineTypesContainer = this.content().querySelector('.trend-line-types');
414         if (!trendLineTypesContainer.querySelector('select')) {
415             this.renderReplace(trendLineTypesContainer, [
416                 element('select', {onchange: this._trendLineTypeDidChange.bind(this)},
417                     ChartTrendLineTypes.map((type) => { return element('option', {value: type.id}, type.label); }))
418             ]);
419         }
420         if (this._trendLineType)
421             trendLineTypesContainer.querySelector('select').value = this._trendLineType.id;
422
423         if (this._renderedTrendLineOptions)
424             return;
425         this._renderedTrendLineOptions = true;
426
427         if (this._trendLineParameters.length) {
428             var configuredParameters = this._trendLineParameters;
429             this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), [
430                 element('h3', 'Parameters'),
431                 element('ul', this._trendLineType.parameterList.map(function (parameter, index) {
432                     var attributes = {type: 'number'};
433                     for (var name in parameter)
434                         attributes[name] = parameter[name];
435                     attributes.value = configuredParameters[index];
436                     var input = element('input', attributes);
437                     input.parameterIndex = index;
438                     input.oninput = self._trendLineParameterDidChange.bind(self);
439                     input.onchange = self._trendLineParameterDidChange.bind(self);
440                     return element('li', element('label', [parameter.label + ': ', input]));
441                 }))
442             ]);
443         } else
444             this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), []);
445     }
446
447     _trendLineTypeDidChange(event)
448     {
449         var newType = ChartTrendLineTypes.find(function (type) { return type.id == event.target.value });
450         if (newType == this._trendLineType)
451             return;
452
453         this._trendLineType = newType;
454         this._trendLineParameters = this._defaultParametersForTrendLine(newType);
455         this._renderedTrendLineOptions = false;
456
457         this._updateTrendLine();
458         this._chartsPage.graphOptionsDidChange();
459         this.enqueueToRender();
460     }
461
462     _defaultParametersForTrendLine(type)
463     {
464         return type && type.parameterList ? type.parameterList.map(function (parameter) { return parameter.value; }) : [];
465     }
466
467     _trendLineParameterDidChange(event)
468     {
469         var input = event.target;
470         var index = input.parameterIndex;
471         var newValue = parseFloat(input.value);
472         if (this._trendLineParameters[index] == newValue)
473             return;
474         this._trendLineParameters[index] = newValue;
475         var self = this;
476         setTimeout(function () { // Some trend lines, e.g. sementations, are expensive.
477             if (self._trendLineParameters[index] != newValue)
478                 return;
479             self._updateTrendLine();
480             self._chartsPage.graphOptionsDidChange();
481         }, 500);
482     }
483
484     _didFetchData()
485     {
486         super._didFetchData();
487         this._updateTrendLine();
488     }
489
490     _updateTrendLine()
491     {
492         if (!this._mainChart.sourceList())
493             return;
494
495         this._trendLineVersion++;
496         var currentTrendLineType = this._trendLineType || ChartTrendLineTypes.DefaultType;
497         var currentTrendLineParameters = this._trendLineParameters || this._defaultParametersForTrendLine(currentTrendLineType);
498         var currentTrendLineVersion = this._trendLineVersion;
499         var self = this;
500         var sourceList = this._mainChart.sourceList();
501
502         if (!currentTrendLineType.execute) {
503             this._mainChart.clearTrendLines();
504             this.enqueueToRender();
505         } else {
506             // Wait for all trendlines to be ready. Otherwise we might see FOC when the domain is expanded.
507             Promise.all(sourceList.map(function (source, sourceIndex) {
508                 return currentTrendLineType.execute.call(null, source, currentTrendLineParameters).then(function (trendlineSeries) {
509                     if (self._trendLineVersion == currentTrendLineVersion)
510                         self._mainChart.setTrendLine(sourceIndex, trendlineSeries);
511                 });
512             })).then(function () {
513                 self.enqueueToRender();
514             });
515         }
516     }
517
518     static paneHeaderTemplate()
519     {
520         return `
521             <header class="chart-pane-header">
522                 <h2 class="chart-pane-title">-</h2>
523                 <nav class="chart-pane-actions">
524                     <ul>
525                         <li><close-button id="close"></close-button></li>
526                     </ul>
527                     <ul class="chart-pane-action-buttons buttoned-toolbar"></ul>
528                     <ul class="chart-pane-alternative-platforms popover" style="display:none"></ul>
529                     <form class="chart-pane-analyze-popover popover" style="display:none">
530                         <input type="text" required>
531                         <button>Create</button>
532                     </form>
533                     <ul class="chart-pane-filtering-options popover" style="display:none">
534                         <li><label><input type="checkbox" class="enable-sampling">Sampling</label></li>
535                         <li><label><input type="checkbox" class="show-outliers">Show outliers</label></li>
536                         <li><button class="mark-as-outlier">Mark selected points as outlier</button></li>
537                     </ul>
538                     <ul class="chart-pane-trend-line-options popover" style="display:none">
539                         <div class="trend-line-types"></div>
540                         <div class="trend-line-parameter-list"></div>
541                     </ul>
542                 </nav>
543             </header>
544         `;
545     }
546
547     static cssTemplate()
548     {
549         return ChartPaneBase.cssTemplate() + `
550             .chart-pane {
551                 border: solid 1px #ccc;
552                 border-radius: 0.5rem;
553                 margin: 1rem;
554                 margin-bottom: 2rem;
555             }
556
557             .chart-pane-body {
558                 height: calc(100% - 2rem);
559             }
560
561             .chart-pane-header {
562                 position: relative;
563                 left: 0;
564                 top: 0;
565                 width: 100%;
566                 height: 2rem;
567                 line-height: 2rem;
568                 border-bottom: solid 1px #ccc;
569             }
570
571             .chart-pane-title {
572                 margin: 0 0.5rem;
573                 padding: 0;
574                 padding-left: 1.5rem;
575                 font-size: 1rem;
576                 font-weight: inherit;
577             }
578
579             .chart-pane-actions {
580                 position: absolute;
581                 display: flex;
582                 flex-direction: row;
583                 justify-content: space-between;
584                 align-items: center;
585                 width: 100%;
586                 height: 2rem;
587                 top: 0;
588                 padding: 0 0;
589             }
590
591             .chart-pane-actions ul {
592                 display: block;
593                 padding: 0;
594                 margin: 0 0.5rem;
595                 font-size: 1rem;
596                 line-height: 1rem;
597                 list-style: none;
598             }
599
600             .chart-pane-actions .chart-pane-action-buttons {
601                 font-size: 0.9rem;
602                 line-height: 0.9rem;
603             }
604
605             .chart-pane-actions .popover {
606                 position: absolute;
607                 top: 0;
608                 right: 0;
609                 border: solid 1px #ccc;
610                 border-radius: 0.2rem;
611                 z-index: 10;
612                 padding: 0.2rem 0;
613                 margin: 0;
614                 margin-top: -0.2rem;
615                 margin-right: -0.2rem;
616                 background: rgba(255, 255, 255, 0.95);
617             }
618
619             @supports ( -webkit-backdrop-filter: blur(0.5rem) ) {
620                 .chart-pane-actions .popover {
621                     background: rgba(255, 255, 255, 0.6);
622                     -webkit-backdrop-filter: blur(0.5rem);
623                 }
624             }
625
626             .chart-pane-actions .popover li {
627             }
628
629             .chart-pane-actions .popover li a {
630                 display: block;
631                 text-decoration: none;
632                 color: inherit;
633                 font-size: 0.9rem;
634                 padding: 0.2rem 0.5rem;
635             }
636
637             .chart-pane-actions .popover a:hover,
638             .chart-pane-actions .popover input:focus {
639                 background: rgba(204, 153, 51, 0.1);
640             }
641
642             .chart-pane-actions .chart-pane-analyze-popover {
643                 padding: 0.5rem;
644             }
645
646             .chart-pane-actions .popover label {
647                 font-size: 0.9rem;
648             }
649
650             .chart-pane-actions .popover.chart-pane-filtering-options {
651                 padding: 0.2rem;
652             }
653
654             .chart-pane-actions .popover.chart-pane-trend-line-options h3 {
655                 font-size: 0.9rem;
656                 line-height: 0.9rem;
657                 font-weight: inherit;
658                 margin: 0;
659                 padding: 0.2rem;
660                 border-bottom: solid 1px #ccc;
661             }
662
663             .chart-pane-actions .popover.chart-pane-trend-line-options select,
664             .chart-pane-actions .popover.chart-pane-trend-line-options label {
665                 margin: 0.2rem;
666             }
667
668             .chart-pane-actions .popover.chart-pane-trend-line-options label {
669                 font-size: 0.8rem;
670             }
671
672             .chart-pane-actions .popover.chart-pane-trend-line-options input {
673                 width: 2.5rem;
674             }
675
676             .chart-pane-actions .popover input[type=text] {
677                 font-size: 1rem;
678                 width: 15rem;
679                 outline: none;
680                 border: solid 1px #ccc;
681             }
682 `;
683     }
684 }