Skip to content

Commit d504db3

Browse files
add extra profilers
1 parent 54b389b commit d504db3

File tree

3 files changed

+182
-19
lines changed

3 files changed

+182
-19
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,28 @@ Contributions welcome:
285285
c.CylcUIServer.ui_build_dir = os.path.expanduser('~/cylc-ui/dist')
286286
```
287287

288-
Note about testing: unlike cylc-flow, cylc-uiserver uses the
288+
**Note about testing:**
289+
290+
Unlike cylc-flow, cylc-uiserver uses the
289291
[pytest-tornasync](https://github.com/eukaryote/pytest-tornasync/) plugin
290292
instead of [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio).
291293
This means you should not decorate async test functions with
292294
`@pytest.mark.asyncio`.
293295

296+
**Profiling:**
297+
298+
There are some built-in profilers in the server, activate them using the
299+
`profile` trait, e.g:
300+
301+
```
302+
cylc gui --CylcUIServer.profile=track_data_store
303+
```
304+
305+
306+
See the
307+
[config docs](https://cylc.github.io/cylc-doc/stable/html/reference/config/ui-server.html#cylc.uiserver.app.CylcUIServer.profile)
308+
for more details.
309+
294310
## Copyright and Terms of Use
295311

296312
Copyright (C) 2019-<span actions:bind='current-year'>2025</span> NIWA & British Crown (Met Office) & Contributors.

cylc/uiserver/app.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
)
6363
import sys
6464
from textwrap import dedent
65-
from types import SimpleNamespace
6665
from typing import (
6766
List,
6867
Optional,
@@ -74,14 +73,13 @@
7473
from tornado import ioloop
7574
from tornado.web import RedirectHandler
7675
from traitlets import (
77-
Bool,
7876
Dict,
7977
Float,
8078
Int,
79+
Unicode,
8180
TraitError,
8281
TraitType,
8382
Undefined,
84-
Unicode,
8583
default,
8684
validate,
8785
)
@@ -90,7 +88,6 @@
9088
from cylc.flow.network.graphql import (
9189
CylcExecutionContext, IgnoreFieldMiddleware
9290
)
93-
from cylc.flow.profiler import Profiler
9491
from cylc.uiserver import __file__ as uis_pkg
9592
from cylc.uiserver.authorise import (
9693
Authorization,
@@ -109,6 +106,7 @@
109106
UIServerGraphQLHandler,
110107
UserProfileHandler,
111108
)
109+
from cylc.uiserver.profilers import get_profiler
112110
from cylc.uiserver.resolvers import Resolvers
113111
from cylc.uiserver.schema import schema
114112
from cylc.uiserver.graphql.tornado_ws import TornadoSubscriptionServer
@@ -336,15 +334,33 @@ class CylcUIServer(ExtensionApp):
336334
''',
337335
default_value=100,
338336
)
339-
profile = Bool(
337+
profile = Unicode(
340338
config=True,
341339
help='''
342-
Turn on Python profiling.
340+
Developer extension: Turn on the specified profiler.
341+
342+
The default (empty string) does not invoke a profiler.
343+
344+
Only one profiler may be run at a time.
345+
346+
Options:
347+
cprofile:
348+
Profile Python code execution time with cprofile.
343349
344-
The profile results will be saved to ~/.cylc/uiserver/profile.prof
345-
in cprofile format.
350+
Results will be saved to ~/.cylc/uiserver/profile.prof
351+
in cprofile format.
352+
track_objects:
353+
Track attributes of the CylcUIServer class.
354+
355+
Results will be saved to
356+
~/.cylc/uiserver/cylc.flow.main_loop.log_memory.pdf.
357+
track_data_store
358+
Track attributes of the DataStoreMgr class.
359+
360+
Results will be saved to
361+
~/.cylc/uiserver/cylc.flow.main_loop.log_memory.pdf.
346362
''',
347-
default_value=False,
363+
default_value='',
348364
)
349365

350366
log_timeout = Float(
@@ -477,14 +493,13 @@ def initialize_settings(self):
477493
)
478494
)
479495

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()
496+
profiler_cls = get_profiler(self.profile)
497+
if profiler_cls:
498+
self.profiler = profiler_cls(self)
499+
ioloop.PeriodicCallback(
500+
self.profiler.periodic,
501+
1000, # PT1S
502+
).start()
488503

489504
# start the async scan task running (do this on server start not init)
490505
ioloop.IOLoop.current().add_callback(
@@ -633,4 +648,7 @@ async def stop_extension(self):
633648

634649
# Destroy ZeroMQ context of all sockets
635650
self.workflows_mgr.context.destroy()
636-
self.profiler.stop()
651+
652+
# stop the profiler
653+
if getattr(self, 'profiler', None):
654+
self.profiler.shutdown()

cylc/uiserver/profilers.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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. It's only intended for developer use.
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 cylc.flow.profiler import Profiler as CylcCProfiler
51+
52+
self.cprofiler = CylcCProfiler(
53+
# the profiler is designed to attach to a Cylc scheduler
54+
schd=SimpleNamespace(workflow_log_dir=USER_CONF_ROOT),
55+
enabled=True,
56+
)
57+
58+
self.cprofiler.start()
59+
60+
def periodic(self):
61+
pass
62+
63+
def shutdown(self):
64+
self.cprofiler.stop()
65+
66+
67+
class TrackObjects(Profiler):
68+
"""Invoke pympler.asized via the cylc.main_loop.log_memory interface."""
69+
70+
def __init__(self, app):
71+
Profiler.__init__(self, app)
72+
73+
from cylc.flow.main_loop.log_memory import (
74+
_compute_sizes,
75+
_plot,
76+
_transpose,
77+
)
78+
79+
self._compute_sizes = _compute_sizes
80+
self._transpose = _transpose
81+
self._plot = _plot
82+
self.data = []
83+
self.min_size = 100
84+
self.obj = app
85+
86+
def periodic(self):
87+
self.data.append(
88+
(
89+
time(),
90+
self._compute_sizes(self.obj, min_size=self.min_size),
91+
)
92+
)
93+
94+
def shutdown(self):
95+
self.periodic()
96+
fields, times = self._transpose(self.data)
97+
self._plot(
98+
fields,
99+
times,
100+
USER_CONF_ROOT,
101+
f'{self.obj} attrs > {self.min_size / 1000}kb',
102+
)
103+
104+
105+
class TrackDataStore(TrackObjects):
106+
"""Like TrackObjects but for the Data Store."""
107+
108+
def __init__(self, app):
109+
TrackObjects.__init__(self, app)
110+
self.obj = self.app.data_store_mgr
111+
112+
113+
PROFILERS = {
114+
'cprofile': CProfiler,
115+
'track_objects': TrackObjects,
116+
'track_data_store': TrackDataStore,
117+
}
118+
119+
120+
def get_profiler(profiler: str):
121+
if not profiler:
122+
return None
123+
try:
124+
return PROFILERS[profiler]
125+
except KeyError:
126+
raise Exception(
127+
f'Unknown profiler: {profiler}'
128+
f'\nValid options: {", ".join(PROFILERS)}'
129+
)

0 commit comments

Comments
 (0)