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.
Files changed (171) hide show
  1. {ezmsg-3.7.3 → ezmsg-3.9.0}/PKG-INFO +6 -2
  2. {ezmsg-3.7.3 → ezmsg-3.9.0}/README.md +1 -1
  3. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/functiondecorators.rst +4 -0
  4. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_configs.py +6 -6
  5. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_toy.py +39 -10
  6. ezmsg-3.9.0/examples/profiling_tui.py +450 -0
  7. ezmsg-3.9.0/examples/settings_tui.py +389 -0
  8. ezmsg-3.9.0/examples/simple_async_publisher.py +33 -0
  9. ezmsg-3.9.0/examples/simple_async_subscriber.py +34 -0
  10. ezmsg-3.9.0/examples/simple_publisher.py +35 -0
  11. ezmsg-3.9.0/examples/simple_subscriber.py +30 -0
  12. ezmsg-3.9.0/examples/topology_tui.py +285 -0
  13. {ezmsg-3.7.3 → ezmsg-3.9.0}/pyproject.toml +7 -2
  14. ezmsg-3.9.0/scripts/perf_ab.py +5 -0
  15. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/__init__.py +20 -1
  16. ezmsg-3.9.0/src/ezmsg/core/backend.py +1032 -0
  17. ezmsg-3.9.0/src/ezmsg/core/backendprocess.py +937 -0
  18. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/collection.py +12 -1
  19. ezmsg-3.9.0/src/ezmsg/core/command.py +112 -0
  20. ezmsg-3.9.0/src/ezmsg/core/commands/__init__.py +17 -0
  21. ezmsg-3.9.0/src/ezmsg/core/commands/common.py +26 -0
  22. ezmsg-3.9.0/src/ezmsg/core/commands/dashboard.py +77 -0
  23. ezmsg-3.9.0/src/ezmsg/core/commands/dashboard_cmd.py +43 -0
  24. ezmsg-3.9.0/src/ezmsg/core/commands/graphviz.py +19 -0
  25. ezmsg-3.9.0/src/ezmsg/core/commands/mermaid.py +77 -0
  26. ezmsg-3.9.0/src/ezmsg/core/commands/serve.py +47 -0
  27. ezmsg-3.9.0/src/ezmsg/core/commands/shutdown.py +26 -0
  28. ezmsg-3.9.0/src/ezmsg/core/commands/start.py +50 -0
  29. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/dag.py +67 -14
  30. ezmsg-3.9.0/src/ezmsg/core/graphcontext.py +793 -0
  31. ezmsg-3.9.0/src/ezmsg/core/graphmeta.py +379 -0
  32. ezmsg-3.9.0/src/ezmsg/core/graphserver.py +1969 -0
  33. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/messagechannel.py +25 -0
  34. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/netprotocol.py +51 -1
  35. ezmsg-3.9.0/src/ezmsg/core/processclient.py +483 -0
  36. ezmsg-3.9.0/src/ezmsg/core/profiling.py +445 -0
  37. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/pubclient.py +90 -19
  38. ezmsg-3.9.0/src/ezmsg/core/relay.py +42 -0
  39. ezmsg-3.9.0/src/ezmsg/core/settingsmeta.py +329 -0
  40. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/stream.py +119 -5
  41. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/subclient.py +54 -8
  42. ezmsg-3.9.0/src/ezmsg/core/sync.py +444 -0
  43. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/unit.py +39 -8
  44. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/debuglog.py +1 -1
  45. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/key.py +2 -2
  46. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/modify.py +1 -1
  47. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/util.py +9 -5
  48. ezmsg-3.9.0/src/ezmsg/util/perf/ab.py +837 -0
  49. ezmsg-3.9.0/src/ezmsg/util/perf/analysis.py +1125 -0
  50. ezmsg-3.9.0/src/ezmsg/util/perf/command.py +39 -0
  51. ezmsg-3.9.0/src/ezmsg/util/perf/hotpath.py +437 -0
  52. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/impl.py +5 -4
  53. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/run.py +77 -26
  54. ezmsg-3.9.0/tests/clean_shutdown_examples_runner.py +171 -0
  55. ezmsg-3.9.0/tests/perf_sync_overhead.py +119 -0
  56. ezmsg-3.9.0/tests/shutdown_runner.py +108 -0
  57. ezmsg-3.9.0/tests/test_clean_shutdown.py +225 -0
  58. ezmsg-3.9.0/tests/test_command.py +224 -0
  59. ezmsg-3.9.0/tests/test_dashboard_commands.py +207 -0
  60. ezmsg-3.9.0/tests/test_graph_session.py +106 -0
  61. ezmsg-3.9.0/tests/test_perf_ab.py +111 -0
  62. ezmsg-3.9.0/tests/test_perf_analysis.py +218 -0
  63. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_perf_configs.py +31 -1
  64. ezmsg-3.9.0/tests/test_perf_hotpath.py +45 -0
  65. ezmsg-3.9.0/tests/test_process_control.py +186 -0
  66. ezmsg-3.9.0/tests/test_process_routing.py +150 -0
  67. ezmsg-3.9.0/tests/test_profiling_api.py +636 -0
  68. ezmsg-3.9.0/tests/test_pubclient.py +203 -0
  69. ezmsg-3.9.0/tests/test_settings_api.py +538 -0
  70. ezmsg-3.9.0/tests/test_subclient.py +150 -0
  71. ezmsg-3.9.0/tests/test_sync_api.py +171 -0
  72. ezmsg-3.9.0/tests/test_topics.py +189 -0
  73. ezmsg-3.9.0/tests/test_topology_api.py +105 -0
  74. ezmsg-3.7.3/examples/lowlevel_api.py +0 -119
  75. ezmsg-3.7.3/src/ezmsg/core/backend.py +0 -576
  76. ezmsg-3.7.3/src/ezmsg/core/backendprocess.py +0 -498
  77. ezmsg-3.7.3/src/ezmsg/core/command.py +0 -225
  78. ezmsg-3.7.3/src/ezmsg/core/graphcontext.py +0 -173
  79. ezmsg-3.7.3/src/ezmsg/core/graphserver.py +0 -614
  80. ezmsg-3.7.3/src/ezmsg/util/perf/analysis.py +0 -499
  81. ezmsg-3.7.3/src/ezmsg/util/perf/command.py +0 -19
  82. {ezmsg-3.7.3 → ezmsg-3.9.0}/.flake8 +0 -0
  83. {ezmsg-3.7.3 → ezmsg-3.9.0}/.git-blame-ignore-revs +0 -0
  84. {ezmsg-3.7.3 → ezmsg-3.9.0}/.github/workflows/ci.yml +0 -0
  85. {ezmsg-3.7.3 → ezmsg-3.9.0}/.github/workflows/docs.yml +0 -0
  86. {ezmsg-3.7.3 → ezmsg-3.9.0}/.github/workflows/publish.yml +0 -0
  87. {ezmsg-3.7.3 → ezmsg-3.9.0}/.gitignore +0 -0
  88. {ezmsg-3.7.3 → ezmsg-3.9.0}/.pre-commit-config.yaml +0 -0
  89. {ezmsg-3.7.3 → ezmsg-3.9.0}/LICENSE +0 -0
  90. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/Makefile +0 -0
  91. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/make.bat +0 -0
  92. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/cloning.png +0 -0
  93. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/ezmsg_logo.png +0 -0
  94. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_graph.png +0 -0
  95. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_graph_compact.png +0 -0
  96. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_graphviz.png +0 -0
  97. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_pipeline.png +0 -0
  98. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/_images/tutorial_system.png +0 -0
  99. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/_static/custom.css +0 -0
  100. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/conf.py +0 -0
  101. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/index.rst +0 -0
  102. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/axisarray-util-units.rst +0 -0
  103. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/axisarray.rst +0 -0
  104. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/components.rst +0 -0
  105. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/content-api.rst +0 -0
  106. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/entrypoint.rst +0 -0
  107. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/API/util-units.rst +0 -0
  108. {ezmsg-3.7.3 → ezmsg-3.9.0}/docs/source/reference/content-reference.rst +0 -0
  109. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/.gitignore +0 -0
  110. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_attach.py +0 -0
  111. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_count.py +0 -0
  112. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_generator.py +0 -0
  113. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_intro.py +0 -0
  114. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_leaky_subscriber.py +0 -0
  115. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_log_to_file.py +0 -0
  116. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_normalterm.py +0 -0
  117. {ezmsg-3.7.3 → ezmsg-3.9.0}/examples/ezmsg_stop.py +0 -0
  118. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/__main__.py +0 -0
  119. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/addressable.py +0 -0
  120. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/backpressure.py +0 -0
  121. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/channelmanager.py +0 -0
  122. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/component.py +0 -0
  123. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/graph_util.py +0 -0
  124. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/message.py +0 -0
  125. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/messagecache.py +0 -0
  126. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/messagemarshal.py +0 -0
  127. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/settings.py +0 -0
  128. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/shm.py +0 -0
  129. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/state.py +0 -0
  130. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/test.py +0 -0
  131. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/core/util.py +0 -0
  132. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/__init__.py +0 -0
  133. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/gen_to_unit.py +0 -0
  134. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/generator.py +0 -0
  135. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagecodec.py +0 -0
  136. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagegate.py +0 -0
  137. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagelogger.py +0 -0
  138. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagequeue.py +0 -0
  139. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messagereplay.py +0 -0
  140. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/__init__.py +0 -0
  141. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/axisarray.py +0 -0
  142. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/messages/chunker.py +0 -0
  143. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/__init__.py +0 -0
  144. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/envinfo.py +0 -0
  145. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/perf/util.py +0 -0
  146. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/profiler.py +0 -0
  147. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/rate.py +0 -0
  148. {ezmsg-3.7.3 → ezmsg-3.9.0}/src/ezmsg/util/terminate.py +0 -0
  149. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/__init__.py +0 -0
  150. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/ez_test_utils.py +0 -0
  151. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/__init__.py +0 -0
  152. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/test_axisarray.py +0 -0
  153. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/test_chunker.py +0 -0
  154. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/test_key.py +0 -0
  155. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/messages/test_modify.py +0 -0
  156. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_addressable.py +0 -0
  157. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_attach.py +0 -0
  158. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_channel.py +0 -0
  159. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_channelmanager.py +0 -0
  160. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_connections.py +0 -0
  161. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_dag.py +0 -0
  162. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_generator.py +0 -0
  163. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_graph.py +0 -0
  164. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_graph_visualization.py +0 -0
  165. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_leaky_subscriber.py +0 -0
  166. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_messagereplay.py +0 -0
  167. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_profiler.py +0 -0
  168. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_run.py +0 -0
  169. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_shm.py +0 -0
  170. {ezmsg-3.7.3 → ezmsg-3.9.0}/tests/test_state.py +0 -0
  171. {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.7.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.InputStream(int)
86
- OUTPUT = ez.OutputStream(int)
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.OutputStream(int)
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.InputStream(int)
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.OutputStream(int)
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.InputStream(int)
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.start_time = time.time()
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.SETTINGS.freq * t)
40
- await asyncio.sleep(1.0 / self.SETTINGS.update_rate)
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.SETTINGS.message}")
58
- yield self.OUTPUT, self.SETTINGS.message
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.InputStream(str)
127
- OUTPUT = ez.OutputStream(str)
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 PING.OUTPUT available on a topic ezmsg_attach.py
197
- (system.PING.OUTPUT, "GLOBAL_PING_TOPIC"),
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()