Skip to content

Commit d5354d5

Browse files
authored
SImulate Profile (#2)
* Refactor: move simulate profile to v_prof with name profile_sim * add scalene implementation * add extra scaelene args * remove some comments * remove unused imports * bugfix * add profiler arg * add cli tests * add run profile tests * formatting * make cprofile the default * use mocker instead of unittest mock * marginally improve compactness * formatting * add mprof back * add to changelog * add to help message * fix help message * fix help formatting
1 parent 2b99ffc commit d5354d5

File tree

7 files changed

+409
-2
lines changed

7 files changed

+409
-2
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
**v0.1.0 - 10/24/2025**
2+
3+
- Convert bash script to python
4+
- add profiling CLI with scalene backend
5+
16
**v0.0.1 - 10/20/2025**
27

38
- Initial release

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"memory_profiler",
5454
"matplotlib",
5555
"seaborn",
56+
"scalene",
5657
]
5758

5859
setup_requires = ["setuptools_scm"]
@@ -96,5 +97,6 @@
9697
[console_scripts]
9798
make_artifacts=vivarium_profiling.tools.cli:make_artifacts
9899
run_benchmark=vivarium_profiling.tools.cli:run_benchmark
100+
profile_sim=vivarium_profiling.tools.cli:profile_sim
99101
""",
100102
)

src/vivarium_profiling/tools/cli.py

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,149 @@
11
import glob
2+
import pstats
3+
import subprocess
4+
from datetime import datetime as dt
25
from pathlib import Path
36

47
import click
58
from loguru import logger
9+
from vivarium.framework.logging import configure_logging_to_file
610
from vivarium.framework.utilities import handle_exceptions
711

812
from vivarium_profiling.constants import metadata, paths
913
from vivarium_profiling.tools import build_artifacts, configure_logging_to_terminal
1014
from vivarium_profiling.tools.run_benchmark import run_benchmark_loop
1115

1216

17+
@click.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
18+
@click.argument(
19+
"model_specification",
20+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
21+
)
22+
@click.option(
23+
"--results_directory",
24+
"-o",
25+
type=click.Path(resolve_path=True),
26+
default=Path("~/vivarium_results/").expanduser(),
27+
show_default=True,
28+
help=(
29+
"The directory to write results to. A folder will be created "
30+
"in this directory with the same name as the configuration file."
31+
),
32+
)
33+
@click.option(
34+
"--skip_writing",
35+
is_flag=True,
36+
help=(
37+
"Skip writing the simulation results to the output directory; the time spent "
38+
"normally writing simulation results to disk will not be included in the profiling "
39+
"statistics."
40+
),
41+
)
42+
@click.option(
43+
"--skip_processing",
44+
is_flag=True,
45+
help=(
46+
"Skip processing the resulting binary file to a human-readable .txt file "
47+
"sorted by cumulative runtime; the resulting .stats file can still be read "
48+
"and processed later using the pstats module."
49+
),
50+
)
51+
@click.option(
52+
"--profiler",
53+
type=click.Choice(["cprofile", "scalene"]),
54+
default="cprofile",
55+
show_default=True,
56+
help=(
57+
"Profiling backend to use. cProfile provides the most detaile function-level"
58+
"runtime information, while scalene provides detailed annotation of source"
59+
"code that may be the source of bottlenecks."
60+
),
61+
)
62+
@click.pass_context
63+
def profile_sim(
64+
ctx: click.Context,
65+
model_specification: Path,
66+
results_directory: Path,
67+
skip_writing: bool,
68+
skip_processing: bool,
69+
profiler: str,
70+
) -> None:
71+
"""Run a simulation based on the provided MODEL_SPECIFICATION and profile the run."""
72+
model_specification = Path(model_specification)
73+
results_directory = Path(results_directory)
74+
results_root = results_directory / f"{dt.now().strftime('%Y_%m_%d_%H_%M_%S')}"
75+
configure_logging_to_file(output_directory=results_root)
76+
77+
if skip_writing:
78+
configuration_override = {}
79+
else:
80+
output_data_root = results_root / "results"
81+
output_data_root.mkdir(parents=True, exist_ok=False)
82+
configuration_override = {
83+
"output_data": {"results_directory": str(output_data_root)},
84+
}
85+
86+
script_path = Path(__file__).parent / "run_profile.py"
87+
88+
config_str = repr(configuration_override)
89+
90+
# Get any extra arguments passed to the profiler
91+
extra_args = ctx.args
92+
93+
if profiler == "scalene":
94+
out_json_file = results_root / f"{model_specification.name}".replace("yaml", "json")
95+
try:
96+
cmd = [
97+
"scalene",
98+
"--json",
99+
"--outfile",
100+
str(out_json_file),
101+
"--off",
102+
]
103+
# Add any additional profiler arguments
104+
cmd.extend(extra_args)
105+
cmd.extend(
106+
[
107+
str(script_path),
108+
str(model_specification),
109+
"--config-override",
110+
config_str,
111+
]
112+
)
113+
subprocess.run(cmd, check=True)
114+
except subprocess.CalledProcessError as e:
115+
logger.error(f"Scalene profiling failed: {e}")
116+
raise
117+
elif profiler == "cprofile":
118+
out_stats_file = results_root / f"{model_specification.name}".replace("yaml", "stats")
119+
try:
120+
subprocess.run(
121+
[
122+
"python",
123+
str(script_path),
124+
str(model_specification),
125+
"--config-override",
126+
config_str,
127+
"--profiler",
128+
"cprofile",
129+
"--output",
130+
str(out_stats_file),
131+
],
132+
check=True,
133+
)
134+
135+
if not skip_processing:
136+
out_txt_file = Path(str(out_stats_file) + ".txt")
137+
with out_txt_file.open("w") as f:
138+
p = pstats.Stats(str(out_stats_file), stream=f)
139+
p.sort_stats("cumulative")
140+
p.print_stats()
141+
142+
except subprocess.CalledProcessError as e:
143+
logger.error(f"cProfile profiling failed: {e}")
144+
raise
145+
146+
13147
@click.command()
14148
@click.option(
15149
"-l",
@@ -66,7 +200,7 @@ def make_artifacts(
66200
@click.command()
67201
@click.option(
68202
"-m",
69-
"--models",
203+
"--model_specifications",
70204
multiple=True,
71205
required=True,
72206
help="Model specification files (supports glob patterns). Can be specified multiple times.",

src/vivarium_profiling/tools/run_benchmark.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def initialize_results_file(results_dir: str) -> str:
5959

6060
def run_memory_profiler(spec: str, output_dir: str) -> None:
6161
"""Run memory profiler on a model specification."""
62-
cmd = ["mprof", "run", "-CM", "simulate", "profile", spec, "-o", output_dir]
62+
cmd = ["mprof", "run", "-CM", "profile_sim", spec, "-o", output_dir]
6363
result = subprocess.run(cmd, check=True, capture_output=False)
6464
if result.returncode != 0:
6565
raise click.ClickException(f"Memory profiler failed for {spec}")
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env python3
2+
"""Standalone script for profiling Vivarium simulations."""
3+
4+
import argparse
5+
import ast
6+
import cProfile
7+
import sys
8+
9+
from scalene import scalene_profiler
10+
from vivarium.framework.engine import SimulationContext
11+
12+
13+
def run_profile_scalene(sim: SimulationContext):
14+
"""Run a Vivarium simulation for scalene profiling purposes."""
15+
with scalene_profiler.enable_profiling():
16+
sim.run_simulation()
17+
18+
19+
def run_profile_cprofile(sim: SimulationContext, output_file: str):
20+
"""Run a Vivarium simulation for cProfile profiling purposes."""
21+
with cProfile.Profile() as profiler:
22+
sim.run_simulation()
23+
profiler.dump_stats(output_file)
24+
25+
26+
def main():
27+
parser = argparse.ArgumentParser(description="Run Vivarium simulation for profiling")
28+
parser.add_argument("model_specification", help="Path to model specification file")
29+
parser.add_argument(
30+
"--config-override", default="{}", help="Configuration override as JSON/dict string"
31+
)
32+
parser.add_argument(
33+
"--profiler",
34+
choices=["scalene", "cprofile"],
35+
default="scalene",
36+
help="Profiling backend to use",
37+
)
38+
parser.add_argument(
39+
"--output", help="Output file for cProfile stats (required when using cprofile)"
40+
)
41+
42+
args = parser.parse_args()
43+
44+
# Parse the configuration override
45+
try:
46+
configuration_override = ast.literal_eval(args.config_override)
47+
except (ValueError, SyntaxError) as e:
48+
print(f"Error parsing config override: {e}", file=sys.stderr)
49+
sys.exit(1)
50+
51+
sim = SimulationContext(args.model_specification, configuration=configuration_override)
52+
53+
if args.profiler == "scalene":
54+
run_profile_scalene(sim)
55+
elif args.profiler == "cprofile":
56+
if not args.output:
57+
print("Error: --output is required when using cprofile", file=sys.stderr)
58+
sys.exit(1)
59+
run_profile_cprofile(sim, args.output)
60+
61+
62+
if __name__ == "__main__":
63+
main()

0 commit comments

Comments
 (0)