Skip to content

ci: add randomized matrix #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .github/workflows/matrix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// The script generates a random subset of valid jdk, os, timezone, and other axes.
// You can preview the results by running "node matrix.js"
// See https://github.com/vlsi/github-actions-random-matrix
let {MatrixBuilder} = require('./matrix_builder');
const matrix = new MatrixBuilder();
matrix.addAxis({
name: 'java_distribution',
values: [
'zulu',
'temurin',
'liberica',
'microsoft',
]
});

// TODO: support different JITs (see https://github.com/actions/setup-java/issues/279)
matrix.addAxis({name: 'jit', title: '', values: ['hotspot']});

matrix.addAxis({
name: 'java_version',
// Strings allow versions like 18-ea
values: [
'8',
'11',
'17',
]
});

matrix.addAxis({
name: 'tz',
values: [
'America/New_York',
'Pacific/Chatham',
'UTC'
]
});

matrix.addAxis({
name: 'os',
title: x => x.replace('-latest', ''),
values: [
'ubuntu-latest',
'windows-latest',
'macos-latest'
]
});

// Test cases when Object#hashCode produces the same results
// It allows capturing cases when the code uses hashCode as a unique identifier
matrix.addAxis({
name: 'hash',
values: [
{value: 'regular', title: '', weight: 42},
{value: 'same', title: 'same hashcode', weight: 1}
]
});
matrix.addAxis({
name: 'locale',
title: x => x.language + '_' + x.country,
values: [
{language: 'de', country: 'DE'},
{language: 'fr', country: 'FR'},
{language: 'ru', country: 'RU'},
{language: 'tr', country: 'TR'},
]
});

matrix.setNamePattern(['java_version', 'java_distribution', 'hash', 'os', 'tz', 'locale']);

// Microsoft Java has no distribution for 8
matrix.exclude({java_distribution: 'microsoft', java_version: 8});
// Ensure at least one job with "same" hashcode exists
matrix.generateRow({hash: {value: 'same'}});
// Ensure at least one Windows and at least one Linux job is present (macOS is almost the same as Linux)
matrix.generateRow({os: 'windows-latest'});
matrix.generateRow({os: 'ubuntu-latest'});
// Ensure there will be at least one job with Java 8
matrix.generateRow({java_version: 8});
// Ensure there will be at least one job with Java 11
matrix.generateRow({java_version: 11});
// Ensure there will be at least one job with Java 17
matrix.generateRow({java_version: 17});
const include = matrix.generateRows(process.env.MATRIX_JOBS || 5);
if (include.length === 0) {
throw new Error('Matrix list is empty');
}
include.sort((a, b) => a.name.localeCompare(b.name, undefined, {numeric: true}));
include.forEach(v => {
let jvmArgs = [];
if (v.hash.value === 'same') {
jvmArgs.push('-XX:+UnlockExperimentalVMOptions', '-XX:hashCode=2');
}
// Gradle does not work in tr_TR locale, so pass locale to test only: https://github.com/gradle/gradle/issues/17361
jvmArgs.push(`-Duser.country=${v.locale.country}`);
jvmArgs.push(`-Duser.language=${v.locale.language}`);
if (v.jit === 'hotspot' && Math.random() > 0.5) {
// The following options randomize instruction selection in JIT compiler
// so it might reveal missing synchronization in TestNG code
v.name += ', stress JIT';
jvmArgs.push('-XX:+UnlockDiagnosticVMOptions');
if (v.java_version >= 8) {
// Randomize instruction scheduling in GCM
// share/opto/c2_globals.hpp
jvmArgs.push('-XX:+StressGCM');
// Randomize instruction scheduling in LCM
// share/opto/c2_globals.hpp
jvmArgs.push('-XX:+StressLCM');
}
if (v.java_version >= 16) {
// Randomize worklist traversal in IGVN
// share/opto/c2_globals.hpp
jvmArgs.push('-XX:+StressIGVN');
}
if (v.java_version >= 17) {
// Randomize worklist traversal in CCP
// share/opto/c2_globals.hpp
jvmArgs.push('-XX:+StressCCP');
}
}
v.testExtraJvmArgs = jvmArgs.join(' ');
delete v.hash;
});

console.log(include);
console.log('::set-output name=matrix::' + JSON.stringify({include}));
158 changes: 158 additions & 0 deletions .github/workflows/matrix_builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// License: Apache-2.0
// Copyright Vladimir Sitnikov, 2021
// See https://github.com/vlsi/github-actions-random-matrix

class Axis {
constructor({name, title, values}) {
this.name = name;
this.title = title;
this.values = values;
// If all entries have same weight, the axis has uniform distribution
this.uniform = values.reduce((a, b) => a === (b.weight || 1) ? a : 0, values[0].weight || 1) !== 0
this.totalWeigth = this.uniform ? values.length : values.reduce((a, b) => a + (b.weight || 1), 0);
}

static matches(row, filter) {
if (typeof filter === 'function') {
return filter(row);
}
if (Array.isArray(filter)) {
// e.g. row={os: 'windows'}; filter=[{os: 'linux'}, {os: 'linux'}]
return filter.find(v => Axis.matches(row, v));
}
if (typeof filter === 'object') {
// e.g. row={jdk: {name: 'openjdk', version: 8}}; filter={jdk: {version: 8}}
for (const [key, value] of Object.entries(filter)) {
if (!row.hasOwnProperty(key) || !Axis.matches(row[key], value)) {
return false;
}
}
return true;
}
return row == filter;
}

pickValue(filter) {
let values = this.values;
if (filter) {
values = values.filter(v => Axis.matches(v, filter));
}
if (values.length == 0) {
const filterStr = typeof filter === 'string' ? filter.toString() : JSON.stringify(filter);
throw Error(`No values produces for axis '${this.name}' from ${JSON.stringify(this.values)}, filter=${filterStr}`);
}
if (values.length == 1) {
return values[0];
}
if (this.uniform) {
return values[Math.floor(Math.random() * values.length)];
}
const totalWeight = !filter ? this.totalWeigth : values.reduce((a, b) => a + (b.weight || 1), 0);
let weight = Math.random() * totalWeight;
for (let i = 0; i < values.length; i++) {
const value = values[i];
weight -= value.weight || 1;
if (weight <= 0) {
return value;
}
}
return values[values.length - 1];
}
}

class MatrixBuilder {
constructor() {
this.axes = [];
this.axisByName = {};
this.rows = [];
this.duplicates = {};
this.excludes = [];
this.includes = [];
}

/**
* Specifies include filter (all the generated rows would comply with all the include filters)
* @param filter
*/
include(filter) {
this.includes.push(filter);
}

/**
* Specifies exclude filter (e.g. exclude a forbidden combination)
* @param filter
*/
exclude(filter) {
this.excludes.push(filter);
}

addAxis({name, title, values}) {
const axis = new Axis({name, title, values});
this.axes.push(axis);
this.axisByName[name] = axis;
return axis;
}

setNamePattern(names) {
this.namePattern = names;
}

/**
* Adds a row that matches the given filter to the resulting matrix.
* filter values could be
* - literal values: filter={os: 'windows-latest'}
* - arrays: filter={os: ['windows-latest', 'linux-latest']}
* - functions: filter={os: x => x!='windows-latest'}
* @param filter object with keys matching axes names
* @returns {*}
*/
generateRow(filter) {
let res;
if (filter) {
// If matching row already exists, no need to generate more
res = this.rows.find(v => Axis.matches(v, filter));
if (res) {
return res;
}
}
for (let i = 0; i < 142; i++) {
res = this.axes.reduce(
(prev, next) =>
Object.assign(prev, {
[next.name]: next.pickValue(filter ? filter[next.name] : undefined)
}),
{}
);
if (this.excludes.length > 0 && this.excludes.find(f => Axis.matches(res, f)) ||
this.includes.length > 0 && !this.includes.find(f => Axis.matches(res, f))) {
continue;
}
const key = JSON.stringify(res);
if (!this.duplicates.hasOwnProperty(key)) {
this.duplicates[key] = true;
res.name =
this.namePattern.map(axisName => {
let value = res[axisName];
const title = value.title;
if (typeof title != 'undefined') {
return title;
}
const computeTitle = this.axisByName[axisName].title;
return computeTitle ? computeTitle(value) : value;
}).filter(Boolean).join(", ");
this.rows.push(res);
return res;
}
}
throw Error(`Unable to generate row. Please check include and exclude filters`);
}

generateRows(maxRows, filter) {
for (let i = 0; this.rows.length < maxRows && i < maxRows; i++) {
this.generateRow(filter);
}
return this.rows;
}
}

module.exports = {Axis, MatrixBuilder};
48 changes: 38 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,55 @@ on:
pull_request:
branches: [ master ]

concurrency:
# On master/release, we don't want any jobs cancelled so the sha is used to name the group
# On PR branches, we cancel the job if new commits are pushed
# More info: https://stackoverflow.com/a/68422069/253468
group: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release' ) && format('ci-main-{0}', github.sha) || format('ci-main-{0}', github.ref) }}
cancel-in-progress: true

jobs:
matrix_prep:
name: Matrix Preparation
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
env:
# Ask matrix.js to produce 7 jobs
MATRIX_JOBS: 7
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 50
- id: set-matrix
run: |
node .github/workflows/matrix.js

build:
needs: matrix_prep
name: '${{ matrix.name }}'
runs-on: ${{ matrix.os }}
env:
TZ: ${{ matrix.tz }}
strategy:
matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}}
fail-fast: false
matrix:
java-version:
- 8
- 17
runs-on: ubuntu-latest
name: 'Test (JDK ${{ matrix.java-version }})'
max-parallel: 4
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 50
submodules: true
- name: 'Set up JDK ${{ matrix.java-version }}'
uses: actions/setup-java@v1
- name: Set up Java ${{ matrix.java_version }}, ${{ matrix.java_distribution }}
uses: actions/setup-java@v2
with:
java-version: ${{ matrix.java-version }}
java-version: ${{ matrix.java_version }}
distribution: ${{ matrix.java_distribution }}
architecture: x64
- name: 'Run tests'
uses: burrunan/gradle-cache-action@v1
with:
job-id: jdk${{ matrix.java-version }}
job-id: jdk${{ matrix.java_version }}
arguments: --scan --no-parallel --no-daemon build
env:
_JAVA_OPTIONS: ${{ matrix.testExtraJvmArgs }}
1 change: 0 additions & 1 deletion src/main/java/net/atomique/ksar/XMLConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public InputSource resolveEntity(String publicId, String systemId)
}

String dtdFile = publicId.substring(KSAR_DTD_PREFIX.length() - 1);
dtdFile = dtdFile.toLowerCase();
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was misbehaving in tr_TR locale for CONFIG.XML input.

InputStream inputStream = getClass().getResourceAsStream(dtdFile);
if (inputStream == null) {
throw new FileNotFoundException("File " + publicId + " is not found in kSar resources");
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/AIX.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/CONFIG.DTD" "config.dtd" >
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/config.dtd" "config.dtd" >
<!--
Description:
Configure statistics and their corresponding graphs (plots+stacks)
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/Config.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/CONFIG.DTD" "config.dtd" >
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/config.dtd" "config.dtd" >
<!--
Description:
Configure color of graphs
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/Esar.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/CONFIG.DTD" "config.dtd">
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/config.dtd" "config.dtd">
<!--
Description:
Configure statistics and their corresponding graphs (plots+stacks)
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/HPUX.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/CONFIG.DTD" "config.dtd">
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/config.dtd" "config.dtd">
<!--
Description:
Configure statistics and their corresponding graphs (plots+stacks)
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/Linux.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/CONFIG.DTD" "config.dtd">
<!DOCTYPE ConfiG PUBLIC "-//NET/ATOMIQUE/KSAR/config.dtd" "config.dtd">
<!--
Description:
Configure statistics and their corresponding graphs (plots+stacks)
Expand Down
Loading