ezmsg 3.7.3__tar.gz → 3.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ezmsg-3.7.3 → ezmsg-3.9.0}/PKG-INFO +6 -2
- {ezmsg-3.7.3 → ezmsg-3.9.0}/README.md +1 -1
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/functiondecorators.rst +4 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_configs.py +6 -6
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_toy.py +39 -10
- ezmsg-3.9.0/examples/profiling_tui.py +450 -0
- ezmsg-3.9.0/examples/settings_tui.py +389 -0
- ezmsg-3.9.0/examples/simple_async_publisher.py +33 -0
- ezmsg-3.9.0/examples/simple_async_subscriber.py +34 -0
- ezmsg-3.9.0/examples/simple_publisher.py +35 -0
- ezmsg-3.9.0/examples/simple_subscriber.py +30 -0
- ezmsg-3.9.0/examples/topology_tui.py +285 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/pyproject.toml +7 -2
- ezmsg-3.9.0/scripts/perf_ab.py +5 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/__init__.py +20 -1
- ezmsg-3.9.0/src/ezmsg/core/backend.py +1032 -0
- ezmsg-3.9.0/src/ezmsg/core/backendprocess.py +937 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/collection.py +12 -1
- ezmsg-3.9.0/src/ezmsg/core/command.py +112 -0
- ezmsg-3.9.0/src/ezmsg/core/commands/__init__.py +17 -0
- ezmsg-3.9.0/src/ezmsg/core/commands/common.py +26 -0
- ezmsg-3.9.0/src/ezmsg/core/commands/dashboard.py +77 -0
- ezmsg-3.9.0/src/ezmsg/core/commands/dashboard_cmd.py +43 -0
- ezmsg-3.9.0/src/ezmsg/core/commands/graphviz.py +19 -0
- ezmsg-3.9.0/src/ezmsg/core/commands/mermaid.py +77 -0
- ezmsg-3.9.0/src/ezmsg/core/commands/serve.py +47 -0
- ezmsg-3.9.0/src/ezmsg/core/commands/shutdown.py +26 -0
- ezmsg-3.9.0/src/ezmsg/core/commands/start.py +50 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/dag.py +67 -14
- ezmsg-3.9.0/src/ezmsg/core/graphcontext.py +793 -0
- ezmsg-3.9.0/src/ezmsg/core/graphmeta.py +379 -0
- ezmsg-3.9.0/src/ezmsg/core/graphserver.py +1969 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/messagechannel.py +25 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/netprotocol.py +51 -1
- ezmsg-3.9.0/src/ezmsg/core/processclient.py +483 -0
- ezmsg-3.9.0/src/ezmsg/core/profiling.py +445 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/pubclient.py +90 -19
- ezmsg-3.9.0/src/ezmsg/core/relay.py +42 -0
- ezmsg-3.9.0/src/ezmsg/core/settingsmeta.py +329 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/stream.py +119 -5
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/subclient.py +54 -8
- ezmsg-3.9.0/src/ezmsg/core/sync.py +444 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/unit.py +39 -8
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/debuglog.py +1 -1
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/key.py +2 -2
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/modify.py +1 -1
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/util.py +9 -5
- ezmsg-3.9.0/src/ezmsg/util/perf/ab.py +837 -0
- ezmsg-3.9.0/src/ezmsg/util/perf/analysis.py +1125 -0
- ezmsg-3.9.0/src/ezmsg/util/perf/command.py +39 -0
- ezmsg-3.9.0/src/ezmsg/util/perf/hotpath.py +437 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/impl.py +5 -4
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/run.py +77 -26
- ezmsg-3.9.0/tests/clean_shutdown_examples_runner.py +171 -0
- ezmsg-3.9.0/tests/perf_sync_overhead.py +119 -0
- ezmsg-3.9.0/tests/shutdown_runner.py +108 -0
- ezmsg-3.9.0/tests/test_clean_shutdown.py +225 -0
- ezmsg-3.9.0/tests/test_command.py +224 -0
- ezmsg-3.9.0/tests/test_dashboard_commands.py +207 -0
- ezmsg-3.9.0/tests/test_graph_session.py +106 -0
- ezmsg-3.9.0/tests/test_perf_ab.py +111 -0
- ezmsg-3.9.0/tests/test_perf_analysis.py +218 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_perf_configs.py +31 -1
- ezmsg-3.9.0/tests/test_perf_hotpath.py +45 -0
- ezmsg-3.9.0/tests/test_process_control.py +186 -0
- ezmsg-3.9.0/tests/test_process_routing.py +150 -0
- ezmsg-3.9.0/tests/test_profiling_api.py +636 -0
- ezmsg-3.9.0/tests/test_pubclient.py +203 -0
- ezmsg-3.9.0/tests/test_settings_api.py +538 -0
- ezmsg-3.9.0/tests/test_subclient.py +150 -0
- ezmsg-3.9.0/tests/test_sync_api.py +171 -0
- ezmsg-3.9.0/tests/test_topics.py +189 -0
- ezmsg-3.9.0/tests/test_topology_api.py +105 -0
- ezmsg-3.7.3/examples/lowlevel_api.py +0 -119
- ezmsg-3.7.3/src/ezmsg/core/backend.py +0 -576
- ezmsg-3.7.3/src/ezmsg/core/backendprocess.py +0 -498
- ezmsg-3.7.3/src/ezmsg/core/command.py +0 -225
- ezmsg-3.7.3/src/ezmsg/core/graphcontext.py +0 -173
- ezmsg-3.7.3/src/ezmsg/core/graphserver.py +0 -614
- ezmsg-3.7.3/src/ezmsg/util/perf/analysis.py +0 -499
- ezmsg-3.7.3/src/ezmsg/util/perf/command.py +0 -19
- {ezmsg-3.7.3 → ezmsg-3.9.0}/.flake8 +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/.git-blame-ignore-revs +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/.github/workflows/ci.yml +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/.github/workflows/docs.yml +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/.github/workflows/publish.yml +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/.gitignore +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/.pre-commit-config.yaml +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/LICENSE +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/Makefile +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/make.bat +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/cloning.png +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/ezmsg_logo.png +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_graph.png +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_graph_compact.png +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_graphviz.png +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_pipeline.png +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_system.png +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/custom.css +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/conf.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/index.rst +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/axisarray-util-units.rst +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/axisarray.rst +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/components.rst +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/content-api.rst +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/entrypoint.rst +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/util-units.rst +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/content-reference.rst +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/.gitignore +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_attach.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_count.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_generator.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_intro.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_leaky_subscriber.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_log_to_file.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_normalterm.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_stop.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/__main__.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/addressable.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/backpressure.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/channelmanager.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/component.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/graph_util.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/message.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/messagecache.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/messagemarshal.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/settings.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/shm.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/state.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/test.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/util.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/__init__.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/gen_to_unit.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/generator.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagecodec.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagegate.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagelogger.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagequeue.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagereplay.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/__init__.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/axisarray.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/chunker.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/__init__.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/envinfo.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/util.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/profiler.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/rate.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/terminate.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/__init__.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/ez_test_utils.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/__init__.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/test_axisarray.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/test_chunker.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/test_key.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/test_modify.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_addressable.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_attach.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_channel.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_channelmanager.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_connections.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_dag.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_generator.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_graph.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_graph_visualization.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_leaky_subscriber.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_messagereplay.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_profiler.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_run.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_shm.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_state.py +0 -0
- {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ezmsg
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.0
|
|
4
4
|
Summary: A simple DAG-based computation model
|
|
5
5
|
Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -20,6 +20,10 @@ Requires-Dist: array-api-compat>=1.12.0
|
|
|
20
20
|
Requires-Dist: typing-extensions>=4.14.1
|
|
21
21
|
Provides-Extra: axisarray
|
|
22
22
|
Requires-Dist: numpy>=2.2.6; extra == 'axisarray'
|
|
23
|
+
Provides-Extra: dashboard
|
|
24
|
+
Requires-Dist: ezmsg-dashboard; (python_version >= '3.11') and extra == 'dashboard'
|
|
25
|
+
Provides-Extra: perf
|
|
26
|
+
Requires-Dist: xarray>=2025.6.1; extra == 'perf'
|
|
23
27
|
Description-Content-Type: text/markdown
|
|
24
28
|
|
|
25
29
|
# <img src="https://raw.githubusercontent.com/iscoe/ezmsg/main/docs/source/logo.png" width="50"/> ezmsg
|
|
@@ -119,4 +123,4 @@ These publications provide insights into the practical applications and impact o
|
|
|
119
123
|
|
|
120
124
|
## Financial Support
|
|
121
125
|
|
|
122
|
-
`ezmsg` is supported by Johns Hopkins University (JHU), the JHU Applied Physics Laboratory (APL), and by the Wyss Center for Bio and Neuro Engineering.
|
|
126
|
+
`ezmsg` is supported by Johns Hopkins University (JHU), the JHU Applied Physics Laboratory (APL), Blackrock Neurotech and by the Wyss Center for Bio and Neuro Engineering.
|
|
@@ -95,4 +95,4 @@ These publications provide insights into the practical applications and impact o
|
|
|
95
95
|
|
|
96
96
|
## Financial Support
|
|
97
97
|
|
|
98
|
-
`ezmsg` is supported by Johns Hopkins University (JHU), the JHU Applied Physics Laboratory (APL), and by the Wyss Center for Bio and Neuro Engineering.
|
|
98
|
+
`ezmsg` is supported by Johns Hopkins University (JHU), the JHU Applied Physics Laboratory (APL), Blackrock Neurotech and by the Wyss Center for Bio and Neuro Engineering.
|
|
@@ -11,6 +11,10 @@ These function decorators can be added to member functions of an ezmsg ``Unit``
|
|
|
11
11
|
|
|
12
12
|
.. autodecorator:: ezmsg.core.thread
|
|
13
13
|
|
|
14
|
+
.. note::
|
|
15
|
+
``@ez.thread`` is deprecated and will be removed in a future release.
|
|
16
|
+
Prefer explicit background work via ``loop.run_in_executor(...)``.
|
|
17
|
+
|
|
14
18
|
.. autodecorator:: ezmsg.core.task
|
|
15
19
|
|
|
16
20
|
.. autodecorator:: ezmsg.core.process
|
|
@@ -82,8 +82,8 @@ class Listener(ez.Unit):
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
class PassthroughCollection(ez.Collection):
|
|
85
|
-
INPUT = ez.
|
|
86
|
-
OUTPUT = ez.
|
|
85
|
+
INPUT = ez.InputTopic(int)
|
|
86
|
+
OUTPUT = ez.OutputTopic(int)
|
|
87
87
|
|
|
88
88
|
def network(self) -> ez.NetworkDefinition:
|
|
89
89
|
return ((self.INPUT, self.OUTPUT),)
|
|
@@ -136,7 +136,7 @@ class NoPubNoSubSystem(ez.Collection):
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
class PubNoSubCollection(ez.Collection):
|
|
139
|
-
OUTPUT = ez.
|
|
139
|
+
OUTPUT = ez.OutputTopic(int)
|
|
140
140
|
GENERATE = Generator()
|
|
141
141
|
LOG = DebugLog()
|
|
142
142
|
|
|
@@ -148,7 +148,7 @@ class PubNoSubCollection(ez.Collection):
|
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
class SubNoPubCollection(ez.Collection):
|
|
151
|
-
INPUT = ez.
|
|
151
|
+
INPUT = ez.InputTopic(int)
|
|
152
152
|
LISTEN = Listener()
|
|
153
153
|
|
|
154
154
|
def network(self) -> ez.NetworkDefinition:
|
|
@@ -175,7 +175,7 @@ class PubNoSubPassthroughCollection(ez.Collection):
|
|
|
175
175
|
COLLECTION = PubNoSubCollection()
|
|
176
176
|
PASSTHROUGH = PassthroughCollection()
|
|
177
177
|
|
|
178
|
-
OUTPUT = ez.
|
|
178
|
+
OUTPUT = ez.OutputTopic(int)
|
|
179
179
|
|
|
180
180
|
def network(self) -> ez.NetworkDefinition:
|
|
181
181
|
return (
|
|
@@ -188,7 +188,7 @@ class SubNoPubPassthroughCollection(ez.Collection):
|
|
|
188
188
|
COLLECTION = SubNoPubCollection()
|
|
189
189
|
PASSTHROUGH = PassthroughCollection()
|
|
190
190
|
|
|
191
|
-
INPUT = ez.
|
|
191
|
+
INPUT = ez.InputTopic(int)
|
|
192
192
|
|
|
193
193
|
def network(self) -> ez.NetworkDefinition:
|
|
194
194
|
return (
|
|
@@ -24,20 +24,33 @@ class LFOSettings(ez.Settings):
|
|
|
24
24
|
update_rate: float = 2.0 # Hz, update rate
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class LFOState(ez.State):
|
|
28
|
+
start_time: float
|
|
29
|
+
cur_settings: LFOSettings
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
class LFO(ez.Unit):
|
|
28
33
|
SETTINGS = LFOSettings
|
|
34
|
+
STATE = LFOState
|
|
29
35
|
|
|
30
36
|
OUTPUT = ez.OutputStream(float)
|
|
31
37
|
|
|
38
|
+
INPUT_SETTINGS = ez.InputStream(LFOSettings)
|
|
39
|
+
|
|
32
40
|
async def initialize(self) -> None:
|
|
33
|
-
self.
|
|
41
|
+
self.STATE.cur_settings = self.SETTINGS
|
|
42
|
+
self.STATE.start_time = time.time()
|
|
34
43
|
|
|
44
|
+
@ez.subscriber(INPUT_SETTINGS)
|
|
45
|
+
async def on_settings(self, msg: LFOSettings) -> None:
|
|
46
|
+
self.STATE.cur_settings = msg
|
|
47
|
+
|
|
35
48
|
@ez.publisher(OUTPUT)
|
|
36
49
|
async def generate(self) -> AsyncGenerator:
|
|
37
50
|
while True:
|
|
38
|
-
t = time.time() - self.start_time
|
|
39
|
-
yield self.OUTPUT, math.sin(2.0 * math.pi * self.
|
|
40
|
-
await asyncio.sleep(1.0 / self.
|
|
51
|
+
t = time.time() - self.STATE.start_time
|
|
52
|
+
yield self.OUTPUT, math.sin(2.0 * math.pi * self.STATE.cur_settings.freq * t)
|
|
53
|
+
await asyncio.sleep(1.0 / self.STATE.cur_settings.update_rate)
|
|
41
54
|
|
|
42
55
|
|
|
43
56
|
# MESSAGE GENERATOR
|
|
@@ -45,17 +58,30 @@ class MessageGeneratorSettings(ez.Settings):
|
|
|
45
58
|
message: str
|
|
46
59
|
|
|
47
60
|
|
|
61
|
+
class MessageGeneratorState(ez.State):
|
|
62
|
+
cur_settings: MessageGeneratorSettings
|
|
63
|
+
|
|
64
|
+
|
|
48
65
|
class MessageGenerator(ez.Unit):
|
|
49
66
|
SETTINGS = MessageGeneratorSettings
|
|
67
|
+
STATE = MessageGeneratorState
|
|
50
68
|
|
|
51
69
|
OUTPUT = ez.OutputStream(str)
|
|
70
|
+
INPUT_SETTINGS = ez.InputStream(MessageGeneratorSettings)
|
|
71
|
+
|
|
72
|
+
async def initialize(self) -> None:
|
|
73
|
+
self.STATE.cur_settings = self.SETTINGS
|
|
74
|
+
|
|
75
|
+
@ez.subscriber(INPUT_SETTINGS)
|
|
76
|
+
async def on_settings(self, msg: MessageGeneratorSettings) -> None:
|
|
77
|
+
self.STATE.cur_settings = msg
|
|
52
78
|
|
|
53
79
|
@ez.publisher(OUTPUT)
|
|
54
80
|
async def spawn_message(self) -> AsyncGenerator:
|
|
55
81
|
while True:
|
|
56
82
|
await asyncio.sleep(1.0)
|
|
57
|
-
ez.logger.info(f"Spawning {self.
|
|
58
|
-
yield self.OUTPUT, self.
|
|
83
|
+
ez.logger.info(f"Spawning {self.STATE.cur_settings.message}")
|
|
84
|
+
yield self.OUTPUT, self.STATE.cur_settings.message
|
|
59
85
|
|
|
60
86
|
@ez.publisher(OUTPUT)
|
|
61
87
|
async def spawn_once(self) -> AsyncGenerator:
|
|
@@ -123,8 +149,8 @@ class ModifierCollection(ez.Collection):
|
|
|
123
149
|
"""This collection will subscribe to messages
|
|
124
150
|
and append the most recent LFO output"""
|
|
125
151
|
|
|
126
|
-
INPUT = ez.
|
|
127
|
-
OUTPUT = ez.
|
|
152
|
+
INPUT = ez.InputTopic(str)
|
|
153
|
+
OUTPUT = ez.OutputTopic(str)
|
|
128
154
|
|
|
129
155
|
SIN = LFO()
|
|
130
156
|
# SIN2 = LFO()
|
|
@@ -152,6 +178,8 @@ class TestSystemSettings(ez.Settings):
|
|
|
152
178
|
class TestSystem(ez.Collection):
|
|
153
179
|
SETTINGS = TestSystemSettings
|
|
154
180
|
|
|
181
|
+
OUTPUT_PING = ez.OutputTopic(str)
|
|
182
|
+
|
|
155
183
|
# Publishers
|
|
156
184
|
PING = MessageGenerator()
|
|
157
185
|
FOO = MessageGenerator()
|
|
@@ -173,6 +201,7 @@ class TestSystem(ez.Collection):
|
|
|
173
201
|
# Define Connections
|
|
174
202
|
def network(self) -> ez.NetworkDefinition:
|
|
175
203
|
return (
|
|
204
|
+
(self.PING.OUTPUT, self.OUTPUT_PING),
|
|
176
205
|
(self.PING.OUTPUT, self.PINGSUB1.INPUT),
|
|
177
206
|
(self.PING.OUTPUT, self.MODIFIER_COLLECTION.INPUT),
|
|
178
207
|
(self.MODIFIER_COLLECTION.OUTPUT, self.PINGSUB2.INPUT),
|
|
@@ -193,7 +222,7 @@ if __name__ == "__main__":
|
|
|
193
222
|
ez.run(
|
|
194
223
|
SYSTEM=system,
|
|
195
224
|
connections=[
|
|
196
|
-
# Make
|
|
197
|
-
(system.
|
|
225
|
+
# Make a system output available on a topic ezmsg_attach.py
|
|
226
|
+
(system.OUTPUT_PING, "GLOBAL_PING_TOPIC"),
|
|
198
227
|
],
|
|
199
228
|
)
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Simple live profiling TUI for ezmsg GraphServer.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- Periodic profiling snapshot view broken out by publisher/subscriber endpoints
|
|
7
|
+
- Live trace sample counts via GraphContext.subscribe_profiling_trace()
|
|
8
|
+
- Optional automatic trace enablement for discovered processes
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
.venv/bin/python examples/profiling_tui.py --host 127.0.0.1 --port 25978
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import asyncio
|
|
18
|
+
import contextlib
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from uuid import UUID
|
|
22
|
+
|
|
23
|
+
from ezmsg.core.graphcontext import GraphContext
|
|
24
|
+
from ezmsg.core.graphmeta import (
|
|
25
|
+
ProcessProfilingSnapshot,
|
|
26
|
+
ProfilingStreamControl,
|
|
27
|
+
ProfilingTraceControl,
|
|
28
|
+
)
|
|
29
|
+
from ezmsg.core.netprotocol import DEFAULT_HOST, GRAPHSERVER_PORT_DEFAULT
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _truncate(text: object, width: int) -> str:
|
|
33
|
+
text = str(text)
|
|
34
|
+
if width <= 3:
|
|
35
|
+
return text[:width]
|
|
36
|
+
if len(text) <= width:
|
|
37
|
+
return text
|
|
38
|
+
return text[: width - 3] + "..."
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _fmt_float(value: float, digits: int = 2) -> str:
|
|
42
|
+
return f"{value:.{digits}f}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class PublisherView:
|
|
47
|
+
process_id: UUID
|
|
48
|
+
topic: str
|
|
49
|
+
endpoint_id: str
|
|
50
|
+
published_total: int
|
|
51
|
+
published_window: int
|
|
52
|
+
publish_rate_hz: float
|
|
53
|
+
publish_delta_ms_avg: float
|
|
54
|
+
inflight_current: int
|
|
55
|
+
inflight_peak: int
|
|
56
|
+
trace_samples_seen: int
|
|
57
|
+
trace_last_age_s: float | None
|
|
58
|
+
backpressure_wait_ms_window: float
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class SubscriberView:
|
|
63
|
+
process_id: UUID
|
|
64
|
+
topic: str
|
|
65
|
+
endpoint_id: str
|
|
66
|
+
channel_kind: str
|
|
67
|
+
received_total: int
|
|
68
|
+
received_window: int
|
|
69
|
+
lease_time_ms_avg: float
|
|
70
|
+
user_span_ms_avg: float
|
|
71
|
+
attributable_backpressure_ms_window: float
|
|
72
|
+
attributable_backpressure_events_total: int
|
|
73
|
+
trace_samples_seen: int
|
|
74
|
+
trace_last_age_s: float | None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ProfilingTUI:
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
ctx: GraphContext,
|
|
81
|
+
*,
|
|
82
|
+
snapshot_interval: float,
|
|
83
|
+
trace_interval: float,
|
|
84
|
+
trace_max_samples: int,
|
|
85
|
+
auto_trace: bool,
|
|
86
|
+
trace_sample_mod: int,
|
|
87
|
+
max_rows: int,
|
|
88
|
+
) -> None:
|
|
89
|
+
self.ctx = ctx
|
|
90
|
+
self.snapshot_interval = max(0.2, snapshot_interval)
|
|
91
|
+
self.trace_interval = max(0.01, trace_interval)
|
|
92
|
+
self.trace_max_samples = max(1, trace_max_samples)
|
|
93
|
+
self.auto_trace = auto_trace
|
|
94
|
+
self.trace_sample_mod = max(1, trace_sample_mod)
|
|
95
|
+
self.max_rows = max(5, max_rows)
|
|
96
|
+
|
|
97
|
+
self.snapshots: dict[UUID, ProcessProfilingSnapshot] = {}
|
|
98
|
+
self.route_units: dict[UUID, str] = {}
|
|
99
|
+
self.trace_enabled_processes: set[UUID] = set()
|
|
100
|
+
self.trace_errors: dict[UUID, str] = {}
|
|
101
|
+
self.trace_samples_seen_by_endpoint: dict[str, int] = {}
|
|
102
|
+
self.trace_last_timestamp_by_endpoint: dict[str, float] = {}
|
|
103
|
+
self.last_snapshot_time: float | None = None
|
|
104
|
+
|
|
105
|
+
self._snapshot_task: asyncio.Task[None] | None = None
|
|
106
|
+
self._trace_task: asyncio.Task[None] | None = None
|
|
107
|
+
|
|
108
|
+
async def start(self) -> None:
|
|
109
|
+
await self._refresh_snapshot()
|
|
110
|
+
self._snapshot_task = asyncio.create_task(self._snapshot_loop())
|
|
111
|
+
self._trace_task = asyncio.create_task(self._trace_loop())
|
|
112
|
+
|
|
113
|
+
async def close(self) -> None:
|
|
114
|
+
for task in (self._snapshot_task, self._trace_task):
|
|
115
|
+
if task is not None:
|
|
116
|
+
task.cancel()
|
|
117
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
118
|
+
await task
|
|
119
|
+
|
|
120
|
+
if not self.auto_trace:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
for process_id, route_unit in self.route_units.items():
|
|
124
|
+
if process_id not in self.trace_enabled_processes:
|
|
125
|
+
continue
|
|
126
|
+
with contextlib.suppress(Exception):
|
|
127
|
+
await self.ctx.process_set_profiling_trace(
|
|
128
|
+
route_unit,
|
|
129
|
+
ProfilingTraceControl(enabled=False),
|
|
130
|
+
timeout=0.5,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def _snapshot_loop(self) -> None:
|
|
134
|
+
while True:
|
|
135
|
+
await self._refresh_snapshot()
|
|
136
|
+
await asyncio.sleep(self.snapshot_interval)
|
|
137
|
+
|
|
138
|
+
async def _refresh_snapshot(self) -> None:
|
|
139
|
+
graph_snapshot = await self.ctx.snapshot()
|
|
140
|
+
route_units: dict[UUID, str] = {}
|
|
141
|
+
for process in graph_snapshot.processes.values():
|
|
142
|
+
if process.units:
|
|
143
|
+
route_units[process.process_id] = process.units[0]
|
|
144
|
+
self.route_units = route_units
|
|
145
|
+
|
|
146
|
+
if self.auto_trace:
|
|
147
|
+
for process_id, route_unit in route_units.items():
|
|
148
|
+
if process_id in self.trace_enabled_processes:
|
|
149
|
+
continue
|
|
150
|
+
try:
|
|
151
|
+
response = await self.ctx.process_set_profiling_trace(
|
|
152
|
+
route_unit,
|
|
153
|
+
ProfilingTraceControl(
|
|
154
|
+
enabled=True,
|
|
155
|
+
sample_mod=self.trace_sample_mod,
|
|
156
|
+
),
|
|
157
|
+
timeout=0.5,
|
|
158
|
+
)
|
|
159
|
+
if response.ok:
|
|
160
|
+
self.trace_enabled_processes.add(process_id)
|
|
161
|
+
self.trace_errors.pop(process_id, None)
|
|
162
|
+
else:
|
|
163
|
+
self.trace_errors[process_id] = str(
|
|
164
|
+
response.error or "unknown error"
|
|
165
|
+
)
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
self.trace_errors[process_id] = str(exc)
|
|
168
|
+
|
|
169
|
+
self.snapshots = await self.ctx.profiling_snapshot_all(
|
|
170
|
+
timeout_per_process=max(0.1, self.snapshot_interval * 0.8)
|
|
171
|
+
)
|
|
172
|
+
self.last_snapshot_time = time.time()
|
|
173
|
+
|
|
174
|
+
async def _trace_loop(self) -> None:
|
|
175
|
+
async for batch in self.ctx.subscribe_profiling_trace(
|
|
176
|
+
ProfilingStreamControl(
|
|
177
|
+
interval=self.trace_interval,
|
|
178
|
+
max_samples=self.trace_max_samples,
|
|
179
|
+
)
|
|
180
|
+
):
|
|
181
|
+
for process_batch in batch.batches.values():
|
|
182
|
+
for sample in process_batch.samples:
|
|
183
|
+
endpoint_id = sample.endpoint_id
|
|
184
|
+
self.trace_samples_seen_by_endpoint[endpoint_id] = (
|
|
185
|
+
self.trace_samples_seen_by_endpoint.get(endpoint_id, 0) + 1
|
|
186
|
+
)
|
|
187
|
+
self.trace_last_timestamp_by_endpoint[endpoint_id] = batch.timestamp
|
|
188
|
+
|
|
189
|
+
def _trace_for_endpoint(self, endpoint_id: str) -> tuple[int, float | None]:
|
|
190
|
+
now = time.time()
|
|
191
|
+
count = self.trace_samples_seen_by_endpoint.get(endpoint_id, 0)
|
|
192
|
+
ts = self.trace_last_timestamp_by_endpoint.get(endpoint_id)
|
|
193
|
+
age = None if ts is None else max(0.0, now - ts)
|
|
194
|
+
return count, age
|
|
195
|
+
|
|
196
|
+
def _publisher_rows(self) -> list[PublisherView]:
|
|
197
|
+
rows: list[PublisherView] = []
|
|
198
|
+
for process_id, snapshot in self.snapshots.items():
|
|
199
|
+
for pub in snapshot.publishers.values():
|
|
200
|
+
trace_count, trace_age = self._trace_for_endpoint(pub.endpoint_id)
|
|
201
|
+
rows.append(
|
|
202
|
+
PublisherView(
|
|
203
|
+
process_id=process_id,
|
|
204
|
+
topic=pub.topic,
|
|
205
|
+
endpoint_id=pub.endpoint_id,
|
|
206
|
+
published_total=pub.messages_published_total,
|
|
207
|
+
published_window=pub.messages_published_window,
|
|
208
|
+
publish_rate_hz=pub.publish_rate_hz_window,
|
|
209
|
+
publish_delta_ms_avg=pub.publish_delta_ns_avg_window / 1_000_000.0,
|
|
210
|
+
inflight_current=pub.inflight_messages_current,
|
|
211
|
+
inflight_peak=pub.inflight_messages_peak_window,
|
|
212
|
+
trace_samples_seen=trace_count,
|
|
213
|
+
trace_last_age_s=trace_age,
|
|
214
|
+
backpressure_wait_ms_window=(
|
|
215
|
+
pub.backpressure_wait_ns_window / 1_000_000.0
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
rows.sort(
|
|
220
|
+
key=lambda row: (
|
|
221
|
+
-row.publish_rate_hz,
|
|
222
|
+
-row.published_total,
|
|
223
|
+
row.process_id,
|
|
224
|
+
row.topic,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
return rows
|
|
228
|
+
|
|
229
|
+
def _subscriber_rows(self) -> list[SubscriberView]:
|
|
230
|
+
rows: list[SubscriberView] = []
|
|
231
|
+
for process_id, snapshot in self.snapshots.items():
|
|
232
|
+
for sub in snapshot.subscribers.values():
|
|
233
|
+
trace_count, trace_age = self._trace_for_endpoint(sub.endpoint_id)
|
|
234
|
+
channel_kind = (
|
|
235
|
+
sub.channel_kind_last.value
|
|
236
|
+
if hasattr(sub.channel_kind_last, "value")
|
|
237
|
+
else str(sub.channel_kind_last)
|
|
238
|
+
)
|
|
239
|
+
rows.append(
|
|
240
|
+
SubscriberView(
|
|
241
|
+
process_id=process_id,
|
|
242
|
+
topic=sub.topic,
|
|
243
|
+
endpoint_id=sub.endpoint_id,
|
|
244
|
+
channel_kind=channel_kind,
|
|
245
|
+
received_total=sub.messages_received_total,
|
|
246
|
+
received_window=sub.messages_received_window,
|
|
247
|
+
lease_time_ms_avg=sub.lease_time_ns_avg_window / 1_000_000.0,
|
|
248
|
+
user_span_ms_avg=sub.user_span_ns_avg_window / 1_000_000.0,
|
|
249
|
+
attributable_backpressure_ms_window=(
|
|
250
|
+
sub.attributable_backpressure_ns_window / 1_000_000.0
|
|
251
|
+
),
|
|
252
|
+
attributable_backpressure_events_total=(
|
|
253
|
+
sub.attributable_backpressure_events_total
|
|
254
|
+
),
|
|
255
|
+
trace_samples_seen=trace_count,
|
|
256
|
+
trace_last_age_s=trace_age,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
rows.sort(
|
|
260
|
+
key=lambda row: (
|
|
261
|
+
-row.lease_time_ms_avg,
|
|
262
|
+
-row.received_total,
|
|
263
|
+
row.process_id,
|
|
264
|
+
row.topic,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
return rows
|
|
268
|
+
|
|
269
|
+
def render(self) -> None:
|
|
270
|
+
print("\x1bc", end="")
|
|
271
|
+
print("ezmsg profiling tui")
|
|
272
|
+
print("Ctrl-C to quit")
|
|
273
|
+
print(
|
|
274
|
+
"snapshot interval="
|
|
275
|
+
f"{self.snapshot_interval:.2f}s, trace interval={self.trace_interval:.2f}s, "
|
|
276
|
+
f"trace max_samples={self.trace_max_samples}, auto_trace={self.auto_trace}"
|
|
277
|
+
)
|
|
278
|
+
if self.last_snapshot_time is not None:
|
|
279
|
+
print(
|
|
280
|
+
"last snapshot age: "
|
|
281
|
+
f"{_fmt_float(max(0.0, time.time() - self.last_snapshot_time), 2)}s"
|
|
282
|
+
)
|
|
283
|
+
print(
|
|
284
|
+
f"processes discovered={len(self.route_units)} "
|
|
285
|
+
f"publishers={sum(len(s.publishers) for s in self.snapshots.values())} "
|
|
286
|
+
f"subscribers={sum(len(s.subscribers) for s in self.snapshots.values())}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
publisher_rows = self._publisher_rows()
|
|
290
|
+
subscriber_rows = self._subscriber_rows()
|
|
291
|
+
|
|
292
|
+
print("\nPublishers")
|
|
293
|
+
pub_header = (
|
|
294
|
+
f"{'Process':<20} {'Topic':<26} {'Endpoint':<24} "
|
|
295
|
+
f"{'Total':>8} {'Win':>6} {'RateHz':>8} {'DeltaMs':>8} "
|
|
296
|
+
f"{'InFl':>5} {'InPk':>5} {'BPmsW':>8} {'Trace':>7} {'TAge':>6}"
|
|
297
|
+
)
|
|
298
|
+
print(pub_header)
|
|
299
|
+
print("-" * len(pub_header))
|
|
300
|
+
if not publisher_rows:
|
|
301
|
+
print("<none>")
|
|
302
|
+
else:
|
|
303
|
+
for row in publisher_rows[: self.max_rows]:
|
|
304
|
+
trace_age = (
|
|
305
|
+
"-" if row.trace_last_age_s is None else _fmt_float(row.trace_last_age_s, 2)
|
|
306
|
+
)
|
|
307
|
+
print(
|
|
308
|
+
f"{_truncate(row.process_id, 20):<20} "
|
|
309
|
+
f"{_truncate(row.topic, 26):<26} "
|
|
310
|
+
f"{_truncate(row.endpoint_id, 24):<24} "
|
|
311
|
+
f"{row.published_total:>8} "
|
|
312
|
+
f"{row.published_window:>6} "
|
|
313
|
+
f"{_fmt_float(row.publish_rate_hz, 2):>8} "
|
|
314
|
+
f"{_fmt_float(row.publish_delta_ms_avg, 2):>8} "
|
|
315
|
+
f"{row.inflight_current:>5} "
|
|
316
|
+
f"{row.inflight_peak:>5} "
|
|
317
|
+
f"{_fmt_float(row.backpressure_wait_ms_window, 2):>8} "
|
|
318
|
+
f"{row.trace_samples_seen:>7} "
|
|
319
|
+
f"{trace_age:>6}"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
print("\nSubscribers")
|
|
323
|
+
sub_header = (
|
|
324
|
+
f"{'Process':<20} {'Topic':<26} {'Endpoint':<24} {'Kind':<6} "
|
|
325
|
+
f"{'Total':>8} {'Win':>6} {'LeaseMs':>8} {'UserMs':>8} "
|
|
326
|
+
f"{'BPmsW':>8} {'BPev':>6} {'Trace':>7} {'TAge':>6}"
|
|
327
|
+
)
|
|
328
|
+
print(sub_header)
|
|
329
|
+
print("-" * len(sub_header))
|
|
330
|
+
if not subscriber_rows:
|
|
331
|
+
print("<none>")
|
|
332
|
+
else:
|
|
333
|
+
for row in subscriber_rows[: self.max_rows]:
|
|
334
|
+
trace_age = (
|
|
335
|
+
"-" if row.trace_last_age_s is None else _fmt_float(row.trace_last_age_s, 2)
|
|
336
|
+
)
|
|
337
|
+
print(
|
|
338
|
+
f"{_truncate(row.process_id, 20):<20} "
|
|
339
|
+
f"{_truncate(row.topic, 26):<26} "
|
|
340
|
+
f"{_truncate(row.endpoint_id, 24):<24} "
|
|
341
|
+
f"{_truncate(row.channel_kind, 6):<6} "
|
|
342
|
+
f"{row.received_total:>8} "
|
|
343
|
+
f"{row.received_window:>6} "
|
|
344
|
+
f"{_fmt_float(row.lease_time_ms_avg, 2):>8} "
|
|
345
|
+
f"{_fmt_float(row.user_span_ms_avg, 2):>8} "
|
|
346
|
+
f"{_fmt_float(row.attributable_backpressure_ms_window, 2):>8} "
|
|
347
|
+
f"{row.attributable_backpressure_events_total:>6} "
|
|
348
|
+
f"{row.trace_samples_seen:>7} "
|
|
349
|
+
f"{trace_age:>6}"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if self.trace_errors:
|
|
353
|
+
print("\ntrace errors:")
|
|
354
|
+
for process_id, err in sorted(self.trace_errors.items(), key=lambda item: str(item[0])):
|
|
355
|
+
print(f" {_truncate(str(process_id), 30)}: {_truncate(err, 120)}")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _parse_address(host: str, port: int) -> tuple[str, int]:
|
|
359
|
+
return (host, port)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
async def _run_tui(args: argparse.Namespace) -> None:
|
|
363
|
+
async with GraphContext(
|
|
364
|
+
_parse_address(args.host, args.port), auto_start=args.auto_start
|
|
365
|
+
) as ctx:
|
|
366
|
+
tui = ProfilingTUI(
|
|
367
|
+
ctx,
|
|
368
|
+
snapshot_interval=args.snapshot_interval,
|
|
369
|
+
trace_interval=args.trace_interval,
|
|
370
|
+
trace_max_samples=args.max_samples,
|
|
371
|
+
auto_trace=args.auto_trace,
|
|
372
|
+
trace_sample_mod=args.sample_mod,
|
|
373
|
+
max_rows=args.max_rows,
|
|
374
|
+
)
|
|
375
|
+
await tui.start()
|
|
376
|
+
try:
|
|
377
|
+
while True:
|
|
378
|
+
tui.render()
|
|
379
|
+
await asyncio.sleep(max(0.1, args.render_interval))
|
|
380
|
+
finally:
|
|
381
|
+
await tui.close()
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
385
|
+
parser = argparse.ArgumentParser(description="ezmsg profiling TUI")
|
|
386
|
+
parser.add_argument("--host", default=DEFAULT_HOST, help="GraphServer host")
|
|
387
|
+
parser.add_argument(
|
|
388
|
+
"--port",
|
|
389
|
+
type=int,
|
|
390
|
+
default=GRAPHSERVER_PORT_DEFAULT,
|
|
391
|
+
help="GraphServer port",
|
|
392
|
+
)
|
|
393
|
+
parser.add_argument(
|
|
394
|
+
"--auto-start",
|
|
395
|
+
action="store_true",
|
|
396
|
+
help="Allow GraphContext to auto-start GraphServer if unavailable",
|
|
397
|
+
)
|
|
398
|
+
parser.add_argument(
|
|
399
|
+
"--snapshot-interval",
|
|
400
|
+
type=float,
|
|
401
|
+
default=1.0,
|
|
402
|
+
help="Seconds between snapshot refreshes",
|
|
403
|
+
)
|
|
404
|
+
parser.add_argument(
|
|
405
|
+
"--trace-interval",
|
|
406
|
+
type=float,
|
|
407
|
+
default=0.02,
|
|
408
|
+
help="Seconds between GraphServer trace stream batches",
|
|
409
|
+
)
|
|
410
|
+
parser.add_argument(
|
|
411
|
+
"--max-samples",
|
|
412
|
+
type=int,
|
|
413
|
+
default=5000,
|
|
414
|
+
help="Max samples per process per streamed batch",
|
|
415
|
+
)
|
|
416
|
+
parser.add_argument(
|
|
417
|
+
"--sample-mod",
|
|
418
|
+
type=int,
|
|
419
|
+
default=1,
|
|
420
|
+
help="Trace sampling divisor when auto-enabling trace",
|
|
421
|
+
)
|
|
422
|
+
parser.add_argument(
|
|
423
|
+
"--render-interval",
|
|
424
|
+
type=float,
|
|
425
|
+
default=0.5,
|
|
426
|
+
help="Seconds between TUI redraws",
|
|
427
|
+
)
|
|
428
|
+
parser.add_argument(
|
|
429
|
+
"--max-rows",
|
|
430
|
+
type=int,
|
|
431
|
+
default=30,
|
|
432
|
+
help="Max publisher/subscriber rows to render per table",
|
|
433
|
+
)
|
|
434
|
+
parser.add_argument(
|
|
435
|
+
"--no-auto-trace",
|
|
436
|
+
action="store_true",
|
|
437
|
+
help="Do not auto-enable trace mode on discovered processes",
|
|
438
|
+
)
|
|
439
|
+
return parser
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def main() -> None:
|
|
443
|
+
parser = _build_parser()
|
|
444
|
+
args = parser.parse_args()
|
|
445
|
+
args.auto_trace = not args.no_auto_trace
|
|
446
|
+
asyncio.run(_run_tui(args))
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
if __name__ == "__main__":
|
|
450
|
+
main()
|