runem 0.0.28__tar.gz → 0.0.29__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.
- {runem-0.0.28 → runem-0.0.29}/HISTORY.md +107 -0
- {runem-0.0.28 → runem-0.0.29}/PKG-INFO +8 -7
- {runem-0.0.28 → runem-0.0.29}/README.md +6 -6
- runem-0.0.29/runem/VERSION +1 -0
- {runem-0.0.28 → runem-0.0.29}/runem/job_execute.py +28 -6
- runem-0.0.29/runem/report.py +252 -0
- {runem-0.0.28 → runem-0.0.29}/runem/run_command.py +18 -0
- {runem-0.0.28 → runem-0.0.29}/runem/runem.py +16 -6
- {runem-0.0.28 → runem-0.0.29}/runem/types.py +15 -1
- {runem-0.0.28 → runem-0.0.29}/runem.egg-info/PKG-INFO +8 -7
- {runem-0.0.28 → runem-0.0.29}/runem.egg-info/SOURCES.txt +2 -1
- {runem-0.0.28 → runem-0.0.29}/runem.egg-info/requires.txt +1 -0
- runem-0.0.29/tests/data/help_output.3.11.txt +71 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_job_execute.py +74 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_report.py +72 -29
- {runem-0.0.28 → runem-0.0.29}/tests/test_run_command.py +12 -1
- {runem-0.0.28 → runem-0.0.29}/tests/test_runem.py +41 -11
- runem-0.0.28/runem/VERSION +0 -1
- runem-0.0.28/runem/report.py +0 -145
- {runem-0.0.28 → runem-0.0.29}/Containerfile +0 -0
- {runem-0.0.28 → runem-0.0.29}/LICENSE +0 -0
- {runem-0.0.28 → runem-0.0.29}/MANIFEST.in +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/__init__.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/__main__.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/base.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/blocking_print.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/cli/initialise_options.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/cli.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/command_line.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/config.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/config_metadata.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/config_parse.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/files.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/informative_dict.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/job.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/job_filter.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/job_runner_simple_command.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/job_wrapper.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/job_wrapper_python.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/log.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/py.typed +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/runem_version.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem/utils.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem.egg-info/dependency_links.txt +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem.egg-info/entry_points.txt +0 -0
- {runem-0.0.28 → runem-0.0.29}/runem.egg-info/top_level.txt +0 -0
- {runem-0.0.28 → runem-0.0.29}/setup.cfg +0 -0
- {runem-0.0.28 → runem-0.0.29}/setup.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/__init__.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/cli/test_initialise_options.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/conftest.py +0 -0
- /runem-0.0.28/tests/data/help_output.txt → /runem-0.0.29/tests/data/help_output.3.10.txt +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/intentional_test_error.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_base.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_blocking_print.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_cli.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_config.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_config_parse.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_files.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_informative_dict.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_job.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_job_filter.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_job_runner_simple_command.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_job_wrapper.py +0 -0
- {runem-0.0.28 → runem-0.0.29}/tests/test_job_wrapper_python.py +0 -0
@@ -4,6 +4,113 @@ Changelog
|
|
4
4
|
|
5
5
|
(unreleased)
|
6
6
|
------------
|
7
|
+
- Merge pull request #40 from lursight/fix/time_saved. [Frank Harrison]
|
8
|
+
|
9
|
+
Fix/time saved
|
10
|
+
- Feat(prettier-bars): clarifies that total is user-space time. [Frank
|
11
|
+
Harrison]
|
12
|
+
|
13
|
+
... not wall-clock or system-time
|
14
|
+
- Feat(prettier-bars): distiguishes the wall-clock bars. [Frank
|
15
|
+
Harrison]
|
16
|
+
|
17
|
+
... from the total/sum and sub-job bars, so that it's slightly easier to
|
18
|
+
see where the time is being really spent.
|
19
|
+
- Fix(time-saved): clarifies which measurement is the wall-clock time
|
20
|
+
for the entire run. [Frank Harrison]
|
21
|
+
- Fix(time-saved): add message about how long we _would_ have waited
|
22
|
+
without runem. [Frank Harrison]
|
23
|
+
- Fix(time-saved): renames all variable associated with timing reports.
|
24
|
+
[Frank Harrison]
|
25
|
+
|
26
|
+
This just makes someting which can become intractable/confusing a lot
|
27
|
+
easier to follow.
|
28
|
+
- Fix(time-saved): check that time-saved is reported correctly. [Frank
|
29
|
+
Harrison]
|
30
|
+
|
31
|
+
Here we add a test first and then fix the missing math to calculate the
|
32
|
+
time-saved by using runem. We broke this in the previous feature for
|
33
|
+
rendering the tree slightly more elegantly.
|
34
|
+
- Feat(hide-single-leafs): only show the job when it has a single child.
|
35
|
+
[Frank Harrison]
|
36
|
+
|
37
|
+
We would get duplicated information for jobs which had single
|
38
|
+
run_command invocations. This only shows sub-tasks/jobs if there are
|
39
|
+
more than one sub-tasks meaning the output looks a lot nicer & clearer.
|
40
|
+
- Chore(deps): adds setuptools as a explicit dep. [Frank Harrison]
|
41
|
+
|
42
|
+
... otherwise we get the following error (more often in python 3.12),
|
43
|
+
perhaps due to setuptools being removed from distros?:
|
44
|
+
|
45
|
+
```text
|
46
|
+
Traceback (most recent call last):
|
47
|
+
File "/var/www/mydir/virtualenvs/dev/bin/pip", line 5, in <module>
|
48
|
+
from pkg_resources import load_entry_point
|
49
|
+
ImportError: No module named pkg_resources
|
50
|
+
```
|
51
|
+
- Merge pull request #39 from lursight/feat/time_all_run_command_calls.
|
52
|
+
[Frank Harrison]
|
53
|
+
|
54
|
+
Feat/time all run command calls
|
55
|
+
- Feat(pretty-tree): refactors out the phase-job report generator.
|
56
|
+
[Frank Harrison]
|
57
|
+
|
58
|
+
This is just to make pylint happy.
|
59
|
+
- Feat(pretty-tree): makes the report tree neater. [Frank Harrison]
|
60
|
+
- Feat(time-all-sub-tasks): re-raise errors for context in ci/cd. [Frank
|
61
|
+
Harrison]
|
62
|
+
|
63
|
+
In github ci/cd we were hitting the asserts but had no context of where
|
64
|
+
they're raised from or why. This should fix that if they still occur.
|
65
|
+
- Feat(time-all-sub-tasks): adds a test to test the time-recording
|
66
|
+
functions. [Frank Harrison]
|
67
|
+
- Feat(time-all-sub-tasks): adds all run_command times to report output.
|
68
|
+
[Frank Harrison]
|
69
|
+
- Chore(type): uses a type-alias instead of manual type. [Frank
|
70
|
+
Harrison]
|
71
|
+
- Merge pull request #37 from
|
72
|
+
lursight/dependabot/github_actions/actions/upload-artifact-4. [Frank
|
73
|
+
Harrison]
|
74
|
+
|
75
|
+
chore(deps): bump actions/upload-artifact from 3 to 4
|
76
|
+
- Chore(deps): bump actions/upload-artifact from 3 to 4.
|
77
|
+
[dependabot[bot]]
|
78
|
+
|
79
|
+
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
|
80
|
+
- [Release notes](https://github.com/actions/upload-artifact/releases)
|
81
|
+
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)
|
82
|
+
|
83
|
+
---
|
84
|
+
updated-dependencies:
|
85
|
+
- dependency-name: actions/upload-artifact
|
86
|
+
dependency-type: direct:production
|
87
|
+
update-type: version-update:semver-major
|
88
|
+
...
|
89
|
+
- Merge pull request #36 from lursight/chore/more_python_versions_in_ci.
|
90
|
+
[Frank Harrison]
|
91
|
+
|
92
|
+
Chore/more python versions in ci
|
93
|
+
- Chore(github-ci): workaround for python 3.12 setuptools issue. [Frank
|
94
|
+
Harrison]
|
95
|
+
|
96
|
+
This fies what looks like an issue with pytest hooks running ci/cd (and
|
97
|
+
on local machine) where we get:
|
98
|
+
ModuleNotFoundError: No module named 'pkg_resources'
|
99
|
+
- Chore(github-ci): updates the python `--help` tests for 3.11 (and
|
100
|
+
later) [Frank Harrison]
|
101
|
+
- Chore(github-ci): test python 3.9, 3.11 and 3.12 in ci. [Frank
|
102
|
+
Harrison]
|
103
|
+
|
104
|
+
We don't bother with 3.10 because we test 3.9 and boundaries for
|
105
|
+
features are on the 3.11 version.
|
106
|
+
|
107
|
+
We don't bother with earlier than 3.9, even though 3.8 is the earliest
|
108
|
+
officially supported version.
|
109
|
+
|
110
|
+
|
111
|
+
0.0.28 (2024-03-01)
|
112
|
+
-------------------
|
113
|
+
- Release: version 0.0.28 🚀 [Frank Harrison]
|
7
114
|
- Merge pull request #35 from lursight/fix/aliases_not_setting_options.
|
8
115
|
[Frank Harrison]
|
9
116
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: runem
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.29
|
4
4
|
Summary: Awesome runem created by lursight
|
5
5
|
Home-page: https://github.com/lursight/runem/
|
6
6
|
Author: lursight
|
@@ -26,6 +26,7 @@ Requires-Dist: pytest-cov==4.1.0; extra == "test"
|
|
26
26
|
Requires-Dist: pytest-profiling==1.7.0; extra == "test"
|
27
27
|
Requires-Dist: pytest-xdist==3.3.1; extra == "test"
|
28
28
|
Requires-Dist: pytest==7.4.3; extra == "test"
|
29
|
+
Requires-Dist: setuptools; extra == "test"
|
29
30
|
Requires-Dist: termplotlib==0.3.9; extra == "test"
|
30
31
|
Requires-Dist: types-PyYAML==6.0.12.12; extra == "test"
|
31
32
|
Requires-Dist: requests-mock==1.10.0; extra == "test"
|
@@ -400,12 +401,12 @@ runem: reports:
|
|
400
401
|
runem: runem: 8.820488s
|
401
402
|
runem: ├runem.pre-build: 0.019031s
|
402
403
|
runem: ├runem.run-phases: 8.801317s
|
403
|
-
runem: ├pre-run (
|
404
|
+
runem: ├pre-run (user-time): 0.00498s
|
404
405
|
runem: │├pre-run.install python requirements: 2.6e-05s
|
405
406
|
runem: │├pre-run.ls -alh runem: 0.004954s
|
406
|
-
runem: ├edit (
|
407
|
+
runem: ├edit (user-time): 0.557559s
|
407
408
|
runem: │├edit.reformat py: 0.557559s
|
408
|
-
runem: ├analysis (
|
409
|
+
runem: ├analysis (user-time): 21.526145s
|
409
410
|
runem: │├analysis.pylint py: 7.457029s
|
410
411
|
runem: │├analysis.flake8 py: 0.693754s
|
411
412
|
runem: │├analysis.mypy py: 1.071956s
|
@@ -430,12 +431,12 @@ runem: reports:
|
|
430
431
|
runem [14.174612] ███████████████▋
|
431
432
|
├runem.pre-build [ 0.025858]
|
432
433
|
├runem.run-phases [14.148587] ███████████████▋
|
433
|
-
├pre-run (
|
434
|
+
├pre-run (user-time) [ 0.005825]
|
434
435
|
│├pre-run.install python requirements [ 0.000028]
|
435
436
|
│├pre-run.ls -alh runem [ 0.005797]
|
436
|
-
├edit (
|
437
|
+
├edit (user-time) [ 0.579153] ▋
|
437
438
|
│├edit.reformat py [ 0.579153] ▋
|
438
|
-
├analysis (
|
439
|
+
├analysis (user-time) [36.231034] ████████████████████████████████████████
|
439
440
|
│├analysis.pylint py [12.738303] ██████████████▏
|
440
441
|
│├analysis.flake8 py [ 0.798575] ▉
|
441
442
|
│├analysis.mypy py [ 0.335984] ▍
|
@@ -367,12 +367,12 @@ runem: reports:
|
|
367
367
|
runem: runem: 8.820488s
|
368
368
|
runem: ├runem.pre-build: 0.019031s
|
369
369
|
runem: ├runem.run-phases: 8.801317s
|
370
|
-
runem: ├pre-run (
|
370
|
+
runem: ├pre-run (user-time): 0.00498s
|
371
371
|
runem: │├pre-run.install python requirements: 2.6e-05s
|
372
372
|
runem: │├pre-run.ls -alh runem: 0.004954s
|
373
|
-
runem: ├edit (
|
373
|
+
runem: ├edit (user-time): 0.557559s
|
374
374
|
runem: │├edit.reformat py: 0.557559s
|
375
|
-
runem: ├analysis (
|
375
|
+
runem: ├analysis (user-time): 21.526145s
|
376
376
|
runem: │├analysis.pylint py: 7.457029s
|
377
377
|
runem: │├analysis.flake8 py: 0.693754s
|
378
378
|
runem: │├analysis.mypy py: 1.071956s
|
@@ -397,12 +397,12 @@ runem: reports:
|
|
397
397
|
runem [14.174612] ███████████████▋
|
398
398
|
├runem.pre-build [ 0.025858]
|
399
399
|
├runem.run-phases [14.148587] ███████████████▋
|
400
|
-
├pre-run (
|
400
|
+
├pre-run (user-time) [ 0.005825]
|
401
401
|
│├pre-run.install python requirements [ 0.000028]
|
402
402
|
│├pre-run.ls -alh runem [ 0.005797]
|
403
|
-
├edit (
|
403
|
+
├edit (user-time) [ 0.579153] ▋
|
404
404
|
│├edit.reformat py [ 0.579153] ▋
|
405
|
-
├analysis (
|
405
|
+
├analysis (user-time) [36.231034] ████████████████████████████████████████
|
406
406
|
│├analysis.pylint py [12.738303] ██████████████▏
|
407
407
|
│├analysis.flake8 py [ 0.798575] ▉
|
408
408
|
│├analysis.mypy py [ 0.335984] ▍
|
@@ -0,0 +1 @@
|
|
1
|
+
0.0.29
|
@@ -11,14 +11,23 @@ from runem.informative_dict import ReadOnlyInformativeDict
|
|
11
11
|
from runem.job import Job
|
12
12
|
from runem.job_wrapper import get_job_wrapper
|
13
13
|
from runem.log import log
|
14
|
-
from runem.types import
|
14
|
+
from runem.types import (
|
15
|
+
FilePathListLookup,
|
16
|
+
JobConfig,
|
17
|
+
JobFunction,
|
18
|
+
JobReturn,
|
19
|
+
JobTags,
|
20
|
+
JobTiming,
|
21
|
+
TimingEntries,
|
22
|
+
TimingEntry,
|
23
|
+
)
|
15
24
|
|
16
25
|
|
17
26
|
def job_execute_inner(
|
18
27
|
job_config: JobConfig,
|
19
28
|
config_metadata: ConfigMetadata,
|
20
29
|
file_lists: FilePathListLookup,
|
21
|
-
) -> typing.Tuple[
|
30
|
+
) -> typing.Tuple[JobTiming, JobReturn]:
|
22
31
|
"""Wrapper for running a job inside a sub-process.
|
23
32
|
|
24
33
|
Returns the time information and any reports the job generated
|
@@ -38,7 +47,19 @@ def job_execute_inner(
|
|
38
47
|
if not file_list:
|
39
48
|
# no files to work on
|
40
49
|
log(f"WARNING: skipping job '{label}', no files for job")
|
41
|
-
return
|
50
|
+
return {
|
51
|
+
"job": (f"{label}: no files!", timedelta(0)),
|
52
|
+
"commands": [],
|
53
|
+
}, None
|
54
|
+
|
55
|
+
sub_command_timings: TimingEntries = []
|
56
|
+
|
57
|
+
def _record_sub_job_time(label: str, timing: timedelta) -> None:
|
58
|
+
"""Record timing information for sub-commands/tasks, atomically.
|
59
|
+
|
60
|
+
For example inside of run_command() calls
|
61
|
+
"""
|
62
|
+
sub_command_timings.append((label, timing))
|
42
63
|
|
43
64
|
if (
|
44
65
|
"ctx" in job_config
|
@@ -71,6 +92,7 @@ def job_execute_inner(
|
|
71
92
|
# unpack useful data points from the job_config
|
72
93
|
label=Job.get_job_name(job_config),
|
73
94
|
job=job_config,
|
95
|
+
record_sub_job_time=_record_sub_job_time,
|
74
96
|
)
|
75
97
|
except BaseException: # pylint: disable=broad-exception-caught
|
76
98
|
# log that we hit an error on this job and re-raise
|
@@ -83,8 +105,8 @@ def job_execute_inner(
|
|
83
105
|
time_taken: timedelta = timedelta(seconds=end - start)
|
84
106
|
if config_metadata.args.verbose:
|
85
107
|
log(f"job: DONE: '{label}': {time_taken}")
|
86
|
-
|
87
|
-
return (
|
108
|
+
this_job_timing_data: TimingEntry = (label, time_taken)
|
109
|
+
return ({"job": this_job_timing_data, "commands": sub_command_timings}, reports)
|
88
110
|
|
89
111
|
|
90
112
|
def job_execute(
|
@@ -92,7 +114,7 @@ def job_execute(
|
|
92
114
|
running_jobs: typing.Dict[str, str],
|
93
115
|
config_metadata: ConfigMetadata,
|
94
116
|
file_lists: FilePathListLookup,
|
95
|
-
) -> typing.Tuple[
|
117
|
+
) -> typing.Tuple[JobTiming, JobReturn]:
|
96
118
|
"""Thin-wrapper around job_execute_inner needed for mocking in tests.
|
97
119
|
|
98
120
|
Needed for faster tests.
|
@@ -0,0 +1,252 @@
|
|
1
|
+
import re
|
2
|
+
import typing
|
3
|
+
from collections import defaultdict
|
4
|
+
from datetime import timedelta
|
5
|
+
|
6
|
+
from runem.log import log
|
7
|
+
from runem.types import (
|
8
|
+
JobReturn,
|
9
|
+
JobRunMetadatasByPhase,
|
10
|
+
JobRunReportByPhase,
|
11
|
+
JobRunTimesByPhase,
|
12
|
+
JobTiming,
|
13
|
+
OrderedPhases,
|
14
|
+
PhaseName,
|
15
|
+
ReportUrlInfo,
|
16
|
+
ReportUrls,
|
17
|
+
TimingEntries,
|
18
|
+
)
|
19
|
+
|
20
|
+
try:
|
21
|
+
import termplotlib
|
22
|
+
except ImportError: # pragma: FIXME: add code coverage
|
23
|
+
termplotlib = None
|
24
|
+
|
25
|
+
|
26
|
+
def _align_bar_graphs_workaround(original_text: str) -> str:
|
27
|
+
"""Module termplotlib doesn't align floats, this fixes that.
|
28
|
+
|
29
|
+
This makes it so we can align the point in the floating point string, without it,
|
30
|
+
larger numbers push their bars right, instead of at the same place.
|
31
|
+
"""
|
32
|
+
# Find the maximum width between '[' and '.' characters
|
33
|
+
max_width = max(
|
34
|
+
int(match.end() - match.start() - 2)
|
35
|
+
for match in re.finditer(r"\[.*?(\d+)\.", original_text)
|
36
|
+
)
|
37
|
+
|
38
|
+
# Replace each line with aligned numbers
|
39
|
+
formatted_text = re.sub(
|
40
|
+
r"\[.*?(\d+)\.", lambda m: f"[{m.group(1):>{max_width}}.", original_text
|
41
|
+
)
|
42
|
+
|
43
|
+
return formatted_text
|
44
|
+
|
45
|
+
|
46
|
+
def _replace_bar_characters(text: str, end_str: str, replace_char: str) -> str:
|
47
|
+
"""Replaces block characters in lines containing `end_str` with give char.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
text_lines (List[str]): A list of strings, each representing a line of text.
|
51
|
+
replace_char (str): The character to replace all bocks with
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
List[str]: The modified list of strings with block characters replaced
|
55
|
+
on specified lines.
|
56
|
+
"""
|
57
|
+
# Define the block character and its light shade replacement
|
58
|
+
block_chars = (
|
59
|
+
"▏▎▋▊█▌▐▄▀─" # Extend this string with any additional block characters you use
|
60
|
+
)
|
61
|
+
|
62
|
+
text_lines: typing.List[str] = text.split("\n")
|
63
|
+
|
64
|
+
# Process each line, replacing block characters if `end_str` is present
|
65
|
+
modified_lines = [
|
66
|
+
line.translate(str.maketrans(block_chars, replace_char * len(block_chars)))
|
67
|
+
if end_str in line
|
68
|
+
else line
|
69
|
+
for line in text_lines
|
70
|
+
]
|
71
|
+
|
72
|
+
return "\n".join(modified_lines)
|
73
|
+
|
74
|
+
|
75
|
+
def _semi_shade_phase_totals(text: str) -> str:
|
76
|
+
light_shade_char = "░"
|
77
|
+
return _replace_bar_characters(text, "(user-time)", light_shade_char)
|
78
|
+
|
79
|
+
|
80
|
+
def _dot_jobs(text: str) -> str:
|
81
|
+
dot_char = "·"
|
82
|
+
return _replace_bar_characters(text, "(+)", dot_char)
|
83
|
+
|
84
|
+
|
85
|
+
def _plot_times(
|
86
|
+
wall_clock_for_runem_main: timedelta,
|
87
|
+
phase_run_oder: OrderedPhases,
|
88
|
+
timing_data: JobRunTimesByPhase,
|
89
|
+
) -> typing.Tuple[timedelta, timedelta]:
|
90
|
+
"""Prints a report to terminal on how well we performed.
|
91
|
+
|
92
|
+
Also calculates the wall-clock time-saved for the user.
|
93
|
+
|
94
|
+
Returns the total system time spent and the time-saved. (system-time-spent,
|
95
|
+
wall-clock-time-saved)
|
96
|
+
"""
|
97
|
+
labels: typing.List[str] = []
|
98
|
+
times: typing.List[float] = []
|
99
|
+
|
100
|
+
# Track active processing time for jobs, distinct from wall-clock time (the
|
101
|
+
# time the user experiences).
|
102
|
+
system_time_spent: timedelta = timedelta() # init to 0
|
103
|
+
|
104
|
+
for idx, phase in enumerate(phase_run_oder):
|
105
|
+
not_last_phase: bool = idx < len(phase_run_oder) - 1
|
106
|
+
utf8_phase = "├" if not_last_phase else "└"
|
107
|
+
utf8_phase_group = "│" if not_last_phase else " "
|
108
|
+
# log(f"Phase '{phase}' jobs took:")
|
109
|
+
phase_start_idx = len(labels)
|
110
|
+
|
111
|
+
phase_job_times: timedelta = _gen_jobs_report(
|
112
|
+
phase,
|
113
|
+
labels,
|
114
|
+
times,
|
115
|
+
utf8_phase_group,
|
116
|
+
timing_data[phase],
|
117
|
+
)
|
118
|
+
labels.insert(phase_start_idx, f"{utf8_phase}{phase} (user-time)")
|
119
|
+
times.insert(phase_start_idx, phase_job_times.total_seconds())
|
120
|
+
system_time_spent += phase_job_times
|
121
|
+
|
122
|
+
runem_app_timing: typing.List[JobTiming] = timing_data["_app"]
|
123
|
+
job_metadata: JobTiming
|
124
|
+
for job_metadata in reversed(runem_app_timing):
|
125
|
+
job_label, job_time_total = job_metadata["job"]
|
126
|
+
labels.insert(0, f"├runem.{job_label}")
|
127
|
+
times.insert(0, job_time_total.total_seconds())
|
128
|
+
labels.insert(0, "runem (total wall-clock)")
|
129
|
+
times.insert(0, wall_clock_for_runem_main.total_seconds())
|
130
|
+
if termplotlib:
|
131
|
+
fig = termplotlib.figure()
|
132
|
+
# cspell:disable-next-line
|
133
|
+
fig.barh(
|
134
|
+
times,
|
135
|
+
labels,
|
136
|
+
force_ascii=False,
|
137
|
+
)
|
138
|
+
shaded_bar_graph: str = _semi_shade_phase_totals(fig.get_string())
|
139
|
+
dotted_bar_graph: str = _dot_jobs(shaded_bar_graph)
|
140
|
+
|
141
|
+
# ensure the graphs get aligned nicely.
|
142
|
+
final_bar_graph: str = _align_bar_graphs_workaround(dotted_bar_graph)
|
143
|
+
print(final_bar_graph)
|
144
|
+
else: # pragma: FIXME: add code coverage
|
145
|
+
for job_label, time in zip(labels, times):
|
146
|
+
log(f"{job_label}: {time}s")
|
147
|
+
|
148
|
+
wall_clock_time_saved: timedelta = system_time_spent - wall_clock_for_runem_main
|
149
|
+
return system_time_spent, wall_clock_time_saved
|
150
|
+
|
151
|
+
|
152
|
+
def _gen_jobs_report(
|
153
|
+
phase: PhaseName,
|
154
|
+
labels: typing.List[str],
|
155
|
+
times: typing.List[float],
|
156
|
+
utf8_phase_group: str,
|
157
|
+
job_timings: typing.List[JobTiming],
|
158
|
+
) -> timedelta:
|
159
|
+
"""Gathers the reports for sub-jobs.
|
160
|
+
|
161
|
+
Split out from _plot_times as the code was getting complex
|
162
|
+
"""
|
163
|
+
job_timing: JobTiming
|
164
|
+
|
165
|
+
# Filter out JobTiming instances with non-zero total_seconds
|
166
|
+
non_zero_timing_data: typing.List[JobTiming] = [
|
167
|
+
job_timing
|
168
|
+
for job_timing in job_timings
|
169
|
+
if job_timing["job"][1].total_seconds() != 0
|
170
|
+
]
|
171
|
+
|
172
|
+
job_time_sum: timedelta = timedelta() # init to 0
|
173
|
+
for idx, job_timing in enumerate(non_zero_timing_data):
|
174
|
+
not_last: bool = idx < len(non_zero_timing_data) - 1
|
175
|
+
utf8_job = "├" if not_last else "└"
|
176
|
+
utf8_sub_jobs = "│" if not_last else " "
|
177
|
+
job_label, job_time_total = job_timing["job"]
|
178
|
+
job_bar_label: str = f"{phase}.{job_label}"
|
179
|
+
labels.append(f"{utf8_phase_group}{utf8_job}{job_bar_label}")
|
180
|
+
times.append(job_time_total.total_seconds())
|
181
|
+
job_time_sum += job_time_total
|
182
|
+
sub_command_times: TimingEntries = job_timing["commands"]
|
183
|
+
|
184
|
+
if len(sub_command_times) <= 1:
|
185
|
+
# we only have one or fewer sub-commands, just show the job-time
|
186
|
+
continue
|
187
|
+
|
188
|
+
# also print the sub-components of the job as we have more than one
|
189
|
+
for idx, (sub_job_label, sub_job_time) in enumerate(sub_command_times):
|
190
|
+
sub_utf8 = "├"
|
191
|
+
if idx == len(sub_command_times) - 1:
|
192
|
+
sub_utf8 = "└"
|
193
|
+
labels.append(
|
194
|
+
f"{utf8_phase_group}{utf8_sub_jobs}{sub_utf8}{job_bar_label}"
|
195
|
+
f".{sub_job_label} (+)"
|
196
|
+
)
|
197
|
+
times.append(sub_job_time.total_seconds())
|
198
|
+
return job_time_sum
|
199
|
+
|
200
|
+
|
201
|
+
def _print_reports_by_phase(
|
202
|
+
phase_run_oder: OrderedPhases, report_data: JobRunReportByPhase
|
203
|
+
) -> None:
|
204
|
+
"""Logs out the reports by grouped by phase."""
|
205
|
+
for phase in phase_run_oder:
|
206
|
+
report_urls: ReportUrls = report_data[phase]
|
207
|
+
job_report_url_info: ReportUrlInfo
|
208
|
+
for job_report_url_info in report_urls:
|
209
|
+
if not job_report_url_info:
|
210
|
+
continue
|
211
|
+
log(f"report: {str(job_report_url_info[0])}: {str(job_report_url_info[1])}")
|
212
|
+
|
213
|
+
|
214
|
+
def report_on_run(
|
215
|
+
phase_run_oder: OrderedPhases,
|
216
|
+
job_run_metadatas: JobRunMetadatasByPhase,
|
217
|
+
wall_clock_for_runem_main: timedelta,
|
218
|
+
) -> typing.Tuple[timedelta, timedelta]:
|
219
|
+
"""Generate high-level reports AND prints out any reports returned by jobs.
|
220
|
+
|
221
|
+
IMPORTANT: returns the wall-clock time saved to the user.
|
222
|
+
"""
|
223
|
+
log("reports:")
|
224
|
+
|
225
|
+
# First, collate all data, timing and reports
|
226
|
+
timing_data: JobRunTimesByPhase = defaultdict(list)
|
227
|
+
report_data: JobRunReportByPhase = defaultdict(list)
|
228
|
+
phase: PhaseName
|
229
|
+
for phase in job_run_metadatas:
|
230
|
+
timing: JobTiming
|
231
|
+
reports: JobReturn
|
232
|
+
for timing, reports in job_run_metadatas[phase]:
|
233
|
+
timing_data[phase].append(timing)
|
234
|
+
if reports:
|
235
|
+
# the job returned some report urls, record them against the
|
236
|
+
# job's phase
|
237
|
+
report_data[phase].extend(reports["reportUrls"])
|
238
|
+
|
239
|
+
# Now plot the times on the terminal to give a visual report of the timing.
|
240
|
+
time_metrics: typing.Tuple[timedelta, timedelta] = _plot_times(
|
241
|
+
wall_clock_for_runem_main=wall_clock_for_runem_main,
|
242
|
+
phase_run_oder=phase_run_oder,
|
243
|
+
timing_data=timing_data,
|
244
|
+
)
|
245
|
+
|
246
|
+
# Penultimate-ly print out the available reports grouped by run-phase.
|
247
|
+
_print_reports_by_phase(phase_run_oder, report_data)
|
248
|
+
|
249
|
+
# Return the key metrics for runem, the system vs wall-clock time saved to
|
250
|
+
# the user
|
251
|
+
# TODO: write this to disk
|
252
|
+
return time_metrics
|
@@ -1,9 +1,11 @@
|
|
1
1
|
import os
|
2
2
|
import pathlib
|
3
3
|
import typing
|
4
|
+
from datetime import timedelta
|
4
5
|
from subprocess import PIPE as SUBPROCESS_PIPE
|
5
6
|
from subprocess import STDOUT as SUBPROCESS_STDOUT
|
6
7
|
from subprocess import Popen
|
8
|
+
from timeit import default_timer as timer
|
7
9
|
|
8
10
|
from runem.log import log
|
9
11
|
|
@@ -18,6 +20,10 @@ class RunCommandUnhandledError(RuntimeError):
|
|
18
20
|
pass
|
19
21
|
|
20
22
|
|
23
|
+
# A function type for recording timing information.
|
24
|
+
RecordSubJobTimeType = typing.Callable[[str, timedelta], None]
|
25
|
+
|
26
|
+
|
21
27
|
def parse_stdout(stdout: str, prefix: str) -> str:
|
22
28
|
"""Prefixes each line of the output with a given label, except trailing new
|
23
29
|
lines."""
|
@@ -91,11 +97,16 @@ def run_command( # noqa: C901
|
|
91
97
|
ignore_fails: bool = False,
|
92
98
|
valid_exit_ids: typing.Optional[typing.Tuple[int, ...]] = None,
|
93
99
|
cwd: typing.Optional[pathlib.Path] = None,
|
100
|
+
record_sub_job_time: typing.Optional[RecordSubJobTimeType] = None,
|
94
101
|
**kwargs: typing.Any,
|
95
102
|
) -> str:
|
96
103
|
"""Runs the given command, returning stdout or throwing on any error."""
|
97
104
|
cmd_string = " ".join(cmd)
|
98
105
|
|
106
|
+
if record_sub_job_time is not None:
|
107
|
+
# start the capture of how long this sub-task takes.
|
108
|
+
start = timer()
|
109
|
+
|
99
110
|
run_env: typing.Dict[str, str] = _prepare_environment(
|
100
111
|
env_overrides,
|
101
112
|
)
|
@@ -174,4 +185,11 @@ def run_command( # noqa: C901
|
|
174
185
|
|
175
186
|
if verbose:
|
176
187
|
log(f"running: done: {label}: {cmd_string}")
|
188
|
+
|
189
|
+
if record_sub_job_time is not None:
|
190
|
+
# Capture how long this run took
|
191
|
+
end = timer()
|
192
|
+
time_taken: timedelta = timedelta(seconds=end - start)
|
193
|
+
record_sub_job_time(label, time_taken)
|
194
|
+
|
177
195
|
return stdout
|
@@ -297,9 +297,11 @@ def _main(
|
|
297
297
|
end = timer()
|
298
298
|
|
299
299
|
job_run_metadatas: JobRunMetadatasByPhase = defaultdict(list)
|
300
|
-
|
301
|
-
(
|
302
|
-
|
300
|
+
pre_build_time: JobTiming = {
|
301
|
+
"job": ("pre-build", (timedelta(seconds=end - start))),
|
302
|
+
"commands": [],
|
303
|
+
}
|
304
|
+
job_run_metadatas["_app"].append((pre_build_time, None))
|
303
305
|
|
304
306
|
start = timer()
|
305
307
|
|
@@ -313,7 +315,10 @@ def _main(
|
|
313
315
|
|
314
316
|
end = timer()
|
315
317
|
|
316
|
-
phase_run_timing: JobTiming =
|
318
|
+
phase_run_timing: JobTiming = {
|
319
|
+
"job": ("run-phases", timedelta(seconds=end - start)),
|
320
|
+
"commands": [],
|
321
|
+
}
|
317
322
|
phase_run_report: JobReturn = None
|
318
323
|
phase_run_metadata: JobRunMetadata = (phase_run_timing, phase_run_report)
|
319
324
|
job_run_metadatas["_app"].append(phase_run_metadata)
|
@@ -333,14 +338,19 @@ def timed_main(argv: typing.List[str]) -> None:
|
|
333
338
|
phase_run_oder, job_run_metadatas, failure_exception = _main(argv)
|
334
339
|
end = timer()
|
335
340
|
time_taken: timedelta = timedelta(seconds=end - start)
|
336
|
-
|
341
|
+
wall_clock_time_saved: timedelta
|
342
|
+
system_time_spent: timedelta
|
343
|
+
system_time_spent, wall_clock_time_saved = report_on_run(
|
344
|
+
phase_run_oder, job_run_metadatas, time_taken
|
345
|
+
)
|
337
346
|
message: str = "DONE: runem took"
|
338
347
|
if failure_exception:
|
339
348
|
message = "FAILED: your jobs failed after"
|
340
349
|
log(
|
341
350
|
(
|
342
351
|
f"{message}: {time_taken.total_seconds()}s, "
|
343
|
-
f"saving you {
|
352
|
+
f"saving you {wall_clock_time_saved.total_seconds()}s, "
|
353
|
+
f"without runem you would have waited {system_time_spent.total_seconds()}s"
|
344
354
|
)
|
345
355
|
)
|
346
356
|
|
@@ -32,7 +32,21 @@ class JobReturnData(typing.TypedDict, total=False):
|
|
32
32
|
reportUrls: ReportUrls # urls containing reports for the user
|
33
33
|
|
34
34
|
|
35
|
-
|
35
|
+
TimingEntry = typing.Tuple[str, timedelta]
|
36
|
+
TimingEntries = typing.List[TimingEntry]
|
37
|
+
|
38
|
+
|
39
|
+
class JobTiming(typing.TypedDict, total=True):
|
40
|
+
"""A hierarchy of timing info. Job->JobCommands.
|
41
|
+
|
42
|
+
The overall time for a job is in 'job', the child calls to run_command are in
|
43
|
+
'commands'
|
44
|
+
"""
|
45
|
+
|
46
|
+
job: TimingEntry # the overall time for a job
|
47
|
+
commands: TimingEntries # timing for each call to `run_command`
|
48
|
+
|
49
|
+
|
36
50
|
JobReturn = typing.Optional[JobReturnData]
|
37
51
|
JobRunMetadata = typing.Tuple[JobTiming, JobReturn]
|
38
52
|
JobRunTimesByPhase = typing.Dict[PhaseName, typing.List[JobTiming]]
|