Skip to content

Commit 9f38c95

Browse files
add extra profilers
1 parent 54b389b commit 9f38c95

File tree

2 files changed

+160
-15
lines changed

2 files changed

+160
-15
lines changed

cylc/uiserver/app.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@
7474
from tornado import ioloop
7575
from tornado.web import RedirectHandler
7676
from traitlets import (
77-
Bool,
7877
Dict,
7978
Float,
8079
Int,
80+
Unicode,
8181
TraitError,
8282
TraitType,
8383
Undefined,
@@ -109,6 +109,7 @@
109109
UIServerGraphQLHandler,
110110
UserProfileHandler,
111111
)
112+
from cylc.uiserver.profilers import get_profiler
112113
from cylc.uiserver.resolvers import Resolvers
113114
from cylc.uiserver.schema import schema
114115
from cylc.uiserver.graphql.tornado_ws import TornadoSubscriptionServer
@@ -336,15 +337,25 @@ class CylcUIServer(ExtensionApp):
336337
''',
337338
default_value=100,
338339
)
339-
profile = Bool(
340+
profile = Unicode(
340341
config=True,
341342
help='''
342-
Turn on Python profiling.
343+
Turn on the specified profiler.
344+
345+
The default (empty string) does not invoke a profiler.
346+
347+
Options:
348+
cprofile:
349+
Profile Python code execution time with cprofile.
343350
344-
The profile results will be saved to ~/.cylc/uiserver/profile.prof
345-
in cprofile format.
351+
Results will be saved to ~/.cylc/uiserver/profile.prof
352+
in cprofile format.
353+
object_tracker:
354+
Track Python object memory usage with Pympler.
355+
356+
Results will be saved to ~/.cylc/uiserver/objects.pdf.
346357
''',
347-
default_value=False,
358+
default_value='',
348359
)
349360

350361
log_timeout = Float(
@@ -477,14 +488,13 @@ def initialize_settings(self):
477488
)
478489
)
479490

480-
# start profiling
481-
self.profiler = Profiler(
482-
# the profiler is designed to attach to a Cylc scheduler
483-
schd=SimpleNamespace(workflow_log_dir=USER_CONF_ROOT),
484-
# profiling is turned on via the "profile" traitlet
485-
enabled=self.profile,
486-
)
487-
self.profiler.start()
491+
profiler_cls = get_profiler(self.profile)
492+
if profiler_cls:
493+
self.profiler = profiler_cls(self)
494+
ioloop.PeriodicCallback(
495+
self.profiler.periodic,
496+
1000, # PT1S
497+
).start()
488498

489499
# start the async scan task running (do this on server start not init)
490500
ioloop.IOLoop.current().add_callback(
@@ -633,4 +643,7 @@ async def stop_extension(self):
633643

634644
# Destroy ZeroMQ context of all sockets
635645
self.workflows_mgr.context.destroy()
636-
self.profiler.stop()
646+
647+
# stop the profiler
648+
if getattr(self, 'profiler', None):
649+
self.profiler.shutdown()

cylc/uiserver/profilers.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
"""Profilers for the ServerApp instance.
18+
19+
This is effectively a cut-down version of the cylc.flow.main_loop plugin
20+
system, but only intended for use in developer extensions.
21+
22+
NOTE: All profiler specific imports are handled in their `__init__` methods
23+
to avoid importing profiler code when not requested.
24+
"""
25+
26+
from time import time
27+
from types import SimpleNamespace
28+
29+
from cylc.uiserver.config_util import USER_CONF_ROOT
30+
31+
32+
class Profiler:
33+
def __init__(self, app):
34+
self.app = app
35+
self.app.log.warning(f'Starting profiler: {self.__class__.__name__}')
36+
37+
def periodic(self):
38+
pass
39+
40+
def shutdown(self):
41+
pass
42+
43+
44+
class CProfiler(Profiler):
45+
"""Invoke cprofile via the cylc.flow.profiler interface."""
46+
47+
def __init__(self, app):
48+
Profiler.__init__(self, app)
49+
50+
from types import SimpleNamespace
51+
52+
from cylc.flow.profiler import Profiler
53+
54+
self.cprofiler = Profiler(
55+
# the profiler is designed to attach to a Cylc scheduler
56+
schd=SimpleNamespace(workflow_log_dir=USER_CONF_ROOT),
57+
enabled=True,
58+
)
59+
60+
self.cprofiler.start()
61+
62+
def periodic(self):
63+
pass
64+
65+
def shutdown(self):
66+
self.cprofiler.stop()
67+
68+
69+
class TrackObjects(Profiler):
70+
"""Invoke pympler.asized via the cylc.main_loop.log_memory interface."""
71+
72+
def __init__(self, app):
73+
Profiler.__init__(self, app)
74+
75+
from cylc.flow.main_loop.log_memory import (
76+
_compute_sizes,
77+
_plot,
78+
_transpose,
79+
)
80+
81+
self._compute_sizes = _compute_sizes
82+
self._transpose = _transpose
83+
self._plot = _plot
84+
self.data = []
85+
self.min_size = 100
86+
self.obj = app
87+
88+
def periodic(self):
89+
self.data.append(
90+
(
91+
time(),
92+
self._compute_sizes(self.obj, min_size=self.min_size),
93+
)
94+
)
95+
96+
def shutdown(self):
97+
self.periodic()
98+
fields, times = self._transpose(self.data)
99+
self._plot(
100+
fields,
101+
times,
102+
USER_CONF_ROOT,
103+
f'{self.obj} attrs > {self.min_size / 1000}kb',
104+
)
105+
106+
107+
class TrackDataStore(TrackObjects):
108+
"""Like TrackObjects but for the Data Store."""
109+
110+
def __init__(self, app):
111+
TrackObjects.__init__(self, app)
112+
self.obj = self.app.data_store_mgr
113+
114+
115+
116+
PROFILERS = {
117+
'cprofile': CProfiler,
118+
'track_objects': TrackObjects,
119+
'track_data_store': TrackDataStore,
120+
}
121+
122+
123+
def get_profiler(profiler: str):
124+
if not profiler:
125+
return None
126+
try:
127+
return PROFILERS[profiler]
128+
except KeyError:
129+
raise Exception(
130+
f'Unknown profiler: {profiler}'
131+
f'\nValid options: {", ".join(PROFILERS)}'
132+
)

0 commit comments

Comments
 (0)