main
js 104 lines 3.24 KB
Raw
1 'use strict';
2
3 const fs = require('fs');
4
5 function readWeights() {
6 const weightsPath = process.env.BUILD_SHARD_WEIGHTS;
7 if (!weightsPath) {
8 return null;
9 }
10 let weights;
11 try {
12 weights = JSON.parse(fs.readFileSync(weightsPath, 'utf8')).weights;
13 } catch (error) {
14 if (error.code === 'ENOENT') {
15 // Expected before the first weights have ever been saved.
16 console.log('No build shard weights found, using round-robin sharding.');
17 return null;
18 }
19 throw error;
20 }
21 if (weights === null || typeof weights !== 'object') {
22 return null;
23 }
24 return weights;
25 }
26
27 // Persists the durations this worker measured so that
28 // process_artifacts_combined can merge them into the shared weights cache.
29 // No-op outside sharded CI builds.
30 function writeShardTimings(timings) {
31 const nodeIndex = process.env.CI_INDEX;
32 if (!process.env.CI_TOTAL || !nodeIndex) {
33 return;
34 }
35 const dir = 'build/__shard_timings__';
36 fs.mkdirSync(dir, {recursive: true});
37 const result = {};
38 timings.forEach(timing => {
39 result[timing.key] = Math.round(timing.seconds * 10) / 10;
40 });
41 fs.writeFileSync(
42 dir + '/' + nodeIndex + '-' + process.env.RELEASE_CHANNEL + '.json',
43 JSON.stringify(result)
44 );
45 }
46
47 // Assigns work items to CI workers. With measured per-item durations (see
48 // scripts/ci/merge-build-weights.js), items are assigned longest-first to
49 // the currently least-loaded worker so that workers finish around the same
50 // time. Every worker computes the full assignment and then picks its own
51 // bin, so the ordering below must stay deterministic. Without weights we
52 // fall back to round-robin.
53 function selectShard(items, keyFn, nodeTotal, nodeIndex) {
54 const weights = readWeights();
55 const weightedKeys = weights === null ? [] : Object.keys(weights);
56 if (weightedKeys.length === 0) {
57 return items.filter((_, i) => i % nodeTotal === nodeIndex);
58 }
59 const keys = items.map(keyFn);
60 const sortedWeights = weightedKeys
61 .map(key => weights[key])
62 .sort((a, b) => a - b);
63 const defaultWeight = sortedWeights[Math.floor(sortedWeights.length / 2)];
64 const weightOf = index => {
65 const weight = weights[keys[index]];
66 return weight === undefined ? defaultWeight : weight;
67 };
68 const order = items
69 .map((_, i) => i)
70 .sort((a, b) => {
71 const delta = weightOf(b) - weightOf(a);
72 if (delta !== 0) {
73 return delta;
74 }
75 if (keys[a] !== keys[b]) {
76 return keys[a] < keys[b] ? -1 : 1;
77 }
78 return a - b;
79 });
80 const bins = [];
81 for (let i = 0; i < nodeTotal; i++) {
82 bins.push({load: 0, indices: []});
83 }
84 order.forEach(i => {
85 // The first bin wins ties so that the assignment stays deterministic.
86 let target = bins[0];
87 for (let j = 1; j < bins.length; j++) {
88 if (bins[j].load < target.load) {
89 target = bins[j];
90 }
91 }
92 target.load += weightOf(i);
93 target.indices.push(i);
94 });
95 const shard = bins[nodeIndex].indices.sort((a, b) => a - b);
96 console.log(
97 `Sharding by measured build time: worker ${nodeIndex + 1}/${nodeTotal} ` +
98 `builds ${shard.length} of ${items.length} bundles ` +
99 `(~${Math.round(bins[nodeIndex].load)}s of rollup time).`
100 );
101 return shard.map(i => items[i]);
102 }
103
104 module.exports = {selectShard, writeShardTimings};