executiontimer 1.0.0__py3-none-any.whl → 1.0.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 +42 -24
- {executiontimer-1.0.0.dist-info → executiontimer-1.0.1.dist-info}/METADATA +5 -3
- executiontimer-1.0.1.dist-info/RECORD +7 -0
- executiontimer-1.0.0.dist-info/RECORD +0 -7
- {executiontimer-1.0.0.dist-info → executiontimer-1.0.1.dist-info}/WHEEL +0 -0
- {executiontimer-1.0.0.dist-info → executiontimer-1.0.1.dist-info}/licenses/LICENSE +0 -0
execution_timer/__init__.py
CHANGED
execution_timer/_timer.py
CHANGED
|
@@ -74,11 +74,13 @@ class _Frame(NamedTuple):
|
|
|
74
74
|
_ACTIVE_CONTEXT: ContextVar[_Frame | None] = ContextVar("execution_timer_context", default=None)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def _ordered_by_hierarchy(keys: Iterable[tuple[str, ...]]) -> list[tuple[str, ...]]:
|
|
77
|
+
def _ordered_by_hierarchy(keys: Iterable[tuple[str, ...]]) -> list[tuple[tuple[str, ...], int]]:
|
|
78
78
|
"""Order section paths depth-first so children always follow their parent.
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
Returns each path with its depth in the recorded tree, which is shallower than the path
|
|
81
|
+
length when an ancestor is missing. Insertion order is preserved within each level, so a
|
|
82
|
+
parent revisited after an unrelated sibling still renders with its own children rather
|
|
83
|
+
than beneath the sibling.
|
|
82
84
|
"""
|
|
83
85
|
keys = list(keys)
|
|
84
86
|
known = set(keys)
|
|
@@ -92,16 +94,28 @@ def _ordered_by_hierarchy(keys: Iterable[tuple[str, ...]]) -> list[tuple[str, ..
|
|
|
92
94
|
else:
|
|
93
95
|
roots.append(key)
|
|
94
96
|
|
|
95
|
-
ordered: list[tuple[str, ...]] = []
|
|
97
|
+
ordered: list[tuple[tuple[str, ...], int]] = []
|
|
96
98
|
# Explicit stack rather than recursion: nesting depth is user-controlled.
|
|
97
|
-
stack =
|
|
99
|
+
stack = [(key, 0) for key in reversed(roots)]
|
|
98
100
|
while stack:
|
|
99
|
-
key = stack.pop()
|
|
100
|
-
ordered.append(key)
|
|
101
|
-
stack.extend(reversed(children.get(key, [])))
|
|
101
|
+
key, depth = stack.pop()
|
|
102
|
+
ordered.append((key, depth))
|
|
103
|
+
stack.extend((child, depth + 1) for child in reversed(children.get(key, [])))
|
|
102
104
|
return ordered
|
|
103
105
|
|
|
104
106
|
|
|
107
|
+
def _top_level_time(timings: dict[tuple[str, ...], _TimesDict]) -> float:
|
|
108
|
+
"""Sum the sections with no recorded parent, matching the roots of ``_ordered_by_hierarchy``.
|
|
109
|
+
|
|
110
|
+
A parent goes missing when timings are cleared while it is active; its children that
|
|
111
|
+
finish afterwards are then top-level and must count toward the total.
|
|
112
|
+
"""
|
|
113
|
+
return sum(
|
|
114
|
+
(info["elapsed_time"] for key, info in timings.items() if len(key) == 1 or key[:-1] not in timings),
|
|
115
|
+
0.0,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
105
119
|
class _ExecutionTimer:
|
|
106
120
|
"""Registry of named, nestable timing sections, shared through ``_TIMER``."""
|
|
107
121
|
|
|
@@ -137,9 +151,10 @@ class _ExecutionTimer:
|
|
|
137
151
|
def stop_timer(self, name: str) -> None:
|
|
138
152
|
"""Stop timing a section and accumulate its elapsed time.
|
|
139
153
|
|
|
140
|
-
Never raises: an exception here would replace
|
|
141
|
-
block. Exiting past still-active inner sections
|
|
142
|
-
holds one open) discards them with a warning,
|
|
154
|
+
Never raises, unless warnings are configured as errors: an exception here would replace
|
|
155
|
+
one already propagating from the timed block. Exiting past still-active inner sections
|
|
156
|
+
(typically a suspended generator that holds one open) discards them with a warning,
|
|
157
|
+
after recording the exit, so the stack cannot stay corrupted.
|
|
143
158
|
Exiting a section that is no longer active, such as one discarded that way when its
|
|
144
159
|
generator is finally closed, does nothing.
|
|
145
160
|
"""
|
|
@@ -150,17 +165,18 @@ class _ExecutionTimer:
|
|
|
150
165
|
frame = frame.parent
|
|
151
166
|
if frame is None:
|
|
152
167
|
return
|
|
168
|
+
with self._lock:
|
|
169
|
+
# A clear detaches this entry from the registry. Updating the detached object
|
|
170
|
+
# cannot resurrect an old sample or add it to a replacement at the same path.
|
|
171
|
+
frame.entry["elapsed_time"] += end_time - frame.start_time
|
|
172
|
+
_ = _ACTIVE_CONTEXT.set(frame.parent)
|
|
153
173
|
if frame is not active and active is not None:
|
|
174
|
+
# Warn only once the state is consistent: warnings configured as errors raise here.
|
|
154
175
|
msg = (
|
|
155
176
|
f"Section '{name}' exited while '{active.path[-1]}' was still active; discarding the "
|
|
156
177
|
"unfinished inner sections. Close sections before a generator yields."
|
|
157
178
|
)
|
|
158
179
|
warnings.warn(msg, RuntimeWarning, stacklevel=3)
|
|
159
|
-
with self._lock:
|
|
160
|
-
# A clear detaches this entry from the registry. Updating the detached object
|
|
161
|
-
# cannot resurrect an old sample or add it to a replacement at the same path.
|
|
162
|
-
frame.entry["elapsed_time"] += end_time - frame.start_time
|
|
163
|
-
_ = _ACTIVE_CONTEXT.set(frame.parent)
|
|
164
180
|
|
|
165
181
|
def _resolve(self, *, flatten: bool) -> dict[tuple[str, ...], _TimesDict]:
|
|
166
182
|
snapshot = self.snapshot()
|
|
@@ -168,23 +184,25 @@ class _ExecutionTimer:
|
|
|
168
184
|
|
|
169
185
|
def report_timings(self, *, flatten: bool = True) -> str:
|
|
170
186
|
"""Build a report of all sections with duration and percentage of total time."""
|
|
171
|
-
|
|
172
|
-
if not
|
|
187
|
+
snapshot = self.snapshot()
|
|
188
|
+
if not snapshot:
|
|
173
189
|
return ""
|
|
174
190
|
|
|
175
|
-
|
|
191
|
+
# Total the unflattened paths, like get_total_time and the JSON export.
|
|
192
|
+
total_time = _top_level_time(snapshot)
|
|
193
|
+
timings = _flatten(snapshot) if flatten else snapshot
|
|
176
194
|
report = [f"Total time: {total_time:.4f} s.\n"]
|
|
177
|
-
for key in _ordered_by_hierarchy(timings):
|
|
195
|
+
for key, depth in _ordered_by_hierarchy(timings):
|
|
178
196
|
elapsed_time = timings[key]["elapsed_time"]
|
|
179
197
|
percentage = (elapsed_time / total_time) * 100 if total_time else 0.0
|
|
180
|
-
report.append(f"{'.. ' *
|
|
198
|
+
report.append(f"{'.. ' * depth}{key[-1]}: {elapsed_time:.4f} s ({percentage:.2f}%)")
|
|
181
199
|
return "\n".join(report)
|
|
182
200
|
|
|
183
201
|
def compute_total_time(self) -> float:
|
|
184
202
|
"""Compute total elapsed time across all top-level sections."""
|
|
185
203
|
# Counter merging cannot change the sum, so there is no snapshot to copy or flatten.
|
|
186
204
|
with self._lock:
|
|
187
|
-
return
|
|
205
|
+
return _top_level_time(self.timings)
|
|
188
206
|
|
|
189
207
|
def compute_total_category_time(self, category: str) -> float:
|
|
190
208
|
"""Compute total elapsed time in a category, counting only top-most entries of that category."""
|
|
@@ -344,7 +362,7 @@ def _build_payload(*, flatten: bool = True) -> TimingsPayload:
|
|
|
344
362
|
"time": round(timings[key]["elapsed_time"], 6),
|
|
345
363
|
"category": timings[key]["category"],
|
|
346
364
|
}
|
|
347
|
-
for key in _ordered_by_hierarchy(timings)
|
|
365
|
+
for key, _ in _ordered_by_hierarchy(timings)
|
|
348
366
|
]
|
|
349
367
|
category_totals: dict[str, float] = {}
|
|
350
368
|
for key, info in snapshot.items():
|
|
@@ -352,7 +370,7 @@ def _build_payload(*, flatten: bool = True) -> TimingsPayload:
|
|
|
352
370
|
if not _has_ancestor_with_category(snapshot, key, category):
|
|
353
371
|
category_totals[category] = category_totals.get(category, 0.0) + info["elapsed_time"]
|
|
354
372
|
return {
|
|
355
|
-
"total_time": round(
|
|
373
|
+
"total_time": round(_top_level_time(snapshot), 6),
|
|
356
374
|
"total_category_time": {cat: round(category_totals[cat], 6) for cat in sorted(category_totals)},
|
|
357
375
|
"sections": sections,
|
|
358
376
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: executiontimer
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.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
|
|
@@ -260,7 +260,8 @@ time the loop that consumes the generator instead.
|
|
|
260
260
|
If the caller's section exits while a paused generator's section is still open, the
|
|
261
261
|
timer never raises: it emits a `RuntimeWarning`, records the caller's section, and
|
|
262
262
|
discards the generator's unfinished one, so later sections nest correctly. Closing that
|
|
263
|
-
generator afterwards does nothing.
|
|
263
|
+
generator afterwards does nothing. If warnings are configured as errors, the warning is
|
|
264
|
+
raised only after that cleanup, so the timings and nesting stay consistent.
|
|
264
265
|
|
|
265
266
|
### Reusing contexts and clearing timings
|
|
266
267
|
|
|
@@ -276,7 +277,8 @@ for item in items:
|
|
|
276
277
|
|
|
277
278
|
Timings accumulate until `clear_execution_timings()` is called. Clearing also discards
|
|
278
279
|
samples from sections that were already active, without disturbing their nesting stack.
|
|
279
|
-
Sections started after the clear are recorded normally
|
|
280
|
+
Sections started after the clear are recorded normally; if their parent was cleared, they
|
|
281
|
+
are reported as top-level sections and count toward the total. Reports include completed calls;
|
|
280
282
|
an active section's current duration is added only when it exits.
|
|
281
283
|
|
|
282
284
|
### Measuring overhead
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
execution_timer/__init__.py,sha256=SDCT34iz3KKhr1CyiCpvMrjjdf1umy55sScSp4bKUBo,959
|
|
2
|
+
execution_timer/_timer.py,sha256=4RP4SDki9TasaCfk56lmJX54EiYnQsvHOd5WnZuuoxI,16778
|
|
3
|
+
execution_timer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
executiontimer-1.0.1.dist-info/METADATA,sha256=o5McC2gZg2rh2G65qv0Hh6tdmGo6Mk_TfPfym_OmKhA,13612
|
|
5
|
+
executiontimer-1.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
executiontimer-1.0.1.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
|
|
7
|
+
executiontimer-1.0.1.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|