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.
Files changed (83) hide show
  1. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/PKG-INFO +29 -23
  2. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/README.md +14 -13
  3. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/pyproject.toml +11 -4
  4. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/__main__.py +40 -1
  5. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/h5reader.py +26 -0
  6. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/h5writer.py +18 -1
  7. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/inspection.py +13 -0
  8. scope_profiler-0.3.2/src/scope_profiler/line_profile_cli.py +78 -0
  9. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/plotting_scripts.py +48 -5
  10. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/post_processing.py +16 -4
  11. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/profile_manager.py +92 -4
  12. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/region_profiler.py +52 -2
  13. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/results.py +18 -0
  14. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/summary.py +47 -4
  15. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_app.py +55 -1
  16. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_inspection.py +30 -2
  17. scope_profiler-0.3.2/src/scope_profiler/tests/test_line_profile_cli.py +41 -0
  18. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_main_cli.py +51 -1
  19. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_post_processing.py +8 -5
  20. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_results_api.py +21 -1
  21. scope_profiler-0.3.2/src/scope_profiler/tests/test_tui.py +141 -0
  22. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_h5writer.py +30 -1
  23. scope_profiler-0.3.2/src/scope_profiler/tui.py +650 -0
  24. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/PKG-INFO +29 -23
  25. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/SOURCES.txt +4 -0
  26. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/requires.txt +9 -3
  27. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/setup.cfg +0 -0
  28. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/__init__.py +0 -0
  29. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/c/Makefile +0 -0
  30. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/c/example.c +0 -0
  31. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/c/scope_profiler.c +0 -0
  32. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/c/scope_profiler.h +0 -0
  33. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/call_stack.py +0 -0
  34. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/diff.py +0 -0
  35. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/fortran/Makefile +0 -0
  36. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/fortran/example.f90 +0 -0
  37. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/fortran/scope_profiler.f90 +0 -0
  38. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/likwid_data.py +0 -0
  39. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mcp_server/__init__.py +0 -0
  40. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mcp_server/__main__.py +0 -0
  41. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mcp_server/server.py +0 -0
  42. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mcp_server/tools.py +0 -0
  43. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/metadata.py +0 -0
  44. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mpi_launch.py +0 -0
  45. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/mpi_region.py +0 -0
  46. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/native_trace.py +0 -0
  47. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/prof_export.py +0 -0
  48. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/profile_config.py +0 -0
  49. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/region.py +0 -0
  50. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/speedscope_export.py +0 -0
  51. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/__init__.py +0 -0
  52. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/check_mpi_launch.py +0 -0
  53. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/examples.py +0 -0
  54. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/examples_pylikwid.py +0 -0
  55. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/pylikwid_readme.py +0 -0
  56. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_buffer_growth.py +0 -0
  57. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_c_api.py +0 -0
  58. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_diff.py +0 -0
  59. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_fortran_api.py +0 -0
  60. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_in_memory_results.py +0 -0
  61. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_label.py +0 -0
  62. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_likwid_data.py +0 -0
  63. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_mcp_server.py +0 -0
  64. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_mcp_tools.py +0 -0
  65. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_metadata.py +0 -0
  66. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_mixed_language.py +0 -0
  67. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_mpi.py +0 -0
  68. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_overhead.py +0 -0
  69. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_prof_export.py +0 -0
  70. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_readme.py +0 -0
  71. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_region_source.py +0 -0
  72. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_repeated_finalize.py +0 -0
  73. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_speedscope_export.py +0 -0
  74. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/test_summary_likwid.py +0 -0
  75. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/__init__.py +0 -0
  76. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_lazy_config.py +0 -0
  77. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_likwid_child_env.py +0 -0
  78. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_merge_results.py +0 -0
  79. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_mpi_launch.py +0 -0
  80. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler/tests/unit/test_payload_collection.py +0 -0
  81. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/dependency_links.txt +0 -0
  82. {scope_profiler-0.3.0 → scope_profiler-0.3.2}/src/scope_profiler.egg-info/entry_points.txt +0 -0
  83. {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.0
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: plot
33
- Requires-Dist: ipykernel; extra == "plot"
34
- Requires-Dist: jupyterlab; extra == "plot"
35
- Requires-Dist: matplotlib; extra == "plot"
36
- Requires-Dist: maxplotlibx>=0.1.6; extra == "plot"
37
- Requires-Dist: pandas; extra == "plot"
38
- Requires-Dist: snakeviz; extra == "plot"
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,plot,test]; extra == "dev"
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,plot]; extra == "docs"
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] min [s] max [s] std [s]
114
- ----------------------------------------------------------------------------------------
115
- main 1 1 0.00150371 0.00150371 0.00150371 0.00150371 0
116
- iteration 1 10 3.832e-06 3.832e-07 2.08e-07 8.75e-07 2.24319e-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] min [s] max [s] std [s]
162
- --------------------------------------------------------------------------------
163
- timestep 2 8 0.139235 0.0174044 0.0165256 0.0176325 0.000338551
164
- solve 2 8 0.0991292 0.0123911 0.0115046 0.0125382 0.000335212
165
- setup 2 2 0.0473326 0.0236663 0.0222938 0.0250388 0.00137254
166
- assemble 2 8 0.0399496 0.0049937 0.00484729 0.0050345 5.5882e-05
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] min [s] max [s] std [s]
58
- ----------------------------------------------------------------------------------------
59
- main 1 1 0.00150371 0.00150371 0.00150371 0.00150371 0
60
- iteration 1 10 3.832e-06 3.832e-07 2.08e-07 8.75e-07 2.24319e-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] min [s] max [s] std [s]
106
- --------------------------------------------------------------------------------
107
- timestep 2 8 0.139235 0.0174044 0.0165256 0.0176325 0.000338551
108
- solve 2 8 0.0991292 0.0123911 0.0115046 0.0125382 0.000335212
109
- setup 2 2 0.0473326 0.0236663 0.0222938 0.0250388 0.00137254
110
- assemble 2 8 0.0399496 0.0049937 0.00484729 0.0050345 5.5882e-05
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.0"
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.plot = [
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,plot,test]",
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,plot]",
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=False,
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
- if plotly_layout:
162
- fig.update_layout(**plotly_layout)
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
- fig_height = max(4.5, 2.5 + 0.35 * num_readers)
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(figsize=(fig_width, fig_height))
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(canvas, metric_filepath, show, backend)
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", True),
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", True),
34
- "speedup": ("scaling across multiple files (2+ files only)", True),
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
- args = parser.parse_args(argv)
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()