| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | 'use strict'; |
| 4 | |
| 5 | // Combines the per-worker shard timings of the current run |
| 6 | // (build/__shard_timings__/*.json, written by scripts/rollup/build.js) into |
| 7 | // build-weights.json, which the workflow then saves back to the actions |
| 8 | // cache. The previous weights (restored to build-weights.json by the |
| 9 | // workflow) are only used to log a diff for variance monitoring; the new |
| 10 | // measurement is always written verbatim so that removed bundles drop out |
| 11 | // instead of accumulating. If the logged variance turns out to be too high |
| 12 | // for stable shards, weights should be aggregated across the last N runs |
| 13 | // instead. Weights feed scripts/rollup/sharding.js. This script never fails: |
| 14 | // the weights are an optimization, so a broken update must not break |
| 15 | // artifact processing. |
| 16 | |
| 17 | const fs = require('fs'); |
| 18 | |
| 19 | const TIMINGS_DIR = 'build/__shard_timings__'; |
| 20 | // The workflow restores the previous weights to OUT_PATH itself, so this |
| 21 | // script reads them from there before overwriting with the new measurement. |
| 22 | const OUT_PATH = 'build-weights.json'; |
| 23 | |
| 24 | function logDiff(fresh, previous) { |
| 25 | const deltas = []; |
| 26 | Object.keys(fresh).forEach(key => { |
| 27 | if (previous[key] !== undefined) { |
| 28 | deltas.push({key, delta: fresh[key] - previous[key]}); |
| 29 | } |
| 30 | }); |
| 31 | if (deltas.length === 0) { |
| 32 | return; |
| 33 | } |
| 34 | deltas.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta)); |
| 35 | const absDeltas = deltas |
| 36 | .map(entry => Math.abs(entry.delta)) |
| 37 | .sort((a, b) => a - b); |
| 38 | const mean = |
| 39 | absDeltas.reduce((sum, delta) => sum + delta, 0) / absDeltas.length; |
| 40 | const p95 = absDeltas[Math.floor(absDeltas.length * 0.95)]; |
| 41 | console.log( |
| 42 | `Weight changes vs the previous run: mean |delta| = ${mean.toFixed(2)}s, ` + |
| 43 | `p95 = ${p95.toFixed(1)}s across ${deltas.length} bundles. ` + |
| 44 | 'High variance here means shards should be determined from multiple runs.' |
| 45 | ); |
| 46 | console.log('Largest changes:'); |
| 47 | deltas.slice(0, 10).forEach(entry => { |
| 48 | console.log( |
| 49 | ` ${entry.delta >= 0 ? '+' : ''}${entry.delta.toFixed(1)}s ${entry.key}` |
| 50 | ); |
| 51 | }); |
| 52 | } |
| 53 | |
| 54 | function main() { |
| 55 | const fresh = {}; |
| 56 | const files = fs |
| 57 | .readdirSync(TIMINGS_DIR) |
| 58 | .filter(name => name.endsWith('.json')); |
| 59 | files.forEach(name => { |
| 60 | const timings = JSON.parse( |
| 61 | fs.readFileSync(TIMINGS_DIR + '/' + name, 'utf8') |
| 62 | ); |
| 63 | Object.keys(timings).forEach(key => { |
| 64 | fresh[key] = timings[key]; |
| 65 | }); |
| 66 | }); |
| 67 | const freshKeys = Object.keys(fresh); |
| 68 | let previous = {}; |
| 69 | try { |
| 70 | previous = JSON.parse(fs.readFileSync(OUT_PATH, 'utf8')).weights; |
| 71 | } catch (error) { |
| 72 | if (error.code !== 'ENOENT') { |
| 73 | throw error; |
| 74 | } |
| 75 | // Expected before the first weights have ever been saved. |
| 76 | console.log('No previous weights found, skipping the diff.'); |
| 77 | } |
| 78 | logDiff(fresh, previous); |
| 79 | fs.writeFileSync( |
| 80 | OUT_PATH, |
| 81 | JSON.stringify({version: 1, weights: fresh}, null, 2) + '\n' |
| 82 | ); |
| 83 | console.log(`Wrote ${freshKeys.length} weights to ${OUT_PATH}.`); |
| 84 | } |
| 85 | |
| 86 | try { |
| 87 | main(); |
| 88 | } catch (error) { |
| 89 | console.log( |
| 90 | 'Could not update build shard weights, keeping the previous ones.', |
| 91 | error |
| 92 | ); |
| 93 | // The restored previous weights may still sit at OUT_PATH; delete them so |
| 94 | // the save step cannot republish data this run did not produce. |
| 95 | fs.rmSync(OUT_PATH, {force: true}); |
| 96 | } |