Skip to content

Commit c2fe586

Browse files
author
David Erb
committed
adds profiler
1 parent 416913a commit c2fe586

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

src/dls_utilpack/profiler.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import logging
2+
import threading
3+
import time
4+
from typing import Optional
5+
6+
logger = logging.getLogger(__name__)
7+
8+
9+
class Profile:
10+
"""
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.
13+
14+
TODO: Make profiles nestable by accumulating them at the context close.
15+
"""
16+
17+
def __init__(self, label):
18+
self.__label = label
19+
self.__start_time = 0
20+
self.__seconds = 0.0
21+
self.__count = 0
22+
23+
def __enter__(self):
24+
self.__start_time = time.time()
25+
26+
def __exit__(self, exc_type, exc_val, exc_tb):
27+
self.__seconds = time.time() - self.__start_time
28+
self.__count += 1
29+
30+
def __str__(self):
31+
if self.__count == 0:
32+
average = 0.0
33+
else:
34+
average = self.__seconds / self.__count
35+
36+
return (
37+
f"{self.__label} called {self.__count} times"
38+
f" for average of {'%0.3f' % average} seconds"
39+
)
40+
41+
42+
class Profiler:
43+
"""
44+
Class that accumulates multiple profiles.
45+
Reports its results as a multi-line string.
46+
"""
47+
48+
def __init__(self):
49+
self.__profiles = {}
50+
51+
def profile(self, label: str) -> Profile:
52+
"""
53+
Return the profile for the given label. Uses previously existing profile, if any, or makes a new instance.
54+
55+
Args:
56+
label (str): label identifying the profile
57+
58+
Returns:
59+
Profile: a new profile object, or previously existing one
60+
"""
61+
profile = self.__profiles.get(label)
62+
if profile is None:
63+
profile = Profile(label)
64+
self.__profiles[label] = profile
65+
66+
return profile
67+
68+
def __str__(self) -> str:
69+
lines = []
70+
for profile in self.__profiles.values():
71+
lines.append(str(profile))
72+
73+
return "\n".join(lines)
74+
75+
76+
# A global instance for convenience.
77+
__profiler: Optional[Profiler] = None
78+
79+
__global_lock = threading.RLock()
80+
81+
82+
def dls_utilpack_global_profiler() -> Profiler:
83+
global __profiler
84+
global __global_lock
85+
86+
with __global_lock:
87+
if __profiler is None:
88+
__profiler = Profiler()
89+
return __profiler

tests/test_profiler.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import asyncio
2+
import logging
3+
4+
from dls_utilpack.profiler import dls_utilpack_global_profiler
5+
6+
# Base class for the tester.
7+
from tests.base_tester import BaseTester
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
# ----------------------------------------------------------------------------------------
13+
class TestProfiler(BaseTester):
14+
def test(self, constants, logging_setup, output_directory):
15+
""" """
16+
17+
self.main(constants, output_directory)
18+
19+
# ----------------------------------------------------------------------------------------
20+
async def _main_coroutine(
21+
self,
22+
constants,
23+
output_directory,
24+
):
25+
""" """
26+
27+
profiler = dls_utilpack_global_profiler()
28+
29+
with profiler.profile("loop1"):
30+
await asyncio.sleep(0.1)
31+
32+
logger.debug(f"profile\n{profiler}")

0 commit comments

Comments
 (0)