Skip to content

Commit 2f3724a

Browse files
mikeprosserniMike Prosser
andauthored
Improve performance for unset values and add performance checker example (#110)
* improve performance for unset values and add performance checker example * refactor: add type hints to measure_get_value_time function * feat: add performance measurement for panel value setting and getting * cleanup * feedback * cleanup * use timeit for performance measurements * refactor: add return type annotations to performance checker functions --------- Co-authored-by: Mike Prosser <[email protected]>
1 parent db935ae commit 2f3724a

13 files changed

+255
-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 measures the performance of a stremlit panel with a graph.
4+
5+
## Features
6+
7+
- Generates sine wave data with varying frequency
8+
- Displays the data in a graph
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: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Check the performance of get_value and set_value methods in nipanel."""
2+
3+
import math
4+
import time
5+
import timeit
6+
from pathlib import Path
7+
8+
import numpy as np
9+
10+
import nipanel
11+
12+
13+
panel_script_path = Path(__file__).with_name("performance_checker_panel.py")
14+
panel = nipanel.create_panel(panel_script_path)
15+
16+
amplitude = 1.0
17+
frequency = 1.0
18+
num_points = 1000
19+
time_points = np.linspace(0, num_points, num_points)
20+
sine_values = amplitude * np.sin(frequency * time_points)
21+
22+
23+
def _set_time_points() -> None:
24+
panel.set_value("time_points", time_points.tolist())
25+
26+
27+
def _set_amplitude() -> None:
28+
panel.set_value("amplitude", amplitude)
29+
30+
31+
def _get_time_points() -> None:
32+
panel.get_value("time_points", [0.0])
33+
34+
35+
def _get_amplitude() -> None:
36+
panel.get_value("amplitude", 1.0)
37+
38+
39+
def _get_unset_value() -> None:
40+
panel.get_value("unset_value", 1.0)
41+
42+
43+
iterations = 100
44+
45+
set_time_points_time = timeit.timeit(_set_time_points, number=iterations) * 1000 / iterations
46+
print(f"Average time to set 'time_points': {set_time_points_time:.2f} ms")
47+
48+
set_amplitude_time = timeit.timeit(_set_amplitude, number=iterations) * 1000 / iterations
49+
print(f"Average time to set 'amplitude': {set_amplitude_time:.2f} ms")
50+
51+
get_time_points_time = timeit.timeit(_get_time_points, number=iterations) * 1000 / iterations
52+
print(f"Average time to get 'time_points': {get_time_points_time:.2f} ms")
53+
54+
get_amplitude_time = timeit.timeit(_get_amplitude, number=iterations) * 1000 / iterations
55+
print(f"Average time to get 'amplitude': {get_amplitude_time:.2f} ms")
56+
57+
get_unset_value_time = timeit.timeit(_get_unset_value, number=iterations) * 1000 / iterations
58+
print(f"Average time to get 'unset_value': {get_unset_value_time:.2f} ms")
59+
60+
try:
61+
print(f"Panel URL: {panel.panel_url}")
62+
print("Press Ctrl+C to exit")
63+
64+
# Generate and update the sine wave data as fast as possible
65+
while True:
66+
time_points = np.linspace(0, num_points, num_points)
67+
sine_values = amplitude * np.sin(frequency * time_points)
68+
69+
panel.set_value("time_points", time_points.tolist())
70+
panel.set_value("sine_values", sine_values.tolist())
71+
panel.set_value("amplitude", amplitude)
72+
panel.set_value("frequency", frequency)
73+
74+
# Slowly vary the frequency for a more dynamic visualization
75+
frequency = 1.0 + 0.5 * math.sin(time.time() / 5.0)
76+
77+
except KeyboardInterrupt:
78+
print("Exiting...")
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""A Streamlit visualization panel for the performance_checker.py example script."""
2+
3+
import statistics
4+
import time
5+
import timeit
6+
from functools import partial
7+
from typing import Any, Tuple
8+
9+
import streamlit as st
10+
from streamlit_echarts import st_echarts
11+
12+
import nipanel
13+
14+
15+
def profile_get_value(
16+
panel: "nipanel.StreamlitPanelValueAccessor",
17+
value_id: str,
18+
default_value: Any = None,
19+
num_runs: int = 5,
20+
) -> Tuple[Any, float]:
21+
"""Measure the time it takes to get a value from the panel.
22+
23+
Args:
24+
panel: The panel accessor object
25+
value_id: The ID of the value to get
26+
default_value: Default value if the value is not found
27+
num_runs: Number of runs for timing
28+
29+
Returns:
30+
A tuple of (value, time_ms) where time_ms is the time in milliseconds
31+
"""
32+
value = panel.get_value(value_id, default_value)
33+
get_value_func = partial(panel.get_value, value_id, default_value)
34+
time_ms = timeit.timeit(get_value_func, number=num_runs) * 1000 / num_runs
35+
return value, time_ms
36+
37+
38+
st.set_page_config(page_title="Performance Checker Example", page_icon="📈", layout="wide")
39+
st.title("Performance Checker Example")
40+
41+
if "refresh_history" not in st.session_state:
42+
st.session_state.refresh_history = [] # List of tuples (timestamp, refresh_time_ms)
43+
44+
# Store current timestamp and calculate time since last refresh
45+
current_time = time.time()
46+
if "last_refresh_time" not in st.session_state:
47+
st.session_state.last_refresh_time = current_time
48+
time_since_last_refresh = 0.0
49+
else:
50+
time_since_last_refresh = (current_time - st.session_state.last_refresh_time) * 1000
51+
st.session_state.last_refresh_time = current_time
52+
53+
# Store refresh times with timestamps, keeping only the last 1 second of data
54+
st.session_state.refresh_history.append((current_time, time_since_last_refresh))
55+
56+
# Remove entries older than 1 second
57+
cutoff_time = current_time - 1.0 # 1 second ago
58+
st.session_state.refresh_history = [
59+
item for item in st.session_state.refresh_history if item[0] >= cutoff_time
60+
]
61+
62+
# Extract just the refresh times for calculations
63+
if st.session_state.refresh_history:
64+
refresh_history = [item[1] for item in st.session_state.refresh_history]
65+
else:
66+
refresh_history = []
67+
68+
min_refresh_time = min(refresh_history) if refresh_history else 0
69+
max_refresh_time = max(refresh_history) if refresh_history else 0
70+
avg_refresh_time = statistics.mean(refresh_history) if refresh_history else 0
71+
72+
panel = nipanel.get_panel_accessor()
73+
74+
num_timing_runs = 5
75+
time_points, time_points_ms = profile_get_value(panel, "time_points", [0.0], num_timing_runs)
76+
sine_values, sine_values_ms = profile_get_value(panel, "sine_values", [0.0], num_timing_runs)
77+
amplitude, amplitude_ms = profile_get_value(panel, "amplitude", 1.0, num_timing_runs)
78+
frequency, frequency_ms = profile_get_value(panel, "frequency", 1.0, num_timing_runs)
79+
unset_value, unset_value_ms = profile_get_value(panel, "unset_value", "default", num_timing_runs)
80+
81+
data = [{"value": [x, y]} for x, y in zip(time_points, sine_values)]
82+
83+
options = {
84+
"animation": False, # Disable animation for smoother updates
85+
"title": {"text": "Sine Wave"},
86+
"tooltip": {"trigger": "axis"},
87+
"xAxis": {"type": "value", "name": "Time (s)", "nameLocation": "middle", "nameGap": 30},
88+
"yAxis": {
89+
"type": "value",
90+
"name": "Amplitude",
91+
"nameLocation": "middle",
92+
"nameGap": 30,
93+
},
94+
"series": [
95+
{
96+
"data": data,
97+
"type": "line",
98+
"showSymbol": True,
99+
"smooth": True,
100+
"lineStyle": {"width": 2, "color": "#1f77b4"},
101+
"areaStyle": {"color": "#1f77b4", "opacity": 0.3},
102+
"name": "Sine Wave",
103+
}
104+
],
105+
}
106+
107+
st_echarts(options=options, height="400px", key="graph")
108+
109+
col1, col2, col3 = st.columns(3)
110+
with col1:
111+
st.metric("Amplitude", f"{amplitude:.2f}")
112+
st.metric("Frequency", f"{frequency:.2f} Hz")
113+
with col2:
114+
st.metric("Refresh Time", f"{time_since_last_refresh:.1f} ms")
115+
st.metric("Min Refresh Time", f"{min_refresh_time:.1f} ms")
116+
st.metric("Max Refresh Time", f"{max_refresh_time:.1f} ms")
117+
st.metric("Avg Refresh Time", f"{avg_refresh_time:.1f} ms")
118+
st.metric("FPS", f"{len(refresh_history)}")
119+
120+
with col3:
121+
st.metric("get time_points", f"{time_points_ms:.1f} ms")
122+
st.metric("get sine_values", f"{sine_values_ms:.1f} ms")
123+
st.metric("get amplitude", f"{amplitude_ms:.1f} ms")
124+
st.metric("get frequency", f"{frequency_ms:.1f} ms")
125+
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)