From c04a7c6ae34ebb75b71d7c80097872da9594e2b9 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 8 Jul 2025 14:18:03 -0500 Subject: [PATCH 1/8] improve performance for unset values and add performance checker example --- examples/performance_checker/README.md | 18 +++ .../performance_checker.py | 36 ++++++ .../performance_checker_panel.py | 118 ++++++++++++++++++ .../pythonpanel/v1/python_panel_service.proto | 3 +- .../v1/python_panel_service_pb2.py | 16 +-- .../v1/python_panel_service_pb2.pyi | 5 +- .../v1/python_panel_service_pb2_grpc.py | 1 - .../v1/python_panel_service_pb2_grpc.pyi | 3 - src/nipanel/_panel_client.py | 11 +- src/nipanel/_panel_value_accessor.py | 18 ++- tests/unit/test_panel_client.py | 10 +- tests/unit/test_streamlit_panel.py | 2 +- tests/utils/_fake_python_panel_servicer.py | 2 +- 13 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 examples/performance_checker/README.md create mode 100644 examples/performance_checker/performance_checker.py create mode 100644 examples/performance_checker/performance_checker_panel.py diff --git a/examples/performance_checker/README.md b/examples/performance_checker/README.md new file mode 100644 index 0000000..71a4333 --- /dev/null +++ b/examples/performance_checker/README.md @@ -0,0 +1,18 @@ +# Performance checker Example + +This example demonstrates using nipanel with Streamlit to display a dynamic sine wave using the `streamlit-echarts` library. + +## Features + +- Generates sine wave data with varying frequency +- Displays the data in an chart +- Updates rapidly +- Shows timing information + +### Required Software + +- Python 3.9 or later + +### Usage + +Run `poetry run python examples/performance_checker/performance_checker.py` diff --git a/examples/performance_checker/performance_checker.py b/examples/performance_checker/performance_checker.py new file mode 100644 index 0000000..46663d1 --- /dev/null +++ b/examples/performance_checker/performance_checker.py @@ -0,0 +1,36 @@ +"""Example of using nipanel to display a sine wave graph using st_echarts.""" + +import math +import time +from pathlib import Path + +import numpy as np + +import nipanel + + +panel_script_path = Path(__file__).with_name("performance_checker_panel.py") +panel = nipanel.create_panel(panel_script_path) + +amplitude = 1.0 +frequency = 1.0 +num_points = 1000 +try: + print(f"Panel URL: {panel.panel_url}") + print("Press Ctrl+C to exit") + + # Generate and update the sine wave data as fast as possible + while True: + time_points = np.linspace(0, num_points, num_points) + sine_values = amplitude * np.sin(frequency * time_points) + + panel.set_value("time_points", time_points.tolist()) + panel.set_value("sine_values", sine_values.tolist()) + panel.set_value("amplitude", amplitude) + panel.set_value("frequency", frequency) + + # Slowly vary the frequency for a more dynamic visualization + frequency = 1.0 + 0.5 * math.sin(time.time() / 5.0) + +except KeyboardInterrupt: + print("Exiting...") diff --git a/examples/performance_checker/performance_checker_panel.py b/examples/performance_checker/performance_checker_panel.py new file mode 100644 index 0000000..b3bc900 --- /dev/null +++ b/examples/performance_checker/performance_checker_panel.py @@ -0,0 +1,118 @@ +"""A Streamlit visualization panel for the perf_check.py example script.""" + +import statistics +import time + +import streamlit as st +from streamlit_echarts import st_echarts + +import nipanel + + +def measure_get_value_time(panel, value_id, default_value=None): + """Measure the time it takes to get a value from the panel. + + Args: + panel: The panel accessor object + value_id: The ID of the value to get + default_value: Default value if the value is not found + + Returns: + A tuple of (value, time_ms) where time_ms is the time in milliseconds + """ + start_time = time.time() + value = panel.get_value(value_id, default_value) + end_time = time.time() + time_ms = (end_time - start_time) * 1000 + return value, time_ms + + +st.set_page_config(page_title="Performance Checker Example", page_icon="📈", layout="wide") +st.title("Performance Checker Example") + +# Initialize refresh history list if it doesn't exist +if "refresh_history" not in st.session_state: + st.session_state.refresh_history = [] + +# Store current timestamp and calculate time since last refresh +current_time = time.time() +if "last_refresh_time" not in st.session_state: + st.session_state.last_refresh_time = current_time + time_since_last_refresh = 0.0 +else: + time_since_last_refresh = (current_time - st.session_state.last_refresh_time) * 1000 + st.session_state.last_refresh_time = current_time + + # Store the last 10 refresh times + st.session_state.refresh_history.append(time_since_last_refresh) + if len(st.session_state.refresh_history) > 10: + st.session_state.refresh_history.pop(0) + +panel = nipanel.get_panel_accessor() + +# Measure time to get each value +time_points, time_points_ms = measure_get_value_time(panel, "time_points", [0.0]) +sine_values, sine_values_ms = measure_get_value_time(panel, "sine_values", [0.0]) +amplitude, amplitude_ms = measure_get_value_time(panel, "amplitude", 1.0) +frequency, frequency_ms = measure_get_value_time(panel, "frequency", 1.0) +unset_value, unset_value_ms = measure_get_value_time(panel, "unset_value", "default") + +if st.session_state.refresh_history: + history = st.session_state.refresh_history +else: + history = [] + +# Calculate statistics +min_time = min(history) if history else 0 +max_time = max(history) if history else 0 +avg_time = statistics.mean(history) if history else 0 + +# Prepare data for echarts +data = [{"value": [x, y]} for x, y in zip(time_points, sine_values)] + +# Configure the chart options +options = { + "animation": False, # Disable animation for smoother updates + "title": {"text": "Sine Wave"}, + "tooltip": {"trigger": "axis"}, + "xAxis": {"type": "value", "name": "Time (s)", "nameLocation": "middle", "nameGap": 30}, + "yAxis": { + "type": "value", + "name": "Amplitude", + "nameLocation": "middle", + "nameGap": 30, + }, + "series": [ + { + "data": data, + "type": "line", + "showSymbol": True, + "smooth": True, + "lineStyle": {"width": 2, "color": "#1f77b4"}, + "areaStyle": {"color": "#1f77b4", "opacity": 0.3}, + "name": "Sine Wave", + } + ], +} + +# Display the chart +st_echarts(options=options, height="400px", key="graph") + +# Create columns for metrics +col1, col2, col3, col4 = st.columns(4) +with col1: + st.metric("Amplitude", f"{amplitude:.2f}") + st.metric("Frequency", f"{frequency:.2f} Hz") +with col2: + st.metric("Refresh Time", f"{time_since_last_refresh:.1f} ms") + st.metric("Min Refresh Time", f"{min_time:.1f} ms") + st.metric("Max Refresh Time", f"{max_time:.1f} ms") + st.metric("Avg Refresh Time", f"{avg_time:.1f} ms") + +with col3: + st.metric("get time_points", f"{time_points_ms:.1f} ms") + st.metric("get sine_values", f"{sine_values_ms:.1f} ms") + st.metric("get amplitude", f"{amplitude_ms:.1f} ms") + st.metric("get frequency", f"{frequency_ms:.1f} ms") +with col4: + st.metric("get unset_value", f"{unset_value_ms:.1f} ms") diff --git a/protos/ni/pythonpanel/v1/python_panel_service.proto b/protos/ni/pythonpanel/v1/python_panel_service.proto index 5b8dd2a..0476d06 100644 --- a/protos/ni/pythonpanel/v1/python_panel_service.proto +++ b/protos/ni/pythonpanel/v1/python_panel_service.proto @@ -34,7 +34,6 @@ service PythonPanelService { // Get a value for a control on the panel // Status Codes for errors: // - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - // - NOT_FOUND: The value with the specified identifier was not found rpc GetValue(GetValueRequest) returns (GetValueResponse); // Set a value for a control on the panel @@ -99,7 +98,7 @@ message GetValueRequest { message GetValueResponse { // The value - google.protobuf.Any value = 1; + optional google.protobuf.Any value = 1; } message SetValueRequest { diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2.py b/src/ni/pythonpanel/v1/python_panel_service_pb2.py index 3435f6b..3d03f69 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.py +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.py @@ -14,7 +14,7 @@ from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,ni/pythonpanel/v1/python_panel_service.proto\x12\x11ni.pythonpanel.v1\x1a\x19google/protobuf/any.proto\"U\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\x12\x13\n\x0bpython_path\x18\x03 \x01(\t\"\'\n\x12StartPanelResponse\x12\x11\n\tpanel_url\x18\x01 \x01(\t\"3\n\x10StopPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"\x13\n\x11StopPanelResponse\"\x18\n\x16\x45numeratePanelsRequest\"J\n\x10PanelInformation\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_url\x18\x02 \x01(\t\x12\x11\n\tvalue_ids\x18\x03 \x03(\t\"N\n\x17\x45numeratePanelsResponse\x12\x33\n\x06panels\x18\x01 \x03(\x0b\x32#.ni.pythonpanel.v1.PanelInformation\"5\n\x0fGetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\"7\n\x10GetValueResponse\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\"j\n\x0fSetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12\x0e\n\x06notify\x18\x04 \x01(\x08\"\x12\n\x10SetValueResponse2\xdb\x03\n\x12PythonPanelService\x12Y\n\nStartPanel\x12$.ni.pythonpanel.v1.StartPanelRequest\x1a%.ni.pythonpanel.v1.StartPanelResponse\x12V\n\tStopPanel\x12#.ni.pythonpanel.v1.StopPanelRequest\x1a$.ni.pythonpanel.v1.StopPanelResponse\x12h\n\x0f\x45numeratePanels\x12).ni.pythonpanel.v1.EnumeratePanelsRequest\x1a*.ni.pythonpanel.v1.EnumeratePanelsResponse\x12S\n\x08GetValue\x12\".ni.pythonpanel.v1.GetValueRequest\x1a#.ni.pythonpanel.v1.GetValueResponse\x12S\n\x08SetValue\x12\".ni.pythonpanel.v1.SetValueRequest\x1a#.ni.pythonpanel.v1.SetValueResponseB\x9a\x01\n\x15\x63om.ni.pythonpanel.v1B\x17PythonPanelServiceProtoP\x01Z\rpythonpanelv1\xf8\x01\x01\xa2\x02\x04NIPP\xaa\x02\"NationalInstruments.PythonPanel.V1\xca\x02\x11NI\\PythonPanel\\V1\xea\x02\x13NI::PythonPanel::V1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,ni/pythonpanel/v1/python_panel_service.proto\x12\x11ni.pythonpanel.v1\x1a\x19google/protobuf/any.proto\"U\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\x12\x13\n\x0bpython_path\x18\x03 \x01(\t\"\'\n\x12StartPanelResponse\x12\x11\n\tpanel_url\x18\x01 \x01(\t\"3\n\x10StopPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"\x13\n\x11StopPanelResponse\"\x18\n\x16\x45numeratePanelsRequest\"J\n\x10PanelInformation\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_url\x18\x02 \x01(\t\x12\x11\n\tvalue_ids\x18\x03 \x03(\t\"N\n\x17\x45numeratePanelsResponse\x12\x33\n\x06panels\x18\x01 \x03(\x0b\x32#.ni.pythonpanel.v1.PanelInformation\"5\n\x0fGetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\"F\n\x10GetValueResponse\x12(\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.AnyH\x00\x88\x01\x01\x42\x08\n\x06_value\"j\n\x0fSetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12\x0e\n\x06notify\x18\x04 \x01(\x08\"\x12\n\x10SetValueResponse2\xdb\x03\n\x12PythonPanelService\x12Y\n\nStartPanel\x12$.ni.pythonpanel.v1.StartPanelRequest\x1a%.ni.pythonpanel.v1.StartPanelResponse\x12V\n\tStopPanel\x12#.ni.pythonpanel.v1.StopPanelRequest\x1a$.ni.pythonpanel.v1.StopPanelResponse\x12h\n\x0f\x45numeratePanels\x12).ni.pythonpanel.v1.EnumeratePanelsRequest\x1a*.ni.pythonpanel.v1.EnumeratePanelsResponse\x12S\n\x08GetValue\x12\".ni.pythonpanel.v1.GetValueRequest\x1a#.ni.pythonpanel.v1.GetValueResponse\x12S\n\x08SetValue\x12\".ni.pythonpanel.v1.SetValueRequest\x1a#.ni.pythonpanel.v1.SetValueResponseB\x9a\x01\n\x15\x63om.ni.pythonpanel.v1B\x17PythonPanelServiceProtoP\x01Z\rpythonpanelv1\xf8\x01\x01\xa2\x02\x04NIPP\xaa\x02\"NationalInstruments.PythonPanel.V1\xca\x02\x11NI\\PythonPanel\\V1\xea\x02\x13NI::PythonPanel::V1b\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ni.pythonpanel.v1.python_panel_service_pb2', globals()) @@ -39,11 +39,11 @@ _GETVALUEREQUEST._serialized_start=478 _GETVALUEREQUEST._serialized_end=531 _GETVALUERESPONSE._serialized_start=533 - _GETVALUERESPONSE._serialized_end=588 - _SETVALUEREQUEST._serialized_start=590 - _SETVALUEREQUEST._serialized_end=696 - _SETVALUERESPONSE._serialized_start=698 - _SETVALUERESPONSE._serialized_end=716 - _PYTHONPANELSERVICE._serialized_start=719 - _PYTHONPANELSERVICE._serialized_end=1194 + _GETVALUERESPONSE._serialized_end=603 + _SETVALUEREQUEST._serialized_start=605 + _SETVALUEREQUEST._serialized_end=711 + _SETVALUERESPONSE._serialized_start=713 + _SETVALUERESPONSE._serialized_end=731 + _PYTHONPANELSERVICE._serialized_start=734 + _PYTHONPANELSERVICE._serialized_end=1209 # @@protoc_insertion_point(module_scope) diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi index 71483cf..aba0543 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi @@ -171,8 +171,9 @@ class GetValueResponse(google.protobuf.message.Message): *, value: google.protobuf.any_pb2.Any | None = ..., ) -> None: ... - def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["value", b"value"]) -> None: ... + def HasField(self, field_name: typing.Literal["_value", b"_value", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_value", b"_value", "value", b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_value", b"_value"]) -> typing.Literal["value"] | None: ... global___GetValueResponse = GetValueResponse diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py index 0f5c31b..01ba9c9 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py @@ -77,7 +77,6 @@ def GetValue(self, request, context): """Get a value for a control on the panel Status Codes for errors: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - - NOT_FOUND: The value with the specified identifier was not found """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi index b2d5fa6..622c2e0 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi @@ -55,7 +55,6 @@ class PythonPanelServiceStub: """Get a value for a control on the panel Status Codes for errors: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - - NOT_FOUND: The value with the specified identifier was not found """ SetValue: grpc.UnaryUnaryMultiCallable[ @@ -104,7 +103,6 @@ class PythonPanelServiceAsyncStub: """Get a value for a control on the panel Status Codes for errors: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - - NOT_FOUND: The value with the specified identifier was not found """ SetValue: grpc.aio.UnaryUnaryMultiCallable[ @@ -161,7 +159,6 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta): """Get a value for a control on the panel Status Codes for errors: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - - NOT_FOUND: The value with the specified identifier was not found """ @abc.abstractmethod diff --git a/src/nipanel/_panel_client.py b/src/nipanel/_panel_client.py index d06e2c0..278378e 100644 --- a/src/nipanel/_panel_client.py +++ b/src/nipanel/_panel_client.py @@ -116,7 +116,7 @@ def set_value(self, panel_id: str, value_id: str, value: object, notify: bool) - ) self._invoke_with_retry(self._get_stub().SetValue, set_value_request) - def get_value(self, panel_id: str, value_id: str) -> object: + def get_value(self, panel_id: str, value_id: str) -> tuple[bool, object]: """Get the value for the control with value_id. Args: @@ -124,12 +124,15 @@ def get_value(self, panel_id: str, value_id: str) -> object: value_id: The ID of the control. Returns: - The value. + A tuple containing a boolean indicating whether the value was successfully retrieved and + the value itself (or None if not present). """ get_value_request = GetValueRequest(panel_id=panel_id, value_id=value_id) response = self._invoke_with_retry(self._get_stub().GetValue, get_value_request) - the_value = from_any(response.value) - return the_value + if response.HasField("value"): + return True, from_any(response.value) + else: + return False, None def _get_stub(self) -> PythonPanelServiceStub: if self._stub is None: diff --git a/src/nipanel/_panel_value_accessor.py b/src/nipanel/_panel_value_accessor.py index c174843..d60f7d7 100644 --- a/src/nipanel/_panel_value_accessor.py +++ b/src/nipanel/_panel_value_accessor.py @@ -60,17 +60,15 @@ def get_value(self, value_id: str, default_value: _T | None = None) -> _T | obje Returns: The value, or the default value if not set """ - try: - value = self._panel_client.get_value(self._panel_id, value_id) - if default_value is not None and not isinstance(value, type(default_value)): - raise TypeError("Value type does not match default value type.") - return value - - except grpc.RpcError as e: - if e.code() == grpc.StatusCode.NOT_FOUND and default_value is not None: + found, value = self._panel_client.get_value(self._panel_id, value_id) + if not found: + if default_value is not None: return default_value - else: - raise e + raise KeyError(f"Value with id '{value_id}' not found on panel '{self._panel_id}'.") + + if default_value is not None and not isinstance(value, type(default_value)): + raise TypeError("Value type does not match default value type.") + return value def set_value(self, value_id: str, value: object) -> None: """Set the value for a control on the panel. diff --git a/tests/unit/test_panel_client.py b/tests/unit/test_panel_client.py index 2a9723d..151d912 100644 --- a/tests/unit/test_panel_client.py +++ b/tests/unit/test_panel_client.py @@ -1,5 +1,4 @@ import grpc -import pytest from nipanel._panel_client import PanelClient @@ -51,11 +50,12 @@ def test___start_panels___stop_panel_1_without_reset___enumerate_has_both_panels } -def test___get_unset_value_raises_exception(fake_panel_channel: grpc.Channel) -> None: +def test___get_unset_value___returns_not_found(fake_panel_channel: grpc.Channel) -> None: client = create_panel_client(fake_panel_channel) - with pytest.raises(Exception): - client.get_value("panel1", "unset_id") + response = client.get_value("panel1", "unset_id") + + assert response == (False, None) def test___set_value___enumerate_panels_shows_value( @@ -73,7 +73,7 @@ def test___set_value___gets_value(fake_panel_channel: grpc.Channel) -> None: client.set_value("panel1", "val1", "value1", notify=False) - assert client.get_value("panel1", "val1") == "value1" + assert client.get_value("panel1", "val1") == (True, "value1") def create_panel_client(fake_panel_channel: grpc.Channel) -> PanelClient: diff --git a/tests/unit/test_streamlit_panel.py b/tests/unit/test_streamlit_panel.py index a271c79..c1c3382 100644 --- a/tests/unit/test_streamlit_panel.py +++ b/tests/unit/test_streamlit_panel.py @@ -131,7 +131,7 @@ def test___panel___get_unset_value_with_no_default___raises_exception( panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) value_id = "test_id" - with pytest.raises(grpc.RpcError): + with pytest.raises(KeyError): panel.get_value(value_id) diff --git a/tests/utils/_fake_python_panel_servicer.py b/tests/utils/_fake_python_panel_servicer.py index 5d450a0..e6d59ee 100644 --- a/tests/utils/_fake_python_panel_servicer.py +++ b/tests/utils/_fake_python_panel_servicer.py @@ -62,7 +62,7 @@ def EnumeratePanels( # noqa: N802 def GetValue(self, request: GetValueRequest, context: Any) -> GetValueResponse: # noqa: N802 """Trivial implementation for testing.""" if request.value_id not in self._panel_value_ids.get(request.panel_id, {}): - context.abort(grpc.StatusCode.NOT_FOUND, "Value ID not found in panel") + return GetValueResponse() value = self._panel_value_ids[request.panel_id][request.value_id] return GetValueResponse(value=value) From 6b8cadc7ee6c2c270b2e6973ff396601ae1f7cdb Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 8 Jul 2025 14:34:04 -0500 Subject: [PATCH 2/8] refactor: add type hints to measure_get_value_time function --- examples/performance_checker/performance_checker_panel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/performance_checker/performance_checker_panel.py b/examples/performance_checker/performance_checker_panel.py index b3bc900..799807e 100644 --- a/examples/performance_checker/performance_checker_panel.py +++ b/examples/performance_checker/performance_checker_panel.py @@ -2,6 +2,7 @@ import statistics import time +from typing import Any, Tuple import streamlit as st from streamlit_echarts import st_echarts @@ -9,7 +10,9 @@ import nipanel -def measure_get_value_time(panel, value_id, default_value=None): +def measure_get_value_time( + panel: "nipanel.StreamlitPanelValueAccessor", value_id: str, default_value: Any = None +) -> Tuple[Any, float]: """Measure the time it takes to get a value from the panel. Args: From 0a65ebc8c51f4f1cab36d20257fe0913b2631674 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 8 Jul 2025 15:05:18 -0500 Subject: [PATCH 3/8] feat: add performance measurement for panel value setting and getting --- .../performance_checker.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/examples/performance_checker/performance_checker.py b/examples/performance_checker/performance_checker.py index 46663d1..181690a 100644 --- a/examples/performance_checker/performance_checker.py +++ b/examples/performance_checker/performance_checker.py @@ -15,6 +15,39 @@ amplitude = 1.0 frequency = 1.0 num_points = 1000 +time_points = np.linspace(0, num_points, num_points) +sine_values = amplitude * np.sin(frequency * time_points) + +start_time = time.time() +for i in range(100): + panel.set_value("time_points", time_points.tolist()) +stop_time = time.time() +print(f"Average time to set 'time_points': {(stop_time - start_time) * 10:.2f} ms") + +start_time = time.time() +for i in range(100): + panel.set_value("amplitude", 1.0) +stop_time = time.time() +print(f"Average time to set 'amplitude': {(stop_time - start_time) * 10:.2f} ms") + +start_time = time.time() +for i in range(100): + panel.get_value("time_points", [0.0]) +stop_time = time.time() +print(f"Average time to get 'time_points': {(stop_time - start_time) * 10:.2f} ms") + +start_time = time.time() +for i in range(100): + panel.get_value("amplitude", 1.0) +stop_time = time.time() +print(f"Average time to get 'amplitude': {(stop_time - start_time) * 10:.2f} ms") + +start_time = time.time() +for i in range(100): + panel.get_value("unset_value", 1.0) +stop_time = time.time() +print(f"Average time to get 'unset_value': {(stop_time - start_time) * 10:.2f} ms") + try: print(f"Panel URL: {panel.panel_url}") print("Press Ctrl+C to exit") From 6660907b8d82357d98aa1df3f0ef77bbaf6e4cfe Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 8 Jul 2025 15:10:31 -0500 Subject: [PATCH 4/8] cleanup --- examples/performance_checker/README.md | 4 +-- .../performance_checker_panel.py | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/performance_checker/README.md b/examples/performance_checker/README.md index 71a4333..d59a804 100644 --- a/examples/performance_checker/README.md +++ b/examples/performance_checker/README.md @@ -1,11 +1,11 @@ # Performance checker Example -This example demonstrates using nipanel with Streamlit to display a dynamic sine wave using the `streamlit-echarts` library. +This example measures the performance of a stremlit panel with a graph. ## Features - Generates sine wave data with varying frequency -- Displays the data in an chart +- Displays the data in a graph - Updates rapidly - Shows timing information diff --git a/examples/performance_checker/performance_checker_panel.py b/examples/performance_checker/performance_checker_panel.py index 799807e..2f96516 100644 --- a/examples/performance_checker/performance_checker_panel.py +++ b/examples/performance_checker/performance_checker_panel.py @@ -1,4 +1,4 @@ -"""A Streamlit visualization panel for the perf_check.py example script.""" +"""A Streamlit visualization panel for the performance_checker.py example script.""" import statistics import time @@ -51,6 +51,16 @@ def measure_get_value_time( if len(st.session_state.refresh_history) > 10: st.session_state.refresh_history.pop(0) +if st.session_state.refresh_history: + refresh_history = st.session_state.refresh_history +else: + refresh_history = [] + +# Calculate statistics for refresh +min_refresh_time = min(refresh_history) if refresh_history else 0 +max_refresh_time = max(refresh_history) if refresh_history else 0 +avg_refresh_time = statistics.mean(refresh_history) if refresh_history else 0 + panel = nipanel.get_panel_accessor() # Measure time to get each value @@ -60,16 +70,6 @@ def measure_get_value_time( frequency, frequency_ms = measure_get_value_time(panel, "frequency", 1.0) unset_value, unset_value_ms = measure_get_value_time(panel, "unset_value", "default") -if st.session_state.refresh_history: - history = st.session_state.refresh_history -else: - history = [] - -# Calculate statistics -min_time = min(history) if history else 0 -max_time = max(history) if history else 0 -avg_time = statistics.mean(history) if history else 0 - # Prepare data for echarts data = [{"value": [x, y]} for x, y in zip(time_points, sine_values)] @@ -108,9 +108,9 @@ def measure_get_value_time( st.metric("Frequency", f"{frequency:.2f} Hz") with col2: st.metric("Refresh Time", f"{time_since_last_refresh:.1f} ms") - st.metric("Min Refresh Time", f"{min_time:.1f} ms") - st.metric("Max Refresh Time", f"{max_time:.1f} ms") - st.metric("Avg Refresh Time", f"{avg_time:.1f} ms") + st.metric("Min Refresh Time", f"{min_refresh_time:.1f} ms") + st.metric("Max Refresh Time", f"{max_refresh_time:.1f} ms") + st.metric("Avg Refresh Time", f"{avg_refresh_time:.1f} ms") with col3: st.metric("get time_points", f"{time_points_ms:.1f} ms") From 8a8d6450f145106bad2a5d0ad7a2ee96939a54f8 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 8 Jul 2025 15:18:51 -0500 Subject: [PATCH 5/8] feedback --- .../performance_checker_panel.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/performance_checker/performance_checker_panel.py b/examples/performance_checker/performance_checker_panel.py index 2f96516..ea19bec 100644 --- a/examples/performance_checker/performance_checker_panel.py +++ b/examples/performance_checker/performance_checker_panel.py @@ -10,7 +10,7 @@ import nipanel -def measure_get_value_time( +def profile_get_value( panel: "nipanel.StreamlitPanelValueAccessor", value_id: str, default_value: Any = None ) -> Tuple[Any, float]: """Measure the time it takes to get a value from the panel. @@ -35,7 +35,7 @@ def measure_get_value_time( # Initialize refresh history list if it doesn't exist if "refresh_history" not in st.session_state: - st.session_state.refresh_history = [] + st.session_state.refresh_history = [] # List of tuples (timestamp, refresh_time_ms) # Store current timestamp and calculate time since last refresh current_time = time.time() @@ -46,13 +46,18 @@ def measure_get_value_time( time_since_last_refresh = (current_time - st.session_state.last_refresh_time) * 1000 st.session_state.last_refresh_time = current_time - # Store the last 10 refresh times - st.session_state.refresh_history.append(time_since_last_refresh) - if len(st.session_state.refresh_history) > 10: - st.session_state.refresh_history.pop(0) + # Store refresh times with timestamps, keeping only the last 1 second of data + st.session_state.refresh_history.append((current_time, time_since_last_refresh)) + # Remove entries older than 1 second + cutoff_time = current_time - 1.0 # 1 second ago + st.session_state.refresh_history = [ + item for item in st.session_state.refresh_history if item[0] >= cutoff_time + ] + +# Extract just the refresh times for calculations if st.session_state.refresh_history: - refresh_history = st.session_state.refresh_history + refresh_history = [item[1] for item in st.session_state.refresh_history] else: refresh_history = [] @@ -64,11 +69,11 @@ def measure_get_value_time( panel = nipanel.get_panel_accessor() # Measure time to get each value -time_points, time_points_ms = measure_get_value_time(panel, "time_points", [0.0]) -sine_values, sine_values_ms = measure_get_value_time(panel, "sine_values", [0.0]) -amplitude, amplitude_ms = measure_get_value_time(panel, "amplitude", 1.0) -frequency, frequency_ms = measure_get_value_time(panel, "frequency", 1.0) -unset_value, unset_value_ms = measure_get_value_time(panel, "unset_value", "default") +time_points, time_points_ms = profile_get_value(panel, "time_points", [0.0]) +sine_values, sine_values_ms = profile_get_value(panel, "sine_values", [0.0]) +amplitude, amplitude_ms = profile_get_value(panel, "amplitude", 1.0) +frequency, frequency_ms = profile_get_value(panel, "frequency", 1.0) +unset_value, unset_value_ms = profile_get_value(panel, "unset_value", "default") # Prepare data for echarts data = [{"value": [x, y]} for x, y in zip(time_points, sine_values)] @@ -111,11 +116,11 @@ def measure_get_value_time( st.metric("Min Refresh Time", f"{min_refresh_time:.1f} ms") st.metric("Max Refresh Time", f"{max_refresh_time:.1f} ms") st.metric("Avg Refresh Time", f"{avg_refresh_time:.1f} ms") + st.metric("FPS", f"{len(refresh_history)}") with col3: st.metric("get time_points", f"{time_points_ms:.1f} ms") st.metric("get sine_values", f"{sine_values_ms:.1f} ms") st.metric("get amplitude", f"{amplitude_ms:.1f} ms") st.metric("get frequency", f"{frequency_ms:.1f} ms") -with col4: st.metric("get unset_value", f"{unset_value_ms:.1f} ms") From ae872ac5035d04a84728360b99981a04f97834f9 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 8 Jul 2025 15:20:02 -0500 Subject: [PATCH 6/8] cleanup --- examples/performance_checker/performance_checker_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/performance_checker/performance_checker_panel.py b/examples/performance_checker/performance_checker_panel.py index ea19bec..87f0358 100644 --- a/examples/performance_checker/performance_checker_panel.py +++ b/examples/performance_checker/performance_checker_panel.py @@ -107,7 +107,7 @@ def profile_get_value( st_echarts(options=options, height="400px", key="graph") # Create columns for metrics -col1, col2, col3, col4 = st.columns(4) +col1, col2, col3 = st.columns(3) with col1: st.metric("Amplitude", f"{amplitude:.2f}") st.metric("Frequency", f"{frequency:.2f} Hz") From db10e5d1d4c5f206c4555f54ac7e3fc21773d245 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Thu, 10 Jul 2025 09:38:06 -0500 Subject: [PATCH 7/8] use timeit for performance measurements --- .../performance_checker.py | 53 +++++++++++-------- .../performance_checker_panel.py | 31 ++++++----- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/examples/performance_checker/performance_checker.py b/examples/performance_checker/performance_checker.py index 181690a..6a5bb26 100644 --- a/examples/performance_checker/performance_checker.py +++ b/examples/performance_checker/performance_checker.py @@ -1,7 +1,8 @@ -"""Example of using nipanel to display a sine wave graph using st_echarts.""" +"""Check the performance of get_value and set_value methods in nipanel.""" import math import time +import timeit from pathlib import Path import numpy as np @@ -18,35 +19,43 @@ time_points = np.linspace(0, num_points, num_points) sine_values = amplitude * np.sin(frequency * time_points) -start_time = time.time() -for i in range(100): + +def _set_time_points(): panel.set_value("time_points", time_points.tolist()) -stop_time = time.time() -print(f"Average time to set 'time_points': {(stop_time - start_time) * 10:.2f} ms") -start_time = time.time() -for i in range(100): - panel.set_value("amplitude", 1.0) -stop_time = time.time() -print(f"Average time to set 'amplitude': {(stop_time - start_time) * 10:.2f} ms") -start_time = time.time() -for i in range(100): +def _set_amplitude(): + panel.set_value("amplitude", amplitude) + + +def _get_time_points(): panel.get_value("time_points", [0.0]) -stop_time = time.time() -print(f"Average time to get 'time_points': {(stop_time - start_time) * 10:.2f} ms") -start_time = time.time() -for i in range(100): + +def _get_amplitude(): panel.get_value("amplitude", 1.0) -stop_time = time.time() -print(f"Average time to get 'amplitude': {(stop_time - start_time) * 10:.2f} ms") -start_time = time.time() -for i in range(100): + +def _get_unset_value(): panel.get_value("unset_value", 1.0) -stop_time = time.time() -print(f"Average time to get 'unset_value': {(stop_time - start_time) * 10:.2f} ms") + + +iterations = 100 + +set_time_points_time = timeit.timeit(_set_time_points, number=iterations) * 1000 / iterations +print(f"Average time to set 'time_points': {set_time_points_time:.2f} ms") + +set_amplitude_time = timeit.timeit(_set_amplitude, number=iterations) * 1000 / iterations +print(f"Average time to set 'amplitude': {set_amplitude_time:.2f} ms") + +get_time_points_time = timeit.timeit(_get_time_points, number=iterations) * 1000 / iterations +print(f"Average time to get 'time_points': {get_time_points_time:.2f} ms") + +get_amplitude_time = timeit.timeit(_get_amplitude, number=iterations) * 1000 / iterations +print(f"Average time to get 'amplitude': {get_amplitude_time:.2f} ms") + +get_unset_value_time = timeit.timeit(_get_unset_value, number=iterations) * 1000 / iterations +print(f"Average time to get 'unset_value': {get_unset_value_time:.2f} ms") try: print(f"Panel URL: {panel.panel_url}") diff --git a/examples/performance_checker/performance_checker_panel.py b/examples/performance_checker/performance_checker_panel.py index 87f0358..e29f58c 100644 --- a/examples/performance_checker/performance_checker_panel.py +++ b/examples/performance_checker/performance_checker_panel.py @@ -2,6 +2,8 @@ import statistics import time +import timeit +from functools import partial from typing import Any, Tuple import streamlit as st @@ -11,7 +13,10 @@ def profile_get_value( - panel: "nipanel.StreamlitPanelValueAccessor", value_id: str, default_value: Any = None + panel: "nipanel.StreamlitPanelValueAccessor", + value_id: str, + default_value: Any = None, + num_runs: int = 5, ) -> Tuple[Any, float]: """Measure the time it takes to get a value from the panel. @@ -19,21 +24,20 @@ def profile_get_value( panel: The panel accessor object value_id: The ID of the value to get default_value: Default value if the value is not found + num_runs: Number of runs for timing Returns: A tuple of (value, time_ms) where time_ms is the time in milliseconds """ - start_time = time.time() value = panel.get_value(value_id, default_value) - end_time = time.time() - time_ms = (end_time - start_time) * 1000 + get_value_func = partial(panel.get_value, value_id, default_value) + time_ms = timeit.timeit(get_value_func, number=num_runs) * 1000 / num_runs return value, time_ms st.set_page_config(page_title="Performance Checker Example", page_icon="📈", layout="wide") st.title("Performance Checker Example") -# Initialize refresh history list if it doesn't exist if "refresh_history" not in st.session_state: st.session_state.refresh_history = [] # List of tuples (timestamp, refresh_time_ms) @@ -61,24 +65,21 @@ def profile_get_value( else: refresh_history = [] -# Calculate statistics for refresh min_refresh_time = min(refresh_history) if refresh_history else 0 max_refresh_time = max(refresh_history) if refresh_history else 0 avg_refresh_time = statistics.mean(refresh_history) if refresh_history else 0 panel = nipanel.get_panel_accessor() -# Measure time to get each value -time_points, time_points_ms = profile_get_value(panel, "time_points", [0.0]) -sine_values, sine_values_ms = profile_get_value(panel, "sine_values", [0.0]) -amplitude, amplitude_ms = profile_get_value(panel, "amplitude", 1.0) -frequency, frequency_ms = profile_get_value(panel, "frequency", 1.0) -unset_value, unset_value_ms = profile_get_value(panel, "unset_value", "default") +num_timing_runs = 5 +time_points, time_points_ms = profile_get_value(panel, "time_points", [0.0], num_timing_runs) +sine_values, sine_values_ms = profile_get_value(panel, "sine_values", [0.0], num_timing_runs) +amplitude, amplitude_ms = profile_get_value(panel, "amplitude", 1.0, num_timing_runs) +frequency, frequency_ms = profile_get_value(panel, "frequency", 1.0, num_timing_runs) +unset_value, unset_value_ms = profile_get_value(panel, "unset_value", "default", num_timing_runs) -# Prepare data for echarts data = [{"value": [x, y]} for x, y in zip(time_points, sine_values)] -# Configure the chart options options = { "animation": False, # Disable animation for smoother updates "title": {"text": "Sine Wave"}, @@ -103,10 +104,8 @@ def profile_get_value( ], } -# Display the chart st_echarts(options=options, height="400px", key="graph") -# Create columns for metrics col1, col2, col3 = st.columns(3) with col1: st.metric("Amplitude", f"{amplitude:.2f}") From 2f936dcdda639f8ed38de73a6084c533ec0ab1c7 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Thu, 10 Jul 2025 09:47:03 -0500 Subject: [PATCH 8/8] refactor: add return type annotations to performance checker functions --- examples/performance_checker/performance_checker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/performance_checker/performance_checker.py b/examples/performance_checker/performance_checker.py index 6a5bb26..3bbc885 100644 --- a/examples/performance_checker/performance_checker.py +++ b/examples/performance_checker/performance_checker.py @@ -20,23 +20,23 @@ sine_values = amplitude * np.sin(frequency * time_points) -def _set_time_points(): +def _set_time_points() -> None: panel.set_value("time_points", time_points.tolist()) -def _set_amplitude(): +def _set_amplitude() -> None: panel.set_value("amplitude", amplitude) -def _get_time_points(): +def _get_time_points() -> None: panel.get_value("time_points", [0.0]) -def _get_amplitude(): +def _get_amplitude() -> None: panel.get_value("amplitude", 1.0) -def _get_unset_value(): +def _get_unset_value() -> None: panel.get_value("unset_value", 1.0)