executiontimer 0.1.0__py3-none-any.whl → 0.1.1__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 +92 -85
- {executiontimer-0.1.0.dist-info → executiontimer-0.1.1.dist-info}/METADATA +44 -4
- executiontimer-0.1.1.dist-info/RECORD +7 -0
- {executiontimer-0.1.0.dist-info → executiontimer-0.1.1.dist-info}/WHEEL +1 -1
- executiontimer-0.1.0.dist-info/RECORD +0 -7
- {executiontimer-0.1.0.dist-info → executiontimer-0.1.1.dist-info}/licenses/LICENSE +0 -0
execution_timer/__init__.py
CHANGED
execution_timer/_timer.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Timings are stored in a process-wide registry guarded by a lock. The active-context stack
|
|
4
4
|
lives in a :class:`~contextvars.ContextVar`, so it is isolated per thread *and* per asyncio
|
|
5
5
|
task: sections recorded concurrently nest independently and merge into one report.
|
|
6
|
-
|
|
6
|
+
Overlapping calls to the same section accumulate their individual durations.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -14,11 +14,12 @@ import json
|
|
|
14
14
|
import logging
|
|
15
15
|
import threading
|
|
16
16
|
import time
|
|
17
|
-
from collections.abc import Callable, Coroutine, Iterable
|
|
17
|
+
from collections.abc import Callable, Coroutine, Iterable, Iterator
|
|
18
18
|
from contextvars import ContextVar
|
|
19
|
+
from itertools import count
|
|
19
20
|
from pathlib import Path
|
|
20
21
|
from types import TracebackType
|
|
21
|
-
from typing import
|
|
22
|
+
from typing import Final, NamedTuple, ParamSpec, TypedDict, TypeVar, cast
|
|
22
23
|
|
|
23
24
|
P = ParamSpec("P")
|
|
24
25
|
R = TypeVar("R")
|
|
@@ -28,9 +29,6 @@ DEFAULT_CATEGORY: Final = "default"
|
|
|
28
29
|
|
|
29
30
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
30
31
|
|
|
31
|
-
# Stack of (name, category) frames for the current thread / asyncio task.
|
|
32
|
-
_ACTIVE_CONTEXT: ContextVar[tuple[tuple[str, str], ...]] = ContextVar("execution_timer_context", default=())
|
|
33
|
-
|
|
34
32
|
|
|
35
33
|
class TimingReport(TypedDict):
|
|
36
34
|
"""Timing entry for one section: elapsed seconds and its category."""
|
|
@@ -57,11 +55,24 @@ class TimingsPayload(TypedDict):
|
|
|
57
55
|
|
|
58
56
|
|
|
59
57
|
class _TimesDict(TypedDict):
|
|
60
|
-
|
|
58
|
+
sequence: int
|
|
61
59
|
elapsed_time: float
|
|
62
60
|
category: str
|
|
63
61
|
|
|
64
62
|
|
|
63
|
+
class _Frame(NamedTuple):
|
|
64
|
+
"""Per-invocation state, with a cached path and an immutable parent link."""
|
|
65
|
+
|
|
66
|
+
path: tuple[str, ...]
|
|
67
|
+
category: str
|
|
68
|
+
start_time: float
|
|
69
|
+
entry: _TimesDict
|
|
70
|
+
parent: _Frame | None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_ACTIVE_CONTEXT: ContextVar[_Frame | None] = ContextVar("execution_timer_context", default=None)
|
|
74
|
+
|
|
75
|
+
|
|
65
76
|
def _ordered_by_hierarchy(keys: Iterable[tuple[str, ...]]) -> list[tuple[str, ...]]:
|
|
66
77
|
"""Order section paths depth-first so children always follow their parent.
|
|
67
78
|
|
|
@@ -91,81 +102,53 @@ def _ordered_by_hierarchy(keys: Iterable[tuple[str, ...]]) -> list[tuple[str, ..
|
|
|
91
102
|
|
|
92
103
|
|
|
93
104
|
class _ExecutionTimer:
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
_instance: ClassVar[_ExecutionTimer | None] = None
|
|
97
|
-
_lock: ClassVar[threading.Lock] = threading.Lock()
|
|
98
|
-
timings: ClassVar[dict[tuple[str, ...], _TimesDict]] = {}
|
|
99
|
-
forbidden_nesting: ClassVar[set[tuple[str, str]]] = set()
|
|
100
|
-
|
|
101
|
-
def __new__(cls) -> _ExecutionTimer:
|
|
102
|
-
if cls._instance is None:
|
|
103
|
-
cls._instance = super().__new__(cls)
|
|
104
|
-
return cls._instance
|
|
105
|
+
"""Registry of named, nestable timing sections, shared through ``_TIMER``."""
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
def __init__(self) -> None:
|
|
108
|
+
self._lock: threading.Lock = threading.Lock()
|
|
109
|
+
self.timings: dict[tuple[str, ...], _TimesDict] = {}
|
|
110
|
+
self.forbidden_nesting: set[tuple[str, str]] = set()
|
|
111
|
+
self._sequence: Iterator[int] = count()
|
|
109
112
|
|
|
110
|
-
def
|
|
113
|
+
def snapshot(self) -> dict[tuple[str, ...], _TimesDict]:
|
|
111
114
|
"""Copy the registry under the lock so readers never iterate a mutating dict."""
|
|
112
115
|
with self._lock:
|
|
113
116
|
return {key: info.copy() for key, info in self.timings.items()}
|
|
114
117
|
|
|
115
118
|
def start_timer(self, name: str, category: str) -> None:
|
|
116
119
|
"""Start timing a section under the given name within the active context."""
|
|
117
|
-
|
|
118
|
-
full_name =
|
|
119
|
-
start_time = time.perf_counter()
|
|
120
|
+
parent = _ACTIVE_CONTEXT.get()
|
|
121
|
+
full_name = (*parent.path, name) if parent is not None else (name,)
|
|
120
122
|
with self._lock:
|
|
121
|
-
|
|
123
|
+
if parent is not None and (parent.category, category) in self.forbidden_nesting:
|
|
124
|
+
msg = f"Category '{category}' is not allowed inside category '{parent.category}'."
|
|
125
|
+
raise ValueError(msg)
|
|
126
|
+
sequence = next(self._sequence)
|
|
127
|
+
entry: _TimesDict | None = self.timings.get(full_name)
|
|
122
128
|
if entry is None:
|
|
123
|
-
|
|
129
|
+
entry = {"sequence": sequence, "elapsed_time": 0.0, "category": category}
|
|
130
|
+
self.timings[full_name] = entry
|
|
124
131
|
else:
|
|
125
|
-
entry["
|
|
132
|
+
entry["sequence"] = sequence
|
|
126
133
|
entry["category"] = category
|
|
134
|
+
_ = _ACTIVE_CONTEXT.set(_Frame(full_name, category, time.perf_counter(), entry, parent))
|
|
127
135
|
|
|
128
136
|
def stop_timer(self, name: str) -> None:
|
|
129
137
|
"""Stop timing a section and accumulate its elapsed time."""
|
|
130
138
|
end_time = time.perf_counter()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
entry = self.timings.get(full_name)
|
|
134
|
-
# The entry is gone if the registry was cleared while this section was running;
|
|
135
|
-
# dropping the sample is preferable to raising out of a ``with`` block.
|
|
136
|
-
if entry is not None:
|
|
137
|
-
entry["elapsed_time"] += end_time - entry["start_time"]
|
|
138
|
-
self._remove_context(name)
|
|
139
|
-
|
|
140
|
-
def _add_context(self, name: str, category: str) -> None:
|
|
141
|
-
stack = _ACTIVE_CONTEXT.get()
|
|
142
|
-
if stack and (stack[-1][1], category) in self.forbidden_nesting:
|
|
143
|
-
msg = f"Category '{category}' is not allowed inside category '{stack[-1][1]}'."
|
|
144
|
-
raise ValueError(msg)
|
|
145
|
-
_ = _ACTIVE_CONTEXT.set((*stack, (name, category)))
|
|
146
|
-
|
|
147
|
-
def _remove_context(self, name: str) -> None:
|
|
148
|
-
"""Pop the innermost context, restoring the stack to its state before ``name`` was entered."""
|
|
149
|
-
stack = _ACTIVE_CONTEXT.get()
|
|
150
|
-
if not stack:
|
|
139
|
+
frame = _ACTIVE_CONTEXT.get()
|
|
140
|
+
if frame is None:
|
|
151
141
|
return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def compute_flattened_timings(self) -> dict[tuple[str, ...], _TimesDict]:
|
|
160
|
-
"""Aggregate elapsed times with counter suffixes removed from section names.
|
|
161
|
-
|
|
162
|
-
When counter variants of one section are merged, elapsed times are summed and the
|
|
163
|
-
most recently recorded category is kept.
|
|
164
|
-
"""
|
|
165
|
-
return _flatten(self._snapshot())
|
|
142
|
+
if frame.path[-1] != name:
|
|
143
|
+
raise RuntimeError(f"Cannot stop '{name}' while '{frame.path[-1]}' is active.")
|
|
144
|
+
with self._lock:
|
|
145
|
+
# A clear detaches this entry from the registry. Updating the detached object
|
|
146
|
+
# cannot resurrect an old sample or add it to a replacement at the same path.
|
|
147
|
+
frame.entry["elapsed_time"] += end_time - frame.start_time
|
|
148
|
+
_ = _ACTIVE_CONTEXT.set(frame.parent)
|
|
166
149
|
|
|
167
150
|
def _resolve(self, *, flatten: bool) -> dict[tuple[str, ...], _TimesDict]:
|
|
168
|
-
snapshot = self.
|
|
151
|
+
snapshot = self.snapshot()
|
|
169
152
|
return _flatten(snapshot) if flatten else snapshot
|
|
170
153
|
|
|
171
154
|
def report_timings(self, *, flatten: bool = True) -> str:
|
|
@@ -185,12 +168,14 @@ class _ExecutionTimer:
|
|
|
185
168
|
|
|
186
169
|
def compute_total_time(self, *, flatten: bool = True) -> float:
|
|
187
170
|
"""Compute total elapsed time across all top-level sections."""
|
|
188
|
-
|
|
189
|
-
|
|
171
|
+
# Counter merging cannot change the sum. Avoid allocating and flattening a snapshot.
|
|
172
|
+
_ = flatten
|
|
173
|
+
with self._lock:
|
|
174
|
+
return sum((info["elapsed_time"] for key, info in self.timings.items() if len(key) == 1), 0.0)
|
|
190
175
|
|
|
191
176
|
def compute_total_category_time(self, category: str) -> float:
|
|
192
177
|
"""Compute total elapsed time in a category, counting only top-most entries of that category."""
|
|
193
|
-
timings = self.
|
|
178
|
+
timings = self.snapshot()
|
|
194
179
|
total_time = 0.0
|
|
195
180
|
for key, info in timings.items():
|
|
196
181
|
if info["category"] != category or _has_ancestor_with_category(timings, key, category):
|
|
@@ -208,6 +193,17 @@ class _ExecutionTimer:
|
|
|
208
193
|
with self._lock:
|
|
209
194
|
self.timings.clear()
|
|
210
195
|
|
|
196
|
+
def register_forbidden_nesting(self, outer: str, inner: str) -> None:
|
|
197
|
+
with self._lock:
|
|
198
|
+
self.forbidden_nesting.add((outer, inner))
|
|
199
|
+
|
|
200
|
+
def clear_forbidden_nesting(self) -> None:
|
|
201
|
+
with self._lock:
|
|
202
|
+
self.forbidden_nesting.clear()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_TIMER: Final = _ExecutionTimer()
|
|
206
|
+
|
|
211
207
|
|
|
212
208
|
def _flatten(timings: dict[tuple[str, ...], _TimesDict]) -> dict[tuple[str, ...], _TimesDict]:
|
|
213
209
|
flat_map: dict[tuple[str, ...], _TimesDict] = {}
|
|
@@ -218,7 +214,9 @@ def _flatten(timings: dict[tuple[str, ...], _TimesDict]) -> dict[tuple[str, ...]
|
|
|
218
214
|
flat_map[flat_key] = info.copy()
|
|
219
215
|
else:
|
|
220
216
|
existing["elapsed_time"] += info["elapsed_time"]
|
|
221
|
-
|
|
217
|
+
if info["sequence"] > existing["sequence"]:
|
|
218
|
+
existing["category"] = info["category"]
|
|
219
|
+
existing["sequence"] = info["sequence"]
|
|
222
220
|
return flat_map
|
|
223
221
|
|
|
224
222
|
|
|
@@ -234,7 +232,7 @@ class TimerContext:
|
|
|
234
232
|
def __init__(self, name: str, category: str = DEFAULT_CATEGORY, counter: int | None = None) -> None:
|
|
235
233
|
self.name: str = _build_name_with_counter(name, counter)
|
|
236
234
|
self.category: str = category
|
|
237
|
-
self.timer: _ExecutionTimer =
|
|
235
|
+
self.timer: _ExecutionTimer = _TIMER
|
|
238
236
|
|
|
239
237
|
def __enter__(self) -> TimerContext:
|
|
240
238
|
self.timer.start_timer(self.name, self.category)
|
|
@@ -259,7 +257,7 @@ class TimerContext:
|
|
|
259
257
|
|
|
260
258
|
@functools.wraps(func)
|
|
261
259
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
262
|
-
with
|
|
260
|
+
with self:
|
|
263
261
|
return func(*args, **kwargs)
|
|
264
262
|
|
|
265
263
|
return wrapper
|
|
@@ -269,7 +267,7 @@ class TimerContext:
|
|
|
269
267
|
|
|
270
268
|
@functools.wraps(func)
|
|
271
269
|
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
272
|
-
with
|
|
270
|
+
with self:
|
|
273
271
|
return await func(*args, **kwargs)
|
|
274
272
|
|
|
275
273
|
return async_wrapper
|
|
@@ -285,42 +283,51 @@ def _build_name_with_counter(name: str, counter: int | None = None) -> str:
|
|
|
285
283
|
def _basic_name_without_counter(name: str) -> str:
|
|
286
284
|
"""Strip a trailing ``[counter]`` from a section name if present."""
|
|
287
285
|
if "[" in name and name.endswith("]"):
|
|
288
|
-
|
|
286
|
+
prefix, _, suffix = name.rpartition("[")
|
|
287
|
+
digits = suffix[:-1].removeprefix("-")
|
|
288
|
+
if digits.isascii() and digits.isdecimal():
|
|
289
|
+
return prefix
|
|
289
290
|
return name
|
|
290
291
|
|
|
291
292
|
|
|
292
293
|
def get_execution_times_report(*, flatten: bool = True) -> str:
|
|
293
294
|
"""Get a formatted report of all recorded sections; flatten counters if requested."""
|
|
294
|
-
return
|
|
295
|
+
return _TIMER.report_timings(flatten=flatten)
|
|
295
296
|
|
|
296
297
|
|
|
297
298
|
def log_execution_times(*, flatten: bool = True, logger: logging.Logger | None = None) -> None:
|
|
298
299
|
"""Log the execution-times report at INFO level; flatten counters if requested."""
|
|
299
|
-
|
|
300
|
+
target = logger if logger is not None else _LOGGER
|
|
301
|
+
if target.isEnabledFor(logging.INFO):
|
|
302
|
+
target.info(get_execution_times_report(flatten=flatten))
|
|
300
303
|
|
|
301
304
|
|
|
302
305
|
def get_execution_timings(*, flatten: bool = True) -> dict[tuple[str, ...], TimingReport]:
|
|
303
306
|
"""Get elapsed seconds and category for every recorded section; flatten counters if requested."""
|
|
304
|
-
return
|
|
307
|
+
return _TIMER.get_execution_timings(flatten=flatten)
|
|
305
308
|
|
|
306
309
|
|
|
307
310
|
def _build_payload(*, flatten: bool = True) -> TimingsPayload:
|
|
308
311
|
"""Build a JSON-serializable snapshot of all timings, including totals and per-category sums."""
|
|
309
|
-
|
|
310
|
-
timings =
|
|
312
|
+
snapshot = _TIMER.snapshot()
|
|
313
|
+
timings = _flatten(snapshot) if flatten else snapshot
|
|
311
314
|
sections: list[SectionRecord] = [
|
|
312
315
|
{
|
|
313
316
|
"name": key[-1],
|
|
314
317
|
"path": list(key),
|
|
315
|
-
"time": round(timings[key]["
|
|
318
|
+
"time": round(timings[key]["elapsed_time"], 6),
|
|
316
319
|
"category": timings[key]["category"],
|
|
317
320
|
}
|
|
318
321
|
for key in _ordered_by_hierarchy(timings)
|
|
319
322
|
]
|
|
320
|
-
|
|
323
|
+
category_totals: dict[str, float] = {}
|
|
324
|
+
for key, info in snapshot.items():
|
|
325
|
+
category = info["category"]
|
|
326
|
+
if not _has_ancestor_with_category(snapshot, key, category):
|
|
327
|
+
category_totals[category] = category_totals.get(category, 0.0) + info["elapsed_time"]
|
|
321
328
|
return {
|
|
322
|
-
"total_time": round(
|
|
323
|
-
"total_category_time": {cat: round(
|
|
329
|
+
"total_time": round(sum((info["elapsed_time"] for key, info in snapshot.items() if len(key) == 1), 0.0), 6),
|
|
330
|
+
"total_category_time": {cat: round(category_totals[cat], 6) for cat in sorted(category_totals)},
|
|
324
331
|
"sections": sections,
|
|
325
332
|
}
|
|
326
333
|
|
|
@@ -339,24 +346,24 @@ def save_execution_timings_json(path: str | Path, *, flatten: bool = True, inden
|
|
|
339
346
|
|
|
340
347
|
def get_total_time(*, flatten: bool = True) -> float:
|
|
341
348
|
"""Get total elapsed seconds across all top-level sections."""
|
|
342
|
-
return
|
|
349
|
+
return _TIMER.compute_total_time(flatten=flatten)
|
|
343
350
|
|
|
344
351
|
|
|
345
352
|
def get_total_category_time(category: str) -> float:
|
|
346
353
|
"""Get total elapsed seconds in a category, counting only top-most entries of that category."""
|
|
347
|
-
return
|
|
354
|
+
return _TIMER.compute_total_category_time(category)
|
|
348
355
|
|
|
349
356
|
|
|
350
357
|
def clear_execution_timings() -> None:
|
|
351
358
|
"""Reset all recorded timings."""
|
|
352
|
-
|
|
359
|
+
_TIMER.clear()
|
|
353
360
|
|
|
354
361
|
|
|
355
362
|
def register_forbidden_nesting(outer: str, inner: str) -> None:
|
|
356
363
|
"""Forbid timing sections of category ``inner`` directly inside sections of category ``outer``."""
|
|
357
|
-
|
|
364
|
+
_TIMER.register_forbidden_nesting(outer, inner)
|
|
358
365
|
|
|
359
366
|
|
|
360
367
|
def clear_forbidden_nesting() -> None:
|
|
361
368
|
"""Remove all forbidden-nesting rules."""
|
|
362
|
-
|
|
369
|
+
_TIMER.clear_forbidden_nesting()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: executiontimer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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
|
|
@@ -145,6 +145,10 @@ get_execution_timings(flatten=True) # {("step",): {"time": 0.158, ...}}
|
|
|
145
145
|
get_execution_timings(flatten=False) # {("step[0]",): ..., ("step[1]",): ..., ...}
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
Flattening removes the final integer suffix (including negative counters). Other
|
|
149
|
+
bracketed names such as `array[index]` are preserved. If merged entries have different
|
|
150
|
+
categories, the category from the most recently entered section is used.
|
|
151
|
+
|
|
148
152
|
### Categories
|
|
149
153
|
|
|
150
154
|
Categories are plain strings — use whatever fits your domain:
|
|
@@ -161,6 +165,10 @@ get_total_category_time("gpu")
|
|
|
161
165
|
`get_total_category_time` counts only the *top-most* section of a category, so a `gpu`
|
|
162
166
|
section nested inside another `gpu` section is not double-counted.
|
|
163
167
|
|
|
168
|
+
Repeated calls to the same path accumulate time. If its category changes, the latest
|
|
169
|
+
category applies to that path's entire accumulated time. Use consistent categories per
|
|
170
|
+
path when you need separate category totals.
|
|
171
|
+
|
|
164
172
|
You can also forbid a category from appearing inside another, which raises a `ValueError`
|
|
165
173
|
as soon as the invalid nesting happens:
|
|
166
174
|
|
|
@@ -198,6 +206,9 @@ save_execution_timings_json("timings.json")
|
|
|
198
206
|
}
|
|
199
207
|
```
|
|
200
208
|
|
|
209
|
+
Sections and totals come from one snapshot. Category totals use the original paths,
|
|
210
|
+
even when flattening merges sections with different categories.
|
|
211
|
+
|
|
201
212
|
### Concurrency
|
|
202
213
|
|
|
203
214
|
The recorded timings live in one process-wide registry guarded by a lock. The *active
|
|
@@ -213,9 +224,38 @@ async def worker(n: int) -> None:
|
|
|
213
224
|
await asyncio.gather(worker(0), worker(1))
|
|
214
225
|
```
|
|
215
226
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
227
|
+
Overlapping calls to the same section path are supported: each call keeps its own start
|
|
228
|
+
time, and their durations are added together. These totals measure accumulated elapsed
|
|
229
|
+
time and can exceed wall-clock duration. Use distinct names (or `counter=`) to report
|
|
230
|
+
concurrent calls separately.
|
|
231
|
+
|
|
232
|
+
New asyncio tasks inherit the timing context in which they are created. Their sections
|
|
233
|
+
nest under that parent; changes to each task's active stack remain independent. Await
|
|
234
|
+
child tasks inside the parent section if you want the parent duration to include them.
|
|
235
|
+
|
|
236
|
+
### Reusing contexts and clearing timings
|
|
237
|
+
|
|
238
|
+
A `TimerContext` can be reused, nested within itself, or shared by concurrent calls.
|
|
239
|
+
For a tight loop, reuse a context to avoid constructing one on every iteration:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
step_timer = TimerContext("step")
|
|
243
|
+
for item in items:
|
|
244
|
+
with step_timer:
|
|
245
|
+
process(item)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Timings accumulate until `clear_execution_timings()` is called. Clearing also discards
|
|
249
|
+
samples from sections that were already active, without disturbing their nesting stack.
|
|
250
|
+
Sections started after the clear are recorded normally. Reports include completed calls;
|
|
251
|
+
an active section's current duration is added only when it exits.
|
|
252
|
+
|
|
253
|
+
### Measuring overhead
|
|
254
|
+
|
|
255
|
+
Run the repeatable benchmark with `uv run python benchmarks/overhead.py`. It measures
|
|
256
|
+
fresh and reused contexts, sync and async decorators, nesting, and reporting. Compare
|
|
257
|
+
results using the same interpreter and machine; see [benchmarks/README.md](benchmarks/README.md).
|
|
258
|
+
`log_execution_times()` skips building a report when its logger has `INFO` disabled.
|
|
219
259
|
|
|
220
260
|
## API
|
|
221
261
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
execution_timer/__init__.py,sha256=WC6Cn6-mGTEHh1386OJ--hrAFaa9Z6eSBFze21brEfw,959
|
|
2
|
+
execution_timer/_timer.py,sha256=RbLD7HshcDBlcz0II7JiLZduuNr6Dao0D_d0gs9cuww,14526
|
|
3
|
+
execution_timer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
executiontimer-0.1.1.dist-info/METADATA,sha256=sAJT1E9gFJHlfd2oVwAwcrWYebEhq3xhgmjMIvv3NS4,11374
|
|
5
|
+
executiontimer-0.1.1.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
6
|
+
executiontimer-0.1.1.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
|
|
7
|
+
executiontimer-0.1.1.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
execution_timer/__init__.py,sha256=6DKOLTSXY0gu_DjW_rIqBRZROUYnlp8haVYvLX6NzTQ,959
|
|
2
|
-
execution_timer/_timer.py,sha256=USfPpUdpW3jk1ffjIcTdOmB-FK-8HQHgcvZ44mJQ2f4,14504
|
|
3
|
-
execution_timer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
executiontimer-0.1.0.dist-info/METADATA,sha256=1ftfFmuikQRb6w1mmfZAUVdezEUUgyweDkxE8ljTLNY,9412
|
|
5
|
-
executiontimer-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
6
|
-
executiontimer-0.1.0.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
|
|
7
|
-
executiontimer-0.1.0.dist-info/RECORD,,
|
|
File without changes
|