-
Notifications
You must be signed in to change notification settings - Fork 1
Support SolverBenchmarks #7
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
MaxenceGollier
wants to merge
60
commits into
JuliaSmoothOptimizers:main
Choose a base branch
from
MaxenceGollier:solver_benchmarks
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
0556eed
add solver benchmarks
MaxenceGollier eb4e67e
run benchmark script on current commit
MaxenceGollier 2466b3e
add LibGit2 dep
MaxenceGollier abf030c
add _withcommit function
MaxenceGollier 74e0fa6
add plotting
MaxenceGollier 7ca2063
create gist
MaxenceGollier 4d1e738
try adding origin to target name
MaxenceGollier 7da9942
fix error message
MaxenceGollier 64984a2
make _shastring more robust
MaxenceGollier 502ca44
resolve sha hashes
MaxenceGollier 7653dd4
convert git hashes to strings
MaxenceGollier d87b0ee
update function name
MaxenceGollier 115938a
debugging: show repo
MaxenceGollier 72e3551
debug show repo_dir and bmark_dir
MaxenceGollier 7c62433
add extra lookup branch step
MaxenceGollier cdb59b8
add bunch of checks
MaxenceGollier b1a04d9
remove hasproperty check
MaxenceGollier fb4418b
improve function
MaxenceGollier 932b79d
fix content
MaxenceGollier fe4663f
add simple_md_report
MaxenceGollier 99f1006
add nothing arguments to write_simple_md_report
MaxenceGollier ad89d3d
update write_simple_md_report
MaxenceGollier c792b41
update files_dict uploaded to gist
MaxenceGollier 2ab42e6
fix project toml
MaxenceGollier fc96c52
remove debugging code
MaxenceGollier 1fbb7e2
apply suggestions from copilot
MaxenceGollier aa5866b
remove todos
MaxenceGollier 9ba1afe
apply other suggestions from copilot
MaxenceGollier 2f68612
return gist_urls
MaxenceGollier f081bf7
add markdown table
MaxenceGollier b9e56bf
update tables
MaxenceGollier b7138c6
fix tables
MaxenceGollier 202282c
format tables
MaxenceGollier 5d177f3
revert fname
MaxenceGollier 3672468
add titles to tables
MaxenceGollier d74a81b
remove readme
MaxenceGollier 953196e
add table_values kwargs
MaxenceGollier 420d48d
add function for default values
MaxenceGollier 0444f42
fix hdr_override values
MaxenceGollier c2b88c7
fix hdr_override
MaxenceGollier 94b9018
force recompilation of package when checking out
MaxenceGollier 7c5c6a8
run result on main branch in separate julia process
MaxenceGollier 05ac796
write and read result from JSON
MaxenceGollier 9c21ce3
fix typo
MaxenceGollier a7269f2
run script in main env
MaxenceGollier 970f9ed
run Base.include
MaxenceGollier 46ab940
save result file as a jld2
MaxenceGollier 8db47ed
add latex table and save results files as jld2
MaxenceGollier 4546cf7
save pdfs
MaxenceGollier 732654f
convert variable to string
MaxenceGollier 3c89a5c
fix no such file or directory
MaxenceGollier 63e8c6f
remove copilot spurios suggestions
MaxenceGollier 57ed7fd
save latex file as txt
MaxenceGollier 0701786
export solver_benchmark options
MaxenceGollier 7a1f03e
save tex file for each table
MaxenceGollier 6e9eae2
use Base.invokelatest to allow user to switch options
MaxenceGollier 7513b75
revert changes to simple md report
MaxenceGollier 59f07a0
update simple md report
MaxenceGollier cd479ca
add doc
MaxenceGollier 306d837
update report
MaxenceGollier 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 hidden or 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 hidden or 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 hidden or 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,239 @@ | ||
| """ | ||
| run_solver_benchmarks(repo_name, bmark_dir; reference_branch="main", gist_url=nothing, script="benchmarks.jl") | ||
|
|
||
| Run a benchmark script, based on the SolverBenchmarks.jl package, for a Julia repository. | ||
|
|
||
| This function executes a benchmark script (`script`) in the specified benchmark directory (`bmark_dir`) for | ||
| the current state of the repository containing `repo_name`. | ||
| The output of the script should be a result of `BenchmarkSolver.bmark_solvers`. If the repository is a Git repository, the | ||
| benchmarks are run on the current commit and optionally compared to a reference branch (default `"main"`). | ||
| The results are saved as `.jld2` files, performance profile plots and summary tables are generated. | ||
| Optionally, results can be uploaded or updated in a GitHub Gist (`gist_url`). | ||
|
|
||
| # Arguments | ||
|
|
||
| - `repo_name::AbstractString` | ||
| The name of the Julia package repository being benchmarked. | ||
|
|
||
| - `bmark_dir::AbstractString` | ||
| Path to the directory containing the benchmark scripts. This is usually a `benchmarks/` folder | ||
| inside the repository. | ||
|
|
||
| # Keyword Arguments | ||
|
|
||
| - `reference_branch::AbstractString = "main"` | ||
| The Git branch used as a reference for comparison in plots and tables. | ||
|
|
||
| - `gist_url::Union{AbstractString, Nothing} = nothing` | ||
| If provided, the function updates the existing Gist at this URL. Otherwise, a new Gist is created. | ||
|
|
||
| - `script::AbstractString = "benchmarks.jl"` | ||
| The Julia script in `bmark_dir` that runs the benchmark suite. Must return a `Dict{Symbol, DataFrame}` | ||
| as produced by `BenchmarkSolver.bmark_solvers`. | ||
|
|
||
| # Output | ||
|
|
||
| Returns a `String` containing the URL of the Gist with benchmark results. If `gist_url` was provided, | ||
| the existing Gist is updated; otherwise, a new Gist URL is returned. | ||
|
|
||
| # Plots and Tables values | ||
|
|
||
| In order to compare specific outputs from the benchmark results, the `script` can override the functions | ||
| JSOBenchmarks.solver_benchmark_profile_values() | ||
| JSOBenchmarks.solver_benchmark_table_values() | ||
| to specify which columns from the DataFrames should be used for the performance profiles and summary tables, respectively. | ||
| Both should return an array of pairs, where the first element is a `Symbol` representing the column name in the DataFrame | ||
| and the second element is a `String` representing the label to be used in the plots and tables. | ||
|
|
||
| # Notes | ||
|
|
||
| - This function is mostly expected to be called from a GitHub workflow. | ||
| - Please refer to `SolverBenchmarks.bmark_solvers` for more information on how to write the benchmark script. | ||
| """ | ||
| function run_solver_benchmarks( | ||
| repo_name::AbstractString, | ||
| bmark_dir::AbstractString; | ||
| reference_branch::AbstractString = "main", | ||
| gist_url::Union{AbstractString, Nothing} = nothing, | ||
| script = "benchmarks.jl", | ||
| ) | ||
|
|
||
| update_gist = gist_url !== nothing | ||
| is_git = isdir(joinpath(bmark_dir, "..", ".git")) | ||
| @info "" is_git update_gist | ||
|
|
||
| local gist_id | ||
| if update_gist | ||
| gist_id = split(gist_url, "/")[end] | ||
| @info "" gist_id | ||
| end | ||
|
|
||
| # if we are running these benchmarks from the git repository | ||
| # we want to develop the package instead of using the release | ||
| if is_git | ||
| Pkg.develop(PackageSpec(path = joinpath(bmark_dir, ".."))) | ||
| else | ||
| Pkg.activate(bmark_dir) | ||
| end | ||
| Pkg.instantiate() | ||
|
|
||
| # name the benchmark after the repo or the sha of HEAD | ||
| bmarkname = is_git ? readchomp(`$git rev-parse HEAD`) : lowercase(repo_name) | ||
| @info "" bmarkname | ||
|
|
||
| # Run the benchmark script on this commit | ||
| this_commit = Base.include(Main, joinpath(bmark_dir, script)) | ||
| @assert this_commit isa Dict{Symbol, DataFrame} "Expected the benchmark script to return a Dict{Symbol, DataFrame}, but got $(typeof(this_commit)). Make sure your benchmark script returns a dict resulting from BenchmarkSolver.bmark_solvers function" | ||
| @save "$(bmarkname)_solver_benchmarks_this_commit.jld2" this_commit | ||
|
|
||
| # Run the benchmark script on the reference branch | ||
| local reference | ||
| if is_git | ||
| repo_dir = joinpath(bmark_dir, "..") | ||
| repo = LibGit2.GitRepo(repo_dir) | ||
| reference = _withcommit(joinpath(bmark_dir, script), repo, reference_branch, bmarkname = bmarkname) | ||
| end | ||
|
|
||
| # Plotting and tables | ||
| local profile_values, table_values | ||
|
|
||
| profile_values = Base.invokelatest(solver_benchmark_profile_values) | ||
| table_values = Base.invokelatest(solver_benchmark_table_values) | ||
|
|
||
| files_dict = Dict{String, Any}() | ||
| svgs = String[] | ||
|
|
||
| solved(df) = (df.status .== :first_order) | ||
| costs = [df -> .!solved(df) * Inf + getproperty(df, value[1]) for value in profile_values] | ||
| costnames = [value[2] for value in profile_values] | ||
|
|
||
| stats_columns = [value[1] for value in table_values] | ||
|
|
||
| tables = "# Solver Benchmarks Tables \n\n" | ||
| if is_git | ||
| for key in keys(this_commit) | ||
| if haskey(reference, key) | ||
| @info "Plotting $key" | ||
| stats_subset = Dict(:this_commit => this_commit[key], :reference => reference[key]) | ||
| p = profile_solvers(stats_subset, costs, costnames, xlabel = "", ylabel = "") | ||
| fname = "this_commit_vs_reference_$(key)" | ||
| savefig("$(fname).svg") | ||
| savefig("profiles_$(fname).pdf") | ||
| push!(svgs, "$(fname).svg") | ||
| content = read("$(fname).svg", String) | ||
| files_dict["$(fname).svg"] = Dict("content" => content) | ||
|
|
||
| @info "Creating tables for $key" | ||
| tables *= "\n## This commit vs reference: $(key)\n\n" | ||
| tables *= "### This commit\n\n\n" | ||
| tables *= sprint(io -> pretty_stats(io, this_commit[key][!, stats_columns], hdr_override = Dict(table_values), tf=tf_markdown)) | ||
| open("this_commit_$(key).tex", "w") do io | ||
| pretty_latex_stats(io, this_commit[key][!, stats_columns], hdr_override = Dict(table_values)) | ||
| end | ||
| tables *= "\n\n### Reference\n\n\n" | ||
| tables *= sprint(io -> pretty_stats(io, reference[key][!, stats_columns], hdr_override = Dict(table_values), tf=tf_markdown)) | ||
| open("reference_$(key).tex", "w") do io | ||
| pretty_latex_stats(io, reference[key][!, stats_columns], hdr_override = Dict(table_values)) | ||
| end | ||
| else | ||
| @warn "$(reference_branch) branch benchmarks do not run the solver $key. Please update the benchmark solver list in a separate PR and rebase." | ||
| end | ||
| end | ||
| end | ||
|
|
||
| files_dict["tables.md"] = Dict("content" => tables) | ||
|
|
||
| @info "creating or updating gist" | ||
| # json description of gist | ||
| json_dict = Dict{String, Any}( | ||
| "description" => "$(repo_name) repository benchmark", | ||
| "public" => true, | ||
| "files" => files_dict, | ||
| ) | ||
|
|
||
| if update_gist | ||
| json_dict["gist_id"] = gist_id | ||
| end | ||
|
|
||
| gist_json = "$(bmarkname).json" | ||
| open(gist_json, "w") do f | ||
| JSON.print(f, json_dict) | ||
| end | ||
|
|
||
| local new_gist_url | ||
| if update_gist | ||
| update_gist_from_json_dict(gist_id, json_dict) | ||
| else | ||
| new_gist = create_gist_from_json_dict(json_dict) | ||
| new_gist_url = string(new_gist.html_url) | ||
| end | ||
|
|
||
| # Update markdown report | ||
| if is_git | ||
| fname = "bmark_$(bmarkname).md" | ||
| open(fname, "a") do f | ||
| write_md_svgs(f, "SolverBenchmark Profiles", gist_url, svgs) | ||
| end | ||
| end | ||
|
|
||
| @info "finished" | ||
| return update_gist ? gist_url : new_gist_url | ||
| end | ||
|
|
||
| function solver_benchmark_profile_values() | ||
| return [(:elapsed_time, "CPU Time"), (:neval_obj, "# Objective Evals"), (:neval_grad, "# Gradient Evals")] | ||
| end | ||
|
|
||
| function solver_benchmark_table_values() | ||
| return [(:name, "Name"), (:objective, "f(x)"), (:elapsed_time, "Time")] | ||
| end | ||
|
|
||
| # Runs a script at a commit on a repo and afterwards goes back | ||
| # to the original commit / branch. | ||
| # This code is based on https://github.com/JuliaCI/PkgBenchmark.jl/blob/master/src/util.jl | ||
| function _withcommit(script, repo, commit; bmarkname = "") | ||
| original_commit = string(LibGit2.GitHash(LibGit2.GitObject(repo, "HEAD"))) | ||
| local result | ||
| LibGit2.transact(repo) do r | ||
| branch = try LibGit2.branch(r) catch err; nothing end | ||
| try | ||
| LibGit2.checkout!(r, _shastring(r, commit)) | ||
|
|
||
| env_to_use = dirname(Pkg.Types.Context().env.project_file) | ||
| save_file_name = "$(bmarkname)_solver_benchmarks_reference" | ||
| exec_str = | ||
| """ | ||
| using JSOBenchmarks | ||
| JSOBenchmarks._run_local($(repr(script)), "$(save_file_name)") | ||
| """ | ||
| run(`$(Base.julia_cmd()) --project=$env_to_use --depwarn=no -e $exec_str`) | ||
|
|
||
| result = load("$(save_file_name).jld2")["result"] | ||
|
|
||
| @assert result isa Dict{Symbol, DataFrame} "Expected the benchmark script to return a Dict{Symbol, DataFrame}, but got $(typeof(result)). Make sure your benchmark script returns a dict resulting from BenchmarkSolver.bmark_solvers function" | ||
| catch err | ||
| rethrow(err) | ||
| finally | ||
| if branch !== nothing | ||
| LibGit2.branch!(r, branch) | ||
| else | ||
| LibGit2.checkout!(r, original_commit) | ||
| end | ||
| end | ||
| end | ||
| return result | ||
| end | ||
|
|
||
| function _run_local(script, save_file_name) | ||
| result = Base.include(Main, script) | ||
| @save "$(save_file_name).jld2" result | ||
| end | ||
|
|
||
| function _shastring(r::LibGit2.GitRepo, targetname) | ||
| branch = LibGit2.lookup_branch(r, targetname) | ||
| branch = branch === nothing ? LibGit2.lookup_branch(r, targetname, true) : branch # Search remote as well if not found locally. | ||
| branch = branch === nothing ? LibGit2.lookup_branch(r, "origin/$(targetname)") : branch | ||
| branch = branch === nothing ? LibGit2.lookup_branch(r, "origin/$(targetname)", true) : branch # Search remote as well if not found locally. | ||
| @assert branch !== nothing "Branch $(targetname) not found in repository." | ||
| return string(LibGit2.GitHash(LibGit2.GitObject(r, LibGit2.name(branch)))) | ||
| end | ||
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.
The reference-branch run is done by
checkout!-ing and thenBase.include(Main, script)in the same Julia process. If the script doesusing <PackageBeingBenchmarked>, Julia will not reload the module after the checkout, so the “reference” benchmarks can accidentally run against the already-loaded current-branch code. To ensure correct comparisons, run each branch/commit’s script in a fresh Julia process (or load into isolated modules/processes and explicitly restart the session between runs) and deserialize the returned results.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.
I think this is fine, it looks like this is how they do it in
PkgBenchmarks.jl