Skip to content

Commit c04a7c6

Browse files
Mike ProsserMike Prosser
authored andcommitted
improve performance for unset values and add performance checker example
1 parent b84232a commit c04a7c6

13 files changed

+206
-37
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Performance checker Example
2+
3+
This example demonstrates using nipanel with Streamlit to display a dynamic sine wave using the `streamlit-echarts` library.
4+
5+
## Features
6+
7+
- Generates sine wave data with varying frequency
8+
- Displays the data in an chart
9+
- Updates rapidly
10+
- Shows timing information
11+
12+
### Required Software
13+
14+
- Python 3.9 or later
15+
16+
### Usage
17+
18+
Run `poetry run python examples/performance_checker/performance_checker.py`
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Example of using nipanel to display a sine wave graph using st_echarts."""
2+
3+
import math
4+
import time
5+
from pathlib import Path
6+
7+
import numpy as np
8+
9+
import nipanel
10+
11+
12+
panel_script_path = Path(__file__).with_name("performance_checker_panel.py")
13+
panel = nipanel.create_panel(panel_script_path)
14+
15+
amplitude = 1.0
16+
frequency = 1.0
17+
num_points = 1000
18+
try:
19+
print(f"Panel URL: {panel.panel_url}")
20+
print("Press Ctrl+C to exit")
21+
22+
# Generate and update the sine wave data as fast as possible
23+
while True:
24+
time_points = np.linspace(0, num_points, num_points)
25+
sine_values = amplitude * np.sin(frequency * time_points)
26+
27+
panel.set_value("time_points", time_points.tolist())
28+
panel.set_value("sine_values", sine_values.tolist())
29+
panel.set_value("amplitude", amplitude)
30+
panel.set_value("frequency", frequency)
31+
32+
# Slowly vary the frequency for a more dynamic visualization
33+
frequency = 1.0 + 0.5 * math.sin(time.time() / 5.0)
34+
35+
except KeyboardInterrupt:
36+
print("Exiting...")
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""A Streamlit visualization panel for the perf_check.py example script."""
2+
3+
import statistics
4+
import time
5+
6+
import streamlit as st
7+
from streamlit_echarts import st_echarts
8+
9+
import nipanel
10+
11+
12+
def measure_get_value_time(panel, value_id, default_value=None):
13+
"""Measure the time it takes to get a value from the panel.
14+
15+
Args:
16+
panel: The panel accessor object
17+
value_id: The ID of the value to get
18+
default_value: Default value if the value is not found
19+
20+
Returns:
21+
A tuple of (value, time_ms) where time_ms is the time in milliseconds
22+
"""
23+
start_time = time.time()
24+
value = panel.get_value(value_id, default_value)
25+
end_time = time.time()
26+
time_ms = (end_time - start_time) * 1000
27+
return value, time_ms
28+
29+
30+
st.set_page_config(page_title="Performance Checker Example", page_icon="📈", layout="wide")
31+
st.title("Performance Checker Example")
32+
33+
# Initialize refresh history list if it doesn't exist
34+
if "refresh_history" not in st.session_state:
35+
st.session_state.refresh_history = []
36+
37+
# Store current timestamp and calculate time since last refresh
38+
current_time = time.time()
39+
if "last_refresh_time" not in st.session_state:
40+
st.session_state.last_refresh_time = current_time
41+
time_since_last_refresh = 0.0
42+
else:
43+
time_since_last_refresh = (current_time - st.session_state.last_refresh_time) * 1000
44+
st.session_state.last_refresh_time = current_time
45+
46+
# Store the last 10 refresh times
47+
st.session_state.refresh_history.append(time_since_last_refresh)
48+
if len(st.session_state.refresh_history) > 10:
49+
st.session_state.refresh_history.pop(0)
50+
51+
panel = nipanel.get_panel_accessor()
52+
53+
# Measure time to get each value
54+
time_points, time_points_ms = measure_get_value_time(panel, "time_points", [0.0])
55+
sine_values, sine_values_ms = measure_get_value_time(panel, "sine_values", [0.0])
56+
amplitude, amplitude_ms = measure_get_value_time(panel, "amplitude", 1.0)
57+
frequency, frequency_ms = measure_get_value_time(panel, "frequency", 1.0)
58+
unset_value, unset_value_ms = measure_get_value_time(panel, "unset_value", "default")
59+
60+
if st.session_state.refresh_history:
61+
history = st.session_state.refresh_history
62+
else:
63+
history = []
64+
65+
# Calculate statistics
66+
min_time = min(history) if history else 0
67+
max_time = max(history) if history else 0
68+
avg_time = statistics.mean(history) if history else 0
69+
70+
# Prepare data for echarts
71+
data = [{"value": [x, y]} for x, y in zip(time_points, sine_values)]
72+
73+
# Configure the chart options
74+
options = {
75+
"animation": False, # Disable animation for smoother updates
76+
"title": {"text": "Sine Wave"},
77+
"tooltip": {"trigger": "axis"},
78+
"xAxis": {"type": "value", "name": "Time (s)", "nameLocation": "middle", "nameGap": 30},
79+
"yAxis": {
80+
"type": "value",
81+
"name": "Amplitude",
82+
"nameLocation": "middle",
83+
"nameGap": 30,
84+
},
85+
"series": [
86+
{
87+
"data": data,
88+
"type": "line",
89+
"showSymbol": True,
90+
"smooth": True,
91+
"lineStyle": {"width": 2, "color": "#1f77b4"},
92+
"areaStyle": {"color": "#1f77b4", "opacity": 0.3},
93+
"name": "Sine Wave",
94+
}
95+
],
96+
}
97+
98+
# Display the chart
99+
st_echarts(options=options, height="400px", key="graph")
100+
101+
# Create columns for metrics
102+
col1, col2, col3, col4 = st.columns(4)
103+
with col1:
104+
st.metric("Amplitude", f"{amplitude:.2f}")
105+
st.metric("Frequency", f"{frequency:.2f} Hz")
106+
with col2:
107+
st.metric("Refresh Time", f"{time_since_last_refresh:.1f} ms")
108+
st.metric("Min Refresh Time", f"{min_time:.1f} ms")
109+
st.metric("Max Refresh Time", f"{max_time:.1f} ms")
110+
st.metric("Avg Refresh Time", f"{avg_time:.1f} ms")
111+
112+
with col3:
113+
st.metric("get time_points", f"{time_points_ms:.1f} ms")
114+
st.metric("get sine_values", f"{sine_values_ms:.1f} ms")
115+
st.metric("get amplitude", f"{amplitude_ms:.1f} ms")
116+
st.metric("get frequency", f"{frequency_ms:.1f} ms")
117+
with col4:
118+
st.metric("get unset_value", f"{unset_value_ms:.1f} ms")

protos/ni/pythonpanel/v1/python_panel_service.proto

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ service PythonPanelService {
3434
// Get a value for a control on the panel
3535
// Status Codes for errors:
3636
// - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
37-
// - NOT_FOUND: The value with the specified identifier was not found
3837
rpc GetValue(GetValueRequest) returns (GetValueResponse);
3938

4039
// Set a value for a control on the panel
@@ -99,7 +98,7 @@ message GetValueRequest {
9998

10099
message GetValueResponse {
101100
// The value
102-
google.protobuf.Any value = 1;
101+
optional google.protobuf.Any value = 1;
103102
}
104103

105104
message SetValueRequest {

src/ni/pythonpanel/v1/python_panel_service_pb2.py

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ni/pythonpanel/v1/python_panel_service_pb2.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,9 @@ class GetValueResponse(google.protobuf.message.Message):
171171
*,
172172
value: google.protobuf.any_pb2.Any | None = ...,
173173
) -> None: ...
174-
def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
175-
def ClearField(self, field_name: typing.Literal["value", b"value"]) -> None: ...
174+
def HasField(self, field_name: typing.Literal["_value", b"_value", "value", b"value"]) -> builtins.bool: ...
175+
def ClearField(self, field_name: typing.Literal["_value", b"_value", "value", b"value"]) -> None: ...
176+
def WhichOneof(self, oneof_group: typing.Literal["_value", b"_value"]) -> typing.Literal["value"] | None: ...
176177

177178
global___GetValueResponse = GetValueResponse
178179

src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ def GetValue(self, request, context):
7777
"""Get a value for a control on the panel
7878
Status Codes for errors:
7979
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
80-
- NOT_FOUND: The value with the specified identifier was not found
8180
"""
8281
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
8382
context.set_details('Method not implemented!')

src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ class PythonPanelServiceStub:
5555
"""Get a value for a control on the panel
5656
Status Codes for errors:
5757
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
58-
- NOT_FOUND: The value with the specified identifier was not found
5958
"""
6059

6160
SetValue: grpc.UnaryUnaryMultiCallable[
@@ -104,7 +103,6 @@ class PythonPanelServiceAsyncStub:
104103
"""Get a value for a control on the panel
105104
Status Codes for errors:
106105
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
107-
- NOT_FOUND: The value with the specified identifier was not found
108106
"""
109107

110108
SetValue: grpc.aio.UnaryUnaryMultiCallable[
@@ -161,7 +159,6 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta):
161159
"""Get a value for a control on the panel
162160
Status Codes for errors:
163161
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
164-
- NOT_FOUND: The value with the specified identifier was not found
165162
"""
166163

167164
@abc.abstractmethod

src/nipanel/_panel_client.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,23 @@ def set_value(self, panel_id: str, value_id: str, value: object, notify: bool) -
116116
)
117117
self._invoke_with_retry(self._get_stub().SetValue, set_value_request)
118118

119-
def get_value(self, panel_id: str, value_id: str) -> object:
119+
def get_value(self, panel_id: str, value_id: str) -> tuple[bool, object]:
120120
"""Get the value for the control with value_id.
121121
122122
Args:
123123
panel_id: The ID of the panel.
124124
value_id: The ID of the control.
125125
126126
Returns:
127-
The value.
127+
A tuple containing a boolean indicating whether the value was successfully retrieved and
128+
the value itself (or None if not present).
128129
"""
129130
get_value_request = GetValueRequest(panel_id=panel_id, value_id=value_id)
130131
response = self._invoke_with_retry(self._get_stub().GetValue, get_value_request)
131-
the_value = from_any(response.value)
132-
return the_value
132+
if response.HasField("value"):
133+
return True, from_any(response.value)
134+
else:
135+
return False, None
133136

134137
def _get_stub(self) -> PythonPanelServiceStub:
135138
if self._stub is None:

src/nipanel/_panel_value_accessor.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,15 @@ def get_value(self, value_id: str, default_value: _T | None = None) -> _T | obje
6060
Returns:
6161
The value, or the default value if not set
6262
"""
63-
try:
64-
value = self._panel_client.get_value(self._panel_id, value_id)
65-
if default_value is not None and not isinstance(value, type(default_value)):
66-
raise TypeError("Value type does not match default value type.")
67-
return value
68-
69-
except grpc.RpcError as e:
70-
if e.code() == grpc.StatusCode.NOT_FOUND and default_value is not None:
63+
found, value = self._panel_client.get_value(self._panel_id, value_id)
64+
if not found:
65+
if default_value is not None:
7166
return default_value
72-
else:
73-
raise e
67+
raise KeyError(f"Value with id '{value_id}' not found on panel '{self._panel_id}'.")
68+
69+
if default_value is not None and not isinstance(value, type(default_value)):
70+
raise TypeError("Value type does not match default value type.")
71+
return value
7472

7573
def set_value(self, value_id: str, value: object) -> None:
7674
"""Set the value for a control on the panel.

0 commit comments

Comments
 (0)