forked from Ecosystem-Infra/wpt-results-analysis
-
Notifications
You must be signed in to change notification settings - Fork 0
/
browser-specific-failures.js
272 lines (242 loc) · 9.61 KB
/
browser-specific-failures.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
'use strict';
/**
* Implements a view of how many browser specific failures each engine has over
* time.
*/
const fetch = require('node-fetch');
const fs = require('fs');
const flags = require('flags');
const Git = require('nodegit');
const lib = require('./lib');
const moment = require('moment');
flags.defineString('from', '2018-07-01', 'Starting date (inclusive)');
flags.defineString('to', moment().format('YYYY-MM-DD'),
'Ending date (exclusive)');
flags.defineString('baseline', null, 'A YYYY-MM-DD date to \'pin\' WPT to. ' +
'Any test name not in existence on the baseline date will be ignored.');
flags.defineStringList('products', ['chrome', 'firefox', 'safari'],
'Browsers to compare. Must match the products used on wpt.fyi');
flags.defineString('output', null,
'Output CSV file to write to. Defaults to ' +
'{stable, experimental}-browser-specific-failures.csv');
flags.defineBoolean('experimental', false,
'Calculate metrics for experimental runs.');
flags.parse();
// YYYY-MM-DD
const BASELINE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
// See documentation of advanceDateToSkipBadDataIfNecessary. These ranges are
// inclusive, exclusive.
const STABLE_BAD_RANGES = [
// This was some form of Safari outage, undiagnosed but a clear erroneous
// spike in failure rates.
[moment('2019-02-06'), moment('2019-03-04')],
// This was a safaridriver outage, resolved by
// https://github.com/web-platform-tests/wpt/pull/18585
[moment('2019-06-27'), moment('2019-08-23')],
];
const EXPERIMENTAL_BAD_RANGES = [
// This was a safaridriver outage, resolved by
// https://github.com/web-platform-tests/wpt/pull/18585
[moment('2019-06-27'), moment('2019-08-23')],
];
// There have been periods where results cannot be considered valid and
// contribute noise to the metrics. Given a date, this function advances it as
// necessary to avoid bad data.
//
// TODO(smcgruer): Take into account --products being used.
function advanceDateToSkipBadDataIfNecessary(date, experimental) {
const ranges = experimental ? EXPERIMENTAL_BAD_RANGES : STABLE_BAD_RANGES;
for (const range of ranges) {
if (date >= range[0] && date < range[1]) {
console.log(`Skipping from ${date.format('YYYY-MM-DD')} to ` +
`${range[1].format('YYYY-MM-DD')} due to bad data`);
return range[1];
}
}
return date;
}
const RUNS_URI = 'https://wpt.fyi/api/runs?aligned=true&max-count=1';
// Fetches aligned runs from the wpt.fyi server, between the |from| and |to|
// dates. If |experimental| is true fetch experimental runs, else stable runs.
// Returns a map of date to list of runs for that date (one per product)
//
// TODO: Known problem: there are periods of time, mostly mid-late 2018, where
// we ran both Safari 11.1 and 12.1, and the results are massively different.
// We should fetch multiple runs for each browser and have upgrade logic.
async function fetchAlignedRunsFromServer(products, from, to, experimental) {
const label = experimental ? 'experimental' : 'stable';
let params = `&label=master&label=${label}`;
for (const product of products) {
params += `&product=${product}`;
}
const runsUri = `${RUNS_URI}${params}`;
console.log(`Fetching aligned runs from ${from.format('YYYY-MM-DD')} ` +
`to ${to.format('YYYY-MM-DD')}`);
let cachedCount = 0;
const before = moment();
const alignedRuns = new Map();
while (from < to) {
const formattedFrom = from.format('YYYY-MM-DD');
from.add(1, 'days');
const formattedTo = from.format('YYYY-MM-DD');
// We advance the date (if necessary) before doing anything more, so that
// code later in the loop body can just 'continue' without checking.
from = advanceDateToSkipBadDataIfNecessary(from, experimental);
// Attempt to read the runs from the cache.
// TODO: Consider https://github.com/tidoust/fetch-filecache-for-crawling
let runs;
const cacheFile =
`cache/${label}-${products.join('-')}-runs-${formattedFrom}.json`;
try {
runs = JSON.parse(await fs.promises.readFile(cacheFile));
if (runs.length) {
cachedCount++;
}
} catch (e) {
// No cache hit; load from the server instead.
const url = `${runsUri}&from=${formattedFrom}&to=${formattedTo}`;
const response = await fetch(url);
// Many days do not have an aligned set of runs, but we always write to
// the cache to speed up future executions of this code.
runs = await response.json();
await fs.promises.writeFile(cacheFile, JSON.stringify(runs));
}
if (!runs.length) {
continue;
}
if (runs.length !== products.length) {
throw new Error(
`Fetched ${runs.length} runs, expected ${products.length}`);
}
alignedRuns.set(formattedFrom, runs);
}
const after = moment();
console.log(`Fetched ${alignedRuns.size} sets of runs in ` +
`${after - before} ms (${cachedCount} cached)`);
return alignedRuns;
}
async function main() {
const baseline = flags.get('baseline');
if (baseline != null && !BASELINE_REGEX.test(baseline)) {
throw new Error('--baseline must be in the form YYYY-MM-DD');
}
// Sort the products so that output files are consistent.
const products = flags.get('products');
if (products.length < 2) {
throw new Error('At least 2 products must be specified for this analysis');
}
products.sort();
const repo = await Git.Repository.open('wpt-results.git');
// First, grab aligned runs from the server for the dates that we are
// interested in.
const from = moment(flags.get('from'));
const to = moment(flags.get('to'));
const experimental = flags.get('experimental');
const alignedRuns = await fetchAlignedRunsFromServer(
products, from, to, experimental);
// Verify that we have data for the fetched runs in the wpt-results repo.
console.log('Getting local set of run ids from repo');
let before = Date.now();
const localRunIds = await lib.results.getLocalRunIds(repo);
let after = Date.now();
console.log(`Found ${localRunIds.size} ids (took ${after - before} ms)`);
let hadErrors = false;
for (const [date, runs] of alignedRuns.entries()) {
for (const run of runs) {
if (!localRunIds.has(run.id)) {
// If you see this, you probably need to run git-write.js or just update
// your wpt-results.git repo; see the README.md.
console.error(`Run ${run.id} missing from local git repo (${date})`);
hadErrors = true;
}
}
}
if (hadErrors) {
throw new Error('Missing data for some runs (see errors logged above). ' +
'Try running "git fetch --all --tags" in wpt-results/');
}
// Load the test result trees into memory; creates a list of recursive tree
// structures: tree = { trees: [...], tests: [...] }. Each 'tree' represents a
// directory, each 'test' is the results from a given test file.
console.log('Iterating over all runs, loading test results');
before = Date.now();
for (const runs of alignedRuns.values()) {
for (const run of runs) {
// Just in case someone ever adds a 'tree' field to the JSON.
if (run.tree) {
throw new Error('Run JSON contains "tree" field; code needs changed.');
}
run.tree = await lib.results.getGitTree(repo, run);
}
}
after = Date.now();
console.log(`Loading ${alignedRuns.size} sets of runs took ` +
`${after - before} ms`);
const options = {};
if (baseline) {
// Gather the union of all test names known to the 'base' run.
console.log(`Determining the set of base 'known' tests.`);
// TODO(smcgruer): Use nearest next date instead.
if (!alignedRuns.has(baseline)) {
throw new Error(`Baseline date ${baseline} not present in test data.`);
}
const testNames = new Set();
for (const run of alignedRuns.get(baseline)) {
lib.results.walkTests(run.tree, (path, test, _) => {
testNames.add(path + '/' + test);
});
}
console.log(`Found ${testNames.size} tests for the base set.`);
options.testFilter = testPath => testNames.has(testPath);
}
// We're ready to score the runs now!
console.log('Calculating browser-specific failures for the runs');
before = Date.now();
const dateToScores = new Map();
for (const [date, runs] of alignedRuns.entries()) {
// The SHA should be the same for all runs, so just grab the first.
const sha = runs[0].full_revision_hash;
try {
const scores = lib.browserSpecific.scoreBrowserSpecificFailures(
runs, new Set(products), options);
dateToScores.set(date, {sha, scores});
} catch (e) {
e.message += `\n\tRuns: ${runs.map(r => r.id)}`;
throw e;
}
}
after = Date.now();
console.log(`Done scoring (took ${after - before} ms)`);
// Finally, time to dump stuff.
let outputFilename = flags.get('output');
if (!outputFilename) {
outputFilename = experimental ?
'experimental-browser-specific-failures.csv' :
'stable-browser-specific-failures.csv';
}
console.log(`Writing data to ${outputFilename}`);
let data = `sha,date,${products.join(',')}\n`;
// ES6 maps iterate in insertion order, and we initially inserted in date
// order, so we can just iterate |dateToScores|.
for (const [date, shaAndScores] of dateToScores) {
const sha = shaAndScores.sha;
const scores = shaAndScores.scores;
if (!scores) {
console.log(`ERROR: ${date} had no scores`);
continue;
}
const csvRecord = [
sha,
date.substr(0, 10),
scores.get('chrome'),
scores.get('firefox'),
scores.get('safari'),
];
data += csvRecord.join(',') + '\n';
}
await fs.promises.writeFile(outputFilename, data, 'utf-8');
}
main().catch(reason => {
console.error(reason);
process.exit(1);
});