scope-profiler 0.2.2__tar.gz → 0.2.4__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 (45) hide show
  1. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/PKG-INFO +37 -4
  2. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/README.md +31 -0
  3. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/pyproject.toml +7 -3
  4. scope_profiler-0.2.4/src/scope_profiler/mpi_launch.py +119 -0
  5. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/post_processing.py +129 -48
  6. scope_profiler-0.2.4/src/scope_profiler/prof_export.py +208 -0
  7. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/profile_config.py +11 -12
  8. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/profile_manager.py +21 -0
  9. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/region_profiler.py +45 -6
  10. scope_profiler-0.2.4/src/scope_profiler/speedscope_export.py +271 -0
  11. scope_profiler-0.2.4/src/scope_profiler/tests/check_mpi_launch.py +66 -0
  12. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/test_app.py +2 -0
  13. scope_profiler-0.2.4/src/scope_profiler/tests/test_prof_export.py +225 -0
  14. scope_profiler-0.2.4/src/scope_profiler/tests/test_repeated_finalize.py +99 -0
  15. scope_profiler-0.2.4/src/scope_profiler/tests/test_speedscope_export.py +227 -0
  16. scope_profiler-0.2.4/src/scope_profiler/tests/unit/__init__.py +0 -0
  17. scope_profiler-0.2.4/src/scope_profiler/tests/unit/test_mpi_launch.py +90 -0
  18. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler.egg-info/PKG-INFO +37 -4
  19. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler.egg-info/SOURCES.txt +10 -1
  20. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler.egg-info/requires.txt +6 -3
  21. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/setup.cfg +0 -0
  22. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/__init__.py +0 -0
  23. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/__main__.py +0 -0
  24. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/h5reader.py +0 -0
  25. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/inspection.py +0 -0
  26. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/metadata.py +0 -0
  27. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/mpi_region.py +0 -0
  28. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/plotting_scripts.py +0 -0
  29. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/region.py +0 -0
  30. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/summary.py +0 -0
  31. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/__init__.py +0 -0
  32. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/examples.py +0 -0
  33. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/examples_pylikwid.py +0 -0
  34. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/pylikwid_readme.py +0 -0
  35. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/test_buffer_growth.py +0 -0
  36. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/test_inspection.py +0 -0
  37. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/test_metadata.py +0 -0
  38. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/test_mpi.py +0 -0
  39. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/test_overhead.py +0 -0
  40. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/test_post_processing.py +0 -0
  41. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/test_reader_api.py +0 -0
  42. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler/tests/test_readme.py +0 -0
  43. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler.egg-info/dependency_links.txt +0 -0
  44. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/src/scope_profiler.egg-info/entry_points.txt +0 -0
  45. {scope_profiler-0.2.2 → scope_profiler-0.2.4}/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.2.2
3
+ Version: 0.2.4
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
@@ -16,7 +16,8 @@ Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  Requires-Dist: h5py
18
18
  Requires-Dist: numpy
19
- Requires-Dist: line-profiler
19
+ Provides-Extra: line-profiler
20
+ Requires-Dist: line-profiler; extra == "line-profiler"
20
21
  Provides-Extra: mpi
21
22
  Requires-Dist: mpi4py; extra == "mpi"
22
23
  Provides-Extra: pproc
@@ -25,11 +26,12 @@ Requires-Dist: jupyterlab; extra == "pproc"
25
26
  Requires-Dist: matplotlib; extra == "pproc"
26
27
  Requires-Dist: maxplotlibx>=0.1.5; extra == "pproc"
27
28
  Requires-Dist: pandas; extra == "pproc"
29
+ Requires-Dist: snakeviz; extra == "pproc"
28
30
  Provides-Extra: dev
29
31
  Requires-Dist: black[jupyter]; extra == "dev"
30
32
  Requires-Dist: isort; extra == "dev"
31
33
  Requires-Dist: ruff; extra == "dev"
32
- Requires-Dist: scope-profiler[docs,mpi,pproc,test]; extra == "dev"
34
+ Requires-Dist: scope-profiler[docs,line-profiler,mpi,pproc,test]; extra == "dev"
33
35
  Provides-Extra: docs
34
36
  Requires-Dist: myst-parser; extra == "docs"
35
37
  Requires-Dist: nbconvert; extra == "docs"
@@ -38,7 +40,7 @@ Requires-Dist: pre-commit; extra == "docs"
38
40
  Requires-Dist: pyproject-fmt; extra == "docs"
39
41
  Requires-Dist: sphinx; extra == "docs"
40
42
  Requires-Dist: sphinx-book-theme; extra == "docs"
41
- Requires-Dist: scope-profiler[pproc]; extra == "docs"
43
+ Requires-Dist: scope-profiler[line-profiler,pproc]; extra == "docs"
42
44
  Provides-Extra: test
43
45
  Requires-Dist: coverage; extra == "test"
44
46
  Requires-Dist: pytest; extra == "test"
@@ -427,3 +429,34 @@ straight from the JSON:
427
429
  scope-profiler pproc profiling_data.h5 -o figures \
428
430
  --export-data --export-data-format json --skip-plot-images
429
431
  ```
432
+
433
+ ### Viewing a run in snakeviz
434
+
435
+ `--export-prof` writes the profile in the `.prof` format of the standard
436
+ library's `cProfile`, so a run can be explored with
437
+ [snakeviz](https://jiffyclub.github.io/snakeviz/) or `python -m pstats`:
438
+
439
+ ```bash
440
+ scope-profiler pproc profiling_data.h5 -o figures --export-prof --skip-plot-images
441
+ snakeviz figures/profile_rank0.prof
442
+ ```
443
+
444
+ Regions become "functions": `cumtime` is a region's total wall time and
445
+ `tottime` is that minus the time spent in its nested regions. One file is
446
+ written per exported rank, since `.prof` has no notion of ranks — see
447
+ [the CLI docs](docs/source/cli.md) for the caveats of the reconstruction.
448
+
449
+ ### Viewing a run in speedscope
450
+
451
+ `--export-speedscope` writes the run as a
452
+ [speedscope](https://www.speedscope.app) JSON file. Unlike `.prof`, it keeps
453
+ every individual call, so the timeline shows the run as it happened:
454
+
455
+ ```bash
456
+ scope-profiler pproc profiling_data.h5 -o figures --export-speedscope --skip-plot-images
457
+ npx speedscope figures/profile.speedscope.json # or drop the file on speedscope.app
458
+ ```
459
+
460
+ One file is written per input, holding one profile per exported rank, all
461
+ sharing a time origin so ranks stay aligned. See
462
+ [the CLI docs](docs/source/cli.md) for details.
@@ -382,3 +382,34 @@ straight from the JSON:
382
382
  scope-profiler pproc profiling_data.h5 -o figures \
383
383
  --export-data --export-data-format json --skip-plot-images
384
384
  ```
385
+
386
+ ### Viewing a run in snakeviz
387
+
388
+ `--export-prof` writes the profile in the `.prof` format of the standard
389
+ library's `cProfile`, so a run can be explored with
390
+ [snakeviz](https://jiffyclub.github.io/snakeviz/) or `python -m pstats`:
391
+
392
+ ```bash
393
+ scope-profiler pproc profiling_data.h5 -o figures --export-prof --skip-plot-images
394
+ snakeviz figures/profile_rank0.prof
395
+ ```
396
+
397
+ Regions become "functions": `cumtime` is a region's total wall time and
398
+ `tottime` is that minus the time spent in its nested regions. One file is
399
+ written per exported rank, since `.prof` has no notion of ranks — see
400
+ [the CLI docs](docs/source/cli.md) for the caveats of the reconstruction.
401
+
402
+ ### Viewing a run in speedscope
403
+
404
+ `--export-speedscope` writes the run as a
405
+ [speedscope](https://www.speedscope.app) JSON file. Unlike `.prof`, it keeps
406
+ every individual call, so the timeline shows the run as it happened:
407
+
408
+ ```bash
409
+ scope-profiler pproc profiling_data.h5 -o figures --export-speedscope --skip-plot-images
410
+ npx speedscope figures/profile.speedscope.json # or drop the file on speedscope.app
411
+ ```
412
+
413
+ One file is written per input, holding one profile per exported rank, all
414
+ sharing a time origin so ranks stay aligned. See
415
+ [the CLI docs](docs/source/cli.md) for details.
@@ -5,7 +5,7 @@ requires = [ "setuptools", "wheel" ]
5
5
 
6
6
  [project]
7
7
  name = "scope-profiler"
8
- version = "0.2.2"
8
+ version = "0.2.4"
9
9
  description = "Profile code regions in python, optionally with LIKWID markers."
10
10
  readme = "README.md"
11
11
  keywords = [ "python" ]
@@ -24,6 +24,9 @@ classifiers = [
24
24
  dependencies = [
25
25
  "h5py",
26
26
  "numpy",
27
+ ]
28
+
29
+ optional-dependencies.line-profiler = [
27
30
  "line-profiler",
28
31
  ]
29
32
 
@@ -37,13 +40,14 @@ optional-dependencies.pproc = [
37
40
  "matplotlib",
38
41
  "maxplotlibx >= 0.1.5",
39
42
  "pandas",
43
+ "snakeviz",
40
44
  ]
41
45
 
42
46
  optional-dependencies.dev = [
43
47
  "black[jupyter]",
44
48
  "isort",
45
49
  "ruff",
46
- "scope-profiler[docs,test,mpi,pproc]",
50
+ "scope-profiler[docs,line-profiler,mpi,pproc,test]",
47
51
  ]
48
52
  # https://medium.com/@pratikdomadiya123/build-project-documentation-quickly-with-the-sphinx-python-2a9732b66594
49
53
  optional-dependencies.docs = [
@@ -54,7 +58,7 @@ optional-dependencies.docs = [
54
58
  "pyproject-fmt",
55
59
  "sphinx",
56
60
  "sphinx-book-theme",
57
- "scope-profiler[pproc]",
61
+ "scope-profiler[line-profiler,pproc]",
58
62
  ]
59
63
  optional-dependencies.test = [
60
64
  "coverage",
@@ -0,0 +1,119 @@
1
+ """Detection of whether the process was launched by an MPI launcher.
2
+
3
+ Importing ``mpi4py.MPI`` calls ``MPI_Init``, and any collective (``bcast``,
4
+ ``Barrier``, ...) issued afterwards costs something even on a single process.
5
+ A plain ``python script.py`` run should therefore never touch MPI at all, even
6
+ when mpi4py happens to be installed. This module answers the only question
7
+ that decides it: was this process started by ``mpirun``/``mpiexec``/``srun``
8
+ (or an equivalent launcher)?
9
+
10
+ The answer is read from the environment the launcher sets up, so it is
11
+ available before mpi4py is imported.
12
+ """
13
+
14
+ import os
15
+ import sys
16
+
17
+ # Per-process variables exported by the process managers behind the common
18
+ # launchers. Each is set only for processes started *by* the launcher, so the
19
+ # presence of any one of them means "this rank belongs to an MPI job".
20
+ # SLURM_PROCID is deliberately absent: it is also set for the script of a
21
+ # plain `sbatch` job, which is not an MPI launch. `srun` is covered by the
22
+ # PMI/PMIX variables its MPI plugin exports.
23
+ _LAUNCHER_ENV_VARS = (
24
+ "OMPI_COMM_WORLD_RANK", # Open MPI (and derivatives: Spectrum, ...)
25
+ "PMI_RANK", # MPICH, Intel MPI, MS-MPI, Cray, srun (pmi2)
26
+ "PMIX_RANK", # PMIx, used by srun --mpi=pmix and Open MPI 5
27
+ "MV2_COMM_WORLD_RANK", # MVAPICH2
28
+ "MPI_LOCALRANKID", # Hydra (mpiexec.hydra)
29
+ "ALPS_APP_PE", # Cray ALPS aprun
30
+ "PALS_RANKID", # Cray PALS palsrun
31
+ )
32
+
33
+ # Escape hatch: force the decision either way without touching code, e.g. for
34
+ # a launcher whose variables are not listed above.
35
+ _OVERRIDE_ENV_VAR = "SCOPE_PROFILER_MPI"
36
+
37
+ _TRUE_VALUES = ("1", "true", "yes", "on")
38
+ _FALSE_VALUES = ("0", "false", "no", "off")
39
+
40
+
41
+ def _override() -> bool | None:
42
+ """Value of ``SCOPE_PROFILER_MPI``, or None if unset/unrecognized."""
43
+ value = os.environ.get(_OVERRIDE_ENV_VAR)
44
+ if value is None:
45
+ return None
46
+ value = value.strip().lower()
47
+ if value in _TRUE_VALUES:
48
+ return True
49
+ if value in _FALSE_VALUES:
50
+ return False
51
+ return None
52
+
53
+
54
+ def launched_under_mpi() -> bool:
55
+ """Whether this process was started by an MPI launcher.
56
+
57
+ Returns
58
+ -------
59
+ bool
60
+ True if a launcher's per-rank environment variable is present, or if
61
+ the application itself already initialized MPI (in which case using
62
+ the communicator is free). ``SCOPE_PROFILER_MPI=0``/``1`` overrides
63
+ the detection.
64
+ """
65
+ override = _override()
66
+ if override is not None:
67
+ return override
68
+
69
+ if any(var in os.environ for var in _LAUNCHER_ENV_VARS):
70
+ return True
71
+
72
+ # The application may have initialized MPI itself (embedded interpreter,
73
+ # or an explicit `from mpi4py import MPI`). Only inspect mpi4py if it is
74
+ # already imported: importing it here is exactly what must be avoided.
75
+ mpi_module = sys.modules.get("mpi4py.MPI")
76
+ if mpi_module is not None:
77
+ try:
78
+ return bool(mpi_module.Is_initialized())
79
+ except AttributeError:
80
+ return False
81
+
82
+ return False
83
+
84
+
85
+ def get_comm(use_mpi: bool | None = None):
86
+ """Return ``MPI.COMM_WORLD``, or None when MPI must not be used.
87
+
88
+ Parameters
89
+ ----------
90
+ use_mpi : bool or None, optional
91
+ None (default) decides via :func:`launched_under_mpi`. True forces the
92
+ communicator (and fails loudly if mpi4py is missing); False disables
93
+ MPI unconditionally.
94
+
95
+ Returns
96
+ -------
97
+ mpi4py.MPI.Intracomm or None
98
+ The world communicator, or None if this is not an MPI run or mpi4py
99
+ is unavailable.
100
+ """
101
+ if use_mpi is False:
102
+ return None
103
+
104
+ if use_mpi is None and not launched_under_mpi():
105
+ return None
106
+
107
+ try:
108
+ from mpi4py import MPI
109
+ except ImportError:
110
+ if use_mpi:
111
+ raise ImportError(
112
+ "MPI profiling was requested (use_mpi=True) but mpi4py is not "
113
+ "installed."
114
+ )
115
+ # Launched by mpirun without mpi4py available: fall back to treating
116
+ # this rank as a standalone process rather than failing the run.
117
+ return None
118
+
119
+ return MPI.COMM_WORLD
@@ -13,6 +13,8 @@ from scope_profiler.plotting_scripts import (
13
13
  plot_speedup,
14
14
  write_region_statistics_json,
15
15
  )
16
+ from scope_profiler.prof_export import export_prof
17
+ from scope_profiler.speedscope_export import export_speedscope
16
18
 
17
19
 
18
20
  def parse_ranks(spec: str, verbose: bool = False) -> list[int]:
@@ -155,14 +157,39 @@ def build_parser() -> argparse.ArgumentParser:
155
157
  "consistent colors."
156
158
  ),
157
159
  )
160
+ parser.add_argument(
161
+ "--export-prof",
162
+ action="store_true",
163
+ help=(
164
+ "Also write one profile_rank<N>.prof file per exported rank in the "
165
+ "cProfile/pstats format, so the run can be browsed with external "
166
+ "tools (`snakeviz profile_rank0.prof`, `python -m pstats ...`). "
167
+ "The call graph is reconstructed from region nesting; only ranks "
168
+ "selected with --ranks are exported (default: rank 0). Requires "
169
+ "-o/--output."
170
+ ),
171
+ )
172
+ parser.add_argument(
173
+ "--export-speedscope",
174
+ action="store_true",
175
+ help=(
176
+ "Also write a profile.speedscope.json file holding one profile per "
177
+ "exported rank, viewable at https://www.speedscope.app (or with "
178
+ "`npx speedscope profile.speedscope.json`). Unlike --export-prof "
179
+ "this keeps every individual call, so the timeline shows the run "
180
+ "as it happened. The call graph is reconstructed from region "
181
+ "nesting; only ranks selected with --ranks are exported (default: "
182
+ "rank 0). Requires -o/--output."
183
+ ),
184
+ )
158
185
  parser.add_argument(
159
186
  "--skip-plot-images",
160
187
  action="store_true",
161
188
  help=(
162
189
  "Do not render/save the PNG plot images, only the outputs from "
163
- "--export-data. Useful when charts are rendered entirely client-"
164
- "side (e.g. with Plotly) from the exported data. Requires "
165
- "--export-data."
190
+ "--export-data/--export-prof/--export-speedscope. Useful when "
191
+ "charts are rendered entirely client-side (e.g. with Plotly) from "
192
+ "the exported data. Requires one of those export options."
166
193
  ),
167
194
  )
168
195
  return parser
@@ -203,8 +230,18 @@ def main(argv: list[str] | None = None):
203
230
  if args.export_data and not args.output:
204
231
  parser.error("--export-data requires -o/--output.")
205
232
 
206
- if args.skip_plot_images and not args.export_data:
207
- parser.error("--skip-plot-images requires --export-data.")
233
+ if args.export_prof and not args.output:
234
+ parser.error("--export-prof requires -o/--output.")
235
+
236
+ if args.export_speedscope and not args.output:
237
+ parser.error("--export-speedscope requires -o/--output.")
238
+
239
+ exports_requested = args.export_data or args.export_prof or args.export_speedscope
240
+ if args.skip_plot_images and not exports_requested:
241
+ parser.error(
242
+ "--skip-plot-images requires --export-data, --export-prof or "
243
+ "--export-speedscope."
244
+ )
208
245
 
209
246
  if args.ranks:
210
247
  ranks = []
@@ -243,6 +280,11 @@ def main(argv: list[str] | None = None):
243
280
  flame_data_path = None
244
281
  durations_data_path = None
245
282
  speedup_data_path = None
283
+ prof_path = None
284
+ prof_paths: list = []
285
+ speedscope_path = None
286
+ speedscope_paths: list = []
287
+ durations_paths: list = []
246
288
  if args.output:
247
289
  os.makedirs(args.output, exist_ok=True)
248
290
  if not args.skip_plot_images:
@@ -266,62 +308,92 @@ def main(argv: list[str] | None = None):
266
308
  speedup_data_path = os.path.join(
267
309
  args.output, f"speedup_data.{data_ext}"
268
310
  )
311
+ if args.export_prof:
312
+ prof_path = os.path.join(args.output, "profile.prof")
313
+ if args.export_speedscope:
314
+ speedscope_path = os.path.join(args.output, "profile.speedscope.json")
269
315
 
270
- plot_gantt(
271
- profiling_data=readers,
272
- filepath=gantt_path,
273
- show=args.show,
274
- include=args.include,
275
- exclude=args.exclude,
276
- ranks=args.ranks,
277
- cmap=args.cmap,
278
- data_filepath=gantt_data_path,
279
- data_format=args.export_data_format,
280
- backend=args.backend,
281
- )
316
+ # --skip-plot-images still needs the plotting functions to produce the
317
+ # --export-data files, but an export-only run should not touch the
318
+ # plotting backend at all.
319
+ render_plots = not args.skip_plot_images or args.export_data
282
320
 
283
- plot_flame(
284
- profiling_data=readers,
285
- filepath=flame_path,
286
- show=args.show,
287
- include=args.include,
288
- exclude=args.exclude,
289
- ranks=args.ranks,
290
- cmap=args.cmap,
291
- data_filepath=flame_data_path,
292
- data_format=args.export_data_format,
293
- backend=args.backend,
294
- )
321
+ if args.export_prof:
322
+ prof_paths = export_prof(
323
+ profiling_data=readers,
324
+ filepath=prof_path,
325
+ ranks=args.ranks,
326
+ include=args.include,
327
+ exclude=args.exclude,
328
+ verbose=False,
329
+ )
295
330
 
296
- durations_paths = plot_durations(
297
- profiling_data=readers,
298
- filepath=durations_path,
299
- show=args.show,
300
- include=args.include,
301
- exclude=args.exclude,
302
- ranks=args.ranks,
303
- metrics=args.metrics,
304
- cmap=args.cmap,
305
- data_filepath=durations_data_path,
306
- data_format=args.export_data_format,
307
- backend=args.backend,
308
- )
331
+ if args.export_speedscope:
332
+ speedscope_paths = export_speedscope(
333
+ profiling_data=readers,
334
+ filepath=speedscope_path,
335
+ ranks=args.ranks,
336
+ include=args.include,
337
+ exclude=args.exclude,
338
+ verbose=False,
339
+ )
340
+
341
+ if render_plots:
342
+ plot_gantt(
343
+ profiling_data=readers,
344
+ filepath=gantt_path,
345
+ show=args.show,
346
+ include=args.include,
347
+ exclude=args.exclude,
348
+ ranks=args.ranks,
349
+ cmap=args.cmap,
350
+ data_filepath=gantt_data_path,
351
+ data_format=args.export_data_format,
352
+ backend=args.backend,
353
+ )
309
354
 
310
- if len(readers) > 1:
311
- plot_speedup(
355
+ plot_flame(
312
356
  profiling_data=readers,
313
- x_field=args.x_field,
357
+ filepath=flame_path,
358
+ show=args.show,
359
+ include=args.include,
360
+ exclude=args.exclude,
314
361
  ranks=args.ranks,
315
- filepath=speedup_path,
362
+ cmap=args.cmap,
363
+ data_filepath=flame_data_path,
364
+ data_format=args.export_data_format,
365
+ backend=args.backend,
366
+ )
367
+
368
+ durations_paths = plot_durations(
369
+ profiling_data=readers,
370
+ filepath=durations_path,
316
371
  show=args.show,
317
372
  include=args.include,
318
373
  exclude=args.exclude,
374
+ ranks=args.ranks,
375
+ metrics=args.metrics,
319
376
  cmap=args.cmap,
320
- data_filepath=speedup_data_path,
377
+ data_filepath=durations_data_path,
321
378
  data_format=args.export_data_format,
322
379
  backend=args.backend,
323
380
  )
324
381
 
382
+ if len(readers) > 1:
383
+ plot_speedup(
384
+ profiling_data=readers,
385
+ x_field=args.x_field,
386
+ ranks=args.ranks,
387
+ filepath=speedup_path,
388
+ show=args.show,
389
+ include=args.include,
390
+ exclude=args.exclude,
391
+ cmap=args.cmap,
392
+ data_filepath=speedup_data_path,
393
+ data_format=args.export_data_format,
394
+ backend=args.backend,
395
+ )
396
+
325
397
  if statistics_path:
326
398
  write_region_statistics_json(
327
399
  profiling_data=readers,
@@ -333,7 +405,7 @@ def main(argv: list[str] | None = None):
333
405
 
334
406
  if args.output and not args.show:
335
407
  saved = [
336
- path
408
+ str(path)
337
409
  for path in (
338
410
  gantt_path,
339
411
  flame_path,
@@ -344,10 +416,19 @@ def main(argv: list[str] | None = None):
344
416
  flame_data_path,
345
417
  durations_data_path,
346
418
  speedup_data_path,
419
+ *prof_paths,
420
+ *speedscope_paths,
347
421
  )
348
422
  if path
349
423
  ]
350
424
  print("Outputs saved to:\n " + "\n ".join(saved))
425
+ if prof_paths:
426
+ print(f"\nView a .prof file with: snakeviz {prof_paths[0]}")
427
+ if speedscope_paths:
428
+ print(
429
+ f"\nView {speedscope_paths[0]} at https://www.speedscope.app "
430
+ "(or: npx speedscope <file>)"
431
+ )
351
432
 
352
433
 
353
434
  if __name__ == "__main__":