Skip to content

Commit cc1bf0d

Browse files
author
David Erb
committed
improves profile
1 parent cddc48c commit cc1bf0d

File tree

2 files changed

+62
-24
lines changed

2 files changed

+62
-24
lines changed

src/dls_utilpack/profiler.py

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import logging
22
import threading
33
import time
4-
from typing import Optional
4+
from typing import Dict, Optional
55

66
logger = logging.getLogger(__name__)
77

88

99
class Context:
1010
"""
11-
Class that holds the total execution time and call count of potentially multiple executions of the same label.
12-
Reports its contents as a single-line string.
11+
Class that holds the start time of an execution block.
12+
Meant to be used in a "with" statement.
13+
Reports its results to the given profiler at the exit.
1314
14-
TODO: Make profiles nestable by accumulating them at the context close.
15+
This class is not publically accessible.
1516
"""
1617

17-
def __init__(self, label: str, profiler: Profiler):
18+
def __init__(self, label: str, profiler):
1819
self.__profiler = profiler
1920

2021
self.__label = label
@@ -32,25 +33,24 @@ def __exit__(self, exc_type, exc_val, exc_tb):
3233

3334
class Profile:
3435
"""
35-
Class that holds the total execution time and call count of potentially multiple executions of the same label.
36+
Class that holds the total execution time and call count
37+
of potentially multiple executions of the same label.
3638
Reports its contents as a single-line string.
37-
38-
TODO: Make profiles nestable by accumulating them at the context close.
3939
"""
4040

4141
def __init__(self, label):
4242
self.__label = label
43-
self.__seconds = 0.0
44-
self.__count = 0
43+
self.seconds = 0.0
44+
self.count = 0
4545

4646
def __str__(self):
47-
if self.__count == 0:
47+
if self.count == 0:
4848
average = 0.0
4949
else:
50-
average = self.__seconds / self.__count
50+
average = self.seconds / self.count
5151

5252
return (
53-
f"{self.__label} called {self.__count} times"
53+
f"{self.__label} called {self.count} times"
5454
f" for average of {'%0.3f' % average} seconds"
5555
)
5656

@@ -62,38 +62,47 @@ class Profiler:
6262
"""
6363

6464
def __init__(self):
65-
self.__profiles = {}
65+
self.__profiles: Dict[str, Profile] = {}
6666
self.__lock = threading.RLock()
6767

68-
def profile(self, label: str) -> Context:
68+
def context(self, label: str) -> Context:
6969
"""
70-
Return a context to hold the profile timing.
70+
Return a context to hold the profile timing for a code block.
7171
7272
Args:
7373
label (str): label identifying the profile
7474
7575
Returns:
76-
Profile: a new profile object, or previously existing one
76+
Context: a new context object
7777
"""
7878
return Context(label, self)
7979

80-
def accumulate(self, label: str, seconds: float) -> None:
80+
def profile(self, label: str) -> Profile:
8181
"""
82-
Accumulate a report into the profile for the given label.
82+
Make the accumulation profile for the given label.
8383
Uses previously existing profile, if any, or makes a new instance.
8484
8585
Args:
8686
label (str): label identifying the profile
87-
88-
Returns:
89-
Profile: a new profile object, or previously existing one
9087
"""
9188
with self.__lock:
9289
profile = self.__profiles.get(label)
9390
if profile is None:
9491
profile = Profile(label)
9592
self.__profiles[label] = profile
9693

94+
return profile
95+
96+
def accumulate(self, label: str, seconds: float) -> None:
97+
"""
98+
Accumulate a report into the profile for the given label.
99+
Uses previously existing profile, if any, or makes a new instance.
100+
101+
Args:
102+
label (str): label identifying the profile
103+
seconds (float): the number of seconds to accumulate
104+
"""
105+
profile = self.profile(label)
97106
profile.seconds += seconds
98107
profile.count += 1
99108

tests/test_profiler.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22
import logging
33

4+
import pytest
5+
46
from dls_utilpack.profiler import dls_utilpack_global_profiler
57

68
# Base class for the tester.
@@ -26,7 +28,34 @@ async def _main_coroutine(
2628

2729
profiler = dls_utilpack_global_profiler()
2830

29-
with profiler.profile("loop1"):
31+
with profiler.context("loop1"):
3032
await asyncio.sleep(0.1)
3133

32-
logger.debug(f"profile\n{profiler}")
34+
assert profiler.profile("loop1").count == 1
35+
assert profiler.profile("loop1").seconds == pytest.approx(0.1, abs=1e-2)
36+
37+
# -----------------------------------------------------
38+
for i in range(0, 2):
39+
with profiler.context("loop2"):
40+
await asyncio.sleep(0.1)
41+
42+
assert profiler.profile("loop2").count == 2
43+
assert profiler.profile("loop2").seconds == pytest.approx(0.2, abs=1e-2)
44+
45+
# -----------------------------------------------------
46+
# Kind of non-sensical to nest them, but check it anyway.
47+
with profiler.context("loop3"):
48+
with profiler.context("loop3"):
49+
with profiler.context("loop3"):
50+
await asyncio.sleep(0.1)
51+
52+
assert profiler.profile("loop3").count == 3
53+
assert profiler.profile("loop3").seconds == pytest.approx(0.3, abs=1e-2)
54+
55+
# -----------------------------------------------------
56+
s = str(profiler)
57+
logger.debug(f"profile\n{s}")
58+
59+
assert "loop1 called 1 times" in s
60+
assert "loop2 called 2 times" in s
61+
assert "loop3 called 3 times" in s

0 commit comments

Comments
 (0)