Test freshness page should use build time instead of commit time to determine the...
[WebKit-https.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
13         this._loadConfig(summaryPageConfiguration);
14     }
15
16     name() { return 'Test-Freshness'; }
17
18     _loadConfig(summaryPageConfiguration)
19     {
20         const platformIdSet = new Set;
21         const metricIdSet = new Set;
22
23         for (const config of summaryPageConfiguration) {
24             for (const platformGroup of config.platformGroups) {
25                 for (const platformId of platformGroup.platforms)
26                     platformIdSet.add(platformId);
27             }
28
29             for (const metricGroup of config.metricGroups) {
30                 for (const subgroup of metricGroup.subgroups) {
31                     for (const metricId of subgroup.metrics)
32                         metricIdSet.add(metricId);
33                 }
34             }
35
36             const excludedConfigs = config.excludedConfigurations;
37             for (const platform in excludedConfigs) {
38                 if (platform in this._excludedConfigurations)
39                     this._excludedConfigurations[platform] = this._excludedConfigurations[platform].concat(excludedConfigs[platform]);
40                 else
41                     this._excludedConfigurations[platform] = excludedConfigs[platform];
42             }
43         }
44         this._platforms = [...platformIdSet].map((platformId) => Platform.findById(platformId));
45         this._metrics = [...metricIdSet].map((metricId) => Metric.findById(metricId));
46     }
47
48     open(state)
49     {
50         this._fetchTestResults();
51         super.open(state);
52     }
53
54     _fetchTestResults()
55     {
56         this._measurementSetFetchTime = Date.now();
57         this._lastDataPointByConfiguration = new Map;
58
59         const startTime = this._measurementSetFetchTime - this._timeDuration;
60
61         for (const platform of this._platforms) {
62             const lastDataPointByMetric = new Map;
63             this._lastDataPointByConfiguration.set(platform, lastDataPointByMetric);
64
65             for (const metric of this._metrics) {
66                 if (!this._isValidPlatformMetricCombination(platform, metric, this._excludedConfigurations))
67                     continue;
68
69                 const measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric));
70                 measurementSet.fetchBetween(startTime, this._measurementSetFetchTime).then(() => {
71                     const currentTimeSeries = measurementSet.fetchedTimeSeries('current', false, false);
72
73                     let timeForLastDataPoint = startTime;
74                     if (currentTimeSeries.lastPoint())
75                         timeForLastDataPoint = currentTimeSeries.lastPoint().build().buildTime();
76
77                     lastDataPointByMetric.set(metric, {time: timeForLastDataPoint, hasCurrentDataPoint: !!currentTimeSeries.lastPoint()});
78                     this.enqueueToRender();
79                 });
80             }
81         }
82     }
83
84     render()
85     {
86         super.render();
87
88         this._renderTableLazily.evaluate(this._platforms, this._metrics);
89
90         for (const [platform, lastDataPointByMetric] of this._lastDataPointByConfiguration.entries()) {
91             for (const [metric, lastDataPoint] of lastDataPointByMetric.entries()) {
92                 const timeDuration = this._measurementSetFetchTime - lastDataPoint.time;
93                 const timeDurationSummaryPrefix = lastDataPoint.hasCurrentDataPoint ? '' : 'More than ';
94                 const timeDurationSummary = BuildRequest.formatTimeInterval(timeDuration);
95                 const testLabel = `"${metric.test().fullName()}" for "${platform.name()}"`;
96                 const summary = `${timeDurationSummaryPrefix}${timeDurationSummary} since last data point on ${testLabel}`;
97                 const url = this._router.url('charts', ChartsPage.createStateForDashboardItem(platform.id(), metric.id(),
98                     this._measurementSetFetchTime - this._timeDuration));
99
100                 const indicator = this._indicatorByConfiguration.get(platform).get(metric);
101                 indicator.update(timeDuration, this._testAgeTolerance, summary, url);
102             }
103         }
104     }
105
106     _renderTable(platforms, metrics)
107     {
108         const element = ComponentBase.createElement;
109         const tableBodyElement = [];
110         const tableHeadElements = [element('th',  {class: 'table-corner'}, 'Platform \\ Test')];
111
112         for (const metric of metrics)
113             tableHeadElements.push(element('th', {class: 'diagonal-header'}, element('div', metric.test().fullName())));
114
115         this._indicatorByConfiguration = new Map;
116         for (const platform of platforms) {
117             const indicatorByMetric = new Map;
118             this._indicatorByConfiguration.set(platform, indicatorByMetric);
119             tableBodyElement.push(element('tr',
120                 [element('th', platform.label()), ...metrics.map((metric) => this._constructTableCell(platform, metric, indicatorByMetric))]));
121         }
122
123         this.renderReplace(this.content('test-health'), [element('thead', tableHeadElements), element('tbody', tableBodyElement)]);
124     }
125
126     _isValidPlatformMetricCombination(platform, metric)
127     {
128         return !(this._excludedConfigurations && this._excludedConfigurations[platform.id()]
129             && this._excludedConfigurations[platform.id()].some((metricId) => metricId == metric.id()))
130             && platform.hasMetric(metric);
131     }
132
133     _constructTableCell(platform, metric, indicatorByMetric)
134     {
135         const element = ComponentBase.createElement;
136
137         if (!this._isValidPlatformMetricCombination(platform, metric))
138             return element('td', {class: 'blank-cell'}, element('div'));
139
140         const indicator = new FreshnessIndicator;
141         indicatorByMetric.set(metric, indicator);
142         return element('td', {class: 'status-cell'}, indicator);
143     }
144
145     static htmlTemplate()
146     {
147         return `<section class="page-with-heading"><table id="test-health"></table></section>`;
148     }
149
150     static cssTemplate()
151     {
152         return `
153             .page-with-heading {
154                 display: flex;
155                 justify-content: center;
156             }
157             #test-health {
158                 font-size: 1rem;
159             }
160             #test-health th.table-corner {
161                 text-align: right;
162                 vertical-align: bottom;
163             }
164             #test-health th {
165                 text-align: left;
166                 border-bottom: 0.1rem solid #ccc;
167                 font-weight: normal;
168             }
169             #test-health th.diagonal-header {
170                 white-space: nowrap;
171                 height: 16rem;
172                 border-bottom: 0rem;
173             }
174             #test-health th.diagonal-header > div {
175                 transform: translate(1rem, 7rem) rotate(315deg);
176                 width: 2rem;
177                 border: 0rem;
178             }
179             #test-health td.status-cell {
180                 margin: 0;
181                 padding: 0;
182                 max-width: 2.2rem;
183                 max-height: 2.2rem;
184                 min-width: 2.2rem;
185                 min-height: 2.2rem;
186             }
187             #test-health td.blank-cell {
188                 margin: 0;
189                 padding: 0;
190                 max-width: 2.2rem;
191                 max-height: 2.2rem;
192                 min-width: 2.2rem;
193                 min-height: 2.2rem;
194             }
195             #test-health td.blank-cell > div  {
196                 background-color: #F9F9F9;
197                 height: 1.6rem;
198                 width: 1.6rem;
199                 margin: 0.1rem;
200                 padding: 0;
201                 position: relative;
202                 overflow: hidden;
203             }
204             #test-health td.blank-cell > div::before {
205               content: "";
206               position: absolute;
207               top: -1px;
208               left: -1px;
209               display: block;
210               width: 0px;
211               height: 0px;
212               border-right: calc(1.6rem + 1px) solid #ddd;
213               border-top: calc(1.6rem + 1px) solid transparent;
214             }
215             #test-health td.blank-cell > div::after {
216               content: "";
217               display: block;
218               position: absolute;
219               top: 1px;
220               left: 1px;
221               width: 0px;
222               height: 0px;
223               border-right: calc(1.6rem - 1px) solid #F9F9F9;
224               border-top: calc(1.6rem - 1px) solid transparent;
225             }
226         `;
227     }
228
229     routeName() { return 'test-freshness'; }
230 }