Skip to content

Commit 0149bd9

Browse files
authored
Merge pull request #216 from Helene/profiler
Add profiler module
2 parents f89e4f1 + 1de57a4 commit 0149bd9

File tree

6 files changed

+118
-2
lines changed

6 files changed

+118
-2
lines changed

source/analytics.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@
3535

3636
global cherrypy_internal_stats
3737
cherrypy_internal_stats = False
38+
39+
global runtime_profiling
40+
runtime_profiling = False

source/collector.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from threading import Thread
3131
from metadata import MetadataHandler
3232
from bridgeLogger import getBridgeLogger
33-
from utils import classattributes, cond_execution_time
33+
from utils import classattributes, cond_execution_time, get_runtime_statistics
3434

3535

3636
local_cache = set()
@@ -86,6 +86,7 @@ def parse_tags(self, filtersMap):
8686
else:
8787
self.tags[_key] = _values.pop()
8888

89+
@get_runtime_statistics(enabled=analytics.runtime_profiling)
8990
def reduce_dps_to_first_not_none(self, reverse_order=False):
9091
"""Reduce multiple data points(dps) of a single
9192
TimeSeries to the first non null value in a sorted order.

source/profiler.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'''
2+
##############################################################################
3+
# Copyright 2024 IBM Corp.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
##############################################################################
17+
18+
Created on May 15, 2024
19+
20+
@author: HWASSMAN
21+
'''
22+
23+
import cherrypy
24+
import io
25+
import os
26+
from cProfile import Profile
27+
from pstats import SortKey, Stats
28+
from metaclasses import Singleton
29+
30+
31+
class Profiler(metaclass=Singleton):
32+
exposed = True
33+
34+
def __init__(self, path=None):
35+
if not path:
36+
path = os.path.join(os.path.dirname(__file__), 'profile')
37+
self.path = path
38+
if not os.path.exists(path):
39+
os.makedirs(path)
40+
41+
def run(self, func, *args, **kwargs):
42+
"""Dump profile data into self.path."""
43+
with Profile() as profile:
44+
filename = f"profiling_{func.__name__}.prof"
45+
result = func(*args, **kwargs)
46+
(
47+
Stats(profile)
48+
.strip_dirs()
49+
.sort_stats(SortKey.CALLS)
50+
.dump_stats(os.path.join(self.path, filename))
51+
)
52+
return result
53+
54+
def statfiles(self):
55+
""" Returns a list of available profiling files"""
56+
return [f for f in os.listdir(self.path)
57+
if f.startswith('profiling_') and f.endswith('.prof')]
58+
59+
def stats(self, filename, sortby='cumulative'):
60+
""" Returns output of print_stats() for the given profiling file"""
61+
sio = io.StringIO()
62+
s = Stats(os.path.join(self.path, filename), stream=sio)
63+
s.strip_dirs()
64+
s.sort_stats(sortby)
65+
s.print_stats()
66+
response = sio.getvalue().splitlines()
67+
sio.close()
68+
return response
69+
70+
def GET(self, **params):
71+
""" Forward GET REST HTTP/s API incoming requests to Profiler
72+
available endpoints:
73+
/profiling
74+
"""
75+
resp = []
76+
# /profiling
77+
if '/profiling' == cherrypy.request.script_name:
78+
del cherrypy.response.headers['Content-Type']
79+
outp = []
80+
runs = self.statfiles()
81+
for name in runs:
82+
outp.extend(self.stats(filename=name))
83+
resp = '\n'.join(outp) + '\n'
84+
cherrypy.response.headers['Content-Type'] = 'text/plain'
85+
return resp

source/queryHandler/QueryHandler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from collections import namedtuple, defaultdict
2929
from itertools import chain
3030
from typing import NamedTuple, Tuple
31-
from utils import cond_execution_time
31+
from utils import cond_execution_time, get_runtime_statistics
3232

3333
from .PerfmonRESTclient import perfHTTPrequestHelper, createRequestDataObj, getAuthHandler
3434

@@ -176,10 +176,12 @@ def __init__(self, query, res_json):
176176
for row in self.rows:
177177
self._add_calculated_row_data(calc, row)
178178

179+
@get_runtime_statistics(enabled=analytics.runtime_profiling)
179180
def __parseHeader(self):
180181
item = self.json['header']
181182
return HeaderData(**item)
182183

184+
@get_runtime_statistics(enabled=analytics.runtime_profiling)
183185
def __parseLegend(self):
184186
legendItems = self.json['legend']
185187
columnInfos = []

source/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from typing import Callable, TypeVar, Any
2626
from functools import wraps
2727
from messages import MSG
28+
from profiler import Profiler
2829

2930
T = TypeVar('T')
3031

@@ -87,6 +88,22 @@ def no_outer(f: Callable[..., T]) -> Callable[..., T]:
8788
return outer if enabled else no_outer
8889

8990

91+
def get_runtime_statistics(enabled: bool = False) -> Callable[[Callable[..., T]], Callable[..., T]]:
92+
""" Conditionally executes the passed through function f with profiling."""
93+
94+
def outer(f: Callable[..., T]) -> Callable[..., T]:
95+
@wraps(f)
96+
def wrapper(*args: Any, **kwargs: Any) -> T:
97+
profiler = Profiler()
98+
result = profiler.run(f, *args, **kwargs)
99+
return result
100+
return wrapper
101+
102+
def no_outer(f: Callable[..., T]) -> Callable[..., T]:
103+
return f
104+
return outer if enabled else no_outer
105+
106+
90107
def classattributes(default_attr: dict, more_allowed_attr: list):
91108
""" class __init__decorator
92109
Parses kwargs attributes, for optional arguments uses default values,

source/zimonGrafanaIntf.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from metadata import MetadataHandler
3939
from opentsdb import OpenTsdbApi
4040
from prometheus import PrometheusExporter
41+
from profiler import Profiler
4142
from watcher import ConfigWatcher
4243
from cherrypy import _cperror
4344
from cherrypy.lib.cpstats import StatsPage
@@ -342,6 +343,13 @@ def main(argv):
342343
}
343344
)
344345
registered_apps.append("Prometheus Exporter Api listening on Prometheus requests")
346+
profiler = Profiler(args.get('logPath'))
347+
# query for list configured zimon sensors
348+
cherrypy.tree.mount(profiler, '/profiling',
349+
{'/':
350+
{'request.dispatch': cherrypy.dispatch.MethodDispatcher()}
351+
}
352+
)
345353
if analytics.cherrypy_internal_stats:
346354
cherrypy.tree.mount(StatsPage(), '/cherrypy_internal_stats')
347355
logger.info("%s", MSG['sysStart'].format(sys.version, cherrypy.__version__))

0 commit comments

Comments
 (0)