Skip to content

Commit 87f4a05

Browse files
authored
Add performance regression tests in CI (#4701)
1 parent 3baca01 commit 87f4a05

File tree

3 files changed

+142
-15
lines changed

3 files changed

+142
-15
lines changed

Diff for: .github/workflows/regression_ci.yml

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: MIT-0
3+
name: Performance Regression Test
4+
5+
on:
6+
pull_request:
7+
branches:
8+
- main
9+
paths-ignore:
10+
- tests/regression/**
11+
12+
jobs:
13+
regression-test:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
# Checkout the code from the pull request branch
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
with:
21+
ref: ${{ github.event.pull_request.head.sha }}
22+
23+
# Install the stable Rust toolchain
24+
- name: Install Rust toolchain
25+
id: toolchain
26+
run: |
27+
rustup toolchain install stable
28+
rustup override set stable
29+
30+
# Update the package list on the runner
31+
- name: Update package list
32+
run: sudo apt-get update
33+
34+
# Download and install Valgrind 3.23 from source
35+
- name: Download Valgrind 3.23 Source
36+
run: |
37+
wget https://sourceware.org/pub/valgrind/valgrind-3.23.0.tar.bz2
38+
tar -xjf valgrind-3.23.0.tar.bz2
39+
cd valgrind-3.23.0
40+
./configure
41+
make
42+
sudo make install
43+
44+
# Generate the necessary bindings
45+
- name: Generate
46+
run: ${{env.ROOT_PATH}}bindings/rust/generate.sh --skip-tests
47+
48+
# Run performance tests using Valgrind for current branch
49+
- name: Run scalar performance test (curr)
50+
env:
51+
PERF_MODE: valgrind
52+
run: cargo test --release --manifest-path=tests/regression/Cargo.toml
53+
54+
# Switch to the main branch
55+
- name: Switch to mainline
56+
run: |
57+
git fetch origin main
58+
git switch main
59+
60+
# Regenerate bindings for main branch
61+
- name: Generate
62+
run: ${{env.ROOT_PATH}}bindings/rust/generate.sh --skip-tests
63+
64+
# Run performance tests using Valgrind for main branch
65+
- name: Run scalar performance test (prev)
66+
env:
67+
PERF_MODE: valgrind
68+
run: cargo test --release --manifest-path=tests/regression/Cargo.toml
69+
70+
# Checkout pull request branch again
71+
# This is required for cg_annotate diff to locate the changes in the PR to properly annotate the output diff file
72+
- name: Checkout pull request branch
73+
run: git checkout ${{ github.event.pull_request.head.sha }}
74+
75+
# Run the differential performance test
76+
- name: Run diff test
77+
env:
78+
PERF_MODE: diff
79+
run: cargo test --release --manifest-path=tests/regression/Cargo.toml
80+
81+
# Upload the performance output artifacts. This runs even if run diff test fails so debug files can be accessed
82+
- name: Upload artifacts
83+
if: ${{ always() }}
84+
uses: actions/upload-artifact@v4
85+
with:
86+
name: regression_artifacts
87+
path: tests/regression/target/regression_artifacts

Diff for: tests/regression/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ The performance benchmarking framework utilizes CPU Instruction count across API
2424

2525
Ensure you have the following installed:
2626
- Rust (with Cargo)
27-
- Valgrind (for cachegrind instrumentation)
27+
- Valgrind (for cachegrind instrumentation): Valgrind 3.23 or newer is required to run the tests, since cachegrind annotation is not included in earlier versions. If this version is not automatically downloaded by running `apt install valgrind`, it can be installed manually by following https://valgrind.org/downloads/
2828

2929
## Running the Harnesses with Valgrind (scalar performance)
3030
To run the harnesses with Valgrind and store the annotated results, run:

Diff for: tests/regression/src/lib.rs

+54-14
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,37 @@ pub mod git {
2727
}
2828

2929
pub fn extract_commit_hash(file: &str) -> String {
30-
// input: "target/$commit_id/test_name.raw"
30+
// input: "target/regression_artifacts/$commit_id/test_name.raw"
3131
// output: "$commit_id"
32-
file.split("target/")
32+
file.split("target/regression_artifacts/")
3333
.nth(1)
3434
.and_then(|s| s.split('/').next())
3535
.map(|s| s.to_string())
3636
.unwrap_or_default() // This will return an empty string if the Option is None
3737
}
38+
39+
pub fn is_mainline(commit_hash: &str) -> bool {
40+
// Execute the git command to check which branches contain the given commit.
41+
let output = Command::new("git")
42+
.args(["branch", "--contains", commit_hash])
43+
.output()
44+
.expect("Failed to execute git branch");
45+
46+
// If the command fails, it indicates that the commit is either detached
47+
// or does not exist in any branches. Meaning, it is not part of mainline.
48+
if !output.status.success() {
49+
return false;
50+
}
51+
52+
// Convert the command output to a string and check each line.
53+
let branches = String::from_utf8_lossy(&output.stdout);
54+
branches.lines().any(|branch| {
55+
// Trim the branch name to remove any leading or trailing whitespace.
56+
// The branch name could be prefixed with '*', indicating the current branch.
57+
// We check for both "main" and "* main" to account for this possibility.
58+
branch.trim() == "main" || branch.trim() == "* main"
59+
})
60+
}
3861
}
3962

4063
#[cfg(test)]
@@ -117,7 +140,7 @@ mod tests {
117140
impl RawProfile {
118141
fn new(test_name: &str) -> Self {
119142
let commit_hash = git::get_current_commit_hash();
120-
create_dir_all(format!("target/{commit_hash}")).unwrap();
143+
create_dir_all(format!("target/regression_artifacts/{commit_hash}")).unwrap();
121144

122145
let raw_profile = Self {
123146
test_name: test_name.to_owned(),
@@ -143,7 +166,10 @@ mod tests {
143166
}
144167

145168
fn path(&self) -> String {
146-
format!("target/{}/{}.raw", self.commit_hash, self.test_name)
169+
format!(
170+
"target/regression_artifacts/{}/{}.raw",
171+
self.commit_hash, self.test_name
172+
)
147173
}
148174

149175
// Returns the annotated profile associated with a raw profile
@@ -154,8 +180,9 @@ mod tests {
154180
/// Return the raw profiles for `test_name` in "git" order. `tuple.0` is older than `tuple.1`
155181
///
156182
/// This method will panic if there are not two profiles.
183+
/// This method will also panic if both commits are on different logs (not mainline).
157184
fn query(test_name: &str) -> (RawProfile, RawProfile) {
158-
let pattern = format!("target/**/*{}.raw", test_name);
185+
let pattern = format!("target/regression_artifacts/**/*{}.raw", test_name);
159186
let raw_files: Vec<String> = glob::glob(&pattern)
160187
.expect("Failed to read glob pattern")
161188
.filter_map(Result::ok)
@@ -167,18 +194,28 @@ mod tests {
167194
test_name: test_name.to_string(),
168195
commit_hash: git::extract_commit_hash(&raw_files[0]),
169196
};
170-
171197
let profile2 = RawProfile {
172198
test_name: test_name.to_string(),
173199
commit_hash: git::extract_commit_hash(&raw_files[1]),
174200
};
175201

176-
if git::is_older_commit(&profile1.commit_hash, &profile2.commit_hash) {
177-
(profile1, profile2)
178-
} else if git::is_older_commit(&profile2.commit_hash, &profile1.commit_hash) {
179-
(profile2, profile1)
202+
// xor returns true if exactly one commit is mainline
203+
if git::is_mainline(&profile1.commit_hash) ^ git::is_mainline(&profile2.commit_hash) {
204+
// Return the mainline as first commit
205+
if git::is_mainline(&profile1.commit_hash) {
206+
(profile1, profile2)
207+
} else {
208+
(profile2, profile1)
209+
}
180210
} else {
181-
panic!("The commits are not in the same log");
211+
// Neither or both profiles are on the mainline, so return the older one first
212+
if git::is_older_commit(&profile1.commit_hash, &profile2.commit_hash) {
213+
(profile1, profile2)
214+
} else if git::is_older_commit(&profile2.commit_hash, &profile1.commit_hash) {
215+
(profile2, profile1)
216+
} else {
217+
panic!("The commits are not in the same log, are identical, or there are not two commits available");
218+
}
182219
}
183220
}
184221
}
@@ -211,7 +248,10 @@ mod tests {
211248
}
212249

213250
fn path(&self) -> String {
214-
format!("target/{}/{}.annotated", self.commit_hash, self.test_name)
251+
format!(
252+
"target/regression_artifacts/{}/{}.annotated",
253+
self.commit_hash, self.test_name
254+
)
215255
}
216256

217257
fn instruction_count(&self) -> i64 {
@@ -240,7 +280,7 @@ mod tests {
240280
assert_command_success(diff_output.clone());
241281

242282
// write the diff to disk
243-
create_dir_all(format!("target/diff")).unwrap();
283+
create_dir_all("target/regression_artifacts/diff").unwrap();
244284
let diff_content = String::from_utf8(diff_output.stdout)
245285
.expect("Invalid UTF-8 in cg_annotate --diff output");
246286
write(diff_profile.path(), diff_content).expect("Failed to write to file");
@@ -249,7 +289,7 @@ mod tests {
249289
}
250290

251291
fn path(&self) -> String {
252-
format!("target/diff/{}.diff", self.test_name)
292+
format!("target/regression_artifacts/diff/{}.diff", self.test_name)
253293
}
254294

255295
fn assert_performance(&self, max_diff: f64) {

0 commit comments

Comments
 (0)