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  }