executiontimer 0.2.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
- execution_timer/__init__.py +1 -1
- execution_timer/_timer.py +34 -23
- {executiontimer-0.2.0.dist-info → executiontimer-1.0.0.dist-info}/METADATA +20 -10
- executiontimer-1.0.0.dist-info/RECORD +7 -0
- executiontimer-0.2.0.dist-info/RECORD +0 -7
- {executiontimer-0.2.0.dist-info → executiontimer-1.0.0.dist-info}/WHEEL +0 -0
- {executiontimer-0.2.0.dist-info → executiontimer-1.0.0.dist-info}/licenses/LICENSE +0 -0
execution_timer/__init__.py
CHANGED
execution_timer/_timer.py
CHANGED
|
@@ -14,6 +14,7 @@ import json
|
|
|
14
14
|
import logging
|
|
15
15
|
import threading
|
|
16
16
|
import time
|
|
17
|
+
import warnings
|
|
17
18
|
from collections.abc import Callable, Coroutine, Iterable, Iterator
|
|
18
19
|
from contextvars import ContextVar
|
|
19
20
|
from itertools import count
|
|
@@ -134,13 +135,27 @@ class _ExecutionTimer:
|
|
|
134
135
|
_ = _ACTIVE_CONTEXT.set(_Frame(full_name, category, time.perf_counter(), entry, parent))
|
|
135
136
|
|
|
136
137
|
def stop_timer(self, name: str) -> None:
|
|
137
|
-
"""Stop timing a section and accumulate its elapsed time.
|
|
138
|
+
"""Stop timing a section and accumulate its elapsed time.
|
|
139
|
+
|
|
140
|
+
Never raises: an exception here would replace one already propagating from the timed
|
|
141
|
+
block. Exiting past still-active inner sections (typically a suspended generator that
|
|
142
|
+
holds one open) discards them with a warning, so the stack cannot stay corrupted.
|
|
143
|
+
Exiting a section that is no longer active, such as one discarded that way when its
|
|
144
|
+
generator is finally closed, does nothing.
|
|
145
|
+
"""
|
|
138
146
|
end_time = time.perf_counter()
|
|
139
|
-
|
|
147
|
+
active = _ACTIVE_CONTEXT.get()
|
|
148
|
+
frame = active
|
|
149
|
+
while frame is not None and frame.path[-1] != name:
|
|
150
|
+
frame = frame.parent
|
|
140
151
|
if frame is None:
|
|
141
152
|
return
|
|
142
|
-
if frame
|
|
143
|
-
|
|
153
|
+
if frame is not active and active is not None:
|
|
154
|
+
msg = (
|
|
155
|
+
f"Section '{name}' exited while '{active.path[-1]}' was still active; discarding the "
|
|
156
|
+
"unfinished inner sections. Close sections before a generator yields."
|
|
157
|
+
)
|
|
158
|
+
warnings.warn(msg, RuntimeWarning, stacklevel=3)
|
|
144
159
|
with self._lock:
|
|
145
160
|
# A clear detaches this entry from the registry. Updating the detached object
|
|
146
161
|
# cannot resurrect an old sample or add it to a replacement at the same path.
|
|
@@ -155,21 +170,19 @@ class _ExecutionTimer:
|
|
|
155
170
|
"""Build a report of all sections with duration and percentage of total time."""
|
|
156
171
|
timings = self._resolve(flatten=flatten)
|
|
157
172
|
if not timings:
|
|
158
|
-
_LOGGER.warning("No timings to report.")
|
|
159
173
|
return ""
|
|
160
174
|
|
|
161
175
|
total_time = sum(info["elapsed_time"] for key, info in timings.items() if len(key) == 1)
|
|
162
|
-
report = [f"
|
|
176
|
+
report = [f"Total time: {total_time:.4f} s.\n"]
|
|
163
177
|
for key in _ordered_by_hierarchy(timings):
|
|
164
178
|
elapsed_time = timings[key]["elapsed_time"]
|
|
165
179
|
percentage = (elapsed_time / total_time) * 100 if total_time else 0.0
|
|
166
180
|
report.append(f"{'.. ' * (len(key) - 1)}{key[-1]}: {elapsed_time:.4f} s ({percentage:.2f}%)")
|
|
167
181
|
return "\n".join(report)
|
|
168
182
|
|
|
169
|
-
def compute_total_time(self
|
|
183
|
+
def compute_total_time(self) -> float:
|
|
170
184
|
"""Compute total elapsed time across all top-level sections."""
|
|
171
|
-
# Counter merging cannot change the sum
|
|
172
|
-
_ = flatten
|
|
185
|
+
# Counter merging cannot change the sum, so there is no snapshot to copy or flatten.
|
|
173
186
|
with self._lock:
|
|
174
187
|
return sum((info["elapsed_time"] for key, info in self.timings.items() if len(key) == 1), 0.0)
|
|
175
188
|
|
|
@@ -232,16 +245,16 @@ class TimerContext:
|
|
|
232
245
|
def __init__(self, name: str, category: str = DEFAULT_CATEGORY, counter: int | None = None) -> None:
|
|
233
246
|
self.name: str = _build_name_with_counter(name, counter)
|
|
234
247
|
self.category: str = category
|
|
235
|
-
self.
|
|
248
|
+
self._timer: _ExecutionTimer = _TIMER
|
|
236
249
|
|
|
237
250
|
def __enter__(self) -> TimerContext:
|
|
238
|
-
self.
|
|
251
|
+
self._timer.start_timer(self.name, self.category)
|
|
239
252
|
return self
|
|
240
253
|
|
|
241
254
|
def __exit__(
|
|
242
255
|
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
243
256
|
) -> None:
|
|
244
|
-
self.
|
|
257
|
+
self._timer.stop_timer(self.name)
|
|
245
258
|
|
|
246
259
|
def __call__(self, func: Callable[P, R]) -> Callable[P, R]:
|
|
247
260
|
"""Decorate a function to time its execution under this context.
|
|
@@ -299,18 +312,20 @@ def _basic_name_without_counter(name: str) -> str:
|
|
|
299
312
|
|
|
300
313
|
|
|
301
314
|
def get_execution_times_report(*, flatten: bool = True) -> str:
|
|
302
|
-
"""Get a formatted report of all recorded sections; flatten counters if requested."""
|
|
315
|
+
"""Get a formatted report of all recorded sections (``""`` if none); flatten counters if requested."""
|
|
303
316
|
return _TIMER.report_timings(flatten=flatten)
|
|
304
317
|
|
|
305
318
|
|
|
306
319
|
def log_execution_times(*, flatten: bool = True, logger: logging.Logger | None = None) -> None:
|
|
307
|
-
"""Log the execution-times report at INFO level
|
|
320
|
+
"""Log the execution-times report at INFO level, or a warning if there is nothing to report."""
|
|
308
321
|
target = logger if logger is not None else _LOGGER
|
|
309
322
|
if target.isEnabledFor(logging.INFO):
|
|
310
323
|
report = get_execution_times_report(flatten=flatten)
|
|
311
|
-
# An empty report has already been warned about; logging it would add a blank record.
|
|
312
324
|
if report:
|
|
313
|
-
|
|
325
|
+
# Start the multi-line report on its own line, after the log record's prefix.
|
|
326
|
+
target.info("\n%s", report)
|
|
327
|
+
else:
|
|
328
|
+
target.warning("No timings to report.")
|
|
314
329
|
|
|
315
330
|
|
|
316
331
|
def get_execution_timings(*, flatten: bool = True) -> dict[tuple[str, ...], TimingReport]:
|
|
@@ -355,13 +370,9 @@ def save_execution_timings_json(path: str | Path, *, flatten: bool = True, inden
|
|
|
355
370
|
return out
|
|
356
371
|
|
|
357
372
|
|
|
358
|
-
def get_total_time(
|
|
359
|
-
"""Get total elapsed seconds across all top-level sections.
|
|
360
|
-
|
|
361
|
-
``flatten`` has no effect, because merging counter variants cannot change the total. It is
|
|
362
|
-
accepted for symmetry with the other reporting functions.
|
|
363
|
-
"""
|
|
364
|
-
return _TIMER.compute_total_time(flatten=flatten)
|
|
373
|
+
def get_total_time() -> float:
|
|
374
|
+
"""Get total elapsed seconds across all top-level sections."""
|
|
375
|
+
return _TIMER.compute_total_time()
|
|
365
376
|
|
|
366
377
|
|
|
367
378
|
def get_total_category_time(category: str) -> float:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: executiontimer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.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://github.com/seba2390/ExecutionTimer#readme
|
|
@@ -11,7 +11,7 @@ Author: Sebastian Yde Madsen
|
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Keywords: benchmark,context-manager,decorator,execution-time,instrumentation,performance,profiling,timing
|
|
14
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: Intended Audience :: Science/Research
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
|
@@ -103,7 +103,7 @@ print(get_execution_times_report())
|
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
```text
|
|
106
|
-
Total
|
|
106
|
+
Total time: 0.3156 s.
|
|
107
107
|
|
|
108
108
|
load_data: 0.1219 s (38.62%)
|
|
109
109
|
solve: 0.1937 s (61.38%)
|
|
@@ -156,6 +156,11 @@ Flattening removes the final integer suffix (including negative counters). Other
|
|
|
156
156
|
bracketed names such as `array[index]` are preserved. If merged entries have different
|
|
157
157
|
categories, the category from the most recently entered section is used.
|
|
158
158
|
|
|
159
|
+
Each counter value is stored as its own section until `clear_execution_timings()` is
|
|
160
|
+
called, so a long-running process that times an unbounded loop with `counter=` keeps
|
|
161
|
+
growing the registry. Clear it periodically, or drop `counter=` to accumulate into one
|
|
162
|
+
section.
|
|
163
|
+
|
|
159
164
|
### Categories
|
|
160
165
|
|
|
161
166
|
Categories are plain strings — use whatever fits your domain:
|
|
@@ -252,6 +257,11 @@ across a `yield` therefore also contains whatever the caller times while the gen
|
|
|
252
257
|
paused, and its duration includes that paused time. Close sections before yielding, or
|
|
253
258
|
time the loop that consumes the generator instead.
|
|
254
259
|
|
|
260
|
+
If the caller's section exits while a paused generator's section is still open, the
|
|
261
|
+
timer never raises: it emits a `RuntimeWarning`, records the caller's section, and
|
|
262
|
+
discards the generator's unfinished one, so later sections nest correctly. Closing that
|
|
263
|
+
generator afterwards does nothing.
|
|
264
|
+
|
|
255
265
|
### Reusing contexts and clearing timings
|
|
256
266
|
|
|
257
267
|
A `TimerContext` can be reused, nested within itself, or shared by concurrent calls.
|
|
@@ -273,7 +283,7 @@ an active section's current duration is added only when it exits.
|
|
|
273
283
|
|
|
274
284
|
Run the repeatable benchmark with `uv run python benchmarks/overhead.py`. It measures
|
|
275
285
|
fresh and reused contexts, sync and async decorators, nesting, and reporting. Compare
|
|
276
|
-
results using the same interpreter and machine; see [benchmarks/README.md](benchmarks/README.md).
|
|
286
|
+
results using the same interpreter and machine; see [benchmarks/README.md](https://github.com/seba2390/ExecutionTimer/blob/main/benchmarks/README.md).
|
|
277
287
|
`log_execution_times()` skips building a report when its logger has `INFO` disabled.
|
|
278
288
|
|
|
279
289
|
## API
|
|
@@ -281,12 +291,12 @@ results using the same interpreter and machine; see [benchmarks/README.md](bench
|
|
|
281
291
|
| Function | Description |
|
|
282
292
|
| --- | --- |
|
|
283
293
|
| `TimerContext(name, category=DEFAULT_CATEGORY, counter=None)` | Context manager **and** decorator for timing a section. |
|
|
284
|
-
| `get_execution_times_report(*, flatten=True)` | Formatted, indented report of all sections. |
|
|
285
|
-
| `log_execution_times(*, flatten=True, logger=None)` | Log that report at `INFO` level. |
|
|
294
|
+
| `get_execution_times_report(*, flatten=True)` | Formatted, indented report of all sections (`""` if none). |
|
|
295
|
+
| `log_execution_times(*, flatten=True, logger=None)` | Log that report at `INFO` level (a warning if empty). |
|
|
286
296
|
| `get_execution_timings(*, flatten=True)` | Timings as `dict[tuple[str, ...], TimingReport]`. |
|
|
287
297
|
| `get_execution_times_json(*, flatten=True, indent=2)` | All timings as a JSON string. |
|
|
288
298
|
| `save_execution_timings_json(path, *, flatten=True, indent=2)` | Write timings to a JSON file; returns the `Path`. |
|
|
289
|
-
| `get_total_time(
|
|
299
|
+
| `get_total_time()` | Total seconds across all top-level sections. |
|
|
290
300
|
| `get_total_category_time(category)` | Total seconds in a category (top-most entries only). |
|
|
291
301
|
| `clear_execution_timings()` | Reset all recorded timings. |
|
|
292
302
|
| `register_forbidden_nesting(outer, inner)` | Forbid `inner` category directly inside `outer`. |
|
|
@@ -309,9 +319,9 @@ uv run ruff check --fix && uv run ruff format
|
|
|
309
319
|
uv run basedpyright
|
|
310
320
|
```
|
|
311
321
|
|
|
312
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow, and
|
|
313
|
-
[CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
322
|
+
See [CONTRIBUTING.md](https://github.com/seba2390/ExecutionTimer/blob/main/CONTRIBUTING.md) for the full workflow, and
|
|
323
|
+
[CHANGELOG.md](https://github.com/seba2390/ExecutionTimer/blob/main/CHANGELOG.md) for release notes.
|
|
314
324
|
|
|
315
325
|
## License
|
|
316
326
|
|
|
317
|
-
MIT — see [LICENSE](LICENSE).
|
|
327
|
+
MIT — see [LICENSE](https://github.com/seba2390/ExecutionTimer/blob/main/LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
execution_timer/__init__.py,sha256=gv9IbtkZkesfgWFh1oDSJ5DDr2BJFVxmKtY32jcUzbI,959
|
|
2
|
+
execution_timer/_timer.py,sha256=iylgfJ1vBeKimsW5A3NVN5KSUvZs75Km-N4guoHKFPo,15932
|
|
3
|
+
execution_timer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
executiontimer-1.0.0.dist-info/METADATA,sha256=ZtN7WHMrB8gTaGIy0VQLHDtu2ySFvd8oZx56KmA5F8s,13386
|
|
5
|
+
executiontimer-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
executiontimer-1.0.0.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
|
|
7
|
+
executiontimer-1.0.0.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
execution_timer/__init__.py,sha256=9A5FYIrOvXbohuQMyIis651hYdNmzvNLQxfR02pk92w,959
|
|
2
|
-
execution_timer/_timer.py,sha256=4PSyP501arREnqOKtXEMwjbsb5sFISVYFRc1EXOXGP0,15366
|
|
3
|
-
execution_timer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
executiontimer-0.2.0.dist-info/METADATA,sha256=B6G1utu8t7BPwVh8KFID_pPr-c9z2bW0M6hd8-eiPWg,12631
|
|
5
|
-
executiontimer-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
-
executiontimer-0.2.0.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
|
|
7
|
-
executiontimer-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|