[perf dashboard] Test fressness popover sometimes point to wrong place
[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 tooltipTable = this.content('tooltip-table');
23         tooltipTable.addEventListener('mouseenter', () => {
24             this._hoveringTooltip = true;
25             this.enqueueToRender();
26         });
27         tooltipTable.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 timeForLatestBuild = startTime;
92                     let lastBuild = null;
93                     let builder = null;
94                     let commitSetOfLastPoint = null;
95                     const lastPoint = currentTimeSeries.lastPoint();
96                     if (lastPoint) {
97                         timeForLatestBuild = lastPoint.build().buildTime().getTime();
98                         const view = currentTimeSeries.viewBetweenPoints(currentTimeSeries.firstPoint(), lastPoint);
99                         for (const point of view) {
100                             const build = point.build();
101                             if (!build)
102                                 continue;
103                             if (build.buildTime().getTime() >= timeForLatestBuild) {
104                                 timeForLatestBuild = build.buildTime().getTime();
105                                 lastBuild = build;
106                                 builder = build.builder();
107                             }
108                         }
109                         commitSetOfLastPoint = lastPoint.commitSet();
110                     }
111
112                     lastDataPointByMetric.set(metric, {time: timeForLatestBuild, hasCurrentDataPoint: !!lastPoint,
113                         lastBuild, builder, commitSetOfLastPoint});
114                     this.enqueueToRender();
115                 });
116             }
117         }
118     }
119
120     render()
121     {
122         super.render();
123
124         this._renderTableLazily.evaluate(this._platforms, this._metrics);
125
126         let buildSummaryForCurrentlyHighlightedIndicator = null;
127         let buildForCurrentlyHighlightedIndicator = null;
128         let commitSetForCurrentHighlightedIndicator = null;
129         const builderForCurrentlyHighlightedIndicator = this._currentlyHighlightedIndicator ? this._builderByIndicator.get(this._currentlyHighlightedIndicator) : null;
130         for (const [platform, lastDataPointByMetric] of this._lastDataPointByConfiguration.entries()) {
131             for (const [metric, lastDataPoint] of lastDataPointByMetric.entries()) {
132                 const timeDuration = this._measurementSetFetchTime - lastDataPoint.time;
133                 const timeDurationSummaryPrefix = lastDataPoint.hasCurrentDataPoint ? '' : 'More than ';
134                 const timeDurationSummary = BuildRequest.formatTimeInterval(timeDuration);
135                 const summary = `${timeDurationSummaryPrefix}${timeDurationSummary} since latest data point.`;
136                 const url = this._router.url('charts', ChartsPage.createStateForDashboardItem(platform.id(), metric.id(),
137                     this._measurementSetFetchTime - this._timeDuration));
138
139                 const indicator = this._indicatorByConfiguration.get(platform).get(metric);
140                 if (this._currentlyHighlightedIndicator && this._currentlyHighlightedIndicator === indicator) {
141                     buildSummaryForCurrentlyHighlightedIndicator = summary;
142                     buildForCurrentlyHighlightedIndicator = lastDataPoint.lastBuild;
143                     commitSetForCurrentHighlightedIndicator = lastDataPoint.commitSetOfLastPoint;
144                 }
145                 this._builderByIndicator.set(indicator, lastDataPoint.builder);
146                 indicator.update(timeDuration, this._testAgeTolerance, url, builderForCurrentlyHighlightedIndicator && builderForCurrentlyHighlightedIndicator === lastDataPoint.builder);
147             }
148         }
149         this._renderTooltipLazily.evaluate(this._currentlyHighlightedIndicator, this._hoveringTooltip, buildSummaryForCurrentlyHighlightedIndicator, buildForCurrentlyHighlightedIndicator, commitSetForCurrentHighlightedIndicator);
150     }
151
152     _renderTooltip(indicator, hoveringTooltip, buildSummary, build, commitSet)
153     {
154         if (!indicator || !buildSummary) {
155             this.content('tooltip-anchor').style.display = hoveringTooltip ? null : 'none';
156             return;
157         }
158         const element = ComponentBase.createElement;
159         const link = ComponentBase.createLink;
160
161         const rect = indicator.element().getBoundingClientRect();
162         const tooltipAnchor = this.content('tooltip-anchor');
163         tooltipAnchor.style.display = null;
164         tooltipAnchor.style.top = rect.top + 'px';
165         tooltipAnchor.style.left = rect.left + rect.width / 2 + 'px';
166
167         let tableContent = [element('tr', element('td', {colspan: 2}, buildSummary))];
168
169         if (commitSet) {
170             if (commitSet.repositories().length)
171                 tableContent.push(element('tr', element('th', {colspan: 2}, 'Latest build information')));
172
173             tableContent.push(Repository.sortByNamePreferringOnesWithURL(commitSet.repositories()).map((repository) => {
174                 const commit = commitSet.commitForRepository(repository);
175                 return element('tr', [
176                     element('td', repository.name()),
177                     element('td', commit.url() ? link(commit.label(), commit.label(), commit.url(), true) : commit.label())
178                 ]);
179             }));
180         }
181
182         if (build) {
183             const url = build.url();
184             const buildNumber = build.buildNumber();
185             tableContent.push(element('tr', [
186                 element('td', 'Build'),
187                 element('td', {colspan: 2}, [
188                     url ? link(buildNumber, build.label(), url, true) : buildNumber
189                 ]),
190             ]));
191         }
192
193         this.renderReplace(this.content("tooltip-table"),  tableContent);
194     }
195
196     _renderTable(platforms, metrics)
197     {
198         const element = ComponentBase.createElement;
199         const tableBodyElement = [];
200         const tableHeadElements = [element('th',  {class: 'table-corner row-head'}, 'Platform \\ Test')];
201
202         for (const metric of metrics)
203             tableHeadElements.push(element('th', {class: 'diagonal-head'}, element('div', metric.test().fullName())));
204
205         this._indicatorByConfiguration = new Map;
206         for (const platform of platforms) {
207             const indicatorByMetric = new Map;
208             this._indicatorByConfiguration.set(platform, indicatorByMetric);
209             tableBodyElement.push(element('tr',
210                 [element('th', {class: 'row-head'}, platform.label()), ...metrics.map((metric) => this._constructTableCell(platform, metric, indicatorByMetric))]));
211         }
212
213         this.renderReplace(this.content('test-health'), [element('thead', tableHeadElements), element('tbody', tableBodyElement)]);
214     }
215
216     _isValidPlatformMetricCombination(platform, metric)
217     {
218         return !(this._excludedConfigurations && this._excludedConfigurations[platform.id()]
219             && this._excludedConfigurations[platform.id()].some((metricId) => metricId == metric.id()))
220             && platform.hasMetric(metric);
221     }
222
223     _constructTableCell(platform, metric, indicatorByMetric)
224     {
225         const element = ComponentBase.createElement;
226
227         if (!this._isValidPlatformMetricCombination(platform, metric))
228             return element('td', {class: 'blank-cell'}, element('div'));
229
230         const indicator = new FreshnessIndicator;
231         indicator.listenToAction('select', (originator) => {
232             this._currentlyHighlightedIndicator = originator;
233             this.enqueueToRender();
234         });
235         indicator.listenToAction('unselect', () => {
236             this._currentlyHighlightedIndicator = null;
237             this.enqueueToRender();
238         });
239         indicatorByMetric.set(metric, indicator);
240         return element('td', {class: 'status-cell'}, indicator);
241     }
242
243     static htmlTemplate()
244     {
245         return `<section class="page-with-heading">
246             <div id="tooltip-anchor">
247                 <table id="tooltip-table"></table>
248             </div>
249             <table id="test-health"></table>
250         </section>`;
251     }
252
253     static cssTemplate()
254     {
255         return `
256             .page-with-heading {
257                 display: flex;
258                 justify-content: center;
259             }
260             #test-health {
261                 font-size: 1rem;
262             }
263             #test-health thead {
264                 display: block;
265                 align: right;
266             }
267             #test-health th.table-corner {
268                 text-align: right;
269                 vertical-align: bottom;
270             }
271             #test-health .row-head {
272                 min-width: 15.5rem;
273             }
274             #test-health th {
275                 text-align: left;
276                 border-bottom: 0.1rem solid #ccc;
277                 font-weight: normal;
278             }
279             #test-health th.diagonal-head {
280                 white-space: nowrap;
281                 height: 16rem;
282                 width: 2.2rem;
283                 border-bottom: 0rem;
284                 padding: 0;
285             }
286             #test-health th.diagonal-head > div {
287                 transform: translate(1.1rem, 7.5rem) rotate(315deg);
288                 transform-origin: center left;
289                 width: 2.2rem;
290                 border: 0rem;
291             }
292             #test-health tbody {
293                 display: block;
294                 overflow: auto;
295                 height: calc(100vh - 24rem);
296             }
297             #test-health td.status-cell {
298                 margin: 0;
299                 padding: 0;
300                 max-width: 2.2rem;
301                 max-height: 2.2rem;
302                 min-width: 2.2rem;
303                 min-height: 2.2rem;
304             }
305             #test-health td.blank-cell {
306                 margin: 0;
307                 padding: 0;
308                 max-width: 2.2rem;
309                 max-height: 2.2rem;
310                 min-width: 2.2rem;
311                 min-height: 2.2rem;
312             }
313             #test-health td.blank-cell > div  {
314                 background-color: #F9F9F9;
315                 height: 1.6rem;
316                 width: 1.6rem;
317                 margin: auto;
318                 padding: 0;
319                 position: relative;
320                 overflow: hidden;
321             }
322             #test-health td.blank-cell > div::before {
323                 content: "";
324                 position: absolute;
325                 top: -1px;
326                 left: -1px;
327                 display: block;
328                 width: 0px;
329                 height: 0px;
330                 border-right: calc(1.6rem + 1px) solid #ddd;
331                 border-top: calc(1.6rem + 1px) solid transparent;
332             }
333             #test-health td.blank-cell > div::after {
334                 content: "";
335                 display: block;
336                 position: absolute;
337                 top: 1px;
338                 left: 1px;
339                 width: 0px;
340                 height: 0px;
341                 border-right: calc(1.6rem - 1px) solid #F9F9F9;
342                 border-top: calc(1.6rem - 1px) solid transparent;
343             }
344             #tooltip-anchor {
345                 width: 0;
346                 height: 0;
347                 position: absolute;
348             }
349             #tooltip-table {
350                 position: absolute;
351                 width: 22rem;
352                 background-color: #34495E;
353                 opacity: 0.9;
354                 margin: 0.3rem;
355                 padding: 0.3rem;
356                 border-radius: 0.4rem;
357                 z-index: 1;
358                 text-align: center;
359                 display: inline-table;
360                 color: white;
361                 bottom: 0;
362                 left: -11.3rem;
363             }
364             #tooltip-table td {
365                 overflow: hidden;
366                 max-width: 22rem;
367                 text-overflow: ellipsis;
368             }
369             #tooltip-table::after {
370                 content: " ";
371                 position: absolute;
372                 top: 100%;
373                 left: 50%;
374                 margin-left: -0.3rem;
375                 border-width: 0.3rem;
376                 border-style: solid;
377                 border-color: #34495E transparent transparent transparent;
378             }
379             #tooltip-table a {
380                 color: #B03A2E;
381                 font-weight: bold;
382             }
383         `;
384     }
385
386     routeName() { return 'test-freshness'; }
387 }