scope-profiler 0.3.0__tar.gz → 0.3.2__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.3.0 → scope_profiler-0.3.2}/PKG-INFO +29 -23
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/README.md +14 -13
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/pyproject.toml +11 -4
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/__main__.py +40 -1
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/h5reader.py +26 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/h5writer.py +18 -1
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/inspection.py +13 -0
- scope_profiler-0.3.2/src/scope_profiler/line_profile_cli.py +78 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/plotting_scripts.py +48 -5
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/post_processing.py +16 -4
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/profile_manager.py +92 -4
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/region_profiler.py +52 -2
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/results.py +18 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/summary.py +47 -4
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_app.py +55 -1
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_inspection.py +30 -2
- scope_profiler-0.3.2/src/scope_profiler/tests/test_line_profile_cli.py +41 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_main_cli.py +51 -1
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_post_processing.py +8 -5
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_results_api.py +21 -1
- scope_profiler-0.3.2/src/scope_profiler/tests/test_tui.py +141 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_h5writer.py +30 -1
- scope_profiler-0.3.2/src/scope_profiler/tui.py +650 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/PKG-INFO +29 -23
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/SOURCES.txt +4 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/requires.txt +9 -3
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/setup.cfg +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/__init__.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/c/Makefile +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/c/example.c +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/c/scope_profiler.c +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/c/scope_profiler.h +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/call_stack.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/diff.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/fortran/Makefile +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/fortran/example.f90 +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/fortran/scope_profiler.f90 +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/likwid_data.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mcp_server/__init__.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mcp_server/__main__.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mcp_server/server.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mcp_server/tools.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/metadata.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mpi_launch.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mpi_region.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/native_trace.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/prof_export.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/profile_config.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/region.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/speedscope_export.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/__init__.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/check_mpi_launch.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/examples.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/examples_pylikwid.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/pylikwid_readme.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_buffer_growth.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_c_api.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_diff.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_fortran_api.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_in_memory_results.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_label.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_likwid_data.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_mcp_server.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_mcp_tools.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_metadata.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_mixed_language.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_mpi.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_overhead.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_prof_export.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_readme.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_region_source.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_repeated_finalize.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_speedscope_export.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_summary_likwid.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/__init__.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_lazy_config.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_likwid_child_env.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_merge_results.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_mpi_launch.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_payload_collection.py +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/dependency_links.txt +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/entry_points.txt +0 -0
- {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scope-profiler
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Profile code regions in python, optionally with LIKWID markers.
|
|
5
5
|
Author: Max
|
|
6
6
|
Project-URL: Source, https://github.com/max-models/scope-profiler
|
|
@@ -29,18 +29,20 @@ Provides-Extra: mcp
|
|
|
29
29
|
Requires-Dist: mcp<2.0.0,>=1.6.0; extra == "mcp"
|
|
30
30
|
Provides-Extra: mpi
|
|
31
31
|
Requires-Dist: mpi4py; extra == "mpi"
|
|
32
|
-
Provides-Extra:
|
|
33
|
-
Requires-Dist: ipykernel; extra == "
|
|
34
|
-
Requires-Dist: jupyterlab; extra == "
|
|
35
|
-
Requires-Dist: matplotlib; extra == "
|
|
36
|
-
Requires-Dist: maxplotlibx>=0.1.6; extra == "
|
|
37
|
-
Requires-Dist: pandas; extra == "
|
|
38
|
-
Requires-Dist: snakeviz; extra == "
|
|
32
|
+
Provides-Extra: pproc
|
|
33
|
+
Requires-Dist: ipykernel; extra == "pproc"
|
|
34
|
+
Requires-Dist: jupyterlab; extra == "pproc"
|
|
35
|
+
Requires-Dist: matplotlib; extra == "pproc"
|
|
36
|
+
Requires-Dist: maxplotlibx>=0.1.6; extra == "pproc"
|
|
37
|
+
Requires-Dist: pandas; extra == "pproc"
|
|
38
|
+
Requires-Dist: snakeviz; extra == "pproc"
|
|
39
|
+
Requires-Dist: tabulate; extra == "pproc"
|
|
40
|
+
Requires-Dist: textual>=0.86; extra == "pproc"
|
|
39
41
|
Provides-Extra: dev
|
|
40
42
|
Requires-Dist: black[jupyter]; extra == "dev"
|
|
41
43
|
Requires-Dist: isort; extra == "dev"
|
|
42
44
|
Requires-Dist: ruff; extra == "dev"
|
|
43
|
-
Requires-Dist: scope-profiler[docs,fortran,line-profiler,mcp,mpi,nvtx,
|
|
45
|
+
Requires-Dist: scope-profiler[docs,fortran,line-profiler,mcp,mpi,nvtx,pproc,test,tui]; extra == "dev"
|
|
44
46
|
Provides-Extra: docs
|
|
45
47
|
Requires-Dist: myst-parser; extra == "docs"
|
|
46
48
|
Requires-Dist: nbconvert; extra == "docs"
|
|
@@ -49,10 +51,13 @@ Requires-Dist: pre-commit; extra == "docs"
|
|
|
49
51
|
Requires-Dist: pyproject-fmt; extra == "docs"
|
|
50
52
|
Requires-Dist: sphinx; extra == "docs"
|
|
51
53
|
Requires-Dist: sphinx-book-theme; extra == "docs"
|
|
52
|
-
Requires-Dist: scope-profiler[line-profiler,
|
|
54
|
+
Requires-Dist: scope-profiler[line-profiler,pproc,tui]; extra == "docs"
|
|
53
55
|
Provides-Extra: test
|
|
54
56
|
Requires-Dist: coverage; extra == "test"
|
|
55
57
|
Requires-Dist: pytest; extra == "test"
|
|
58
|
+
Provides-Extra: tui
|
|
59
|
+
Requires-Dist: tabulate; extra == "tui"
|
|
60
|
+
Requires-Dist: textual>=0.86; extra == "tui"
|
|
56
61
|
|
|
57
62
|
# scope-profiler
|
|
58
63
|
|
|
@@ -110,11 +115,11 @@ Execution:
|
|
|
110
115
|
```bash
|
|
111
116
|
❯ python test.py
|
|
112
117
|
profiling_data.h5 (1 rank(s))
|
|
113
|
-
region ranks calls total [s] avg [s]
|
|
114
|
-
|
|
115
|
-
main 1 1 0.00150371 0.00150371
|
|
116
|
-
iteration 1 10 3.832e-06 3.832e-07
|
|
117
|
-
|
|
118
|
+
region ranks calls total [s] avg [s]
|
|
119
|
+
--------------------------------------------------
|
|
120
|
+
main 1 1 0.00150371 0.00150371
|
|
121
|
+
iteration 1 10 3.832e-06 3.832e-07
|
|
122
|
+
--------------------------------------------------
|
|
118
123
|
TOTAL 11 0.00150754
|
|
119
124
|
|
|
120
125
|
```
|
|
@@ -158,13 +163,13 @@ Metadata
|
|
|
158
163
|
python/3.11.7
|
|
159
164
|
|
|
160
165
|
Regions (4)
|
|
161
|
-
region ranks calls total [s] avg [s]
|
|
162
|
-
|
|
163
|
-
timestep 2 8 0.139235 0.0174044
|
|
164
|
-
solve 2 8 0.0991292 0.0123911
|
|
165
|
-
setup 2 2 0.0473326 0.0236663
|
|
166
|
-
assemble 2 8 0.0399496 0.0049937
|
|
167
|
-
|
|
166
|
+
region ranks calls total [s] avg [s]
|
|
167
|
+
---------------------------------------------
|
|
168
|
+
timestep 2 8 0.139235 0.0174044
|
|
169
|
+
solve 2 8 0.0991292 0.0123911
|
|
170
|
+
setup 2 2 0.0473326 0.0236663
|
|
171
|
+
assemble 2 8 0.0399496 0.0049937
|
|
172
|
+
---------------------------------------------
|
|
168
173
|
TOTAL 26 0.325647
|
|
169
174
|
```
|
|
170
175
|
|
|
@@ -314,7 +319,8 @@ script's own code is instrumented (the standard library and installed
|
|
|
314
319
|
packages are skipped) to keep overhead low; pass `--all` to trace
|
|
315
320
|
everything. Results are written to `profiling_data.h5` by default
|
|
316
321
|
(`-o`/`--outfile` to change it), and a per-region summary is printed unless
|
|
317
|
-
`-q`/`--quiet` is given.
|
|
322
|
+
`-q`/`--quiet` is given. Pass `--line-profile` to also persist line-by-line
|
|
323
|
+
timings for the traced functions; this requires `scope-profiler[line-profiler]`.
|
|
318
324
|
|
|
319
325
|
See `examples/ex_cli_profiling.py` for a script with no scope-profiler
|
|
320
326
|
imports at all, run with:
|
|
@@ -54,11 +54,11 @@ Execution:
|
|
|
54
54
|
```bash
|
|
55
55
|
❯ python test.py
|
|
56
56
|
profiling_data.h5 (1 rank(s))
|
|
57
|
-
region ranks calls total [s] avg [s]
|
|
58
|
-
|
|
59
|
-
main 1 1 0.00150371 0.00150371
|
|
60
|
-
iteration 1 10 3.832e-06 3.832e-07
|
|
61
|
-
|
|
57
|
+
region ranks calls total [s] avg [s]
|
|
58
|
+
--------------------------------------------------
|
|
59
|
+
main 1 1 0.00150371 0.00150371
|
|
60
|
+
iteration 1 10 3.832e-06 3.832e-07
|
|
61
|
+
--------------------------------------------------
|
|
62
62
|
TOTAL 11 0.00150754
|
|
63
63
|
|
|
64
64
|
```
|
|
@@ -102,13 +102,13 @@ Metadata
|
|
|
102
102
|
python/3.11.7
|
|
103
103
|
|
|
104
104
|
Regions (4)
|
|
105
|
-
region ranks calls total [s] avg [s]
|
|
106
|
-
|
|
107
|
-
timestep 2 8 0.139235 0.0174044
|
|
108
|
-
solve 2 8 0.0991292 0.0123911
|
|
109
|
-
setup 2 2 0.0473326 0.0236663
|
|
110
|
-
assemble 2 8 0.0399496 0.0049937
|
|
111
|
-
|
|
105
|
+
region ranks calls total [s] avg [s]
|
|
106
|
+
---------------------------------------------
|
|
107
|
+
timestep 2 8 0.139235 0.0174044
|
|
108
|
+
solve 2 8 0.0991292 0.0123911
|
|
109
|
+
setup 2 2 0.0473326 0.0236663
|
|
110
|
+
assemble 2 8 0.0399496 0.0049937
|
|
111
|
+
---------------------------------------------
|
|
112
112
|
TOTAL 26 0.325647
|
|
113
113
|
```
|
|
114
114
|
|
|
@@ -258,7 +258,8 @@ script's own code is instrumented (the standard library and installed
|
|
|
258
258
|
packages are skipped) to keep overhead low; pass `--all` to trace
|
|
259
259
|
everything. Results are written to `profiling_data.h5` by default
|
|
260
260
|
(`-o`/`--outfile` to change it), and a per-region summary is printed unless
|
|
261
|
-
`-q`/`--quiet` is given.
|
|
261
|
+
`-q`/`--quiet` is given. Pass `--line-profile` to also persist line-by-line
|
|
262
|
+
timings for the traced functions; this requires `scope-profiler[line-profiler]`.
|
|
262
263
|
|
|
263
264
|
See `examples/ex_cli_profiling.py` for a script with no scope-profiler
|
|
264
265
|
imports at all, run with:
|
|
@@ -5,7 +5,7 @@ requires = [ "setuptools", "wheel" ]
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "scope-profiler"
|
|
8
|
-
version = "0.3.
|
|
8
|
+
version = "0.3.2"
|
|
9
9
|
description = "Profile code regions in python, optionally with LIKWID markers."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
keywords = [ "python" ]
|
|
@@ -53,20 +53,22 @@ optional-dependencies.mpi = [
|
|
|
53
53
|
"mpi4py",
|
|
54
54
|
]
|
|
55
55
|
|
|
56
|
-
optional-dependencies.
|
|
56
|
+
optional-dependencies.pproc = [
|
|
57
57
|
"ipykernel",
|
|
58
58
|
"jupyterlab",
|
|
59
59
|
"matplotlib",
|
|
60
60
|
"maxplotlibx >= 0.1.6",
|
|
61
61
|
"pandas",
|
|
62
62
|
"snakeviz",
|
|
63
|
+
"tabulate",
|
|
64
|
+
"textual>=0.86",
|
|
63
65
|
]
|
|
64
66
|
|
|
65
67
|
optional-dependencies.dev = [
|
|
66
68
|
"black[jupyter]",
|
|
67
69
|
"isort",
|
|
68
70
|
"ruff",
|
|
69
|
-
"scope-profiler[docs,fortran,line-profiler,mcp,mpi,nvtx,
|
|
71
|
+
"scope-profiler[docs,fortran,line-profiler,mcp,mpi,nvtx,pproc,test,tui]",
|
|
70
72
|
]
|
|
71
73
|
# https://medium.com/@pratikdomadiya123/build-project-documentation-quickly-with-the-sphinx-python-2a9732b66594
|
|
72
74
|
optional-dependencies.docs = [
|
|
@@ -77,12 +79,17 @@ optional-dependencies.docs = [
|
|
|
77
79
|
"pyproject-fmt",
|
|
78
80
|
"sphinx",
|
|
79
81
|
"sphinx-book-theme",
|
|
80
|
-
"scope-profiler[line-profiler,
|
|
82
|
+
"scope-profiler[line-profiler,pproc,tui]",
|
|
81
83
|
]
|
|
82
84
|
optional-dependencies.test = [
|
|
83
85
|
"coverage",
|
|
84
86
|
"pytest",
|
|
85
87
|
]
|
|
88
|
+
|
|
89
|
+
optional-dependencies.tui = [
|
|
90
|
+
"tabulate",
|
|
91
|
+
"textual>=0.86",
|
|
92
|
+
]
|
|
86
93
|
urls."Source" = "https://github.com/max-models/scope-profiler"
|
|
87
94
|
|
|
88
95
|
scripts.scope-profiler = "scope_profiler.__main__:main"
|
|
@@ -18,6 +18,11 @@ Six subcommands:
|
|
|
18
18
|
per-region statistics table (including LIKWID hardware counters, when the
|
|
19
19
|
run recorded any) for merged HDF5 profiling output, without producing any
|
|
20
20
|
plots. See ``scope_profiler.inspection``.
|
|
21
|
+
- ``scope-profiler tui file.h5`` -- opens an interactive Textual browser for
|
|
22
|
+
metadata, region statistics, per-rank calls, LIKWID counters and the raw
|
|
23
|
+
HDF5 tree.
|
|
24
|
+
- ``scope-profiler line-profile file.h5 [...]`` -- prints persisted
|
|
25
|
+
line-profiler timings from an HDF5 profile.
|
|
21
26
|
- ``scope-profiler diff a.h5 b.h5`` -- compares region statistics between two
|
|
22
27
|
merged HDF5 profiling files, region by region, so a regression (or
|
|
23
28
|
improvement) between two runs shows up in one table. See
|
|
@@ -62,6 +67,12 @@ def _parse_run_args(argv):
|
|
|
62
67
|
help="Also instrument standard-library and installed-package calls "
|
|
63
68
|
"(default: only the script's own code)",
|
|
64
69
|
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--line-profile",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Also collect line-by-line timings via line_profiler "
|
|
74
|
+
"(requires scope-profiler[line-profiler])",
|
|
75
|
+
)
|
|
65
76
|
parser.add_argument(
|
|
66
77
|
"--buffer-limit",
|
|
67
78
|
type=int,
|
|
@@ -91,7 +102,7 @@ def _run(argv):
|
|
|
91
102
|
ProfileManager.setup(
|
|
92
103
|
recursive_profile=True,
|
|
93
104
|
use_likwid=False,
|
|
94
|
-
use_line_profiler=
|
|
105
|
+
use_line_profiler=args.line_profile,
|
|
95
106
|
buffer_limit=args.buffer_limit,
|
|
96
107
|
file_path=args.outfile,
|
|
97
108
|
)
|
|
@@ -127,6 +138,20 @@ def _inspect(argv):
|
|
|
127
138
|
return inspect_main(argv)
|
|
128
139
|
|
|
129
140
|
|
|
141
|
+
def _tui(argv):
|
|
142
|
+
"""Handle ``scope-profiler tui``: open the interactive HDF5 browser."""
|
|
143
|
+
from scope_profiler.tui import main as tui_main
|
|
144
|
+
|
|
145
|
+
return tui_main(argv)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _line_profile(argv):
|
|
149
|
+
"""Handle ``scope-profiler line-profile``."""
|
|
150
|
+
from scope_profiler.line_profile_cli import main as line_profile_main
|
|
151
|
+
|
|
152
|
+
return line_profile_main(argv)
|
|
153
|
+
|
|
154
|
+
|
|
130
155
|
def _diff(argv):
|
|
131
156
|
"""Handle ``scope-profiler diff``: delegate to the diff CLI."""
|
|
132
157
|
from scope_profiler.diff import main as diff_main
|
|
@@ -213,6 +238,8 @@ _COMMANDS = {
|
|
|
213
238
|
"plot": _plot,
|
|
214
239
|
"export": _export,
|
|
215
240
|
"inspect": _inspect,
|
|
241
|
+
"tui": _tui,
|
|
242
|
+
"line-profile": _line_profile,
|
|
216
243
|
"diff": _diff,
|
|
217
244
|
"check": _check,
|
|
218
245
|
"import-native": _import_fortran,
|
|
@@ -256,6 +283,18 @@ def main(argv=None):
|
|
|
256
283
|
help="Print metadata and region statistics of HDF5 profiling data "
|
|
257
284
|
"(see `scope-profiler inspect --help`)",
|
|
258
285
|
)
|
|
286
|
+
subparsers.add_parser(
|
|
287
|
+
"tui",
|
|
288
|
+
add_help=False,
|
|
289
|
+
help="Interactively browse HDF5 profiling data "
|
|
290
|
+
"(see `scope-profiler tui --help`)",
|
|
291
|
+
)
|
|
292
|
+
subparsers.add_parser(
|
|
293
|
+
"line-profile",
|
|
294
|
+
add_help=False,
|
|
295
|
+
help="Print persisted line-profiler timings from HDF5 data "
|
|
296
|
+
"(see `scope-profiler line-profile --help`)",
|
|
297
|
+
)
|
|
259
298
|
subparsers.add_parser(
|
|
260
299
|
"diff",
|
|
261
300
|
add_help=False,
|
|
@@ -66,6 +66,26 @@ def _read_likwid_group(group) -> dict:
|
|
|
66
66
|
return results
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _read_line_profile_group(group) -> list:
|
|
70
|
+
"""Read one rank's persisted line-profiler records."""
|
|
71
|
+
records = []
|
|
72
|
+
for function_grp in group.values():
|
|
73
|
+
attrs = function_grp.attrs
|
|
74
|
+
records.append(
|
|
75
|
+
{
|
|
76
|
+
"region": _decode_attribute(attrs.get("region", "")),
|
|
77
|
+
"filename": _decode_attribute(attrs.get("filename", "")),
|
|
78
|
+
"function": _decode_attribute(attrs.get("function", "")),
|
|
79
|
+
"first_lineno": int(attrs.get("first_lineno", 0)),
|
|
80
|
+
"line_numbers": function_grp["line_numbers"][()],
|
|
81
|
+
"hits": function_grp["hits"][()],
|
|
82
|
+
"times": function_grp["times"][()],
|
|
83
|
+
"unit": float(attrs.get("unit", 1.0)),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
return records
|
|
87
|
+
|
|
88
|
+
|
|
69
89
|
def load_h5(file_path: str | Path, verbose: bool = False) -> dict:
|
|
70
90
|
"""
|
|
71
91
|
Parse a merged profiling file into :class:`ProfilingResults` arguments.
|
|
@@ -98,6 +118,7 @@ def load_h5(file_path: str | Path, verbose: bool = False) -> dict:
|
|
|
98
118
|
metadata: dict = {}
|
|
99
119
|
# rank -> {tag: LikwidRegionResult}; empty unless the run used LIKWID.
|
|
100
120
|
likwid: dict[int, dict[str, LikwidRegionResult]] = {}
|
|
121
|
+
line_profile: dict[int, list] = {}
|
|
101
122
|
if not file_path.exists():
|
|
102
123
|
raise FileNotFoundError(f"HDF5 file not found: {file_path}")
|
|
103
124
|
|
|
@@ -130,6 +151,10 @@ def load_h5(file_path: str | Path, verbose: bool = False) -> dict:
|
|
|
130
151
|
|
|
131
152
|
if LIKWID_GROUP in rank_group:
|
|
132
153
|
likwid[rank] = _read_likwid_group(rank_group[LIKWID_GROUP])
|
|
154
|
+
if "line_profile" in rank_group:
|
|
155
|
+
line_profile[rank] = _read_line_profile_group(
|
|
156
|
+
rank_group["line_profile"]
|
|
157
|
+
)
|
|
133
158
|
|
|
134
159
|
if "regions" not in rank_group:
|
|
135
160
|
continue
|
|
@@ -176,6 +201,7 @@ def load_h5(file_path: str | Path, verbose: bool = False) -> dict:
|
|
|
176
201
|
"metadata": metadata,
|
|
177
202
|
"num_ranks": num_ranks,
|
|
178
203
|
"likwid": likwid,
|
|
204
|
+
"line_profile": line_profile,
|
|
179
205
|
"file_path": file_path,
|
|
180
206
|
}
|
|
181
207
|
|
|
@@ -91,6 +91,22 @@ def write_regions(
|
|
|
91
91
|
region_grp.attrs.create("tags", list(tags[name]), dtype=h5py.string_dtype())
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def write_line_profile(group, records: list | None) -> None:
|
|
95
|
+
"""Write copied line-profiler records for one rank."""
|
|
96
|
+
if not records:
|
|
97
|
+
return
|
|
98
|
+
profile_grp = group.create_group("line_profile")
|
|
99
|
+
for index, record in enumerate(records):
|
|
100
|
+
function_grp = profile_grp.create_group(str(index))
|
|
101
|
+
for key in ("region", "filename", "function"):
|
|
102
|
+
function_grp.attrs[key] = record[key]
|
|
103
|
+
function_grp.attrs["first_lineno"] = record["first_lineno"]
|
|
104
|
+
function_grp.attrs["unit"] = record["unit"]
|
|
105
|
+
function_grp.create_dataset("line_numbers", data=record["line_numbers"])
|
|
106
|
+
function_grp.create_dataset("hits", data=record["hits"])
|
|
107
|
+
function_grp.create_dataset("times", data=record["times"])
|
|
108
|
+
|
|
109
|
+
|
|
94
110
|
def write_rank_payload(h5file, rank: int, payload) -> bool:
|
|
95
111
|
"""Write one rank's payload into ``rank<N>``.
|
|
96
112
|
|
|
@@ -111,7 +127,7 @@ def write_rank_payload(h5file, rank: int, payload) -> bool:
|
|
|
111
127
|
bool
|
|
112
128
|
True if a group was created, False if the payload was empty.
|
|
113
129
|
"""
|
|
114
|
-
if not payload.regions and not payload.likwid:
|
|
130
|
+
if not payload.regions and not payload.likwid and not payload.line_profile:
|
|
115
131
|
return False
|
|
116
132
|
|
|
117
133
|
# create_group, not require_group: each rank is written exactly once, so a
|
|
@@ -125,6 +141,7 @@ def write_rank_payload(h5file, rank: int, payload) -> bool:
|
|
|
125
141
|
payload.likwid.values(),
|
|
126
142
|
environment=payload.likwid_environment,
|
|
127
143
|
)
|
|
144
|
+
write_line_profile(group, payload.line_profile)
|
|
128
145
|
return True
|
|
129
146
|
|
|
130
147
|
|
|
@@ -18,6 +18,7 @@ import numpy as np
|
|
|
18
18
|
from scope_profiler.h5reader import read_h5
|
|
19
19
|
from scope_profiler.results import ProfilingResults
|
|
20
20
|
from scope_profiler.summary import (
|
|
21
|
+
REGION_TABLE_COLUMNS,
|
|
21
22
|
SORT_KEYS,
|
|
22
23
|
print_likwid_tables,
|
|
23
24
|
print_region_table,
|
|
@@ -171,6 +172,7 @@ def inspect_file(
|
|
|
171
172
|
show_regions: bool = True,
|
|
172
173
|
full: bool = False,
|
|
173
174
|
source: list | None = None,
|
|
175
|
+
columns=None,
|
|
174
176
|
stream=None,
|
|
175
177
|
) -> None:
|
|
176
178
|
"""Print a summary of one profiling HDF5 file.
|
|
@@ -195,6 +197,9 @@ def inspect_file(
|
|
|
195
197
|
block or decorated function that defines each region -- see
|
|
196
198
|
``ProfileManager.profile_region`` and issue #161). Printed after the
|
|
197
199
|
region table, regardless of ``include``/``exclude``.
|
|
200
|
+
columns : list of str or str, optional
|
|
201
|
+
Region summary columns to print. Defaults to ``region``, ``ranks``,
|
|
202
|
+
``calls``, ``total`` and ``avg``.
|
|
198
203
|
stream : file-like, optional
|
|
199
204
|
Where to write (default: stdout).
|
|
200
205
|
"""
|
|
@@ -233,6 +238,7 @@ def inspect_file(
|
|
|
233
238
|
title=f"Regions ({len(rows)})",
|
|
234
239
|
stream=stream,
|
|
235
240
|
total_time=results.total_time,
|
|
241
|
+
columns=columns,
|
|
236
242
|
)
|
|
237
243
|
print_likwid_tables(
|
|
238
244
|
results, include=include, exclude=exclude, ranks=ranks, stream=stream
|
|
@@ -357,6 +363,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
357
363
|
default="total",
|
|
358
364
|
help="Order regions by this column (default: total)",
|
|
359
365
|
)
|
|
366
|
+
parser.add_argument(
|
|
367
|
+
"--columns",
|
|
368
|
+
nargs="+",
|
|
369
|
+
choices=REGION_TABLE_COLUMNS,
|
|
370
|
+
help="Region table columns to print (default: region ranks calls total avg)",
|
|
371
|
+
)
|
|
360
372
|
parser.add_argument(
|
|
361
373
|
"--full",
|
|
362
374
|
action="store_true",
|
|
@@ -424,6 +436,7 @@ def main(argv: list | None = None):
|
|
|
424
436
|
show_regions=not args.metadata_only,
|
|
425
437
|
full=args.full,
|
|
426
438
|
source=args.source,
|
|
439
|
+
columns=args.columns,
|
|
427
440
|
)
|
|
428
441
|
printed += 1
|
|
429
442
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""CLI for displaying line-profiler records persisted in HDF5 files."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import linecache
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from scope_profiler.h5reader import read_h5
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def print_line_profile(file_path, ranks=None, function=None, region=None, stream=None):
|
|
12
|
+
"""Print persisted line-profiler timings from one HDF5 profile."""
|
|
13
|
+
stream = sys.stdout if stream is None else stream
|
|
14
|
+
results = read_h5(file_path)
|
|
15
|
+
selected_ranks = results.line_profile.keys() if ranks is None else ranks
|
|
16
|
+
function_re = re.compile(function) if function else None
|
|
17
|
+
region_re = re.compile(region) if region else None
|
|
18
|
+
record_count = 0
|
|
19
|
+
|
|
20
|
+
print(f"Line profile: {results.file_path}", file=stream)
|
|
21
|
+
for rank in sorted(selected_ranks):
|
|
22
|
+
for record in results.line_profile.get(rank, []):
|
|
23
|
+
if function_re and not function_re.search(record["function"]):
|
|
24
|
+
continue
|
|
25
|
+
if region_re and not region_re.search(record["region"]):
|
|
26
|
+
continue
|
|
27
|
+
record_count += 1
|
|
28
|
+
unit = record["unit"]
|
|
29
|
+
print(
|
|
30
|
+
f"\nRank {rank} | {record['region']} | {record['function']} "
|
|
31
|
+
f"({record['filename']}:{record['first_lineno']})",
|
|
32
|
+
file=stream,
|
|
33
|
+
)
|
|
34
|
+
total_time = float(record["times"].sum())
|
|
35
|
+
print(
|
|
36
|
+
"line hits time [s] per hit [s] % time source",
|
|
37
|
+
file=stream,
|
|
38
|
+
)
|
|
39
|
+
for line, hits, elapsed in zip(
|
|
40
|
+
record["line_numbers"], record["hits"], record["times"]
|
|
41
|
+
):
|
|
42
|
+
seconds = float(elapsed) * unit
|
|
43
|
+
per_hit = seconds / int(hits) if hits else 0.0
|
|
44
|
+
percent = float(elapsed) / total_time * 100 if total_time else 0.0
|
|
45
|
+
source = linecache.getline(record["filename"], int(line)).rstrip()
|
|
46
|
+
print(
|
|
47
|
+
f"{int(line):4d} {int(hits):11d} {seconds:14.6g} "
|
|
48
|
+
f"{per_hit:14.6g} {percent:7.2f} {source}",
|
|
49
|
+
file=stream,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if record_count == 0:
|
|
53
|
+
print("\nNo line-profile records matched.", file=stream)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main(argv=None):
|
|
57
|
+
"""Handle ``scope-profiler line-profile``."""
|
|
58
|
+
parser = argparse.ArgumentParser(
|
|
59
|
+
prog="scope-profiler line-profile",
|
|
60
|
+
description="Print persisted line-profiler timings from an HDF5 profile.",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument("file", help="HDF5 profiling file")
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--rank",
|
|
65
|
+
type=int,
|
|
66
|
+
action="append",
|
|
67
|
+
dest="ranks",
|
|
68
|
+
help="rank to show (repeatable)",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--function", help="regular expression selecting function names"
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument("--region", help="regular expression selecting region names")
|
|
74
|
+
args = parser.parse_args(argv)
|
|
75
|
+
print_line_profile(
|
|
76
|
+
args.file, ranks=args.ranks, function=args.function, region=args.region
|
|
77
|
+
)
|
|
78
|
+
return 0
|
|
@@ -154,12 +154,16 @@ def _render(
|
|
|
154
154
|
show: bool,
|
|
155
155
|
backend: str,
|
|
156
156
|
plotly_layout: dict | None = None,
|
|
157
|
+
x_tick_rotation: float | None = None,
|
|
157
158
|
) -> None:
|
|
158
159
|
"""Save and/or display a canvas."""
|
|
159
160
|
if backend == "plotly":
|
|
160
161
|
fig = canvas.plot_plotly(show=False)
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
layout = dict(plotly_layout or {})
|
|
163
|
+
if x_tick_rotation is not None:
|
|
164
|
+
layout["xaxis_tickangle"] = x_tick_rotation
|
|
165
|
+
if layout:
|
|
166
|
+
fig.update_layout(**layout)
|
|
163
167
|
if filepath:
|
|
164
168
|
if Path(filepath).suffix.lower() in {".html", ".htm"}:
|
|
165
169
|
fig.write_html(filepath)
|
|
@@ -176,6 +180,30 @@ def _render(
|
|
|
176
180
|
fig.show()
|
|
177
181
|
return
|
|
178
182
|
|
|
183
|
+
if x_tick_rotation is not None and backend == "matplotlib":
|
|
184
|
+
# maxplotlib does not currently expose tick-label rotation through
|
|
185
|
+
# its backend-neutral Canvas API, so rotate the labels after the
|
|
186
|
+
# Matplotlib figure has been materialized and before saving/showing.
|
|
187
|
+
fig, axes = canvas.plot(backend="matplotlib", savefig=bool(filepath))
|
|
188
|
+
for axis in np.asarray(axes).reshape(-1):
|
|
189
|
+
for label in axis.get_xticklabels():
|
|
190
|
+
label.set_rotation(x_tick_rotation)
|
|
191
|
+
# Anchor the end of each vertical label at the tick so the
|
|
192
|
+
# text grows upward into the figure instead of below it.
|
|
193
|
+
label.set_ha("right")
|
|
194
|
+
if filepath:
|
|
195
|
+
savefig_kwargs = {}
|
|
196
|
+
if getattr(canvas, "dpi", None) is not None:
|
|
197
|
+
savefig_kwargs["dpi"] = canvas.dpi
|
|
198
|
+
fig.savefig(filepath, **savefig_kwargs)
|
|
199
|
+
if show:
|
|
200
|
+
import matplotlib.pyplot as plt
|
|
201
|
+
|
|
202
|
+
plt.show()
|
|
203
|
+
else:
|
|
204
|
+
_close_matplotlib_figure(canvas)
|
|
205
|
+
return
|
|
206
|
+
|
|
179
207
|
if filepath:
|
|
180
208
|
canvas.savefig(filepath, backend=backend)
|
|
181
209
|
if show:
|
|
@@ -1129,7 +1157,13 @@ def plot_durations(
|
|
|
1129
1157
|
num_readers = len(runs)
|
|
1130
1158
|
colors = _get_cmap_colors(cmap, max(num_readers, 1))
|
|
1131
1159
|
fig_width = max(10, 0.85 * len(region_names) + 2)
|
|
1132
|
-
|
|
1160
|
+
# Angled tick labels consume space below the axes in proportion to the
|
|
1161
|
+
# longest region name. Grow both the figure and its bottom margin so long
|
|
1162
|
+
# labels remain inside the exported figure without reserving excessive
|
|
1163
|
+
# space for short names.
|
|
1164
|
+
label_space = max(0.8, 0.06 * max(map(len, region_names), default=0) + 0.25)
|
|
1165
|
+
fig_height = max(4.5, 2.5 + 0.35 * num_readers, 3.0 + label_space)
|
|
1166
|
+
bottom_margin = (label_space + 0.25) / fig_height
|
|
1133
1167
|
width = min(0.8 / max(num_readers, 1), 0.35)
|
|
1134
1168
|
|
|
1135
1169
|
saved_paths: list[str] = []
|
|
@@ -1153,7 +1187,10 @@ def plot_durations(
|
|
|
1153
1187
|
for region_name, value in zip(region_names, file_values):
|
|
1154
1188
|
data_rows.append([label, region_name, metric_key, value])
|
|
1155
1189
|
|
|
1156
|
-
canvas = Canvas(
|
|
1190
|
+
canvas = Canvas(
|
|
1191
|
+
figsize=(fig_width, fig_height),
|
|
1192
|
+
gridspec_kw={"bottom": bottom_margin},
|
|
1193
|
+
)
|
|
1157
1194
|
|
|
1158
1195
|
# Create grouped bar chart
|
|
1159
1196
|
x_positions = np.arange(len(region_names))
|
|
@@ -1187,7 +1224,13 @@ def plot_durations(
|
|
|
1187
1224
|
)
|
|
1188
1225
|
saved_paths.append(metric_filepath)
|
|
1189
1226
|
|
|
1190
|
-
_render(
|
|
1227
|
+
_render(
|
|
1228
|
+
canvas,
|
|
1229
|
+
metric_filepath,
|
|
1230
|
+
show,
|
|
1231
|
+
backend,
|
|
1232
|
+
x_tick_rotation=45,
|
|
1233
|
+
)
|
|
1191
1234
|
|
|
1192
1235
|
if data_filepath:
|
|
1193
1236
|
if data_format == "json":
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import argparse
|
|
4
4
|
import glob
|
|
5
5
|
import os
|
|
6
|
+
import sys
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -28,10 +29,10 @@ from scope_profiler.speedscope_export import export_speedscope
|
|
|
28
29
|
# so the three can never drift out of sync.
|
|
29
30
|
_PLOT_CATALOG: dict[str, tuple[str, bool]] = {
|
|
30
31
|
"gantt": ("per-rank timeline of every call", True),
|
|
31
|
-
"flame": ("reconstructed call-stack flame graph",
|
|
32
|
+
"flame": ("reconstructed call-stack flame graph", False),
|
|
32
33
|
"durations": ("bar chart of duration statistics per region", True),
|
|
33
|
-
"timeseries": ("duration per call over wall-clock time",
|
|
34
|
-
"speedup": ("scaling across multiple files (2+ files only)",
|
|
34
|
+
"timeseries": ("duration per call over wall-clock time", False),
|
|
35
|
+
"speedup": ("scaling across multiple files (2+ files only)", False),
|
|
35
36
|
"histogram": ("call-duration distribution per region", False),
|
|
36
37
|
"imbalance": ("per-rank duration comparison, to spot stragglers", False),
|
|
37
38
|
"likwid": ("one LIKWID hardware-counter metric (needs --likwid-metric)", False),
|
|
@@ -799,7 +800,18 @@ def _render_selected_plots(
|
|
|
799
800
|
def main(argv: list[str] | None = None):
|
|
800
801
|
"""Render one plot kind or plot preset from HDF5 profiling data."""
|
|
801
802
|
parser = build_parser()
|
|
802
|
-
|
|
803
|
+
plot_argv = list(sys.argv[1:] if argv is None else argv)
|
|
804
|
+
# Keep the concise form ``scope-profiler plot FILE [OPTIONS]`` as an
|
|
805
|
+
# alias for the default preset. Explicit plot kinds continue to work as
|
|
806
|
+
# subcommands, e.g. ``scope-profiler plot gantt FILE``.
|
|
807
|
+
plot_kinds = {"list", "default", "all", "quick", *_PLOT_CATALOG}
|
|
808
|
+
if (
|
|
809
|
+
plot_argv
|
|
810
|
+
and not plot_argv[0].startswith("-")
|
|
811
|
+
and plot_argv[0] not in plot_kinds
|
|
812
|
+
):
|
|
813
|
+
plot_argv.insert(0, "default")
|
|
814
|
+
args = parser.parse_args(plot_argv)
|
|
803
815
|
|
|
804
816
|
if args.plot_kind == "list":
|
|
805
817
|
_print_plot_list()
|