Skip to content

Instantly share code, notes, and snippets.

@abravalheri
Last active June 20, 2022 18:48
Show Gist options
  • Save abravalheri/d561ca67405a9b3b1d182811a0eaf398 to your computer and use it in GitHub Desktop.
Save abravalheri/d561ca67405a9b3b1d182811a0eaf398 to your computer and use it in GitHub Desktop.
Simple profiling script to verify `setuptools` execution time.
.venv
build
dist
*.pstats
*.html
*.egg-info

Small script to profile setuptools execution time. It requires Python 3.8+ installed + virtualenv.

Running the profiler:

./profile.sh

It uses setup.py instead of setup.cfg or pyproject.toml for configuration to skip parsing/validation times.

print("nothing to see here")
#!/usr/bin/env bash
# This script requires `virtualenv` to be installed
DIR=$(pwd)
cd "$(dirname "${BASH_SOURCE[0]}")" || {
echo "Unable to switch to ${BASH_SOURCE[0]}"
exit 1
}
BASE_DIR=$(pwd)
if [[ ! -d ".venv" ]]; then
virtualenv .venv
.venv/bin/python -m pip install -U pip setuptools wheel pyinstrument snakeviz
fi
.venv/bin/python run_cprofile.py "$@"
if [[ -f "cprofile.pstats" ]]; then
.venv/bin/snakeviz cprofile.pstats
fi
rm -rf build dist
cd "$DIR"
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
"""Quick and dirty script to profile setuptools.
Assumption: the script runs in a virtualenv with all the dependencies already
installed. Nevertheless it also runs ``get_requires_for_...`` to include its
time in the profiling because in most real world scenarios it will count.
"""
import argparse
import cProfile
import os
import shutil
from contextlib import contextmanager
from tempfile import TemporaryDirectory
import pyinstrument
from setuptools import build_meta as backend
CACHE = ["myproj.egg-info", "build"]
def run_backend(output_dir, metadata_dir):
backend.get_requires_for_build_sdist()
sdist = backend.build_sdist(output_dir)
print(f" ------------- sdist ----------- {sdist}")
backend.get_requires_for_build_wheel()
dist_info = backend.prepare_metadata_for_build_wheel(metadata_dir)
dist_info_dir = os.path.join(metadata_dir, dist_info)
wheel = backend.build_wheel(output_dir, None, dist_info_dir)
print(f" ------------- wheel ----------- {wheel}")
def run_experiment(profiler_context):
for d in CACHE:
if os.path.isdir(d):
shutil.rmtree(d)
name = profiler_context.func.__name__.replace("_context", "")
print(f"======================== Exp. {name} =========================")
with TemporaryDirectory() as tmp:
output_dir = os.path.join(tmp, "dist")
os.mkdir(output_dir)
metadata_dir = os.path.join(tmp, "metadata")
os.mkdir(metadata_dir)
with profiler_context:
run_backend(output_dir, metadata_dir)
@contextmanager
def cprofile_context(save_file):
with cProfile.Profile() as pr:
yield
save_file = save_file or "cprofile.pstats"
pr.dump_stats(save_file)
print(f"\n\nINFO: Output saved to {save_file}\n\n")
print(
f" You can run `.venv/bin/snakeviz {save_file}` for visualisation\n"
f" (don't forget to increase `Depth` to the maximum).\n\n"
)
@contextmanager
def pyinstrument_context(save_file):
with pyinstrument.Profiler() as profiler:
yield
profiler.print()
html = profiler.output_html()
save_file = save_file or "pyinstrument.html"
with open(save_file, "w") as f:
f.write(html)
print(f"\n\nINFO: Output saved to {save_file}\n\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--cprofile",
action="store_const",
dest="context",
const=cprofile_context,
)
parser.add_argument(
"--pyinstrument",
action="store_const",
dest="context",
const=pyinstrument_context,
)
parser.add_argument("-o", "--output-file")
parser.set_defaults(context=cprofile_context)
args = parser.parse_args()
run_experiment(args.context(args.output_file))
from setuptools import setup
setup(
name="myproj",
version="42",
py_modules=["myproj"]
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment