k8s.io/test-infra/triage@v0.0.0-20240520184403-27c6b4c223d8/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            (opts.reXText && opts.reXText.test(cluster.text))) {
   184          continue;
   185        }
   186        if (opts.sig && opts.sig.length && opts.sig.indexOf(cluster.owner) < 0) {
   187          continue;
   188        }
   189        var testsOut = [];
   190        for (let test of cluster.tests) {
   191          if ((opts.reTest && !opts.reTest.test(test.name)) ||
   192              (opts.reXTest && opts.reXTest.test(test.name))) {
   193            continue;
   194          }
   195          var jobsOut = [];
   196          for (let job of test.jobs) {
   197            if ((opts.reJob && !opts.reJob.test(job.name)) ||
   198                (opts.reXJob && opts.reXJob.test(job.name))) {
   199              continue;
   200            }
   201            if (job.name.startsWith("pr:")) {
   202              if (opts.pr) jobsOut.push(job);
   203            } else if (job.name.indexOf(":") === -1) {
   204              if (opts.ci) jobsOut.push(job);
   205            }
   206          }
   207          if (jobsOut.length > 0) {
   208            jobsOut = sortByKey(jobsOut, j => [-j.builds.length, j.name]);
   209            testsOut.push({name: test.name, jobs: jobsOut});
   210          }
   211        }
   212        if (testsOut.length > 0) {
   213          testsOut = sortByKey(testsOut, t => [-sum(t.jobs, j => j.builds.length)]);
   214          out.push(Object.assign({}, cluster, {tests: testsOut}));
   215        }
   216      }
   217  
   218      if (opts.sort) {
   219        var keyFunc = {
   220          total: c => [-clustersSum(c.tests)],
   221          message: c => [c.text],
   222          day: c => [-getHitsInLastDay(c), -clustersSum(c.tests)],
   223        }[opts.sort];
   224        out = sortByKey(out, keyFunc);
   225      }
   226  
   227      return new Clusters(out);
   228    }
   229  
   230    makeCounts(clusterId) {
   231      let start = builds.timespan[0];
   232      let width = 60 * 60 * 8;  // 8 hours
   233  
   234      function pickBucket(ts) {
   235        return ((ts - start) / width) | 0;
   236      }
   237  
   238      let size = pickBucket(builds.timespan[1]) + 1;
   239  
   240      let counts = {};
   241  
   242      function incr(key, bucket) {
   243        if (counts[key] === undefined) {
   244          counts[key] = new Uint32Array(size);
   245        }
   246        counts[key][bucket]++;
   247      }
   248  
   249      for (let [build, job, test] of this.buildsWithContextForClusterById(clusterId)) {
   250        let bucket = pickBucket(build.started);
   251        incr('', bucket);
   252        incr(job, bucket);
   253        incr(test, bucket);
   254        incr(job + " " + test, bucket);
   255      }
   256  
   257      return counts;
   258    }
   259  }
   260  
   261  if (typeof module !== 'undefined' && module.exports) {
   262    // enable node.js `require` to work for testing
   263    module.exports = {
   264      Builds: Builds,
   265      Clusters: Clusters,
   266    }
   267  }