Test freshness page should improve the ability to correlating issues from same builder.
[WebKit.git] / Websites / perf.webkit.org / public / v3 / pages / test-freshness-page.js
1
2 class TestFreshnessPage extends PageWithHeading {
3     constructor(summaryPageConfiguration, testAgeToleranceInHours)
4     {
5         super('test-freshness', null);
6         this._testAgeTolerance = (testAgeToleranceInHours || 24) * 3600 * 1000;
7         this._timeDuration = this._testAgeTolerance * 2;
8         this._excludedConfigurations = {};
9         this._lastDataPointByConfiguration = null;
10         this._indicatorByConfiguration = null;
11         this._renderTableLazily = new LazilyEvaluatedFunction(this._renderTable.bind(this));
12         this._currentlyHighlightedIndicator = null;
13         this._hoveringTooltip = false;
14         this._builderByIndicator = null;
15         this._renderTooltipLazily = new LazilyEvaluatedFunction(this._renderTooltip.bind(this));
16
17         this._loadConfig(summaryPageConfiguration);
18     }
19
20     didConstructShadowTree()
21     {
22         const tooltipContainer = this.content('tooltip-container');
23         tooltipContainer.addEventListener('mouseenter', () => {
24             this._hoveringTooltip = true;
25             this.enqueueToRender();
26         });
27         tooltipContainer.addEventListener('mouseleave', () => {
28             this._hoveringTooltip = false;
29             this.enqueueToRender();
30         });
31     }
32
33     name() { return 'Test-Freshness'; }
34
35     _loadConfig(summaryPageConfiguration)
36     {
37         const platformIdSet = new Set;
38         const metricIdSet = new Set;
39
40         for (const config of summaryPageConfiguration) {
41             for (const platformGroup of config.platformGroups) {
42                 for (const platformId of platformGroup.platforms)
43                     platformIdSet.add(platformId);
44             }
45
46             for (const metricGroup of config.metricGroups) {
47                 for (const subgroup of metricGroup.subgroups) {
48                     for (const metricId of subgroup.metrics)
49                         metricIdSet.add(metricId);
50                 }
51             }
52
53             const excludedConfigs = config.excludedConfigurations;
54             for (const platform in excludedConfigs) {
55                 if (platform in this._excludedConfigurations)
56                     this._excludedConfigurations[platform] = this._excludedConfigurations[platform].concat(excludedConfigs[platform]);
57                 else
58                     this._excludedConfigurations[platform] = excludedConfigs[platform];
59             }
60         }
61         this._platforms = [...platformIdSet].map((platformId) => Platform.findById(platformId));
62         this._metrics = [...metricIdSet].map((metricId) => Metric.findById(metricId));
63     }
64
65     open(state)
66     {
67         this._fetchTestResults();
68         super.open(state);
69     }
70
71     _fetchTestResults()
72     {
73         this._measurementSetFetchTime = Date.now();
74         this._lastDataPointByConfiguration = new Map;
75         this._builderByIndicator = new Map;
76
77         const startTime = this._measurementSetFetchTime - this._timeDuration;
78
79         for (const platform of this._platforms) {
80             const lastDataPointByMetric = new Map;
81             this._lastDataPointByConfiguration.set(platform, lastDataPointByMetric);
82
83             for (const metric of this._metrics) {
84                 if (!this._isValidPlatformMetricCombination(platform, metric, this._excludedConfigurations))
85                     continue;
86
87                 const measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric));
88                 measurementSet.fetchBetween(startTime, this._measurementSetFetchTime).then(() => {
89                     const currentTimeSeries = measurementSet.fetchedTimeSeries('current', false, false);
90
91                     let timeForLastDataPoint = startTime;
92                     let lastBuildLink = null;
93                     let builder = null;
94                     const lastPoint = currentTimeSeries.lastPoint();
95                     if (lastPoint) {
96                         timeForLastDataPoint = lastPoint.build().buildTime();
97                         lastBuildLink = lastPoint.build().url();
98                         builder = lastPoint.build().builder();
99                     }
100
101                     lastDataPointByMetric.set(metric, {time: timeForLastDataPoint, hasCurrentDataPoint: !!currentTimeSeries.lastPoint(),
102                         lastBuildLink, builder});
103                     this.enqueueToRender();
104                 });
105             }
106         }
107     }
108
109     render()
110     {
111         super.render();
112
113         this._renderTableLazily.evaluate(this._platforms, this._metrics);
114
115         let buildSummaryForCurrentlyHighlightedIndicator = null;
116         let buildLinkForCurrentlyHighlightedIndicator = null;
117         const builderForCurrentlyHighlightedIndicator = this._currentlyHighlightedIndicator ? this._builderByIndicator.get(this._currentlyHighlightedIndicator) : null;
118         for (const [platform, lastDataPointByMetric] of this._lastDataPointByConfiguration.entries()) {
119             for (const [metric, lastDataPoint] of lastDataPointByMetric.entries()) {
120                 const timeDuration = this._measurementSetFetchTime - lastDataPoint.time;
121                 const timeDurationSummaryPrefix = lastDataPoint.hasCurrentDataPoint ? '' : 'More than ';
122                 const timeDurationSummary = BuildRequest.formatTimeInterval(timeDuration);
123                 const summary = `${timeDurationSummaryPrefix}${timeDurationSummary} since latest data point.`;
124                 const url = this._router.url('charts', ChartsPage.createStateForDashboardItem(platform.id(), metric.id(),
125                     this._measurementSetFetchTime - this._timeDuration));
126
127                 const indicator = this._indicatorByConfiguration.get(platform).get(metric);
128                 if (this._currentlyHighlightedIndicator && this._currentlyHighlightedIndicator === indicator) {
129                     buildSummaryForCurrentlyHighlightedIndicator = summary;
130                     buildLinkForCurrentlyHighlightedIndicator = lastDataPoint.lastBuildLink;
131                 }
132                 this._builderByIndicator.set(indicator, lastDataPoint.builder);
133                 indicator.update(timeDuration, this._testAgeTolerance, url, builderForCurrentlyHighlightedIndicator && builderForCurrentlyHighlightedIndicator === lastDataPoint.builder);
134             }
135         }
136         this._renderTooltipLazily.evaluate(this._currentlyHighlightedIndicator, this._hoveringTooltip, buildSummaryForCurrentlyHighlightedIndicator, buildLinkForCurrentlyHighlightedIndicator);
137     }
138
139     _renderTooltip(indicator, hoveringTooltip, buildSummary, buildLink)
140     {
141         if (!indicator || !buildSummary) {
142             this.content('tooltip-container').style.display = hoveringTooltip ? null : 'none';
143             return;
144         }
145         const element = ComponentBase.createElement;
146
147         const rect = indicator.element().getBoundingClientRect();
148         const tooltipContainer = this.content('tooltip-container');
149         tooltipContainer.style.display = null;
150
151         const tooltipContainerComputedStyle = getComputedStyle(tooltipContainer);
152         const containerMarginTop = parseFloat(tooltipContainerComputedStyle.paddingTop);
153         const containerMarginLeft = parseFloat(tooltipContainerComputedStyle.marginLeft);
154
155         tooltipContainer.style.position = 'absolute';
156         tooltipContainer.style.top = rect.top - (tooltipContainer.offsetHeight + containerMarginTop)  + 'px';
157         tooltipContainer.style.left = rect.left + rect.width / 2 - tooltipContainer.offsetWidth / 2 + containerMarginLeft + 'px';
158
159         this.renderReplace(tooltipContainer, [element('p', buildSummary), buildLink ? element('a', {href: buildLink}, 'Latest Build') : []]);
160     }
161
162     _renderTable(platforms, metrics)
163     {
164         const element = ComponentBase.createElement;
165         const tableBodyElement = [];
166         const tableHeadElements = [element('th',  {class: 'table-corner row-head'}, 'Platform \\ Test')];
167
168         for (const metric of metrics)
169             tableHeadElements.push(element('th', {class: 'diagonal-head'}, element('div', metric.test().fullName())));
170
171         this._indicatorByConfiguration = new Map;
172         for (const platform of platforms) {
173             const indicatorByMetric = new Map;
174             this._indicatorByConfiguration.set(platform, indicatorByMetric);
175             tableBodyElement.push(element('tr',
176                 [element('th', {class: 'row-head'}, platform.label()), ...metrics.map((metric) => this._constructTableCell(platform, metric, indicatorByMetric))]));
177         }
178
179         this.renderReplace(this.content('test-health'), [element('thead', tableHeadElements), element('tbody', tableBodyElement)]);
180     }
181
182     _isValidPlatformMetricCombination(platform, metric)
183     {
184         return !(this._excludedConfigurations && this._excludedConfigurations[platform.id()]
185             && this._excludedConfigurations[platform.id()].some((metricId) => metricId == metric.id()))
186             && platform.hasMetric(metric);
187     }
188
189     _constructTableCell(platform, metric, indicatorByMetric)
190     {
191         const element = ComponentBase.createElement;
192
193         if (!this._isValidPlatformMetricCombination(platform, metric))
194             return element('td', {class: 'blank-cell'}, element('div'));
195
196         const indicator = new FreshnessIndicator;
197         indicator.listenToAction('select', (originator) => {
198             this._currentlyHighlightedIndicator = originator;
199             this.enqueueToRender();
200         });
201         indicator.listenToAction('unselect', () => {
202             this._currentlyHighlightedIndicator = null;
203             this.enqueueToRender();
204         });
205         indicatorByMetric.set(metric, indicator);
206         return element('td', {class: 'status-cell'}, indicator);
207     }
208
209     static htmlTemplate()
210     {
211         return `<section class="page-with-heading"><div id="tooltip-container"></div><table id="test-health"></table></section>`;
212     }
213
214     static cssTemplate()
215     {
216         return `
217             .page-with-heading {
218                 display: flex;
219                 justify-content: center;
220             }
221             #test-health {
222                 font-size: 1rem;
223             }
224             #test-health thead {
225                 display: block;
226                 align: right;
227             }
228             #test-health th.table-corner {
229                 text-align: right;
230                 vertical-align: bottom;
231             }
232             #test-health .row-head {
233                 min-width: 15.5rem;
234             }
235             #test-health th {
236                 text-align: left;
237                 border-bottom: 0.1rem solid #ccc;
238                 font-weight: normal;
239             }
240             #test-health th.diagonal-head {
241                 white-space: nowrap;
242                 height: 16rem;
243                 border-bottom: 0rem;
244             }
245             #test-health th.diagonal-head > div {
246                 transform: translate(1rem, 7rem) rotate(315deg);
247                 width: 2rem;
248                 border: 0rem;
249             }
250             #test-health tbody {
251                 display: block;
252                 overflow: auto;
253                 height: calc(100vh - 24rem);
254             }
255             #test-health td.status-cell {
256                 margin: 0;
257                 padding: 0;
258                 max-width: 2.2rem;
259                 max-height: 2.2rem;
260                 min-width: 2.2rem;
261                 min-height: 2.2rem;
262             }
263             #test-health td.blank-cell {
264                 margin: 0;
265                 padding: 0;
266                 max-width: 2.2rem;
267                 max-height: 2.2rem;
268                 min-width: 2.2rem;
269                 min-height: 2.2rem;
270             }
271             #test-health td.blank-cell > div  {
272                 background-color: #F9F9F9;
273                 height: 1.6rem;
274                 width: 1.6rem;
275                 margin: 0.1rem;
276                 padding: 0;
277                 position: relative;
278                 overflow: hidden;
279             }
280             #test-health td.blank-cell > div::before {
281                 content: "";
282                 position: absolute;
283                 top: -1px;
284                 left: -1px;
285                 display: block;
286                 width: 0px;
287                 height: 0px;
288                 border-right: calc(1.6rem + 1px) solid #ddd;
289                 border-top: calc(1.6rem + 1px) solid transparent;
290             }
291             #test-health td.blank-cell > div::after {
292                 content: "";
293                 display: block;
294                 position: absolute;
295                 top: 1px;
296                 left: 1px;
297                 width: 0px;
298                 height: 0px;
299                 border-right: calc(1.6rem - 1px) solid #F9F9F9;
300                 border-top: calc(1.6rem - 1px) solid transparent;
301             }
302             #tooltip-container {
303                 width: 22rem;
304                 height: 2rem;
305                 background-color: #34495E;
306                 opacity: 0.9;
307                 margin: 0.3rem;
308                 padding: 0.3rem;
309                 border-radius: 0.4rem;
310                 z-index: 1;
311                 text-align: center;
312             }
313             #tooltip-container::after {
314                 content: " ";
315                 position: absolute;
316                 top: 100%;
317                 left: 50%;
318                 margin-left: -1rem;
319                 border-width: 0.2rem;
320                 border-style: solid;
321                 border-color: #34495E transparent transparent transparent;
322             }
323             #tooltip-container p {
324                 color: white;
325                 margin: 0;
326             }
327             #tooltip-container a {
328                 color: #B03A2E;
329                 font-weight: bold;
330             }
331         `;
332     }
333
334     routeName() { return 'test-freshness'; }
335 }