github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/triage/model.js (about) 1 "use strict"; 2 3 // Return the minimum and maximum value of arr. 4 // Doesn't cause stack overflows like Math.min(...arr). 5 function minMaxArray(arr) { 6 var min = arr[0]; 7 var max = arr[0]; 8 for (var i = 1; i < arr.length; i++) { 9 if (arr[i] < min) min = arr[i]; 10 if (arr[i] > max) max = arr[i]; 11 } 12 return [min, max]; 13 } 14 15 function tsToString(ts) { 16 return new Date(ts * 1000).toLocaleString(); 17 } 18 19 // Store information about individual builds. 20 class Builds { 21 constructor(dict) { 22 this.jobs = dict.jobs; 23 this.jobPaths = dict.job_paths; 24 this.cols = dict.cols; 25 this.colStarted = this.cols.started; 26 this.colPr = this.cols.pr; 27 this.timespan = minMaxArray(this.cols.started); 28 this.runCount = this.cols.started.length; 29 } 30 31 // Create a build object given a job and build number. 32 get(job, number) { 33 let indices = this.jobs[job]; 34 if (indices.constructor === Array) { 35 let [start, count, base] = indices; 36 if (number < start || number > start + count) { 37 console.error('job ' + job + ' number ' + number + ' out of range.'); 38 return; 39 } 40 var index = base + (number - start); 41 } else { 42 var index = indices[number]; 43 } 44 // Add more columns as necessary. 45 // This is faster than dynamically adding properties to an object. 46 return { 47 job: job, 48 number: number, 49 started: this.colStarted[index], 50 pr: this.colPr[index], 51 }; 52 } 53 54 // Count how many builds a job has. 55 count(job) { 56 let indices = this.jobs[job]; 57 if (indices.constructor === Array) { 58 return indices[1]; 59 } 60 return Object.keys(indices).length; 61 } 62 63 getStartTime() { 64 return tsToString(this.timespan[0]); 65 } 66 67 getEndTime() { 68 return tsToString(this.timespan[1]); 69 } 70 } 71 72 function sum(arr, keyFunc) { 73 if (arr.length === 0) 74 return 0; 75 return arr.map(keyFunc).reduce((a, b) => a + b); 76 } 77 78 function clustersSum(tests) { 79 return sum(tests, t => sum(t.jobs, j => j.builds.length)); 80 } 81 82 // Return arr sorted by value according to keyFunc, which 83 // should take an element of arr and return an array of values. 84 function sortByKey(arr, keyFunc) { 85 var vals = arr.map((x, i) => [keyFunc(x), x]); 86 vals.sort((a, b) => { 87 for (var i = 0; i < a[0].length; i++) { 88 let elA = a[0][i], elB = b[0][i]; 89 if (elA > elB) return 1; 90 if (elA < elB) return -1; 91 } 92 }); 93 return vals.map(x => x[1]); 94 } 95 96 // Return a build for each test run that failed in the given cluster. 97 // Builds will be duplicated if it has multiple failed tests in the cluster. 98 function *buildsForCluster(entry) { 99 for (let test of entry.tests) { 100 for (let job of test.jobs) { 101 for (let number of job.builds) { 102 let build = builds.get(job.name, number); 103 if (build) { 104 yield build; 105 } 106 } 107 } 108 } 109 } 110 111 function *buildsWithContextForCluster(entry) { 112 for (let test of entry.tests) { 113 for (let job of test.jobs) { 114 for (let number of job.builds) { 115 let build = builds.get(job.name, number); 116 if (build) { 117 yield [build, job.name, test.name]; 118 } 119 } 120 } 121 } 122 } 123 124 // Return the number of builds that completed in the last day's worth of data. 125 function getHitsInLastDay(entry) { 126 if (entry.dayHits) { 127 return entry.dayHits; 128 } 129 var minStarted = builds.timespan[1] - 60 * 60 * 24; 130 var count = 0; 131 for (let build of buildsForCluster(entry)) { 132 if (build.started > minStarted) { 133 count++; 134 } 135 } 136 entry.dayHits = count; 137 return count; 138 } 139 140 // Store test clusters and support iterating and refiltering through them. 141 class Clusters { 142 constructor(clustered, clusterId) { 143 this.data = clustered; 144 this.length = this.data.length; 145 this.sum = sum(this.data, c => clustersSum(c.tests)); 146 this.sumRecent = sum(this.data, c => c.dayHits || 0); 147 this.byId = {}; 148 for (let cluster of this.data) { 149 let keyId = cluster.id; 150 if (!this.byId[keyId]) { 151 this.byId[keyId] = cluster; 152 } 153 } 154 if (clusterId !== undefined) { 155 this.clusterId = clusterId; 156 } 157 } 158 159 buildsForClusterById(clusterId) { 160 return buildsForCluster(this.byId[clusterId]); 161 } 162 163 buildsWithContextForClusterById(clusterId) { 164 return buildsWithContextForCluster(this.byId[clusterId]); 165 } 166 167 getHitsInLastDayById(clusterId) { 168 return getHitsInLastDay(this.byId[clusterId]); 169 } 170 171 // Iterate through all builds. Can return duplicates. 172 *allBuilds() { 173 for (let entry of this.data) { 174 yield *buildsForCluster(entry); 175 } 176 } 177 178 // Return a new Clusters object, with the given filters applied. 179 refilter(opts) { 180 var out = []; 181 for (let cluster of this.data) { 182 if (opts.reText && !opts.reText.test(cluster.text)) { 183 continue; 184 } 185 if (opts.sig && opts.sig.length && opts.sig.indexOf(cluster.owner) < 0) { 186 continue; 187 } 188 var testsOut = []; 189 for (let test of cluster.tests) { 190 if (opts.reTest && !opts.reTest.test(test.name)) { 191 continue; 192 } 193 var jobsOut = []; 194 for (let job of test.jobs) { 195 if (opts.reJob && !opts.reJob.test(job.name)) { 196 continue; 197 } 198 if (job.name.startsWith("pr:")) { 199 if (opts.pr) jobsOut.push(job); 200 } else if (job.name.indexOf(":") === -1) { 201 if (opts.ci) jobsOut.push(job); 202 } 203 } 204 if (jobsOut.length > 0) { 205 jobsOut = sortByKey(jobsOut, j => [-j.builds.length, j.name]); 206 testsOut.push({name: test.name, jobs: jobsOut}); 207 } 208 } 209 if (testsOut.length > 0) { 210 testsOut = sortByKey(testsOut, t => [-sum(t.jobs, j => j.builds.length)]); 211 out.push(Object.assign({}, cluster, {tests: testsOut})); 212 } 213 } 214 215 if (opts.sort) { 216 var keyFunc = { 217 total: c => [-clustersSum(c.tests)], 218 message: c => [c.text], 219 day: c => [-getHitsInLastDay(c), -clustersSum(c.tests)], 220 }[opts.sort]; 221 out = sortByKey(out, keyFunc); 222 } 223 224 return new Clusters(out); 225 } 226 227 makeCounts(clusterId) { 228 let start = builds.timespan[0]; 229 let width = 60 * 60 * 8; // 8 hours 230 231 function pickBucket(ts) { 232 return ((ts - start) / width) | 0; 233 } 234 235 let size = pickBucket(builds.timespan[1]) + 1; 236 237 let counts = {}; 238 239 function incr(key, bucket) { 240 if (counts[key] === undefined) { 241 counts[key] = new Uint32Array(size); 242 } 243 counts[key][bucket]++; 244 } 245 246 for (let [build, job, test] of this.buildsWithContextForClusterById(clusterId)) { 247 let bucket = pickBucket(build.started); 248 incr('', bucket); 249 incr(job, bucket); 250 incr(test, bucket); 251 incr(job + " " + test, bucket); 252 } 253 254 return counts; 255 } 256 } 257 258 if (typeof module !== 'undefined' && module.exports) { 259 // enable node.js `require` to work for testing 260 module.exports = { 261 Builds: Builds, 262 Clusters: Clusters, 263 } 264 }