Skip to content

Commit 5d89bd2

Browse files
committed
Add metrics
1 parent e8e3224 commit 5d89bd2

File tree

1 file changed

+133
-0
lines changed

1 file changed

+133
-0
lines changed

singlestoredb/functions/ext/timer.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import json
2+
import time
3+
from typing import Any
4+
from typing import Dict
5+
from typing import List
6+
7+
from . import utils
8+
9+
logger = utils.get_logger('singlestoredb.functions.ext.metrics')
10+
11+
12+
class RoundedFloatEncoder(json.JSONEncoder):
13+
14+
def encode(self, obj: Any) -> str:
15+
if isinstance(obj, dict):
16+
return '{' + ', '.join(
17+
f'"{k}": {self._format_value(v)}'
18+
for k, v in obj.items()
19+
) + '}'
20+
return super().encode(obj)
21+
22+
def _format_value(self, value: Any) -> str:
23+
if isinstance(value, float):
24+
return f'{value:.2f}'
25+
return json.dumps(value)
26+
27+
28+
class Timer:
29+
"""
30+
Timer context manager that supports nested timing using a stack.
31+
32+
Example
33+
-------
34+
timer = Timer()
35+
36+
with timer('total'):
37+
with timer('receive_data'):
38+
time.sleep(0.1)
39+
with timer('parse_input'):
40+
time.sleep(0.2)
41+
with timer('call_function'):
42+
with timer('inner_operation'):
43+
time.sleep(0.05)
44+
time.sleep(0.3)
45+
46+
print(timer.metrics)
47+
# {'receive_data': 0.1, 'parse_input': 0.2, 'inner_operation': 0.05,
48+
# 'call_function': 0.35, 'total': 0.65}
49+
"""
50+
51+
def __init__(self, **kwargs: Any) -> None:
52+
"""
53+
Initialize the Timer.
54+
55+
Parameters
56+
----------
57+
metrics : Dict[str, float]
58+
Dictionary to store the timing results
59+
60+
"""
61+
self.metadata: Dict[str, Any] = kwargs
62+
self.metrics: Dict[str, float] = dict()
63+
self._stack: List[Dict[str, Any]] = []
64+
self.start_time = time.perf_counter()
65+
66+
def __call__(self, key: str) -> 'Timer':
67+
"""
68+
Set the key for the next context manager usage.
69+
70+
Parameters
71+
----------
72+
key : str
73+
The key to store the execution time under
74+
75+
Returns
76+
-------
77+
Timer
78+
Self, to be used as context manager
79+
80+
"""
81+
self._current_key = key
82+
return self
83+
84+
def __enter__(self) -> 'Timer':
85+
"""Enter the context manager and start timing."""
86+
if not hasattr(self, '_current_key'):
87+
raise ValueError(
88+
"No key specified. Use timer('key_name') as context manager.",
89+
)
90+
91+
# Push current timing info onto stack
92+
timing_info = {
93+
'key': self._current_key,
94+
'start_time': time.perf_counter(),
95+
}
96+
self._stack.append(timing_info)
97+
98+
# Clear current key for next use
99+
delattr(self, '_current_key')
100+
101+
return self
102+
103+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
104+
"""Exit the context manager and store the elapsed time."""
105+
if not self._stack:
106+
return
107+
108+
# Pop the current timing from stack
109+
timing_info = self._stack.pop()
110+
elapsed = time.perf_counter() - timing_info['start_time']
111+
self.metrics[timing_info['key']] = elapsed
112+
113+
def finish(self) -> None:
114+
"""Finish the current timing context and store the elapsed time."""
115+
if self._stack:
116+
raise RuntimeError(
117+
'finish() called without a matching __enter__(). '
118+
'Use the context manager instead.',
119+
)
120+
121+
self.metrics['total'] = time.perf_counter() - self.start_time
122+
123+
self.log_metrics()
124+
125+
def reset(self) -> None:
126+
"""Clear all stored times and reset the stack."""
127+
self.metrics.clear()
128+
self._stack.clear()
129+
130+
def log_metrics(self) -> None:
131+
if self.metadata.get('function'):
132+
result = dict(type='function_metrics', **self.metadata, **self.metrics)
133+
logger.info(json.dumps(result, cls=RoundedFloatEncoder))

0 commit comments

Comments
 (0)