Skip to content

Commit bd81e2d

Browse files
authored
Add opt-in scope-profiler regions to the template (#36)
1 parent 6639961 commit bd81e2d

9 files changed

Lines changed: 236 additions & 70 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
77

88
### Added
99

10+
- Opt-in scope-profiler regions, as in plasmacoll, wellbalanced and patchwork: `_profiling.py`
11+
with the package's tag (`template_python`, renamed by `setup_project.sh`), registered on
12+
import, and an example region on `main`. Off unless the application enables the tag (e.g.
13+
`scope_profiler.setup(enable_tags=["template_python"])`). Adds the `scope-profiler>=0.7.5`
14+
dependency.
1015
- `PyPI dependencies` workflow and `scripts/check_pypi_dependencies.py`: on every pull request to
1116
`main`, fail if a dependency is a direct reference (`name @ git+https://...`), is not on PyPI, or
1217
has a version range that no release satisfies, instead of failing the release after the merge.

‎docs/src/content/docs/getting-started/quickstart.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,21 @@ main()
2424
```
2525

2626
See the [API reference](/template-python/api/app/) for every module and function.
27+
28+
## Profiling
29+
30+
template-python's coarse operations are [scope-profiler](https://github.com/max-models/scope-profiler)
31+
regions (`template_python.main` here), tagged with the opt-in tag `template_python`: they are
32+
off by default and cost a fraction of a microsecond per call. A code that profiles itself with
33+
scope-profiler turns them on with the tag:
34+
35+
```python
36+
import scope_profiler as sp
37+
38+
sp.setup(enable_tags=["template_python"])
39+
```
40+
41+
or without editing the code, with `SCOPE_PROFILER_ENABLE_TAGS=template_python`,
42+
`scope-profiler run --enable-tag template_python` or `enable_tags = ["template_python"]` in the
43+
TOML configuration. New regions use `@sp.profile("template_python.<name>", tags=PROFILE_TAGS)`
44+
with `PROFILE_TAGS` from the package's `_profiling` module.

‎pyproject.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ classifiers = [
2424
# max-models packages come from their devel branches through [tool.uv.sources]
2525
dependencies = [
2626
"numpy",
27+
"scope-profiler>=0.7.5",
2728
]
2829
optional-dependencies.dev = [
2930
"jupyterlab",
@@ -81,3 +82,4 @@ report.show_missing = true
8182
[tool.uv.sources]
8283
# one entry per max-models package in [project], spelled exactly like this in every repo:
8384
# cunumpy = { git = "https://github.com/max-models/cunumpy", branch = "devel" }
85+
scope-profiler = { git = "https://github.com/max-models/scope-profiler", branch = "devel" }

‎setup_project.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ mv "src/${PACKAGE}" "src/${APPNAME_UNDERSCORE}"
5555

5656
# 3. Imports, the entry point, the API reference and the docs links
5757
replace "from ${PACKAGE}\\.main import main" "from ${APPNAME_UNDERSCORE}.main import main"
58+
replace "from ${PACKAGE}\\._profiling import" "from ${APPNAME_UNDERSCORE}._profiling import"
5859
replace "${PACKAGE}\\.main:main" "${APPNAME_UNDERSCORE}.main:main"
5960
replace "${PACKAGE}\\.main\\.main" "${APPNAME_UNDERSCORE}.main.main"
6061
replace "/api/${PACKAGE}/" "/api/${APPNAME_UNDERSCORE}/"

‎src/app/__init__.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
from importlib.metadata import PackageNotFoundError, version
44

5+
import scope_profiler as sp
6+
7+
from app._profiling import PROFILE_TAGS
8+
59
try:
610
__version__ = version("template-python")
711
except PackageNotFoundError: # not installed, e.g. run from a source tree
812
__version__ = "0.0.0+unknown"
13+
14+
# The decorated functions pick the tag up, so registering after the imports is in time.
15+
sp.register_opt_in_tag(*PROFILE_TAGS)

‎src/app/_profiling.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""The scope-profiler tag of template_python's profiling regions.
2+
3+
The regions are opt-in: :mod:`template_python` registers the tag on import, so they
4+
record nothing unless the application enables it, e.g. with
5+
``scope_profiler.setup(enable_tags=["template_python"])``.
6+
"""
7+
8+
#: Tags of every template_python region; the first is the opt-in tag.
9+
PROFILE_TAGS = ("template_python",)

‎src/app/main.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
"""Main module of the python package."""
22

3+
import scope_profiler as sp
34

5+
from app._profiling import PROFILE_TAGS
6+
7+
8+
@sp.profile("template_python.main", tags=PROFILE_TAGS)
49
def main() -> None:
510
"""Print a greeting; the entry point of the ``template-python`` command."""
611
print("Hello, world")
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""The opt-in scope-profiler regions: off unless the application enables the tag."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
import scope_profiler as sp
7+
8+
from app.main import main
9+
10+
11+
@pytest.fixture(autouse=True)
12+
def _fresh_profiler(monkeypatch):
13+
"""Keep the profiling state of these tests out of the others."""
14+
monkeypatch.delenv("SCOPE_PROFILER_ENABLE_TAGS", raising=False)
15+
sp.reset()
16+
yield
17+
sp.reset()
18+
19+
20+
def _region_names(**setup_kwargs) -> set[str]:
21+
sp.setup(output=None, **setup_kwargs)
22+
# an application region, recorded either way
23+
with sp.region("user.run"):
24+
main()
25+
results = sp.finalize(verbose=False, return_results=True)
26+
return set(results.region_names)
27+
28+
29+
def test_regions_are_off_by_default() -> None:
30+
assert _region_names() == {"user.run"}
31+
32+
33+
@pytest.mark.parametrize("tags", [["template_python"], ["*"]])
34+
def test_enabling_the_tag_records_the_regions(tags: list[str]) -> None:
35+
assert _region_names(enable_tags=tags) == {"template_python.main", "user.run"}

0 commit comments

Comments
 (0)