scope-profiler 0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scope_profiler-0.1/PKG-INFO +108 -0
- scope_profiler-0.1/README.md +69 -0
- scope_profiler-0.1/pyproject.toml +62 -0
- scope_profiler-0.1/setup.cfg +4 -0
- scope_profiler-0.1/src/scope_profiler/h5reader.py +220 -0
- scope_profiler-0.1/src/scope_profiler/post_processing.py +55 -0
- scope_profiler-0.1/src/scope_profiler/profiling.py +422 -0
- scope_profiler-0.1/src/scope_profiler/tests/__init__.py +0 -0
- scope_profiler-0.1/src/scope_profiler/tests/examples.py +40 -0
- scope_profiler-0.1/src/scope_profiler/tests/test_app.py +73 -0
- scope_profiler-0.1/src/scope_profiler/tests/test_overhead.py +75 -0
- scope_profiler-0.1/src/scope_profiler.egg-info/PKG-INFO +108 -0
- scope_profiler-0.1/src/scope_profiler.egg-info/SOURCES.txt +15 -0
- scope_profiler-0.1/src/scope_profiler.egg-info/dependency_links.txt +1 -0
- scope_profiler-0.1/src/scope_profiler.egg-info/entry_points.txt +2 -0
- scope_profiler-0.1/src/scope_profiler.egg-info/requires.txt +24 -0
- scope_profiler-0.1/src/scope_profiler.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scope-profiler
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Profile code regions in python, optionally with LIKWID markers.
|
|
5
|
+
Author: Max
|
|
6
|
+
Project-URL: Source, https://github.com/max-models/scope_profiler
|
|
7
|
+
Keywords: python
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: h5py
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: black[jupyter]; extra == "dev"
|
|
23
|
+
Requires-Dist: isort; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Requires-Dist: scope-profiler[docs,test]; extra == "dev"
|
|
26
|
+
Provides-Extra: docs
|
|
27
|
+
Requires-Dist: ipykernel; extra == "docs"
|
|
28
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
29
|
+
Requires-Dist: nbconvert; extra == "docs"
|
|
30
|
+
Requires-Dist: nbsphinx; extra == "docs"
|
|
31
|
+
Requires-Dist: jupyterlab; extra == "docs"
|
|
32
|
+
Requires-Dist: pre-commit; extra == "docs"
|
|
33
|
+
Requires-Dist: pyproject-fmt; extra == "docs"
|
|
34
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
35
|
+
Requires-Dist: sphinx-book-theme; extra == "docs"
|
|
36
|
+
Provides-Extra: test
|
|
37
|
+
Requires-Dist: coverage; extra == "test"
|
|
38
|
+
Requires-Dist: pytest; extra == "test"
|
|
39
|
+
|
|
40
|
+
# scope-profiler - Python Profiling System with Optional LIKWID Integration
|
|
41
|
+
|
|
42
|
+
This module provides a unified profiling system for Python applications, with optional integration of [LIKWID](https://github.com/RRZE-HPC/likwid) markers using the [pylikwid](https://github.com/RRZE-HPC/pylikwid) marker API for hardware performance counters. It allows you to:
|
|
43
|
+
- Configure profiling globally via a singleton ProfilingConfig.
|
|
44
|
+
- Collect timing data via context-managed profiling regions.
|
|
45
|
+
- Use a clean decorator syntax to profile functions.
|
|
46
|
+
- Optionally record time traces in HDF5 files.
|
|
47
|
+
- Automatically initialize and close LIKWID markers only when needed.
|
|
48
|
+
- Print aggregated summaries of all profiling regions.
|
|
49
|
+
|
|
50
|
+
Documentation: https://max-models.github.io/scope-profiler/
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
pip install scope-profiler
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from scope_profiler.profiling import (
|
|
62
|
+
ProfilingConfig,
|
|
63
|
+
ProfileManager,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
config = ProfilingConfig(
|
|
67
|
+
use_likwid=False,
|
|
68
|
+
time_trace=True,
|
|
69
|
+
flush_to_disk=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@ProfileManager.profile("main")
|
|
73
|
+
def main():
|
|
74
|
+
x = 0
|
|
75
|
+
for i in range(10):
|
|
76
|
+
with ProfileManager.profile_region(region_name="iteration"):
|
|
77
|
+
x += 1
|
|
78
|
+
|
|
79
|
+
main()
|
|
80
|
+
|
|
81
|
+
ProfileManager.print_summary()
|
|
82
|
+
|
|
83
|
+
ProfileManager.finalize()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Execution:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
❯ python test.py
|
|
90
|
+
Profiling Summary:
|
|
91
|
+
========================================
|
|
92
|
+
Region: main
|
|
93
|
+
Number of Calls: 1
|
|
94
|
+
Total Duration: 0.000315 seconds
|
|
95
|
+
Average Duration: 0.000315 seconds
|
|
96
|
+
Min Duration: 0.000315 seconds
|
|
97
|
+
Max Duration: 0.000315 seconds
|
|
98
|
+
Std Deviation: 0.000000 seconds
|
|
99
|
+
----------------------------------------
|
|
100
|
+
Region: iteration
|
|
101
|
+
Number of Calls: 10
|
|
102
|
+
Total Duration: 0.000007 seconds
|
|
103
|
+
Average Duration: 0.000001 seconds
|
|
104
|
+
Min Duration: 0.000000 seconds
|
|
105
|
+
Max Duration: 0.000003 seconds
|
|
106
|
+
Std Deviation: 0.000001 seconds
|
|
107
|
+
----------------------------------------
|
|
108
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# scope-profiler - Python Profiling System with Optional LIKWID Integration
|
|
2
|
+
|
|
3
|
+
This module provides a unified profiling system for Python applications, with optional integration of [LIKWID](https://github.com/RRZE-HPC/likwid) markers using the [pylikwid](https://github.com/RRZE-HPC/pylikwid) marker API for hardware performance counters. It allows you to:
|
|
4
|
+
- Configure profiling globally via a singleton ProfilingConfig.
|
|
5
|
+
- Collect timing data via context-managed profiling regions.
|
|
6
|
+
- Use a clean decorator syntax to profile functions.
|
|
7
|
+
- Optionally record time traces in HDF5 files.
|
|
8
|
+
- Automatically initialize and close LIKWID markers only when needed.
|
|
9
|
+
- Print aggregated summaries of all profiling regions.
|
|
10
|
+
|
|
11
|
+
Documentation: https://max-models.github.io/scope-profiler/
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pip install scope-profiler
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from scope_profiler.profiling import (
|
|
23
|
+
ProfilingConfig,
|
|
24
|
+
ProfileManager,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
config = ProfilingConfig(
|
|
28
|
+
use_likwid=False,
|
|
29
|
+
time_trace=True,
|
|
30
|
+
flush_to_disk=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@ProfileManager.profile("main")
|
|
34
|
+
def main():
|
|
35
|
+
x = 0
|
|
36
|
+
for i in range(10):
|
|
37
|
+
with ProfileManager.profile_region(region_name="iteration"):
|
|
38
|
+
x += 1
|
|
39
|
+
|
|
40
|
+
main()
|
|
41
|
+
|
|
42
|
+
ProfileManager.print_summary()
|
|
43
|
+
|
|
44
|
+
ProfileManager.finalize()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Execution:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
❯ python test.py
|
|
51
|
+
Profiling Summary:
|
|
52
|
+
========================================
|
|
53
|
+
Region: main
|
|
54
|
+
Number of Calls: 1
|
|
55
|
+
Total Duration: 0.000315 seconds
|
|
56
|
+
Average Duration: 0.000315 seconds
|
|
57
|
+
Min Duration: 0.000315 seconds
|
|
58
|
+
Max Duration: 0.000315 seconds
|
|
59
|
+
Std Deviation: 0.000000 seconds
|
|
60
|
+
----------------------------------------
|
|
61
|
+
Region: iteration
|
|
62
|
+
Number of Calls: 10
|
|
63
|
+
Total Duration: 0.000007 seconds
|
|
64
|
+
Average Duration: 0.000001 seconds
|
|
65
|
+
Min Duration: 0.000000 seconds
|
|
66
|
+
Max Duration: 0.000003 seconds
|
|
67
|
+
Std Deviation: 0.000001 seconds
|
|
68
|
+
----------------------------------------
|
|
69
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
|
|
4
|
+
requires = [ "setuptools", "wheel" ]
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "scope-profiler"
|
|
8
|
+
version = "0.1"
|
|
9
|
+
description = "Profile code regions in python, optionally with LIKWID markers."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
keywords = [ "python" ]
|
|
12
|
+
license = { file = "LICENSE.txt" }
|
|
13
|
+
authors = [ { name = "Max" } ]
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"h5py",
|
|
27
|
+
"numpy",
|
|
28
|
+
"matplotlib",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
optional-dependencies.dev = [
|
|
32
|
+
"black[jupyter]",
|
|
33
|
+
"isort",
|
|
34
|
+
"ruff",
|
|
35
|
+
"scope-profiler[test,docs]",
|
|
36
|
+
]
|
|
37
|
+
# https://medium.com/@pratikdomadiya123/build-project-documentation-quickly-with-the-sphinx-python-2a9732b66594
|
|
38
|
+
optional-dependencies.docs = [
|
|
39
|
+
"ipykernel",
|
|
40
|
+
"myst-parser",
|
|
41
|
+
"nbconvert",
|
|
42
|
+
"nbsphinx",
|
|
43
|
+
"jupyterlab",
|
|
44
|
+
"pre-commit",
|
|
45
|
+
"pyproject-fmt",
|
|
46
|
+
"sphinx",
|
|
47
|
+
"sphinx-book-theme",
|
|
48
|
+
]
|
|
49
|
+
optional-dependencies.test = [
|
|
50
|
+
"coverage",
|
|
51
|
+
"pytest"
|
|
52
|
+
]
|
|
53
|
+
urls."Source" = "https://github.com/max-models/scope_profiler"
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
scope-profiler-pproc = "scope_profiler.post_processing:main"
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.packages.find]
|
|
59
|
+
where = [ "src" ]
|
|
60
|
+
|
|
61
|
+
[tool.isort]
|
|
62
|
+
profile = "black"
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
import h5py
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Region:
|
|
10
|
+
def __init__(self, start_times: np.ndarray, end_times: np.ndarray) -> None:
|
|
11
|
+
self._start_times = start_times
|
|
12
|
+
self._end_times = end_times
|
|
13
|
+
self._durations = end_times - start_times
|
|
14
|
+
|
|
15
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
16
|
+
"""Return a summary of the region’s statistics as a dictionary."""
|
|
17
|
+
return {
|
|
18
|
+
"num_calls": self.num_calls,
|
|
19
|
+
"total_duration": self.total_duration,
|
|
20
|
+
"average_duration": self.average_duration,
|
|
21
|
+
"min_duration": self.min_duration,
|
|
22
|
+
"max_duration": self.max_duration,
|
|
23
|
+
"std_duration": self.std_duration,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
"""Print summaries for all regions in the file."""
|
|
28
|
+
# print(f"\nProfiling data summary for: {self.file_path}")
|
|
29
|
+
_out = "-" * 60 + "\n"
|
|
30
|
+
stats = self.get_summary()
|
|
31
|
+
for key, value in stats.items():
|
|
32
|
+
_out += f" {key:>18}: {value}\n"
|
|
33
|
+
_out += "-" * 60 + "\n\n"
|
|
34
|
+
return _out
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def start_times(self) -> np.ndarray:
|
|
38
|
+
return self._start_times
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def end_times(self) -> np.ndarray:
|
|
42
|
+
return self._end_times
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def durations(self) -> np.ndarray:
|
|
46
|
+
return self._durations
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def num_calls(self) -> int:
|
|
50
|
+
"""Number of recorded calls."""
|
|
51
|
+
return len(self._durations)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def total_duration(self) -> float:
|
|
55
|
+
"""Total time spent in this region (sum of all durations)."""
|
|
56
|
+
return float(np.sum(self._durations)) if self.num_calls else 0.0
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def average_duration(self) -> float:
|
|
60
|
+
"""Average duration per call."""
|
|
61
|
+
return float(np.mean(self._durations)) if self.num_calls else 0.0
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def min_duration(self) -> float:
|
|
65
|
+
"""Minimum duration among all calls."""
|
|
66
|
+
return float(np.min(self._durations)) if self.num_calls else 0.0
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def max_duration(self) -> float:
|
|
70
|
+
"""Maximum duration among all calls."""
|
|
71
|
+
return float(np.max(self._durations)) if self.num_calls else 0.0
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def std_duration(self) -> float:
|
|
75
|
+
"""Standard deviation of durations."""
|
|
76
|
+
return float(np.std(self._durations)) if self.num_calls else 0.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ProfilingH5Reader:
|
|
80
|
+
"""
|
|
81
|
+
Reads profiling data stored by ProfileRegion in an HDF5 file.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, file_path: str | Path):
|
|
85
|
+
self._file_path = Path(file_path)
|
|
86
|
+
if not self.file_path.exists():
|
|
87
|
+
raise FileNotFoundError(f"HDF5 file not found: {self.file_path}")
|
|
88
|
+
|
|
89
|
+
# Verify it's an HDF5 file
|
|
90
|
+
try:
|
|
91
|
+
with h5py.File(self.file_path, "r") as f:
|
|
92
|
+
if "regions" not in f:
|
|
93
|
+
raise ValueError("Invalid profiling file: missing 'regions' group.")
|
|
94
|
+
except OSError as e:
|
|
95
|
+
raise ValueError(f"Cannot open {self.file_path} as an HDF5 file") from e
|
|
96
|
+
|
|
97
|
+
# Read the file
|
|
98
|
+
self._region_dict = {}
|
|
99
|
+
with h5py.File(self.file_path, "r") as f:
|
|
100
|
+
# region_names = list(f["regions"].keys())
|
|
101
|
+
|
|
102
|
+
for region_name, region in f["regions"].items():
|
|
103
|
+
# grp = f[f"regions/{region_name}"]
|
|
104
|
+
self._region_dict[region_name] = Region(
|
|
105
|
+
region["start_times"][()],
|
|
106
|
+
region["end_times"][()],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def get_region(self, region_name: str) -> Region:
|
|
110
|
+
return self._region_dict[region_name]
|
|
111
|
+
|
|
112
|
+
def plot_gantt(
|
|
113
|
+
self,
|
|
114
|
+
regions: List[str] | str | None = None,
|
|
115
|
+
filepath: str | None = None,
|
|
116
|
+
show: bool = False,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Plot a Gantt chart of all (or selected) regions.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
regions : list[str] | None
|
|
124
|
+
List of region names to plot. If None, plot all.
|
|
125
|
+
"""
|
|
126
|
+
if regions is None:
|
|
127
|
+
regions = list(self._region_dict.keys())
|
|
128
|
+
elif isinstance(regions, str):
|
|
129
|
+
regions = [regions]
|
|
130
|
+
|
|
131
|
+
fig, ax = plt.subplots(figsize=(10, 0.7 * len(regions)))
|
|
132
|
+
colors = plt.cm.tab20(np.linspace(0, 1, len(regions)))
|
|
133
|
+
|
|
134
|
+
for i, region_name in enumerate(regions):
|
|
135
|
+
region = self._region_dict[region_name]
|
|
136
|
+
for start, end in zip(region.start_times, region.end_times):
|
|
137
|
+
ax.barh(
|
|
138
|
+
y=i,
|
|
139
|
+
width=end - start,
|
|
140
|
+
left=start,
|
|
141
|
+
height=0.4,
|
|
142
|
+
color=colors[i],
|
|
143
|
+
edgecolor="black",
|
|
144
|
+
alpha=0.7,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
ax.set_xlabel("Time (seconds)")
|
|
148
|
+
ax.set_yticks(range(len(regions)))
|
|
149
|
+
ax.set_yticklabels(regions)
|
|
150
|
+
ax.set_title("Profiling Gantt Chart")
|
|
151
|
+
ax.grid(True, axis="x", linestyle="--", alpha=0.5)
|
|
152
|
+
fig.tight_layout()
|
|
153
|
+
if filepath:
|
|
154
|
+
plt.savefig(filepath, dpi=300)
|
|
155
|
+
if show:
|
|
156
|
+
plt.show()
|
|
157
|
+
|
|
158
|
+
def plot_durations(
|
|
159
|
+
self,
|
|
160
|
+
regions: List[str] | str | None = None,
|
|
161
|
+
filepath: str | None = None,
|
|
162
|
+
show: bool = False,
|
|
163
|
+
bins: int = 30,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Plot duration histograms for each region.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
regions : list[str] | None
|
|
171
|
+
List of region names to plot. If None, plot all.
|
|
172
|
+
bins : int
|
|
173
|
+
Number of histogram bins.
|
|
174
|
+
"""
|
|
175
|
+
if regions is None:
|
|
176
|
+
regions = list(self._region_dict.keys())
|
|
177
|
+
elif isinstance(regions, str):
|
|
178
|
+
regions = [regions]
|
|
179
|
+
|
|
180
|
+
n = len(regions)
|
|
181
|
+
fig, axes = plt.subplots(nrows=n, ncols=1, figsize=(8, 3 * n))
|
|
182
|
+
if n == 1:
|
|
183
|
+
axes = [axes]
|
|
184
|
+
|
|
185
|
+
for ax, region_name in zip(axes, regions):
|
|
186
|
+
region = self._region_dict[region_name]
|
|
187
|
+
durations = region.durations
|
|
188
|
+
if len(durations) == 0:
|
|
189
|
+
ax.text(0.5, 0.5, "No data", ha="center", va="center")
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
ax.hist(
|
|
193
|
+
durations, bins=bins, color="steelblue", alpha=0.7, edgecolor="black"
|
|
194
|
+
)
|
|
195
|
+
ax.set_title(f"Region: {region_name}")
|
|
196
|
+
ax.set_xlabel("Duration (s)")
|
|
197
|
+
ax.set_ylabel("Frequency")
|
|
198
|
+
ax.grid(True, alpha=0.4)
|
|
199
|
+
|
|
200
|
+
fig.suptitle("Region Duration Distributions", fontsize=14)
|
|
201
|
+
fig.tight_layout()
|
|
202
|
+
if filepath:
|
|
203
|
+
plt.savefig(filepath, dpi=300)
|
|
204
|
+
if show:
|
|
205
|
+
plt.show()
|
|
206
|
+
|
|
207
|
+
def __repr__(self) -> str:
|
|
208
|
+
_out = ""
|
|
209
|
+
for region_name, region in self._region_dict.items():
|
|
210
|
+
_out += f"Region: {region_name}\n"
|
|
211
|
+
_out += str(region)
|
|
212
|
+
return _out
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def file_path(self) -> Path:
|
|
216
|
+
return self._file_path
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def regions(self) -> List[Region]:
|
|
220
|
+
return self._regions
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from scope_profiler.h5reader import ProfilingH5Reader
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
"""Main function for reading and summarizing profiling HDF5 data."""
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
description="Read and summarize profiling HDF5 data."
|
|
11
|
+
)
|
|
12
|
+
parser.add_argument("file", type=str, help="Path to the profiling_data.h5 file")
|
|
13
|
+
parser.add_argument("--region", type=str, help="Region name to inspect (optional)")
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--show",
|
|
16
|
+
action="store_true",
|
|
17
|
+
help="Show plots interactively (default: do not show plots)",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"-o",
|
|
21
|
+
"--output",
|
|
22
|
+
type=str,
|
|
23
|
+
help="Directory or file prefix to save plots instead of displaying them",
|
|
24
|
+
)
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
reader = ProfilingH5Reader(args.file)
|
|
28
|
+
|
|
29
|
+
# Handle optional region selection
|
|
30
|
+
if args.region:
|
|
31
|
+
regions = [args.region]
|
|
32
|
+
print(f"\nRegion: {args.region}")
|
|
33
|
+
print(reader.get_region(args.region))
|
|
34
|
+
else:
|
|
35
|
+
regions = None
|
|
36
|
+
print(reader)
|
|
37
|
+
|
|
38
|
+
# Prepare output filepaths if requested
|
|
39
|
+
gantt_path = durations_path = None
|
|
40
|
+
if args.output:
|
|
41
|
+
os.makedirs(args.output, exist_ok=True)
|
|
42
|
+
gantt_path = os.path.join(args.output, "gantt_plot.png")
|
|
43
|
+
durations_path = os.path.join(args.output, "durations_plot.png")
|
|
44
|
+
|
|
45
|
+
# Call the plotting functions with the appropriate arguments
|
|
46
|
+
reader.plot_gantt(regions, filepath=gantt_path, show=args.show)
|
|
47
|
+
reader.plot_durations(regions, filepath=durations_path, show=args.show)
|
|
48
|
+
|
|
49
|
+
# If saving only (no show), print confirmation
|
|
50
|
+
if args.output and not args.show:
|
|
51
|
+
print(f"Plots saved to:\n {gantt_path}\n {durations_path}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
profiling.py
|
|
3
|
+
|
|
4
|
+
This module provides a centralized profiling configuration and management system
|
|
5
|
+
using LIKWID markers. It includes:
|
|
6
|
+
- A singleton class for managing profiling configuration.
|
|
7
|
+
- A context manager for profiling specific code regions.
|
|
8
|
+
- Initialization and cleanup functions for LIKWID markers.
|
|
9
|
+
- Convenience functions for setting and getting the profiling configuration.
|
|
10
|
+
|
|
11
|
+
LIKWID is imported only when profiling is enabled to avoid unnecessary overhead.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import functools
|
|
15
|
+
import inspect
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
# Import the profiling configuration class and context manager
|
|
19
|
+
from functools import lru_cache
|
|
20
|
+
from typing import Callable, Dict
|
|
21
|
+
|
|
22
|
+
import h5py
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=None) # Cache the import result to avoid repeated imports
|
|
27
|
+
def _import_pylikwid():
|
|
28
|
+
import pylikwid
|
|
29
|
+
|
|
30
|
+
return pylikwid
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ProfilingConfig:
|
|
34
|
+
"""Singleton class for managing global profiling configuration."""
|
|
35
|
+
|
|
36
|
+
_instance = None
|
|
37
|
+
_initialized = False
|
|
38
|
+
|
|
39
|
+
def __new__(cls, *args, **kwargs):
|
|
40
|
+
if cls._instance is None:
|
|
41
|
+
cls._instance = super().__new__(cls)
|
|
42
|
+
# Default values
|
|
43
|
+
cls._instance.profiling_activated = True
|
|
44
|
+
cls._instance.use_likwid = False
|
|
45
|
+
cls._instance.time_trace = False
|
|
46
|
+
cls._instance.flush_to_disk = False
|
|
47
|
+
return cls._instance
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
profiling_activated: bool = True,
|
|
52
|
+
use_likwid: bool = False,
|
|
53
|
+
time_trace: bool = True,
|
|
54
|
+
flush_to_disk: bool = False,
|
|
55
|
+
):
|
|
56
|
+
|
|
57
|
+
if self._initialized:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Only update if value provided
|
|
61
|
+
self.profiling_activated = profiling_activated
|
|
62
|
+
self.use_likwid = use_likwid
|
|
63
|
+
self.time_trace = time_trace
|
|
64
|
+
self.flush_to_disk = flush_to_disk
|
|
65
|
+
|
|
66
|
+
self._pylikwid = None
|
|
67
|
+
if self.use_likwid:
|
|
68
|
+
try:
|
|
69
|
+
import pylikwid
|
|
70
|
+
|
|
71
|
+
self._pylikwid = pylikwid
|
|
72
|
+
except ImportError as e:
|
|
73
|
+
raise ImportError(
|
|
74
|
+
"LIKWID profiling requested but pylikwid module not installed"
|
|
75
|
+
) from e
|
|
76
|
+
self._initialized = True
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def reset(cls):
|
|
80
|
+
"""Reset the singleton so it can be reinitialized."""
|
|
81
|
+
cls._instance = None
|
|
82
|
+
cls._initialized = False
|
|
83
|
+
|
|
84
|
+
def pylikwid_markerinit(self):
|
|
85
|
+
"""Initialize LIKWID profiling markers."""
|
|
86
|
+
if self.use_likwid and self._pylikwid:
|
|
87
|
+
self._pylikwid.markerinit()
|
|
88
|
+
|
|
89
|
+
def pylikwid_markerclose(self):
|
|
90
|
+
"""Close LIKWID profiling markers."""
|
|
91
|
+
if self.use_likwid and self._pylikwid:
|
|
92
|
+
self._pylikwid.markerclose()
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def profiling_activated(self) -> bool:
|
|
96
|
+
return self._profiling_activated
|
|
97
|
+
|
|
98
|
+
@profiling_activated.setter
|
|
99
|
+
def profiling_activated(self, value: bool) -> None:
|
|
100
|
+
assert isinstance(value, bool)
|
|
101
|
+
self._profiling_activated = value
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def use_likwid(self) -> bool:
|
|
105
|
+
return self._likwid
|
|
106
|
+
|
|
107
|
+
@use_likwid.setter
|
|
108
|
+
def use_likwid(self, value: bool) -> None:
|
|
109
|
+
assert isinstance(value, bool)
|
|
110
|
+
self._likwid = value
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def time_trace(self) -> bool:
|
|
114
|
+
return self._time_trace
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def flush_to_disk(self) -> bool:
|
|
118
|
+
return self._flush_to_disk
|
|
119
|
+
|
|
120
|
+
@flush_to_disk.setter
|
|
121
|
+
def flush_to_disk(self, value) -> None:
|
|
122
|
+
if not isinstance(value, bool):
|
|
123
|
+
raise TypeError("flush_to_disk must be a bool")
|
|
124
|
+
self._flush_to_disk = value
|
|
125
|
+
|
|
126
|
+
@time_trace.setter
|
|
127
|
+
def time_trace(self, value: bool) -> None:
|
|
128
|
+
assert isinstance(value, bool)
|
|
129
|
+
self._time_trace = value
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ProfileRegion:
|
|
133
|
+
"""Context manager for profiling specific code regions using LIKWID markers."""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
region_name: str,
|
|
138
|
+
time_trace: bool = False,
|
|
139
|
+
buffer_limit: int = 100000,
|
|
140
|
+
file_path: str | None = None,
|
|
141
|
+
flush_to_disk: bool = False,
|
|
142
|
+
profiling_activated: bool = True,
|
|
143
|
+
):
|
|
144
|
+
if hasattr(self, "_initialized") and self._initialized:
|
|
145
|
+
return
|
|
146
|
+
self._config = ProfilingConfig()
|
|
147
|
+
self._region_name = region_name
|
|
148
|
+
self._time_trace = time_trace
|
|
149
|
+
self._buffer_limit = buffer_limit
|
|
150
|
+
self._file_path = file_path or "profiling_data.h5"
|
|
151
|
+
self._flush_to_disk = flush_to_disk
|
|
152
|
+
self._profiling_activated = profiling_activated
|
|
153
|
+
|
|
154
|
+
# Timer data
|
|
155
|
+
self._ncalls = 0
|
|
156
|
+
self._start_times = []
|
|
157
|
+
self._end_times = []
|
|
158
|
+
self._duration = 0.0
|
|
159
|
+
self._started = False
|
|
160
|
+
|
|
161
|
+
# Create file and datasets if not existing
|
|
162
|
+
if self.flush_to_disk and self._time_trace:
|
|
163
|
+
with h5py.File(self._file_path, "a") as f:
|
|
164
|
+
grp = f.require_group(f"regions/{self._region_name}")
|
|
165
|
+
for name in ["start_times", "end_times", "durations"]:
|
|
166
|
+
if name not in grp:
|
|
167
|
+
grp.create_dataset(
|
|
168
|
+
name,
|
|
169
|
+
shape=(0,),
|
|
170
|
+
maxshape=(None,),
|
|
171
|
+
dtype="f8",
|
|
172
|
+
chunks=True,
|
|
173
|
+
compression="gzip",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def __enter__(self):
|
|
177
|
+
if not self.profiling_activated:
|
|
178
|
+
return self
|
|
179
|
+
if self.config.use_likwid:
|
|
180
|
+
self._pylikwid().markerstartregion(self.region_name)
|
|
181
|
+
|
|
182
|
+
if self._time_trace:
|
|
183
|
+
|
|
184
|
+
self._start_time = time.perf_counter()
|
|
185
|
+
self._start_times.append(self._start_time)
|
|
186
|
+
self._started = True
|
|
187
|
+
|
|
188
|
+
self._ncalls += 1
|
|
189
|
+
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
193
|
+
if not self.profiling_activated:
|
|
194
|
+
return
|
|
195
|
+
if self.config.use_likwid:
|
|
196
|
+
self._pylikwid().markerstopregion(self.region_name)
|
|
197
|
+
if self._time_trace and self.started:
|
|
198
|
+
end_time = time.perf_counter()
|
|
199
|
+
self._end_times.append(end_time)
|
|
200
|
+
self._started = False
|
|
201
|
+
|
|
202
|
+
if self.flush_to_disk and len(self._start_times) >= self._buffer_limit:
|
|
203
|
+
self.flush()
|
|
204
|
+
|
|
205
|
+
def flush(self) -> None:
|
|
206
|
+
"""Append buffered profiling data to the HDF5 file and clear memory."""
|
|
207
|
+
if not self._start_times:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
starts = self.start_times # np.array(self._start_times, dtype=np.float64)
|
|
211
|
+
ends = self.end_times # np.array(self._end_times, dtype=np.float64)
|
|
212
|
+
durations = self.durations
|
|
213
|
+
|
|
214
|
+
with h5py.File(self._file_path, "a") as f:
|
|
215
|
+
grp = f.require_group(f"regions/{self._region_name}")
|
|
216
|
+
for name, data in [
|
|
217
|
+
("start_times", starts),
|
|
218
|
+
("end_times", ends),
|
|
219
|
+
("durations", durations),
|
|
220
|
+
]:
|
|
221
|
+
ds = grp[name]
|
|
222
|
+
old_size = ds.shape[0]
|
|
223
|
+
new_size = old_size + len(data)
|
|
224
|
+
ds.resize((new_size,))
|
|
225
|
+
ds[old_size:new_size] = data
|
|
226
|
+
|
|
227
|
+
self._start_times.clear()
|
|
228
|
+
self._end_times.clear()
|
|
229
|
+
|
|
230
|
+
def _pylikwid(self):
|
|
231
|
+
return _import_pylikwid()
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def profiling_activated(self) -> bool:
|
|
235
|
+
return self._profiling_activated
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def config(self) -> ProfilingConfig:
|
|
239
|
+
return self._config
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def durations(self) -> np.ndarray:
|
|
243
|
+
return self.end_times - self.start_times
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def end_times(self) -> np.ndarray:
|
|
247
|
+
return np.array(self._end_times)
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def flush_to_disk(self) -> bool:
|
|
251
|
+
return self._flush_to_disk
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def num_calls(self) -> int:
|
|
255
|
+
return self._ncalls
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def region_name(self) -> str:
|
|
259
|
+
return self._region_name
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def start_times(self) -> np.ndarray:
|
|
263
|
+
return np.array(self._start_times)
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def started(self) -> bool:
|
|
267
|
+
return self._started
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class ProfileManager:
|
|
271
|
+
"""
|
|
272
|
+
Singleton class to manage and track all ProfileRegion instances.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
_regions = {}
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def reset(cls) -> None:
|
|
279
|
+
cls._regions = {}
|
|
280
|
+
|
|
281
|
+
@classmethod
|
|
282
|
+
def profile_region(cls, region_name) -> ProfileRegion:
|
|
283
|
+
"""
|
|
284
|
+
Get an existing ProfileRegion by name, or create a new one if it doesn't exist.
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
region_name: str
|
|
289
|
+
The name of the profiling region.
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
ProfileRegion: The ProfileRegion instance.
|
|
294
|
+
"""
|
|
295
|
+
if region_name in cls._regions:
|
|
296
|
+
# print(f"Using existing region '{region_name}'...")
|
|
297
|
+
return cls._regions[region_name]
|
|
298
|
+
else:
|
|
299
|
+
# print(f"Creating new region '{region_name}'...")
|
|
300
|
+
# Create and register a new ProfileRegion
|
|
301
|
+
cls._regions[region_name] = ProfileRegion(
|
|
302
|
+
region_name,
|
|
303
|
+
time_trace=ProfilingConfig().time_trace,
|
|
304
|
+
flush_to_disk=ProfilingConfig().flush_to_disk,
|
|
305
|
+
profiling_activated=ProfilingConfig().profiling_activated,
|
|
306
|
+
)
|
|
307
|
+
return cls._regions[region_name]
|
|
308
|
+
|
|
309
|
+
@classmethod
|
|
310
|
+
def profile(cls, region_name: str | None = None) -> Callable:
|
|
311
|
+
"""
|
|
312
|
+
Decorator factory for profiling a function.
|
|
313
|
+
|
|
314
|
+
Usage:
|
|
315
|
+
@ProfileManager.profile # region name defaults to function.__name__
|
|
316
|
+
def foo(...): ...
|
|
317
|
+
|
|
318
|
+
@ProfileManager.profile("myregion")
|
|
319
|
+
def bar(...): ...
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
def decorator(func: Callable) -> Callable:
|
|
323
|
+
# Default to function.__name__ if region_name is None
|
|
324
|
+
name = region_name or func.__name__
|
|
325
|
+
region = cls.profile_region(name)
|
|
326
|
+
|
|
327
|
+
if inspect.iscoroutinefunction(func):
|
|
328
|
+
# async function wrapper
|
|
329
|
+
@functools.wraps(func)
|
|
330
|
+
async def async_wrapper(*args, **kwargs):
|
|
331
|
+
with region:
|
|
332
|
+
return await func(*args, **kwargs)
|
|
333
|
+
|
|
334
|
+
return async_wrapper
|
|
335
|
+
else:
|
|
336
|
+
|
|
337
|
+
@functools.wraps(func)
|
|
338
|
+
def sync_wrapper(*args, **kwargs):
|
|
339
|
+
with region:
|
|
340
|
+
return func(*args, **kwargs)
|
|
341
|
+
|
|
342
|
+
return sync_wrapper
|
|
343
|
+
|
|
344
|
+
# If decorator used without parentheses: @ProfileManager.profile
|
|
345
|
+
# Python will pass the function directly to the decorator factory call,
|
|
346
|
+
# but because this is a factory we should allow that too:
|
|
347
|
+
if callable(region_name):
|
|
348
|
+
# invoked as @ProfileManager.profile with no args
|
|
349
|
+
func = region_name
|
|
350
|
+
region_name = None
|
|
351
|
+
return decorator(func)
|
|
352
|
+
|
|
353
|
+
return decorator
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def finalize(cls) -> None:
|
|
357
|
+
if ProfilingConfig().flush_to_disk:
|
|
358
|
+
for name, region in cls.get_all_regions().items():
|
|
359
|
+
region.flush()
|
|
360
|
+
|
|
361
|
+
@classmethod
|
|
362
|
+
def get_region(cls, region_name) -> ProfileRegion:
|
|
363
|
+
"""
|
|
364
|
+
Get a registered ProfileRegion by name.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
region_name: str
|
|
369
|
+
The name of the profiling region.
|
|
370
|
+
|
|
371
|
+
Returns
|
|
372
|
+
-------
|
|
373
|
+
ProfileRegion or None: The registered ProfileRegion instance or None if not found.
|
|
374
|
+
"""
|
|
375
|
+
return cls._regions.get(region_name)
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def get_all_regions(cls) -> Dict[str, "ProfileRegion"]:
|
|
379
|
+
"""
|
|
380
|
+
Get all registered ProfileRegion instances.
|
|
381
|
+
|
|
382
|
+
Returns
|
|
383
|
+
-------
|
|
384
|
+
dict: Dictionary of all registered ProfileRegion instances.
|
|
385
|
+
"""
|
|
386
|
+
return cls._regions
|
|
387
|
+
|
|
388
|
+
@classmethod
|
|
389
|
+
def print_summary(cls) -> None:
|
|
390
|
+
"""
|
|
391
|
+
Print a summary of the profiling data for all regions.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
_config = ProfilingConfig()
|
|
395
|
+
if not _config.time_trace:
|
|
396
|
+
print(
|
|
397
|
+
"time_trace is not set to True --> Time traces are not measured --> Skip printing summary...",
|
|
398
|
+
)
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
print("Profiling Summary:")
|
|
402
|
+
print("=" * 40)
|
|
403
|
+
for name, region in cls._regions.items():
|
|
404
|
+
if region.num_calls > 0:
|
|
405
|
+
total_duration = sum(region.durations)
|
|
406
|
+
average_duration = total_duration / region.num_calls
|
|
407
|
+
min_duration = min(region.durations)
|
|
408
|
+
max_duration = max(region.durations)
|
|
409
|
+
std_duration = np.std(region.durations)
|
|
410
|
+
else:
|
|
411
|
+
total_duration = average_duration = min_duration = max_duration = (
|
|
412
|
+
std_duration
|
|
413
|
+
) = 0
|
|
414
|
+
|
|
415
|
+
print(f"Region: {name}")
|
|
416
|
+
print(f" Number of Calls: {region.num_calls}")
|
|
417
|
+
print(f" Total Duration: {total_duration:.6f} seconds")
|
|
418
|
+
print(f" Average Duration: {average_duration:.6f} seconds")
|
|
419
|
+
print(f" Min Duration: {min_duration:.6f} seconds")
|
|
420
|
+
print(f" Max Duration: {max_duration:.6f} seconds")
|
|
421
|
+
print(f" Std Deviation: {std_duration:.6f} seconds")
|
|
422
|
+
print("-" * 40)
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from scope_profiler.profiling import ProfileManager
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def loop(
|
|
5
|
+
label,
|
|
6
|
+
num_loops: int = 100,
|
|
7
|
+
):
|
|
8
|
+
s = 0
|
|
9
|
+
for i in range(num_loops):
|
|
10
|
+
with ProfileManager.profile_region(region_name=label):
|
|
11
|
+
s += 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
from scope_profiler.profiling import (
|
|
16
|
+
ProfilingConfig,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
config = ProfilingConfig(
|
|
20
|
+
use_likwid=False,
|
|
21
|
+
time_trace=True,
|
|
22
|
+
flush_to_disk=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
ProfileManager.reset()
|
|
26
|
+
num_loops = 10
|
|
27
|
+
|
|
28
|
+
loop(
|
|
29
|
+
label="loop1",
|
|
30
|
+
num_loops=num_loops,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
loop(
|
|
34
|
+
label="loop2",
|
|
35
|
+
num_loops=num_loops * 2,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
ProfileManager.print_summary()
|
|
39
|
+
|
|
40
|
+
ProfileManager.finalize()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import scope_profiler.tests.examples as examples
|
|
4
|
+
from scope_profiler.profiling import (
|
|
5
|
+
ProfileManager,
|
|
6
|
+
ProfilingConfig,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize("time_trace", [True, False])
|
|
11
|
+
@pytest.mark.parametrize("use_likwid", [False])
|
|
12
|
+
@pytest.mark.parametrize("num_loops", [10, 50, 100])
|
|
13
|
+
@pytest.mark.parametrize("profiling_activated", [True, False])
|
|
14
|
+
def test_profile_manager(
|
|
15
|
+
time_trace: bool,
|
|
16
|
+
use_likwid: bool,
|
|
17
|
+
num_loops: int,
|
|
18
|
+
profiling_activated: bool,
|
|
19
|
+
):
|
|
20
|
+
|
|
21
|
+
ProfilingConfig().reset()
|
|
22
|
+
config = ProfilingConfig(
|
|
23
|
+
use_likwid=use_likwid,
|
|
24
|
+
time_trace=time_trace,
|
|
25
|
+
profiling_activated=profiling_activated,
|
|
26
|
+
)
|
|
27
|
+
ProfileManager.reset()
|
|
28
|
+
|
|
29
|
+
examples.loop(
|
|
30
|
+
label="loop1",
|
|
31
|
+
num_loops=num_loops,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
examples.loop(
|
|
35
|
+
label="loop2",
|
|
36
|
+
num_loops=num_loops * 2,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@ProfileManager.profile
|
|
40
|
+
def test_decorator():
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
for i in range(num_loops):
|
|
44
|
+
test_decorator()
|
|
45
|
+
|
|
46
|
+
with ProfileManager.profile_region("main"):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
if config.time_trace:
|
|
50
|
+
ProfileManager.print_summary()
|
|
51
|
+
|
|
52
|
+
ProfileManager.finalize()
|
|
53
|
+
|
|
54
|
+
regions = ProfileManager.get_all_regions()
|
|
55
|
+
|
|
56
|
+
if profiling_activated:
|
|
57
|
+
assert regions["loop1"].num_calls == num_loops
|
|
58
|
+
assert regions["loop2"].num_calls == num_loops * 2
|
|
59
|
+
assert regions["test_decorator"].num_calls == num_loops
|
|
60
|
+
assert regions["main"].num_calls == 1
|
|
61
|
+
else:
|
|
62
|
+
assert regions["loop1"].num_calls == 0
|
|
63
|
+
assert regions["loop2"].num_calls == 0
|
|
64
|
+
assert regions["test_decorator"].num_calls == 0
|
|
65
|
+
assert regions["main"].num_calls == 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
test_profile_manager(
|
|
70
|
+
time_trace=True,
|
|
71
|
+
use_likwid=False,
|
|
72
|
+
num_loops=100,
|
|
73
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import random
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
import scope_profiler.tests.examples as examples
|
|
8
|
+
from scope_profiler.profiling import (
|
|
9
|
+
ProfileManager,
|
|
10
|
+
ProfilingConfig,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def random_math(N=100_000):
|
|
15
|
+
s = 0.0
|
|
16
|
+
for _ in range(N):
|
|
17
|
+
x = random.random()
|
|
18
|
+
s += math.sin(x) * math.sqrt(x + 1.2345)
|
|
19
|
+
return s
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_overhead():
|
|
23
|
+
|
|
24
|
+
config = ProfilingConfig(
|
|
25
|
+
use_likwid=False,
|
|
26
|
+
time_trace=True,
|
|
27
|
+
flush_to_disk=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
ProfileManager.reset()
|
|
31
|
+
num_computations = 1
|
|
32
|
+
num_tests = 10
|
|
33
|
+
N = 1_000_000
|
|
34
|
+
|
|
35
|
+
# Without ProfileManager
|
|
36
|
+
elapsed_no_manager = []
|
|
37
|
+
for _ in range(num_tests):
|
|
38
|
+
t0 = time.perf_counter()
|
|
39
|
+
for _ in range(num_computations):
|
|
40
|
+
# examples.axpy(N = 50)
|
|
41
|
+
random_math(N)
|
|
42
|
+
t1 = time.perf_counter()
|
|
43
|
+
elapsed_no_manager.append(t1 - t0)
|
|
44
|
+
|
|
45
|
+
# With ProfileManager
|
|
46
|
+
elapsed_with_manager = []
|
|
47
|
+
for _ in range(num_tests):
|
|
48
|
+
t0 = time.perf_counter()
|
|
49
|
+
for _ in range(num_computations):
|
|
50
|
+
with ProfileManager.profile_region("main"):
|
|
51
|
+
# examples.axpy(N = 50)
|
|
52
|
+
random_math(N)
|
|
53
|
+
t1 = time.perf_counter()
|
|
54
|
+
elapsed_with_manager.append(t1 - t0)
|
|
55
|
+
|
|
56
|
+
elapsed_no_manager = np.array(elapsed_no_manager)
|
|
57
|
+
elapsed_with_manager = np.array(elapsed_with_manager)
|
|
58
|
+
|
|
59
|
+
time_no_manager = np.min(elapsed_no_manager)
|
|
60
|
+
time_manager = np.min(elapsed_with_manager)
|
|
61
|
+
ratio = time_manager / time_no_manager
|
|
62
|
+
print(f"{time_no_manager = }")
|
|
63
|
+
print(f"{time_manager = }")
|
|
64
|
+
print(f"Overhead ratio = {ratio}")
|
|
65
|
+
|
|
66
|
+
# Very low bar, make sure the overhead is <3%
|
|
67
|
+
assert ratio < 1.03
|
|
68
|
+
|
|
69
|
+
ProfileManager.finalize()
|
|
70
|
+
|
|
71
|
+
# ProfileManager.print_summary()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
test_overhead()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scope-profiler
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Profile code regions in python, optionally with LIKWID markers.
|
|
5
|
+
Author: Max
|
|
6
|
+
Project-URL: Source, https://github.com/max-models/scope_profiler
|
|
7
|
+
Keywords: python
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: h5py
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: black[jupyter]; extra == "dev"
|
|
23
|
+
Requires-Dist: isort; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Requires-Dist: scope-profiler[docs,test]; extra == "dev"
|
|
26
|
+
Provides-Extra: docs
|
|
27
|
+
Requires-Dist: ipykernel; extra == "docs"
|
|
28
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
29
|
+
Requires-Dist: nbconvert; extra == "docs"
|
|
30
|
+
Requires-Dist: nbsphinx; extra == "docs"
|
|
31
|
+
Requires-Dist: jupyterlab; extra == "docs"
|
|
32
|
+
Requires-Dist: pre-commit; extra == "docs"
|
|
33
|
+
Requires-Dist: pyproject-fmt; extra == "docs"
|
|
34
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
35
|
+
Requires-Dist: sphinx-book-theme; extra == "docs"
|
|
36
|
+
Provides-Extra: test
|
|
37
|
+
Requires-Dist: coverage; extra == "test"
|
|
38
|
+
Requires-Dist: pytest; extra == "test"
|
|
39
|
+
|
|
40
|
+
# scope-profiler - Python Profiling System with Optional LIKWID Integration
|
|
41
|
+
|
|
42
|
+
This module provides a unified profiling system for Python applications, with optional integration of [LIKWID](https://github.com/RRZE-HPC/likwid) markers using the [pylikwid](https://github.com/RRZE-HPC/pylikwid) marker API for hardware performance counters. It allows you to:
|
|
43
|
+
- Configure profiling globally via a singleton ProfilingConfig.
|
|
44
|
+
- Collect timing data via context-managed profiling regions.
|
|
45
|
+
- Use a clean decorator syntax to profile functions.
|
|
46
|
+
- Optionally record time traces in HDF5 files.
|
|
47
|
+
- Automatically initialize and close LIKWID markers only when needed.
|
|
48
|
+
- Print aggregated summaries of all profiling regions.
|
|
49
|
+
|
|
50
|
+
Documentation: https://max-models.github.io/scope-profiler/
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
pip install scope-profiler
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from scope_profiler.profiling import (
|
|
62
|
+
ProfilingConfig,
|
|
63
|
+
ProfileManager,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
config = ProfilingConfig(
|
|
67
|
+
use_likwid=False,
|
|
68
|
+
time_trace=True,
|
|
69
|
+
flush_to_disk=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@ProfileManager.profile("main")
|
|
73
|
+
def main():
|
|
74
|
+
x = 0
|
|
75
|
+
for i in range(10):
|
|
76
|
+
with ProfileManager.profile_region(region_name="iteration"):
|
|
77
|
+
x += 1
|
|
78
|
+
|
|
79
|
+
main()
|
|
80
|
+
|
|
81
|
+
ProfileManager.print_summary()
|
|
82
|
+
|
|
83
|
+
ProfileManager.finalize()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Execution:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
❯ python test.py
|
|
90
|
+
Profiling Summary:
|
|
91
|
+
========================================
|
|
92
|
+
Region: main
|
|
93
|
+
Number of Calls: 1
|
|
94
|
+
Total Duration: 0.000315 seconds
|
|
95
|
+
Average Duration: 0.000315 seconds
|
|
96
|
+
Min Duration: 0.000315 seconds
|
|
97
|
+
Max Duration: 0.000315 seconds
|
|
98
|
+
Std Deviation: 0.000000 seconds
|
|
99
|
+
----------------------------------------
|
|
100
|
+
Region: iteration
|
|
101
|
+
Number of Calls: 10
|
|
102
|
+
Total Duration: 0.000007 seconds
|
|
103
|
+
Average Duration: 0.000001 seconds
|
|
104
|
+
Min Duration: 0.000000 seconds
|
|
105
|
+
Max Duration: 0.000003 seconds
|
|
106
|
+
Std Deviation: 0.000001 seconds
|
|
107
|
+
----------------------------------------
|
|
108
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/scope_profiler/h5reader.py
|
|
4
|
+
src/scope_profiler/post_processing.py
|
|
5
|
+
src/scope_profiler/profiling.py
|
|
6
|
+
src/scope_profiler.egg-info/PKG-INFO
|
|
7
|
+
src/scope_profiler.egg-info/SOURCES.txt
|
|
8
|
+
src/scope_profiler.egg-info/dependency_links.txt
|
|
9
|
+
src/scope_profiler.egg-info/entry_points.txt
|
|
10
|
+
src/scope_profiler.egg-info/requires.txt
|
|
11
|
+
src/scope_profiler.egg-info/top_level.txt
|
|
12
|
+
src/scope_profiler/tests/__init__.py
|
|
13
|
+
src/scope_profiler/tests/examples.py
|
|
14
|
+
src/scope_profiler/tests/test_app.py
|
|
15
|
+
src/scope_profiler/tests/test_overhead.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
h5py
|
|
2
|
+
numpy
|
|
3
|
+
matplotlib
|
|
4
|
+
|
|
5
|
+
[dev]
|
|
6
|
+
black[jupyter]
|
|
7
|
+
isort
|
|
8
|
+
ruff
|
|
9
|
+
scope-profiler[docs,test]
|
|
10
|
+
|
|
11
|
+
[docs]
|
|
12
|
+
ipykernel
|
|
13
|
+
myst-parser
|
|
14
|
+
nbconvert
|
|
15
|
+
nbsphinx
|
|
16
|
+
jupyterlab
|
|
17
|
+
pre-commit
|
|
18
|
+
pyproject-fmt
|
|
19
|
+
sphinx
|
|
20
|
+
sphinx-book-theme
|
|
21
|
+
|
|
22
|
+
[test]
|
|
23
|
+
coverage
|
|
24
|
+
pytest
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scope_profiler
|