-
Notifications
You must be signed in to change notification settings - Fork 9
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
Manual/semi-automatic performance regression checking #356
Merged
MarionBWeinzierl
merged 26 commits into
develop
from
issue256-manual-performance-regression-checking
Dec 3, 2024
Merged
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
95ec3bc
correct PModel constructor signature use
MarionBWeinzierl 939cd21
start writing script using git worktree
MarionBWeinzierl bd7d4f9
Merge remote-tracking branch 'origin/develop' into issue256-manual-pe…
MarionBWeinzierl 20359e0
added proof-of-concept scripts for simple performance regression chec…
MarionBWeinzierl 11e4918
changed script to accept commit hashs
MarionBWeinzierl b5b8f3a
put 'exclude' part back in
MarionBWeinzierl 92e503a
added some error checks and regression tolerance
MarionBWeinzierl 50d5c28
make the script fail if performance regression
MarionBWeinzierl af03991
change default comparison to origin/develop
MarionBWeinzierl 5240072
Update CONTRIBUTING.md to reflect change in how profiling is run
MarionBWeinzierl 871f7a5
Remove trailing whitespace and shorten line
MarionBWeinzierl 932a7cf
Update CONTRIBUTING.md with further information on how to run the ben…
MarionBWeinzierl 440e6c9
Remove trailing whitespaces
MarionBWeinzierl f21dd0a
Update CONTRIBUTING.md with information on running profiling test suit
MarionBWeinzierl 2073628
fix formatting
MarionBWeinzierl 0619c93
Update CONTRIBUTING.md
MarionBWeinzierl 1d1051b
added conditional changes in flag for mac and linux
MarionBWeinzierl 9da026f
accept @j-emberton 's suggestion from review
MarionBWeinzierl 58512ba
added profiling database exclusion to .gitignore
j-emberton fc08721
Suggested simplification of profiling validation CI
davidorme b8ecb73
profiling validation in CI doesn't need to wait for tests to complete
davidorme b1f70c9
Accept @davidorme 's suggestion from the code review
MarionBWeinzierl d6328b0
Merge branch 'develop' into issue256-manual-performance-regression-ch…
MarionBWeinzierl 74bfce8
Delete .github/workflows/pyrealm_profiling_on_approve.yaml
MarionBWeinzierl 3c49d98
Delete .github/workflows/pyrealm_profiling_after_push.yaml
MarionBWeinzierl 955528d
Delete .github/workflows/pyrealm_profiling_without_benchmarking.yaml
MarionBWeinzierl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
#!/bin/bash | ||
|
||
if [[ $# -eq 0 ]] ; then | ||
echo "No input arguments, comparing HEAD to origin/develop" | ||
new_commit=HEAD | ||
old_commit=origin/develop | ||
else | ||
while getopts n:o: flag | ||
do | ||
case "${flag}" in | ||
n) new_commit=${OPTARG};; | ||
o) old_commit=${OPTARG};; | ||
*) echo "Invalid input argument"; exit 1;; | ||
esac | ||
done | ||
fi | ||
|
||
cd .. | ||
git checkout $new_commit | ||
|
||
# Remember where we start from | ||
current_repo=`pwd` | ||
|
||
#This is the where we want to check the other worktree out to | ||
cmp_repo=$current_repo/../pyrealm_performance_check | ||
|
||
# Adding the worktree | ||
echo "Add worktree" $cmp_repo | ||
git worktree add $cmp_repo $old_commit | ||
|
||
# Go there and activate poetry environment | ||
cd $cmp_repo | ||
poetry install | ||
#source .venv/bin/activate | ||
|
||
# Run the profiling on old commit | ||
echo "Run profiling tests on old commit" | ||
poetry run /usr/bin/time -v pytest -m "profiling" --profile-svg | ||
if [ "$?" != "0" ]; then | ||
echo "Profiling the current code went wrong." | ||
exit 1 | ||
fi | ||
|
||
# Go back into the current repo and run there | ||
cd $current_repo | ||
poetry install | ||
echo "Run profiling tests on new commit" | ||
poetry run /usr/bin/time -v pytest -m "profiling" --profile-svg | ||
if [ "$?" != "0" ]; then | ||
echo "Profiling the new code went wrong." | ||
exit 1 | ||
fi | ||
|
||
# Compare the profiling outputs | ||
cd profiling | ||
python -c " | ||
from pathlib import Path | ||
import simple_benchmarking | ||
import pandas as pd | ||
import sys | ||
|
||
prof_path_old = Path('$cmp_repo'+'/prof/combined.prof') | ||
print(prof_path_old) | ||
df_old = simple_benchmarking.run_simple_benchmarking(prof_path=prof_path_old) | ||
cumtime_old = (df_old.sum(numeric_only=True)['cumtime']) | ||
print('Old time:', cumtime_old) | ||
|
||
prof_path_new = Path('$current_repo'+'/prof/combined.prof') | ||
print(prof_path_new) | ||
df_new = simple_benchmarking.run_simple_benchmarking(prof_path=prof_path_new) | ||
cumtime_new = (df_new.sum(numeric_only=True)['cumtime']) | ||
print('New time:', cumtime_new) | ||
|
||
if cumtime_old < 0.95*cumtime_new: | ||
print('We got slower. :(') | ||
sys.exit(1) | ||
elif cumtime_new < 0.95*cumtime_old: | ||
print('We got quicker! :)') | ||
else: | ||
print('Times haven\'t changed') | ||
" | ||
|
||
benchmarking_out="$?" | ||
|
||
cd .. | ||
# Remove the working tree for the comparison commit | ||
echo "Clean up" | ||
git worktree remove --force $cmp_repo | ||
git worktree prune | ||
|
||
if [ $benchmarking_out != "0" ]; then | ||
echo "The new code is more than 5% slower than the old one." | ||
exit 1 | ||
fi | ||
|
||
echo "No significant performance regression detected." |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @davidorme would you prefer me to use this to replace the old |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
"""Run profile benchmarking and generate benchmarking graphics.""" | ||
|
||
import datetime | ||
import pstats | ||
import sys | ||
import textwrap | ||
from argparse import ArgumentParser | ||
from io import StringIO | ||
from pathlib import Path | ||
|
||
import pandas as pd | ||
|
||
|
||
def run_simple_benchmarking( | ||
prof_path: Path, | ||
exclude: list[str] = ["{.*", "<.*", "/lib/", "/tests/", "virtualenv", "venv"], | ||
) -> pd.DataFrame: | ||
"""Run a simplified benchmarking version. | ||
|
||
The function reads the contents of a ``.prof`` file (typically | ||
``prof/combined.prof``) generated by running the profiling test suite and returns | ||
the profiling data as a standardised `pandas.DataFrame`. | ||
|
||
The profiling results include a field 'filename:lineno(function)', which identifies | ||
each profiled code object. Many of these will be from functions outside of the | ||
`pyrealm` codebase and these are excluded using a list of regex patterns: | ||
|
||
* '/lib/' excludes standard and site packages, | ||
* '{.*' excludes profiling of '{built-in ... } and similar, | ||
* '<.*' excludes profiling of '<frozen ...>` and similar. | ||
* '/tests/' excludes the test functions and classes calling the pyrealm code. | ||
* 'virtualenv' and 'venv' exclude standard packages in virtual environments | ||
|
||
Args: | ||
prof_path: Path to the profiling output. | ||
exclude: A list of patterns used to exclude rows from the profiling stats. | ||
""" | ||
|
||
# Import the profile data, write the stats report to a StringIO and seek the start | ||
# to allow the data to be read. The print_stats() explicitly does not filter for | ||
# 'pyrealm' because the string can be found in virtual environment paths and leads | ||
# to inconsistent behaviour across platforms | ||
sio = StringIO() | ||
p = pstats.Stats(str(prof_path), stream=sio) | ||
p.sort_stats(pstats.SortKey.CUMULATIVE).print_stats() | ||
sio.seek(0) | ||
|
||
# Consume lines from the report to find the header row | ||
header_found = False | ||
while not header_found: | ||
header = sio.readline() | ||
if "ncalls" in header: | ||
header_found = True | ||
|
||
# Set replacement non-duplicated headers | ||
column_names = [ | ||
"ncalls", | ||
"tottime", | ||
"tottime_percall", | ||
"cumtime", | ||
"cumtime_percall", | ||
"filename:lineno(function)", | ||
] | ||
|
||
# Convert to a DataFrame using fixed width format | ||
df = pd.read_fwf(sio, engine="python", names=column_names, infer_nrows=10) | ||
|
||
# Reduce to rows not matching any of the regex exclude patterns | ||
exclude_rows = pd.DataFrame( | ||
[df["filename:lineno(function)"].str.contains(excl) for excl in exclude] | ||
).any() | ||
df = df[~exclude_rows] | ||
|
||
# Add a timestamp from the file creation date | ||
m_time = datetime.datetime.fromtimestamp(prof_path.stat().st_mtime) | ||
df["timestamp"] = m_time.isoformat(timespec="seconds") | ||
|
||
df.to_csv("profiling-database.csv") | ||
|
||
return df | ||
|
||
|
||
def run_simple_benchmarking_cli() -> None: | ||
"""Run the simple benchmarking.""" | ||
|
||
if run_simple_benchmarking_cli.__doc__ is not None: | ||
doc = " " + run_simple_benchmarking_cli.__doc__ | ||
else: | ||
doc = "Python in -OO mode" | ||
|
||
parser = ArgumentParser( | ||
description=textwrap.dedent(doc), | ||
) | ||
parser.add_argument( | ||
"prof_path", | ||
type=Path, | ||
help="Path to pytest-profiling output", | ||
) | ||
|
||
args = parser.parse_args() | ||
|
||
# Copy the profiling results to the current folder | ||
if not args.prof_path.exists(): | ||
raise FileNotFoundError(f"Cannot find the profiling file at {args.prof_path}.") | ||
|
||
success = run_simple_benchmarking(prof_path=args.prof_path) | ||
|
||
if not success: | ||
print("Benchmarking failed.") | ||
sys.exit(1) | ||
|
||
print("Benchmarking passed.") | ||
sys.exit(0) | ||
|
||
|
||
if __name__ == "__main__": | ||
run_simple_benchmarking_cli() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That link doe not work. Looking at https://github.com/ImperialCollegeLondon/pyrealm/blob/develop/docs/source/development/profiling_and_benchmarking.md , a lot of that information is also no longer valid. We will need to adapt that when we have decided how to proceed (i.e., keep the old benchmarking code, if and when to run the new code automatically, etc.)