executiontimer 1.0.3__tar.gz → 1.1.0__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.
- {executiontimer-1.0.3 → executiontimer-1.1.0}/CHANGELOG.md +14 -1
- {executiontimer-1.0.3 → executiontimer-1.1.0}/PKG-INFO +6 -6
- {executiontimer-1.0.3 → executiontimer-1.1.0}/README.md +5 -5
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/getting-started.md +11 -9
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/counters.md +5 -5
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/reports.md +12 -8
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/timing-code.md +4 -4
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/index.md +5 -5
- {executiontimer-1.0.3 → executiontimer-1.1.0}/src/execution_timer/__init__.py +1 -1
- {executiontimer-1.0.3 → executiontimer-1.1.0}/src/execution_timer/_timer.py +19 -4
- {executiontimer-1.0.3 → executiontimer-1.1.0}/tests/execution_timer_test.py +22 -3
- {executiontimer-1.0.3 → executiontimer-1.1.0}/.gitignore +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/CONTRIBUTING.md +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/LICENSE +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/benchmarks/README.md +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/benchmarks/overhead.py +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/_static/icon-dark.svg +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/_static/icon-light.svg +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/api.md +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/changelog.md +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/conf.py +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/contributing.md +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/categories.md +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/concurrency.md +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/long-running.md +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/overhead.md +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/pyproject.toml +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/src/execution_timer/py.typed +0 -0
- {executiontimer-1.0.3 → executiontimer-1.1.0}/tests/docs_examples_test.py +0 -0
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The text report, from `get_execution_times_report()` and `log_execution_times()`, shows
|
|
15
|
+
each time with three decimals in `µs`, `ms` or `s`, whichever keeps the number below
|
|
16
|
+
1000: `48.213 µs`, `315.612 ms`, `2.500 s`. It used to show seconds with four
|
|
17
|
+
decimals, so any section under 50 µs read `0.0000 s`, and fast sections, including a
|
|
18
|
+
whole recursive call tree, looked like zeros. Code that parses the report text needs
|
|
19
|
+
updating. `get_execution_timings()` and the JSON export are unchanged and still return
|
|
20
|
+
seconds.
|
|
21
|
+
|
|
10
22
|
## [1.0.3] - 2026-09-30
|
|
11
23
|
|
|
12
24
|
### Changed
|
|
@@ -194,7 +206,8 @@ First public release on PyPI.
|
|
|
194
206
|
`py.typed` marker so type checkers use the inline annotations.
|
|
195
207
|
- `__version__` attribute on the package.
|
|
196
208
|
|
|
197
|
-
[Unreleased]: https://github.com/seba2390/ExecutionTimer/compare/v1.0
|
|
209
|
+
[Unreleased]: https://github.com/seba2390/ExecutionTimer/compare/v1.1.0...HEAD
|
|
210
|
+
[1.1.0]: https://github.com/seba2390/ExecutionTimer/compare/v1.0.3...v1.1.0
|
|
198
211
|
[1.0.3]: https://github.com/seba2390/ExecutionTimer/compare/v1.0.2...v1.0.3
|
|
199
212
|
[1.0.2]: https://github.com/seba2390/ExecutionTimer/compare/v1.0.1...v1.0.2
|
|
200
213
|
[1.0.1]: https://github.com/seba2390/ExecutionTimer/compare/v1.0.0...v1.0.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: executiontimer
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Hierarchical execution timing with user-defined categories.
|
|
5
5
|
Project-URL: Homepage, https://github.com/seba2390/ExecutionTimer
|
|
6
6
|
Project-URL: Documentation, https://seba2390.github.io/ExecutionTimer/
|
|
@@ -84,12 +84,12 @@ print(get_execution_times_report())
|
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
```text
|
|
87
|
-
Total time:
|
|
87
|
+
Total time: 312.838 ms.
|
|
88
88
|
|
|
89
|
-
load_data:
|
|
90
|
-
solve:
|
|
91
|
-
.. step:
|
|
92
|
-
.. postprocess:
|
|
89
|
+
load_data: 124.682 ms (39.85%)
|
|
90
|
+
solve: 188.157 ms (60.15%)
|
|
91
|
+
.. step: 154.718 ms (49.46%)
|
|
92
|
+
.. postprocess: 33.295 ms (10.64%)
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
`TimerContext` also works as a decorator, including on `async def` functions:
|
|
@@ -53,12 +53,12 @@ print(get_execution_times_report())
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
|
-
Total time:
|
|
56
|
+
Total time: 312.838 ms.
|
|
57
57
|
|
|
58
|
-
load_data:
|
|
59
|
-
solve:
|
|
60
|
-
.. step:
|
|
61
|
-
.. postprocess:
|
|
58
|
+
load_data: 124.682 ms (39.85%)
|
|
59
|
+
solve: 188.157 ms (60.15%)
|
|
60
|
+
.. step: 154.718 ms (49.46%)
|
|
61
|
+
.. postprocess: 33.295 ms (10.64%)
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
`TimerContext` also works as a decorator, including on `async def` functions:
|
|
@@ -42,9 +42,9 @@ print(get_execution_times_report())
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
|
-
Total time:
|
|
45
|
+
Total time: 104.727 ms.
|
|
46
46
|
|
|
47
|
-
load_data:
|
|
47
|
+
load_data: 104.727 ms (100.00%)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Timings are recorded in one registry for the whole process. Every section you time, in any
|
|
@@ -72,12 +72,12 @@ print(get_execution_times_report())
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
```text
|
|
75
|
-
Total time:
|
|
75
|
+
Total time: 89.826 ms.
|
|
76
76
|
|
|
77
|
-
pipeline:
|
|
78
|
-
.. load:
|
|
79
|
-
.. transform:
|
|
80
|
-
.. .. validate:
|
|
77
|
+
pipeline: 89.826 ms (100.00%)
|
|
78
|
+
.. load: 22.073 ms (24.57%)
|
|
79
|
+
.. transform: 67.726 ms (75.40%)
|
|
80
|
+
.. .. validate: 12.549 ms (13.97%)
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
How to read the report:
|
|
@@ -86,8 +86,10 @@ How to read the report:
|
|
|
86
86
|
`pipeline`.
|
|
87
87
|
- **Total time** is the sum of the top-level sections. Nested sections are part of their
|
|
88
88
|
parent's time, so they aren't added again.
|
|
89
|
-
- **Percentages** are shares of the total, at every level. `validate` took 13.
|
|
90
|
-
whole run, not 13.
|
|
89
|
+
- **Percentages** are shares of the total, at every level. `validate` took 13.97% of the
|
|
90
|
+
whole run, not 13.97% of `transform`.
|
|
91
|
+
- **Times** have three decimals, in `µs`, `ms` or `s`, whichever keeps the number below
|
|
92
|
+
1000, so even very fast sections show a real value.
|
|
91
93
|
|
|
92
94
|
Calling {func}`~execution_timer.clear_execution_timings` starts over with an empty
|
|
93
95
|
registry.
|
|
@@ -37,12 +37,12 @@ print(get_execution_times_report(flatten=False))
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
|
-
Total time:
|
|
40
|
+
Total time: 67.003 ms.
|
|
41
41
|
|
|
42
|
-
solve:
|
|
43
|
-
.. step[0]:
|
|
44
|
-
.. step[1]:
|
|
45
|
-
.. step[2]:
|
|
42
|
+
solve: 67.003 ms (100.00%)
|
|
43
|
+
.. step[0]: 12.542 ms (18.72%)
|
|
44
|
+
.. step[1]: 23.851 ms (35.60%)
|
|
45
|
+
.. step[2]: 30.498 ms (45.52%)
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Use the separate view to find a slow iteration, and the merged view to see the loop's
|
|
@@ -32,16 +32,20 @@ print(get_execution_times_report())
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
```text
|
|
35
|
-
Total time:
|
|
35
|
+
Total time: 48.159 ms.
|
|
36
36
|
|
|
37
|
-
load_data:
|
|
38
|
-
solve:
|
|
39
|
-
.. step:
|
|
37
|
+
load_data: 24.199 ms (50.25%)
|
|
38
|
+
solve: 23.960 ms (49.75%)
|
|
39
|
+
.. step: 23.904 ms (49.64%)
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Sections appear depth-first, each followed by its children, in the order they were first
|
|
43
43
|
entered. Percentages are shares of the total time at every level.
|
|
44
44
|
|
|
45
|
+
Times have three decimals, in `µs`, `ms` or `s`, whichever keeps the number below 1000.
|
|
46
|
+
A 48 µs section reads `48.213 µs` rather than rounding to zero. The dictionary and JSON
|
|
47
|
+
readers below return plain seconds.
|
|
48
|
+
|
|
45
49
|
## Logging
|
|
46
50
|
|
|
47
51
|
{func}`~execution_timer.log_execution_times` logs the same report at `INFO` level, starting
|
|
@@ -58,11 +62,11 @@ log_execution_times()
|
|
|
58
62
|
|
|
59
63
|
```text
|
|
60
64
|
INFO:execution_timer._timer:
|
|
61
|
-
Total time:
|
|
65
|
+
Total time: 48.159 ms.
|
|
62
66
|
|
|
63
|
-
load_data:
|
|
64
|
-
solve:
|
|
65
|
-
.. step:
|
|
67
|
+
load_data: 24.199 ms (50.25%)
|
|
68
|
+
solve: 23.960 ms (49.75%)
|
|
69
|
+
.. step: 23.904 ms (49.64%)
|
|
66
70
|
```
|
|
67
71
|
|
|
68
72
|
It logs to the `execution_timer._timer` logger unless you pass your own with
|
|
@@ -152,11 +152,11 @@ print(get_execution_times_report())
|
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
```text
|
|
155
|
-
Total time:
|
|
155
|
+
Total time: 34.704 ms.
|
|
156
156
|
|
|
157
|
-
walk:
|
|
158
|
-
.. walk:
|
|
159
|
-
.. .. walk:
|
|
157
|
+
walk: 34.704 ms (100.00%)
|
|
158
|
+
.. walk: 24.161 ms (69.62%)
|
|
159
|
+
.. .. walk: 11.603 ms (33.44%)
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
For deep recursion, time the top-level call only, for example with a `with` block around
|
|
@@ -23,12 +23,12 @@ print(get_execution_times_report())
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
```text
|
|
26
|
-
Total time:
|
|
26
|
+
Total time: 312.838 ms.
|
|
27
27
|
|
|
28
|
-
load_data:
|
|
29
|
-
solve:
|
|
30
|
-
.. step:
|
|
31
|
-
.. postprocess:
|
|
28
|
+
load_data: 124.682 ms (39.85%)
|
|
29
|
+
solve: 188.157 ms (60.15%)
|
|
30
|
+
.. step: 154.718 ms (49.46%)
|
|
31
|
+
.. postprocess: 33.295 ms (10.64%)
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Why executiontimer
|
|
@@ -198,11 +198,11 @@ class _ExecutionTimer:
|
|
|
198
198
|
# Total the unflattened paths, like get_total_time and the JSON export.
|
|
199
199
|
total_time = _top_level_time(snapshot)
|
|
200
200
|
timings = _flatten(snapshot) if flatten else snapshot
|
|
201
|
-
report = [f"Total time: {total_time
|
|
201
|
+
report = [f"Total time: {_format_duration(total_time)}.\n"]
|
|
202
202
|
for key, depth in _ordered_by_hierarchy(timings):
|
|
203
203
|
elapsed_time = timings[key]["elapsed_time"]
|
|
204
204
|
percentage = (elapsed_time / total_time) * 100 if total_time else 0.0
|
|
205
|
-
report.append(f"{'.. ' * depth}{key[-1]}: {elapsed_time
|
|
205
|
+
report.append(f"{'.. ' * depth}{key[-1]}: {_format_duration(elapsed_time)} ({percentage:.2f}%)")
|
|
206
206
|
return "\n".join(report)
|
|
207
207
|
|
|
208
208
|
def compute_total_time(self) -> float:
|
|
@@ -243,6 +243,20 @@ class _ExecutionTimer:
|
|
|
243
243
|
_TIMER: Final = _ExecutionTimer()
|
|
244
244
|
|
|
245
245
|
|
|
246
|
+
def _format_duration(seconds: float) -> str:
|
|
247
|
+
"""Format seconds with three decimals in the largest of µs, ms and s that stays below 1000.
|
|
248
|
+
|
|
249
|
+
The unit is chosen after rounding, so 999.9996 µs reads ``1.000 ms`` rather than
|
|
250
|
+
``1000.000 µs``. Sections of any length stay readable, where fixed seconds would show a
|
|
251
|
+
fast section as zero.
|
|
252
|
+
"""
|
|
253
|
+
for scale, unit in ((1e6, "µs"), (1e3, "ms")):
|
|
254
|
+
scaled = seconds * scale
|
|
255
|
+
if round(scaled, 3) < 1000:
|
|
256
|
+
return f"{scaled:.3f} {unit}"
|
|
257
|
+
return f"{seconds:.3f} s"
|
|
258
|
+
|
|
259
|
+
|
|
246
260
|
def _flatten(timings: dict[tuple[str, ...], _TimesDict]) -> dict[tuple[str, ...], _TimesDict]:
|
|
247
261
|
flat_map: dict[tuple[str, ...], _TimesDict] = {}
|
|
248
262
|
for key, info in timings.items():
|
|
@@ -378,8 +392,9 @@ def _basic_name_without_counter(name: str) -> str:
|
|
|
378
392
|
def get_execution_times_report(*, flatten: bool = True) -> str:
|
|
379
393
|
"""Get a formatted, indented report of all recorded sections.
|
|
380
394
|
|
|
381
|
-
Each line shows a section's accumulated
|
|
382
|
-
Indentation reflects nesting.
|
|
395
|
+
Each line shows a section's accumulated time and its share of the total time.
|
|
396
|
+
Indentation reflects nesting. Times have three decimals, in ``µs``, ``ms`` or ``s``,
|
|
397
|
+
whichever keeps the number below 1000.
|
|
383
398
|
|
|
384
399
|
Args:
|
|
385
400
|
flatten: Merge ``counter`` variants such as ``step[0]`` and ``step[1]`` into ``step``.
|
|
@@ -208,6 +208,25 @@ class TestReporting:
|
|
|
208
208
|
assert get_execution_times_report() == ""
|
|
209
209
|
assert caplog.records == []
|
|
210
210
|
|
|
211
|
+
@pytest.mark.parametrize(
|
|
212
|
+
("seconds", "expected"),
|
|
213
|
+
[
|
|
214
|
+
(0.0000004, "0.400 µs"),
|
|
215
|
+
(0.0000482134, "48.213 µs"),
|
|
216
|
+
(0.0009999994, "999.999 µs"),
|
|
217
|
+
(0.0009999996, "1.000 ms"),
|
|
218
|
+
(0.3156, "315.600 ms"),
|
|
219
|
+
(0.9999996, "1.000 s"),
|
|
220
|
+
(2.5, "2.500 s"),
|
|
221
|
+
(3725.0, "3725.000 s"),
|
|
222
|
+
],
|
|
223
|
+
)
|
|
224
|
+
def test_report_picks_a_unit_that_keeps_short_sections_visible(self, seconds: float, expected: str) -> None:
|
|
225
|
+
with patch.object(time, "perf_counter", side_effect=[0.0, seconds]), TimerContext("section"):
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
assert get_execution_times_report() == f"Total time: {expected}.\n\nsection: {expected} (100.00%)"
|
|
229
|
+
|
|
211
230
|
def test_report_flatten_flag(self) -> None:
|
|
212
231
|
with patch.object(time, "perf_counter", side_effect=[0, 1, 1, 2]):
|
|
213
232
|
for i in range(2):
|
|
@@ -757,7 +776,7 @@ class TestReportOrdering:
|
|
|
757
776
|
pass
|
|
758
777
|
|
|
759
778
|
report = get_execution_times_report()
|
|
760
|
-
assert "instant: 0.
|
|
779
|
+
assert "instant: 0.000 µs (0.00%)" in report
|
|
761
780
|
|
|
762
781
|
|
|
763
782
|
class TestRobustness:
|
|
@@ -1059,7 +1078,7 @@ class TestLifecycle:
|
|
|
1059
1078
|
assert list(raw_timings()) == [("parent", "child")]
|
|
1060
1079
|
# The cleared parent is gone, so its child is top-level: it counts toward the total
|
|
1061
1080
|
# and is not indented beneath an unrelated section.
|
|
1062
|
-
assert get_execution_times_report() == "Total time: 2.
|
|
1081
|
+
assert get_execution_times_report() == "Total time: 2.000 s.\n\nchild: 2.000 s (100.00%)"
|
|
1063
1082
|
assert get_total_time() == 2.0
|
|
1064
1083
|
payload = cast(TimingsPayload, json.loads(get_execution_times_json()))
|
|
1065
1084
|
assert payload["total_time"] == 2.0
|
|
@@ -1078,7 +1097,7 @@ class TestLifecycle:
|
|
|
1078
1097
|
with TimerContext("after"):
|
|
1079
1098
|
pass
|
|
1080
1099
|
assert get_execution_times_report() == (
|
|
1081
|
-
"Total time: 5.
|
|
1100
|
+
"Total time: 5.000 s.\n\nload: 4.000 s (80.00%)\n.. parse: 1.000 s (20.00%)\nafter: 1.000 s (20.00%)"
|
|
1082
1101
|
)
|
|
1083
1102
|
assert get_total_time() == 5.0
|
|
1084
1103
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|