Skip to content

Commit 0ca313a

Browse files
committed
ci: add randomized build matrix
See https://github.com/vlsi/github-actions-random-matrix
1 parent 301410a commit 0ca313a

File tree

3 files changed

+321
-10
lines changed

3 files changed

+321
-10
lines changed

.github/workflows/matrix.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// The script generates a random subset of valid jdk, os, timezone, and other axes.
2+
// You can preview the results by running "node matrix.js"
3+
// See https://github.com/vlsi/github-actions-random-matrix
4+
let {MatrixBuilder} = require('./matrix_builder');
5+
const matrix = new MatrixBuilder();
6+
matrix.addAxis({
7+
name: 'java_distribution',
8+
values: [
9+
'zulu',
10+
'temurin',
11+
'liberica',
12+
'microsoft',
13+
]
14+
});
15+
16+
// TODO: support different JITs (see https://github.com/actions/setup-java/issues/279)
17+
matrix.addAxis({name: 'jit', title: '', values: ['hotspot']});
18+
19+
matrix.addAxis({
20+
name: 'java_version',
21+
// Strings allow versions like 18-ea
22+
values: [
23+
'8',
24+
'11',
25+
'17',
26+
]
27+
});
28+
29+
matrix.addAxis({
30+
name: 'tz',
31+
values: [
32+
'America/New_York',
33+
'Pacific/Chatham',
34+
'UTC'
35+
]
36+
});
37+
38+
matrix.addAxis({
39+
name: 'os',
40+
title: x => x.replace('-latest', ''),
41+
values: [
42+
'ubuntu-latest',
43+
'windows-latest',
44+
'macos-latest'
45+
]
46+
});
47+
48+
// Test cases when Object#hashCode produces the same results
49+
// It allows capturing cases when the code uses hashCode as a unique identifier
50+
matrix.addAxis({
51+
name: 'hash',
52+
values: [
53+
{value: 'regular', title: '', weight: 42},
54+
{value: 'same', title: 'same hashcode', weight: 1}
55+
]
56+
});
57+
matrix.addAxis({
58+
name: 'locale',
59+
title: x => x.language + '_' + x.country,
60+
values: [
61+
{language: 'de', country: 'DE'},
62+
{language: 'fr', country: 'FR'},
63+
{language: 'ru', country: 'RU'},
64+
{language: 'tr', country: 'TR'},
65+
]
66+
});
67+
68+
matrix.setNamePattern(['java_version', 'java_distribution', 'hash', 'os', 'tz', 'locale']);
69+
70+
// Microsoft Java has no distribution for 8
71+
matrix.exclude({java_distribution: 'microsoft', java_version: 8});
72+
// Ensure at least one job with "same" hashcode exists
73+
matrix.generateRow({hash: {value: 'same'}});
74+
// Ensure at least one Windows and at least one Linux job is present (macOS is almost the same as Linux)
75+
matrix.generateRow({os: 'windows-latest'});
76+
matrix.generateRow({os: 'ubuntu-latest'});
77+
// Ensure there will be at least one job with Java 8
78+
matrix.generateRow({java_version: 8});
79+
// Ensure there will be at least one job with Java 11
80+
matrix.generateRow({java_version: 11});
81+
// Ensure there will be at least one job with Java 17
82+
matrix.generateRow({java_version: 17});
83+
const include = matrix.generateRows(process.env.MATRIX_JOBS || 5);
84+
if (include.length === 0) {
85+
throw new Error('Matrix list is empty');
86+
}
87+
include.sort((a, b) => a.name.localeCompare(b.name, undefined, {numeric: true}));
88+
include.forEach(v => {
89+
let jvmArgs = [];
90+
if (v.hash.value === 'same') {
91+
jvmArgs.push('-XX:+UnlockExperimentalVMOptions', '-XX:hashCode=2');
92+
}
93+
// Gradle does not work in tr_TR locale, so pass locale to test only: https://github.com/gradle/gradle/issues/17361
94+
jvmArgs.push(`-Duser.country=${v.locale.country}`);
95+
jvmArgs.push(`-Duser.language=${v.locale.language}`);
96+
if (v.jit === 'hotspot' && Math.random() > 0.5) {
97+
// The following options randomize instruction selection in JIT compiler
98+
// so it might reveal missing synchronization in TestNG code
99+
v.name += ', stress JIT';
100+
jvmArgs.push('-XX:+UnlockDiagnosticVMOptions');
101+
if (v.java_version >= 8) {
102+
// Randomize instruction scheduling in GCM
103+
// share/opto/c2_globals.hpp
104+
jvmArgs.push('-XX:+StressGCM');
105+
// Randomize instruction scheduling in LCM
106+
// share/opto/c2_globals.hpp
107+
jvmArgs.push('-XX:+StressLCM');
108+
}
109+
if (v.java_version >= 16) {
110+
// Randomize worklist traversal in IGVN
111+
// share/opto/c2_globals.hpp
112+
jvmArgs.push('-XX:+StressIGVN');
113+
}
114+
if (v.java_version >= 17) {
115+
// Randomize worklist traversal in CCP
116+
// share/opto/c2_globals.hpp
117+
jvmArgs.push('-XX:+StressCCP');
118+
}
119+
}
120+
v.testExtraJvmArgs = jvmArgs.join(' ');
121+
delete v.hash;
122+
});
123+
124+
console.log(include);
125+
console.log('::set-output name=matrix::' + JSON.stringify({include}));
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// License: Apache-2.0
2+
// Copyright Vladimir Sitnikov, 2021
3+
// See https://github.com/vlsi/github-actions-random-matrix
4+
5+
class Axis {
6+
constructor({name, title, values}) {
7+
this.name = name;
8+
this.title = title;
9+
this.values = values;
10+
// If all entries have same weight, the axis has uniform distribution
11+
this.uniform = values.reduce((a, b) => a === (b.weight || 1) ? a : 0, values[0].weight || 1) !== 0
12+
this.totalWeigth = this.uniform ? values.length : values.reduce((a, b) => a + (b.weight || 1), 0);
13+
}
14+
15+
static matches(row, filter) {
16+
if (typeof filter === 'function') {
17+
return filter(row);
18+
}
19+
if (Array.isArray(filter)) {
20+
// e.g. row={os: 'windows'}; filter=[{os: 'linux'}, {os: 'linux'}]
21+
return filter.find(v => Axis.matches(row, v));
22+
}
23+
if (typeof filter === 'object') {
24+
// e.g. row={jdk: {name: 'openjdk', version: 8}}; filter={jdk: {version: 8}}
25+
for (const [key, value] of Object.entries(filter)) {
26+
if (!row.hasOwnProperty(key) || !Axis.matches(row[key], value)) {
27+
return false;
28+
}
29+
}
30+
return true;
31+
}
32+
return row == filter;
33+
}
34+
35+
pickValue(filter) {
36+
let values = this.values;
37+
if (filter) {
38+
values = values.filter(v => Axis.matches(v, filter));
39+
}
40+
if (values.length == 0) {
41+
const filterStr = typeof filter === 'string' ? filter.toString() : JSON.stringify(filter);
42+
throw Error(`No values produces for axis '${this.name}' from ${JSON.stringify(this.values)}, filter=${filterStr}`);
43+
}
44+
if (values.length == 1) {
45+
return values[0];
46+
}
47+
if (this.uniform) {
48+
return values[Math.floor(Math.random() * values.length)];
49+
}
50+
const totalWeight = !filter ? this.totalWeigth : values.reduce((a, b) => a + (b.weight || 1), 0);
51+
let weight = Math.random() * totalWeight;
52+
for (let i = 0; i < values.length; i++) {
53+
const value = values[i];
54+
weight -= value.weight || 1;
55+
if (weight <= 0) {
56+
return value;
57+
}
58+
}
59+
return values[values.length - 1];
60+
}
61+
}
62+
63+
class MatrixBuilder {
64+
constructor() {
65+
this.axes = [];
66+
this.axisByName = {};
67+
this.rows = [];
68+
this.duplicates = {};
69+
this.excludes = [];
70+
this.includes = [];
71+
}
72+
73+
/**
74+
* Specifies include filter (all the generated rows would comply with all the include filters)
75+
* @param filter
76+
*/
77+
include(filter) {
78+
this.includes.push(filter);
79+
}
80+
81+
/**
82+
* Specifies exclude filter (e.g. exclude a forbidden combination)
83+
* @param filter
84+
*/
85+
exclude(filter) {
86+
this.excludes.push(filter);
87+
}
88+
89+
addAxis({name, title, values}) {
90+
const axis = new Axis({name, title, values});
91+
this.axes.push(axis);
92+
this.axisByName[name] = axis;
93+
return axis;
94+
}
95+
96+
setNamePattern(names) {
97+
this.namePattern = names;
98+
}
99+
100+
/**
101+
* Adds a row that matches the given filter to the resulting matrix.
102+
* filter values could be
103+
* - literal values: filter={os: 'windows-latest'}
104+
* - arrays: filter={os: ['windows-latest', 'linux-latest']}
105+
* - functions: filter={os: x => x!='windows-latest'}
106+
* @param filter object with keys matching axes names
107+
* @returns {*}
108+
*/
109+
generateRow(filter) {
110+
let res;
111+
if (filter) {
112+
// If matching row already exists, no need to generate more
113+
res = this.rows.find(v => Axis.matches(v, filter));
114+
if (res) {
115+
return res;
116+
}
117+
}
118+
for (let i = 0; i < 142; i++) {
119+
res = this.axes.reduce(
120+
(prev, next) =>
121+
Object.assign(prev, {
122+
[next.name]: next.pickValue(filter ? filter[next.name] : undefined)
123+
}),
124+
{}
125+
);
126+
if (this.excludes.length > 0 && this.excludes.find(f => Axis.matches(res, f)) ||
127+
this.includes.length > 0 && !this.includes.find(f => Axis.matches(res, f))) {
128+
continue;
129+
}
130+
const key = JSON.stringify(res);
131+
if (!this.duplicates.hasOwnProperty(key)) {
132+
this.duplicates[key] = true;
133+
res.name =
134+
this.namePattern.map(axisName => {
135+
let value = res[axisName];
136+
const title = value.title;
137+
if (typeof title != 'undefined') {
138+
return title;
139+
}
140+
const computeTitle = this.axisByName[axisName].title;
141+
return computeTitle ? computeTitle(value) : value;
142+
}).filter(Boolean).join(", ");
143+
this.rows.push(res);
144+
return res;
145+
}
146+
}
147+
throw Error(`Unable to generate row. Please check include and exclude filters`);
148+
}
149+
150+
generateRows(maxRows, filter) {
151+
for (let i = 0; this.rows.length < maxRows && i < maxRows; i++) {
152+
this.generateRow(filter);
153+
}
154+
return this.rows;
155+
}
156+
}
157+
158+
module.exports = {Axis, MatrixBuilder};

.github/workflows/test.yml

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,55 @@ on:
1010
pull_request:
1111
branches: [ master ]
1212

13+
concurrency:
14+
# On master/release, we don't want any jobs cancelled so the sha is used to name the group
15+
# On PR branches, we cancel the job if new commits are pushed
16+
# More info: https://stackoverflow.com/a/68422069/253468
17+
group: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release' ) && format('ci-main-{0}', github.sha) || format('ci-main-{0}', github.ref) }}
18+
cancel-in-progress: true
19+
1320
jobs:
21+
matrix_prep:
22+
name: Matrix Preparation
23+
runs-on: ubuntu-latest
24+
outputs:
25+
matrix: ${{ steps.set-matrix.outputs.matrix }}
26+
env:
27+
# Ask matrix.js to produce 7 jobs
28+
MATRIX_JOBS: 7
29+
steps:
30+
- uses: actions/checkout@v2
31+
with:
32+
fetch-depth: 50
33+
- id: set-matrix
34+
run: |
35+
node .github/workflows/matrix.js
36+
1437
build:
38+
needs: matrix_prep
39+
name: '${{ matrix.name }}'
40+
runs-on: ${{ matrix.os }}
41+
env:
42+
TZ: ${{ matrix.tz }}
1543
strategy:
44+
matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}}
1645
fail-fast: false
17-
matrix:
18-
java-version:
19-
- 8
20-
- 17
21-
runs-on: ubuntu-latest
22-
name: 'Test (JDK ${{ matrix.java-version }})'
46+
max-parallel: 4
2347
steps:
2448
- uses: actions/checkout@v2
2549
with:
2650
fetch-depth: 50
2751
submodules: true
28-
- name: 'Set up JDK ${{ matrix.java-version }}'
29-
uses: actions/setup-java@v1
52+
- name: Set up Java ${{ matrix.java_version }}, ${{ matrix.java_distribution }}
53+
uses: actions/setup-java@v2
3054
with:
31-
java-version: ${{ matrix.java-version }}
55+
java-version: ${{ matrix.java_version }}
56+
distribution: ${{ matrix.java_distribution }}
57+
architecture: x64
3258
- name: 'Run tests'
3359
uses: burrunan/gradle-cache-action@v1
3460
with:
35-
job-id: jdk${{ matrix.java-version }}
61+
job-id: jdk${{ matrix.java_version }}
3662
arguments: --scan --no-parallel --no-daemon build
63+
env:
64+
_JAVA_OPTIONS: ${{ matrix.testExtraJvmArgs }}

0 commit comments

Comments
 (0)